@dyyz1993/create-agent 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/create.ts +31 -31
- package/src/commands/workspace.ts +112 -105
- package/src/lib/copy.ts +24 -1
- package/templates/agent/electron/main.js +46 -0
- package/templates/agent/electron/preload.js +5 -0
- package/templates/agent/electron-builder.json +40 -0
- package/templates/agent/eslint.config.mjs +17 -2
- package/templates/agent/package.json +69 -3
- package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
- package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/agent/src/bun/index.ts +96 -41
- package/templates/agent/src/bun/three.d.ts +1 -0
- package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
- package/templates/agent/src/gateway/http-routes.ts +1 -1
- package/templates/agent/src/gateway/ws-handler.ts +84 -56
- package/templates/agent/src/mainview/App.tsx +29 -26
- package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
- package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
- package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
- package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
- package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
- package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
- package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
- package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/agent/src/mainview/lib/api-client.ts +179 -172
- package/templates/agent/src/mainview/main.tsx +4 -10
- package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/agent/src/server-config.ts +34 -32
- package/templates/agent/src/server.ts +42 -54
- package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
- package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
- package/templates/agent/src/shared/handlers/bash.ts +65 -65
- package/templates/agent/src/shared/handlers/chat.ts +189 -155
- package/templates/agent/src/shared/handlers/debug.ts +12 -0
- package/templates/agent/src/shared/handlers/feed.ts +31 -32
- package/templates/agent/src/shared/handlers/git.ts +286 -243
- package/templates/agent/src/shared/handlers/index.ts +1 -0
- package/templates/agent/src/shared/handlers/rules.ts +31 -31
- package/templates/agent/src/shared/handlers/todo.ts +31 -31
- package/templates/agent/src/shared/http-routes.ts +250 -0
- package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
- package/templates/agent/src/shared/lib/bash-security.ts +43 -43
- package/templates/agent/src/shared/lib/logger.ts +117 -95
- package/templates/agent/src/shared/lib/web-server.ts +128 -0
- package/templates/agent/src/shared/modules/debug.ts +8 -0
- package/templates/agent/src/shared/rpc-schema.ts +16 -3
- package/templates/agent/test-upload.txt +0 -0
- package/templates/agent/tsconfig.ipc.json +14 -0
- package/templates/agent/tsconfig.json +13 -3
- package/templates/chat/eslint.config.mjs +15 -2
- package/templates/chat/package.json +13 -2
- package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
- package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/chat/src/__tests__/server-config.test.ts +46 -44
- package/templates/chat/src/bun/index.ts +94 -44
- package/templates/chat/src/bun/three.d.ts +1 -0
- package/templates/chat/src/gateway/http-routes.ts +1 -1
- package/templates/chat/src/gateway/ws-handler.ts +84 -56
- package/templates/chat/src/mainview/App.tsx +6 -0
- package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/chat/src/mainview/lib/api-client.ts +180 -156
- package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/chat/src/mainview/main.tsx +10 -6
- package/templates/chat/src/server-config.ts +33 -31
- package/templates/chat/src/server.ts +42 -60
- package/templates/chat/src/shared/handlers/chat.ts +196 -162
- package/templates/chat/src/shared/handlers/debug.ts +12 -0
- package/templates/chat/src/shared/handlers/index.ts +1 -0
- package/templates/chat/src/shared/http-routes.ts +250 -0
- package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
- package/templates/chat/src/shared/lib/logger.ts +108 -95
- package/templates/chat/src/shared/lib/web-server.ts +128 -0
- package/templates/chat/src/shared/modules/debug.ts +8 -0
- package/templates/chat/src/shared/rpc-schema.ts +3 -2
- package/templates/chat/test-upload.txt +0 -0
- package/templates/chat/tsconfig.ipc.json +14 -0
- package/templates/chat/tsconfig.json +12 -2
- package/templates/general/eslint.config.mjs +15 -2
- package/templates/general/package.json +13 -2
- package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
- package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
- package/templates/general/src/__tests__/server-config.test.ts +46 -44
- package/templates/general/src/bun/index.ts +94 -44
- package/templates/general/src/bun/three.d.ts +1 -0
- package/templates/general/src/gateway/http-routes.ts +1 -1
- package/templates/general/src/gateway/ws-handler.ts +84 -56
- package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
- package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
- package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
- package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
- package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
- package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
- package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
- package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
- package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/general/src/mainview/lib/api-client.ts +180 -156
- package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/general/src/mainview/main.tsx +10 -6
- package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/general/src/server-config.ts +33 -31
- package/templates/general/src/server.ts +42 -60
- package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
- package/templates/general/src/shared/handlers/chat.ts +189 -155
- package/templates/general/src/shared/handlers/debug.ts +12 -0
- package/templates/general/src/shared/handlers/feed.ts +38 -40
- package/templates/general/src/shared/handlers/git.ts +286 -243
- package/templates/general/src/shared/handlers/index.ts +1 -0
- package/templates/general/src/shared/http-routes.ts +250 -0
- package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
- package/templates/general/src/shared/lib/logger.ts +117 -95
- package/templates/general/src/shared/lib/web-server.ts +128 -0
- package/templates/general/src/shared/modules/debug.ts +8 -0
- package/templates/general/src/shared/rpc-schema.ts +12 -2
- package/templates/general/test-upload.txt +0 -0
- package/templates/general/tsconfig.ipc.json +14 -0
- package/templates/general/tsconfig.json +12 -2
- package/templates/shared/__tests__/cors-security.test.ts +165 -0
- package/templates/shared/__tests__/file-path-security.test.ts +260 -0
- package/templates/shared/__tests__/http-routes.test.ts +174 -0
- package/templates/shared/__tests__/logger.test.ts +186 -0
- package/templates/shared/components/ErrorBoundary.tsx +59 -0
- package/templates/shared/env.d.ts +11 -0
- package/templates/shared/http-routes.ts +250 -0
- package/templates/shared/logger.ts +129 -0
- package/templates/shared/test-upload.txt +0 -0
- package/templates/shared/vite-base.config.ts +92 -0
- package/templates/shared/vitest-base.config.ts +42 -0
- package/templates/agent/eslint-plugin-rpc/index.js +0 -50
- package/templates/agent/eslint-plugin-rpc/package.json +0 -6
- package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
- package/templates/chat/eslint-plugin-rpc/index.js +0 -50
- package/templates/chat/eslint-plugin-rpc/package.json +0 -6
- package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
- package/templates/general/eslint-plugin-rpc/index.js +0 -50
- package/templates/general/eslint-plugin-rpc/package.json +0 -6
- package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
- package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
- package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
- package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
- package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
- package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
- package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("Server Config 安全策略", () => {
|
|
4
|
+
const originalEnv = process.env;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.resetModules();
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
process.env = originalEnv;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("生产环境未设 AUTH_TOKEN 应抛出致命错误", async () => {
|
|
16
|
+
process.env.NODE_ENV = "production";
|
|
17
|
+
delete process.env.AUTH_TOKEN;
|
|
18
|
+
await expect(import("../server-config")).rejects.toThrow(/AUTH_TOKEN/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("开发环境未设 AUTH_TOKEN 应自动生成随机 token", async () => {
|
|
22
|
+
process.env.NODE_ENV = "development";
|
|
23
|
+
delete process.env.AUTH_TOKEN;
|
|
24
|
+
const mod = await import("../server-config");
|
|
25
|
+
expect(mod.config.authToken).toBeTruthy();
|
|
26
|
+
expect(mod.config.authToken).not.toBe("pi-agent-template-token");
|
|
27
|
+
expect(mod.config.authToken.length).toBeGreaterThan(20);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("显式设置 AUTH_TOKEN 应使用用户提供的值", async () => {
|
|
31
|
+
delete process.env.NODE_ENV;
|
|
32
|
+
process.env.AUTH_TOKEN = "my-custom-token-12345";
|
|
33
|
+
const mod = await import("../server-config");
|
|
34
|
+
expect(mod.config.authToken).toBe("my-custom-token-12345");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("自动生成的 token 每次不同", async () => {
|
|
38
|
+
process.env.NODE_ENV = "development";
|
|
39
|
+
delete process.env.AUTH_TOKEN;
|
|
40
|
+
vi.resetModules();
|
|
41
|
+
const mod1 = await import("../server-config");
|
|
42
|
+
const token1 = mod1.config.authToken;
|
|
43
|
+
vi.resetModules();
|
|
44
|
+
const mod2 = await import("../server-config");
|
|
45
|
+
const token2 = mod2.config.authToken;
|
|
46
|
+
expect(token1).not.toBe(token2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("生产环境设了 AUTH_TOKEN 不应抛错", async () => {
|
|
50
|
+
process.env.NODE_ENV = "production";
|
|
51
|
+
process.env.AUTH_TOKEN = "production-secure-token";
|
|
52
|
+
const mod = await import("../server-config");
|
|
53
|
+
expect(mod.config.authToken).toBe("production-secure-token");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -2,58 +2,60 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import { parseEnvInt } from "../server-config";
|
|
3
3
|
|
|
4
4
|
describe("parseEnvInt", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
it("should return default when value is undefined", () => {
|
|
6
|
+
expect(parseEnvInt("TEST_KEY", undefined, 3100, 1024, 65535)).toBe(3100);
|
|
7
|
+
});
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
it("should return default when value is empty string", () => {
|
|
10
|
+
expect(parseEnvInt("TEST_KEY", "", 3100, 1024, 65535)).toBe(3100);
|
|
11
|
+
});
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
it("should parse valid number", () => {
|
|
14
|
+
expect(parseEnvInt("TEST_KEY", "8080", 3100, 1024, 65535)).toBe(8080);
|
|
15
|
+
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
it("should return default for NaN input", () => {
|
|
18
|
+
expect(parseEnvInt("TEST_KEY", "abc", 3100, 1024, 65535)).toBe(3100);
|
|
19
|
+
});
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
it("should return default for out-of-range value (too low)", () => {
|
|
22
|
+
expect(parseEnvInt("TEST_KEY", "80", 3100, 1024, 65535)).toBe(3100);
|
|
23
|
+
});
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
it("should return default for out-of-range value (too high)", () => {
|
|
26
|
+
expect(parseEnvInt("TEST_KEY", "99999", 3100, 1024, 65535)).toBe(3100);
|
|
27
|
+
});
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
it("should accept boundary min value", () => {
|
|
30
|
+
expect(parseEnvInt("TEST_KEY", "1024", 3100, 1024, 65535)).toBe(1024);
|
|
31
|
+
});
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
it("should accept boundary max value", () => {
|
|
34
|
+
expect(parseEnvInt("TEST_KEY", "65535", 3100, 1024, 65535)).toBe(65535);
|
|
35
|
+
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
describe("server-config exports", () => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
it("should export config with all required fields", async () => {
|
|
40
|
+
const { config } = await import("../server-config");
|
|
41
|
+
expect(config).toHaveProperty("port");
|
|
42
|
+
expect(config).toHaveProperty("authToken");
|
|
43
|
+
expect(config).toHaveProperty("maxUploadSize");
|
|
44
|
+
expect(config).toHaveProperty("logDir");
|
|
45
|
+
expect(config).toHaveProperty("enableWebService");
|
|
46
|
+
expect(config).toHaveProperty("corsOrigin");
|
|
47
|
+
expect(typeof config.port).toBe("number");
|
|
48
|
+
expect(typeof config.authToken).toBe("string");
|
|
49
|
+
expect(typeof config.maxUploadSize).toBe("number");
|
|
50
|
+
expect(typeof config.enableWebService).toBe("boolean");
|
|
51
|
+
expect(typeof config.corsOrigin).toBe("string");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should have sensible defaults", async () => {
|
|
55
|
+
const { config } = await import("../server-config");
|
|
56
|
+
expect(config.port).toBeGreaterThanOrEqual(1024);
|
|
57
|
+
expect(config.port).toBeLessThanOrEqual(65535);
|
|
58
|
+
expect(config.maxUploadSize).toBeGreaterThan(0);
|
|
59
|
+
expect(config.corsOrigin).toBeTruthy();
|
|
60
|
+
});
|
|
59
61
|
});
|
|
@@ -3,23 +3,24 @@ import { RPCServer } from "@dyyz1993/rpc-core";
|
|
|
3
3
|
import { ElectrobunTransport } from "../gateway/ipc-transport";
|
|
4
4
|
import { registerAllHandlers } from "../shared/register-all-handlers";
|
|
5
5
|
import { createLogger, configureLogDir } from "../shared/lib/logger";
|
|
6
|
+
import { config } from "../server-config";
|
|
6
7
|
|
|
7
8
|
configureLogDir("logs");
|
|
8
9
|
const log = createLogger("server");
|
|
9
10
|
|
|
10
11
|
async function getMainViewUrl(): Promise<string> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
const channel = await Updater.localInfo.channel();
|
|
13
|
+
const DEV_SERVER_URL = "http://localhost:5173";
|
|
14
|
+
if (channel === "dev") {
|
|
15
|
+
try {
|
|
16
|
+
await fetch(DEV_SERVER_URL, { method: "HEAD" });
|
|
17
|
+
log.info(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`);
|
|
18
|
+
return DEV_SERVER_URL;
|
|
19
|
+
} catch {
|
|
20
|
+
log.info("Vite dev server not running.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "views://mainview/index.html";
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const url = await getMainViewUrl();
|
|
@@ -27,46 +28,95 @@ const url = await getMainViewUrl();
|
|
|
27
28
|
const transport = new ElectrobunTransport();
|
|
28
29
|
const server = new RPCServer(transport);
|
|
29
30
|
|
|
30
|
-
// --- 注册 RPC handlers(自动导入 handlers barrel) ---
|
|
31
31
|
registerAllHandlers(server, { platform: "desktop" });
|
|
32
32
|
|
|
33
|
-
// --- 创建窗口 ---
|
|
34
|
-
|
|
35
33
|
const mainWindow = new BrowserWindow({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
title: "Pi Agent",
|
|
35
|
+
url,
|
|
36
|
+
frame: {
|
|
37
|
+
width: 1200,
|
|
38
|
+
height: 800,
|
|
39
|
+
x: 200,
|
|
40
|
+
y: 200,
|
|
41
|
+
},
|
|
42
|
+
rpc: BrowserView.defineRPC({
|
|
43
|
+
maxRequestTime: 60000,
|
|
44
|
+
handlers: {
|
|
45
|
+
requests: {},
|
|
46
|
+
messages: {
|
|
47
|
+
// @ts-expect-error Electrobun's messages schema doesn't support custom keys at compile time
|
|
48
|
+
"rpc-message": (data: unknown) => {
|
|
49
|
+
try {
|
|
50
|
+
const message = typeof data === "string" ? JSON.parse(data) : data;
|
|
51
|
+
transport.handleMessage(message);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log.error("Failed to parse RPC message", { error });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
61
59
|
});
|
|
62
60
|
|
|
63
61
|
transport.setBrowserView(mainWindow.webview);
|
|
64
62
|
|
|
65
63
|
log.info("Pi Agent desktop app started!");
|
|
66
64
|
|
|
65
|
+
if (config.enableWebService) {
|
|
66
|
+
try {
|
|
67
|
+
const { createWebServer, getLocalIP } = await import("../shared/lib/web-server");
|
|
68
|
+
|
|
69
|
+
const webServer = await createWebServer({
|
|
70
|
+
port: config.port,
|
|
71
|
+
authToken: config.authToken,
|
|
72
|
+
maxUploadSize: config.maxUploadSize,
|
|
73
|
+
corsOrigin: "*",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const localIp = getLocalIP();
|
|
77
|
+
log.info(`[Hybrid] Web service started: http://${localIp}:${webServer.port}`);
|
|
78
|
+
log.info(`[Hybrid] WebSocket endpoint: ws://${localIp}:${webServer.port}/ws`);
|
|
79
|
+
log.info(
|
|
80
|
+
`[Hybrid] Share URL: http://${localIp}:${webServer.port}?token=${webServer.authToken}`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
server.emitEvent("hybrid.ready", {
|
|
84
|
+
url: `http://${localIp}:${webServer.port}`,
|
|
85
|
+
wsUrl: `ws://${localIp}:${webServer.port}/ws`,
|
|
86
|
+
token: webServer.authToken,
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
log.error("[Hybrid] Failed to start web service", {
|
|
90
|
+
error: err instanceof Error ? err.message : String(err),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
ApplicationMenu.setApplicationMenu([
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
{
|
|
97
|
+
label: "Pi Agent",
|
|
98
|
+
submenu: [
|
|
99
|
+
{ role: "about" },
|
|
100
|
+
{ type: "separator" },
|
|
101
|
+
{ role: "hide" },
|
|
102
|
+
{ role: "hideOthers" },
|
|
103
|
+
{ role: "showAll" },
|
|
104
|
+
{ type: "separator" },
|
|
105
|
+
{ role: "quit" },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: "Edit",
|
|
110
|
+
submenu: [
|
|
111
|
+
{ role: "undo" },
|
|
112
|
+
{ role: "redo" },
|
|
113
|
+
{ type: "separator" },
|
|
114
|
+
{ role: "cut" },
|
|
115
|
+
{ role: "copy" },
|
|
116
|
+
{ role: "paste" },
|
|
117
|
+
{ role: "selectAll" },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{ label: "View", submenu: [{ role: "toggleFullScreen" }] },
|
|
121
|
+
{ label: "Window", submenu: [{ role: "minimize" }, { role: "zoom" }] },
|
|
72
122
|
]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "three";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createHttpHandler, type HttpRouteDeps } from "
|
|
1
|
+
export { createHttpHandler, type HttpRouteDeps } from "../shared/http-routes";
|
|
@@ -13,74 +13,102 @@ import { createLogger } from "../shared/lib/logger";
|
|
|
13
13
|
const log = createLogger("gateway");
|
|
14
14
|
|
|
15
15
|
function safeEqual(a: string, b: string): boolean {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const bufA = Buffer.from(a);
|
|
17
|
+
const bufB = Buffer.from(b);
|
|
18
|
+
if (bufA.length !== bufB.length) return false;
|
|
19
|
+
return timingSafeEqual(bufA, bufB);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface WsHandlerDeps {
|
|
23
|
-
|
|
23
|
+
config: { readonly port: number; readonly authToken: string; readonly maxUploadSize: number };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function createWsHandler(httpServer: Server, deps: WsHandlerDeps): WebSocketServer {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const { config: cfg } = deps;
|
|
28
|
+
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
wss.on("connection", (ws: WebSocket, req) => {
|
|
31
|
+
// Token 验证(header 优先,query fallback)
|
|
32
|
+
const url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
33
|
+
const headerToken = (req.headers["sec-websocket-protocol"] as string) || undefined;
|
|
34
|
+
const queryToken = url?.searchParams.get("token");
|
|
35
|
+
const token = headerToken || queryToken;
|
|
36
|
+
if (!safeEqual(token || "", cfg.authToken)) {
|
|
37
|
+
log.warn("Connection rejected: invalid token", {
|
|
38
|
+
providedPrefix: token ? `${token.substring(0, 4)}***` : "(none)",
|
|
39
|
+
});
|
|
40
|
+
ws.close(4001, "Unauthorized");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
log.info("Client connected", { total: wss.clients.size });
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return () => ws.off("message", listener);
|
|
59
|
-
},
|
|
60
|
-
onError: (): (() => void) => {
|
|
61
|
-
return () => {};
|
|
62
|
-
},
|
|
63
|
-
onDisconnect: (): (() => void) => {
|
|
64
|
-
return () => {};
|
|
65
|
-
},
|
|
66
|
-
isConnected: (): boolean => ws.readyState === WebSocket.OPEN,
|
|
67
|
-
close: (): void => {},
|
|
68
|
-
};
|
|
46
|
+
const PING_INTERVAL = 30000;
|
|
47
|
+
let pongReceived = true;
|
|
48
|
+
const pingTimer = setInterval(() => {
|
|
49
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
50
|
+
clearInterval(pingTimer);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!pongReceived) {
|
|
54
|
+
log.warn("Client heartbeat timeout, closing connection");
|
|
55
|
+
clearInterval(pingTimer);
|
|
56
|
+
ws.terminate();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
pongReceived = false;
|
|
60
|
+
ws.ping();
|
|
61
|
+
}, PING_INTERVAL);
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
ws.on("pong", () => {
|
|
64
|
+
pongReceived = true;
|
|
65
|
+
});
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const wsTransport = {
|
|
68
|
+
send: async (message: unknown): Promise<void> => {
|
|
69
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
70
|
+
ws.send(JSON.stringify(message));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
onMessage: (handler: (message: unknown) => void): (() => void) => {
|
|
74
|
+
const listener = (data: Buffer) => {
|
|
75
|
+
try {
|
|
76
|
+
const msg = JSON.parse(data.toString());
|
|
77
|
+
handler(msg);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
log.error("Failed to parse message", {
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
ws.on("message", listener);
|
|
85
|
+
return () => ws.off("message", listener);
|
|
86
|
+
},
|
|
87
|
+
onError: (): (() => void) => {
|
|
88
|
+
return () => {};
|
|
89
|
+
},
|
|
90
|
+
onDisconnect: (): (() => void) => {
|
|
91
|
+
return () => {};
|
|
92
|
+
},
|
|
93
|
+
isConnected: (): boolean => ws.readyState === WebSocket.OPEN,
|
|
94
|
+
close: (): void => {},
|
|
95
|
+
};
|
|
74
96
|
|
|
75
|
-
|
|
76
|
-
log.info("Client disconnected", { total: wss.clients.size });
|
|
77
|
-
rpcServer.close();
|
|
78
|
-
});
|
|
97
|
+
const rpcServer = new RPCServer(wsTransport as Transport);
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
});
|
|
99
|
+
// 自动导入并注册所有 handlers
|
|
100
|
+
registerAllHandlers(rpcServer, { platform: "web" });
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
ws.on("close", () => {
|
|
103
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
104
|
+
log.info("Client disconnected", { total: wss.clients.size });
|
|
105
|
+
rpcServer.close();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
ws.on("error", (err: Error) => {
|
|
109
|
+
log.error("Client error", { error: err.message });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return wss;
|
|
86
114
|
}
|
|
@@ -10,6 +10,8 @@ import { useBreakpointSync } from "./hooks/use-breakpoint";
|
|
|
10
10
|
import { ActivityBar } from "./components/activity-bar/ActivityBar";
|
|
11
11
|
import { MobileTabBar } from "./components/activity-bar/MobileTabBar";
|
|
12
12
|
import { ChatPanel } from "./components/chat/ChatPanel";
|
|
13
|
+
import { ThemeToggle } from "./components/common/ThemeToggle";
|
|
14
|
+
import { LanguageSwitcher } from "./components/common/LanguageSwitcher";
|
|
13
15
|
|
|
14
16
|
function App() {
|
|
15
17
|
const { t } = useTranslation();
|
|
@@ -90,6 +92,10 @@ function App() {
|
|
|
90
92
|
{mode === "desktop" ? t("app.mode.desktop") : t("app.mode.web")}
|
|
91
93
|
</span>
|
|
92
94
|
<span className="ml-3 text-[var(--color-text-tertiary)]">{t("app.title")}</span>
|
|
95
|
+
<div className="ml-auto flex items-center gap-1">
|
|
96
|
+
<LanguageSwitcher />
|
|
97
|
+
<ThemeToggle />
|
|
98
|
+
</div>
|
|
93
99
|
</div>
|
|
94
100
|
)}
|
|
95
101
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
|
|
5
|
+
function extractVarNames(css: string, selector: string): string[] {
|
|
6
|
+
const regex = new RegExp(`${selector.replace(".", "\\.")}\\s*\\{([^}]+)\\}`, "s");
|
|
7
|
+
const match = css.match(regex);
|
|
8
|
+
if (!match) return [];
|
|
9
|
+
const vars = match[1].match(/--[\w-]+/g);
|
|
10
|
+
return vars ? [...new Set(vars)].sort() : [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("CSS Variables", () => {
|
|
14
|
+
const cssPath = resolve(dirname(import.meta.url.replace("file://", "")), "../index.css");
|
|
15
|
+
const css = readFileSync(cssPath, "utf-8");
|
|
16
|
+
|
|
17
|
+
it("should have matching variables in :root and .dark", () => {
|
|
18
|
+
const rootVars = extractVarNames(css, ":root");
|
|
19
|
+
const darkVars = extractVarNames(css, ".dark");
|
|
20
|
+
|
|
21
|
+
expect(rootVars.length).toBeGreaterThan(0);
|
|
22
|
+
expect(darkVars.length).toBeGreaterThan(0);
|
|
23
|
+
expect(rootVars).toEqual(darkVars);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should have all required variable categories", () => {
|
|
27
|
+
const allVars = css.match(/--[\w-]+/g) || [];
|
|
28
|
+
const uniqueVars = [...new Set(allVars)];
|
|
29
|
+
|
|
30
|
+
const requiredPrefixes = ["bg-", "text-", "border-", "accent-"];
|
|
31
|
+
for (const prefix of requiredPrefixes) {
|
|
32
|
+
const hasPrefix = uniqueVars.some((v) => v.includes(prefix));
|
|
33
|
+
expect(hasPrefix, `Missing CSS variables with prefix: ${prefix}`).toBe(true);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -5,6 +5,7 @@ export function LanguageSwitcher() {
|
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
7
|
<button
|
|
8
|
+
data-testid="language-switcher"
|
|
8
9
|
onClick={() => setLocale(locale === "en" ? "zh" : "en")}
|
|
9
10
|
className="px-1.5 py-0.5 rounded text-xs hover:bg-[var(--color-bg-hover)] transition-colors text-[var(--color-text-secondary)]"
|
|
10
11
|
title={locale === "en" ? "切换到中文" : "Switch to English"}
|
|
@@ -6,6 +6,7 @@ export function ThemeToggle() {
|
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
8
|
<button
|
|
9
|
+
data-testid="theme-toggle"
|
|
9
10
|
onClick={toggleTheme}
|
|
10
11
|
className="p-1.5 rounded-md hover:bg-[var(--color-bg-hover)] transition-colors"
|
|
11
12
|
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|