支持单位授权与客户端限制

This commit is contained in:
Codex
2026-06-01 11:58:14 +08:00
parent 28adc8143a
commit c136cc2cc8
36 changed files with 861 additions and 49 deletions

View File

@@ -20,7 +20,7 @@ import {
getUpdateState,
openReleasesPage,
} from "../services/autoUpdater";
import { getDeviceId } from "../services/system/deviceId";
import { getDeviceId, getPrimaryLocalIp } from "../services/system/deviceId";
import { getTrayManager } from "../window/trayManager";
import { getAutoLaunchManager } from "../window/autoLaunchManager";
import { APP_DISPLAY_NAME } from "@shared/constants";
@@ -284,6 +284,10 @@ export function registerAppHandlers(ctx: HandlerContext): void {
return getDeviceId();
});
ipcMain.handle("app:getPrimaryLocalIp", () => {
return getPrimaryLocalIp();
});
ipcMain.handle("app:checkUpdate", async () => {
try {
return await checkForUpdates();

View File

@@ -314,7 +314,7 @@ describe("AcpEngine.prompt", () => {
{ messageID: "rid-timeout-001" },
);
await vi.advanceTimersByTimeAsync(60_001);
await vi.advanceTimersByTimeAsync(600_001);
await expect(promptPromise).resolves.toBeDefined();
expect(onPromptEnd).toHaveBeenCalledTimes(1);

View File

@@ -28,6 +28,18 @@ export function getDeviceId(): string {
return cachedDeviceId;
}
export function getPrimaryLocalIp(): string | null {
const interfaces = os.networkInterfaces();
for (const entries of Object.values(interfaces)) {
for (const entry of entries ?? []) {
if (entry.family === "IPv4" && !entry.internal) {
return entry.address;
}
}
}
return null;
}
export function logSystemInfo(): void {
// 固定字段尽量保持稳定输出,便于跨平台日志分析与检索。
const baseInfo = {

View File

@@ -1,5 +1,5 @@
// Main process system services
export { getDeviceId } from './deviceId';
export { getDeviceId, getPrimaryLocalIp } from './deviceId';
export { getAppEnv, setMirrorConfig } from './dependencies';
export {
getAppDataDir,

View File

@@ -503,6 +503,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
openReleasesPage: () => ipcRenderer.invoke("app:openReleasesPage"),
getUpdateDebugInfo: () => ipcRenderer.invoke("app:getUpdateDebugInfo"),
getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"),
getPrimaryLocalIp: () => ipcRenderer.invoke("app:getPrimaryLocalIp"),
},
// Permissions (macOS)

View File

@@ -192,6 +192,7 @@ export interface ClientRegisterParams {
password: string;
savedKey?: string;
deviceId?: string;
name?: string;
sandboxConfigValue: SandboxValue;
}

View File

@@ -28,6 +28,7 @@ vi.stubGlobal("window", {
electronAPI: {
app: {
getDeviceId: vi.fn(async () => "mock-device-id"),
getPrimaryLocalIp: vi.fn(async () => "192.168.1.23"),
},
settings: {
get: mockSettingsGet,
@@ -166,6 +167,7 @@ describe("auth - savedKey 认证 (快捷登录)", () => {
expect(params.username).toBe("");
expect(params.password).toBe("");
expect(params.savedKey).toBe(SAVED_KEY);
expect(params.name).toBe("ip-192.168.1.23");
});
it("username/password 为 null + 有 savedKey → 应正常同步", async () => {
@@ -313,6 +315,9 @@ describe("auth - savedKey 认证 (快捷登录)", () => {
expect(store["auth.password"]).toBeUndefined();
// savedKey 应保存
expect(store["auth.saved_key"]).toBe(CONFIG_KEY_FROM_SERVER);
const [params] = mockRegisterClient.mock.calls[0];
expect(params.name).toBe("ip-192.168.1.23");
});
});

View File

@@ -203,6 +203,17 @@ async function getLocalSandboxValue(): Promise<SandboxValue> {
};
}
async function getDefaultSandboxName(): Promise<string | undefined> {
try {
const ip = await window.electronAPI?.app.getPrimaryLocalIp?.();
const normalized = typeof ip === "string" ? ip.trim() : "";
return normalized ? `ip-${normalized}` : undefined;
} catch (error) {
logger.warn("Failed to resolve default sandbox name", "Auth", error);
return undefined;
}
}
// ========== 错误处理 ===
/**
* 获取友好的错误信息
@@ -288,11 +299,13 @@ export async function loginAndRegister(
// 构建注册参数
const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = {
username,
password,
savedKey: savedKey || undefined,
deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(),
};
@@ -471,11 +484,13 @@ export async function reRegisterClient(): Promise<ClientRegisterResponse | null>
logger.info("Re-registering client (using savedKey)...", "Auth");
const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = {
username: username || "",
password: "", // 密码不持久化,使用 savedKey 认证
savedKey,
deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(),
};
@@ -575,11 +590,13 @@ export async function syncConfigToServer(options?: {
}
const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = {
username: username || "",
password: "", // 密码不持久化,使用 savedKey 认证
savedKey,
deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(),
};

View File

@@ -152,7 +152,7 @@ export const PROCESS_KILL_ESCALATION_TIMEOUT = 5000;
export const ACP_ABORT_TIMEOUT = 15_000;
/** ACP prompt 等待响应超时 (ms) */
export const ACP_PROMPT_TIMEOUT = 60_000;
export const ACP_PROMPT_TIMEOUT = 600_000;
/**
* 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。

View File

@@ -515,6 +515,7 @@ export interface AppAPI {
error?: string;
}>;
getDeviceId: () => Promise<string>;
getPrimaryLocalIp: () => Promise<string | null>;
}
export type PermissionStatus = "granted" | "denied" | "unknown";