@dyyz1993/create-agent 2.0.0 → 2.0.1

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.
Files changed (141) hide show
  1. package/package.json +1 -1
  2. package/src/lib/copy.ts +23 -1
  3. package/templates/agent/eslint.config.mjs +15 -2
  4. package/templates/agent/package.json +10 -2
  5. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  6. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  7. package/templates/agent/src/bun/index.ts +96 -41
  8. package/templates/agent/src/bun/three.d.ts +1 -0
  9. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  10. package/templates/agent/src/gateway/http-routes.ts +1 -1
  11. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  12. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  13. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  14. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  15. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  16. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  17. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  18. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  19. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  20. package/templates/agent/src/mainview/lib/api-client.ts +182 -172
  21. package/templates/agent/src/server-config.ts +34 -32
  22. package/templates/agent/src/server.ts +43 -54
  23. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  24. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  25. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  26. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  27. package/templates/agent/src/shared/handlers/debug.ts +8 -0
  28. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  29. package/templates/agent/src/shared/handlers/index.ts +1 -0
  30. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  31. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  32. package/templates/agent/src/shared/http-routes.ts +250 -0
  33. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  34. package/templates/agent/src/shared/lib/logger.ts +117 -95
  35. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  36. package/templates/agent/src/shared/modules/debug.ts +8 -0
  37. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  38. package/templates/agent/test-upload.txt +0 -0
  39. package/templates/agent/tsconfig.ipc.json +10 -0
  40. package/templates/agent/tsconfig.json +10 -2
  41. package/templates/chat/eslint.config.mjs +15 -2
  42. package/templates/chat/package.json +10 -2
  43. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  44. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  45. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  46. package/templates/chat/src/bun/index.ts +94 -44
  47. package/templates/chat/src/bun/three.d.ts +1 -0
  48. package/templates/chat/src/gateway/http-routes.ts +1 -1
  49. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  50. package/templates/chat/src/mainview/App.tsx +6 -0
  51. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  52. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  53. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  54. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  55. package/templates/chat/src/mainview/lib/api-client.ts +183 -156
  56. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  57. package/templates/chat/src/mainview/main.tsx +4 -3
  58. package/templates/chat/src/server-config.ts +33 -31
  59. package/templates/chat/src/server.ts +43 -60
  60. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  61. package/templates/chat/src/shared/handlers/debug.ts +8 -0
  62. package/templates/chat/src/shared/handlers/index.ts +1 -0
  63. package/templates/chat/src/shared/http-routes.ts +250 -0
  64. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  65. package/templates/chat/src/shared/lib/logger.ts +108 -95
  66. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  67. package/templates/chat/src/shared/modules/debug.ts +8 -0
  68. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  69. package/templates/chat/test-upload.txt +0 -0
  70. package/templates/chat/tsconfig.ipc.json +10 -0
  71. package/templates/general/eslint.config.mjs +15 -2
  72. package/templates/general/package.json +10 -2
  73. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  74. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  75. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  76. package/templates/general/src/bun/index.ts +94 -44
  77. package/templates/general/src/bun/three.d.ts +1 -0
  78. package/templates/general/src/gateway/http-routes.ts +1 -1
  79. package/templates/general/src/gateway/ws-handler.ts +84 -56
  80. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  81. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  82. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  83. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  84. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  85. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  86. package/templates/general/src/mainview/lib/api-client.ts +183 -156
  87. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  88. package/templates/general/src/mainview/main.tsx +4 -3
  89. package/templates/general/src/server-config.ts +33 -31
  90. package/templates/general/src/server.ts +43 -60
  91. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  92. package/templates/general/src/shared/handlers/chat.ts +189 -155
  93. package/templates/general/src/shared/handlers/debug.ts +8 -0
  94. package/templates/general/src/shared/handlers/feed.ts +38 -40
  95. package/templates/general/src/shared/handlers/index.ts +1 -0
  96. package/templates/general/src/shared/http-routes.ts +250 -0
  97. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  98. package/templates/general/src/shared/lib/logger.ts +117 -95
  99. package/templates/general/src/shared/lib/web-server.ts +128 -0
  100. package/templates/general/src/shared/modules/debug.ts +8 -0
  101. package/templates/general/src/shared/rpc-schema.ts +12 -2
  102. package/templates/general/test-upload.txt +0 -0
  103. package/templates/general/tsconfig.ipc.json +10 -0
  104. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  105. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  106. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  107. package/templates/shared/__tests__/logger.test.ts +186 -0
  108. package/templates/shared/components/ErrorBoundary.tsx +58 -0
  109. package/templates/shared/env.d.ts +11 -0
  110. package/templates/shared/http-routes.ts +230 -0
  111. package/templates/shared/logger.ts +129 -0
  112. package/templates/shared/test-upload.txt +0 -0
  113. package/templates/shared/vite-base.config.ts +92 -0
  114. package/templates/shared/vitest-base.config.ts +42 -0
  115. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  116. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  117. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  118. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  119. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  120. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  121. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  122. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  123. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  124. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  125. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  126. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  127. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  128. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  129. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  130. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  131. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  132. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  133. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  134. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  135. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  136. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  137. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  138. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  139. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  140. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  141. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyyz1993/create-agent",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Create Agent project scaffolding CLI",
5
5
  "repository": {
6
6
  "type": "git",
package/src/lib/copy.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "fs";
2
2
  import { join, resolve } from "path";
3
3
  import { execSync } from "child_process";
4
4
 
@@ -73,6 +73,11 @@ function copySharedModules(monorepoRoot: string, targetDir: string, projectName:
73
73
  const destSharedDir = join(targetDir, "shared");
74
74
  copyAndReplace(sharedDir, destSharedDir, projectName);
75
75
 
76
+ for (const f of ["http-routes.ts", "logger.ts"]) {
77
+ const p = join(destSharedDir, f);
78
+ if (existsSync(p)) unlinkSync(p);
79
+ }
80
+
76
81
  const tsconfigPath = join(targetDir, "tsconfig.json");
77
82
  if (existsSync(tsconfigPath)) {
78
83
  let tsconfig = readFileSync(tsconfigPath, "utf-8");
@@ -224,6 +229,23 @@ export async function copyTemplate(options: CopyOptions): Promise<void> {
224
229
  if (isMonorepo) {
225
230
  copyTraeRules(localRoot, targetDir, projectName);
226
231
  copySharedModules(localRoot, targetDir, projectName);
232
+ } else {
233
+ const publishedShared = resolve(import.meta.dir, "..", "..", "templates", "shared");
234
+ if (existsSync(publishedShared)) {
235
+ copyAndReplace(publishedShared, join(targetDir, "shared"), projectName);
236
+ for (const f of ["http-routes.ts", "logger.ts"]) {
237
+ const p = join(targetDir, "shared", f);
238
+ if (existsSync(p)) unlinkSync(p);
239
+ }
240
+ const tsconfigPath = join(targetDir, "tsconfig.json");
241
+ if (existsSync(tsconfigPath)) {
242
+ let tsconfig = readFileSync(tsconfigPath, "utf-8");
243
+ tsconfig = tsconfig.replace(/"\.\.\/shared\/\*"/g, '"./shared/*"');
244
+ tsconfig = tsconfig.replace(/"\.\.\/shared"/g, '"./shared"');
245
+ writeFileSync(tsconfigPath, tsconfig);
246
+ }
247
+ cleanSharedViteConfig(join(targetDir, "shared"));
248
+ }
227
249
  }
228
250
  cleanViteConfig(targetDir);
229
251
  updatePackageJson(targetDir, projectName);
@@ -1,6 +1,9 @@
1
1
  import js from '@eslint/js';
2
2
  import tseslint from 'typescript-eslint';
3
- import rpcPlugin from './eslint-plugin-rpc/index.js';
3
+ import rpcPlugin from '@dyyz1993/eslint-plugin-rpc';
4
+ import reactHooks from 'eslint-plugin-react-hooks';
5
+ import reactRefresh from 'eslint-plugin-react-refresh';
6
+ import jsxA11y from 'eslint-plugin-jsx-a11y';
4
7
 
5
8
  export default tseslint.config(
6
9
  js.configs.recommended,
@@ -10,7 +13,6 @@ export default tseslint.config(
10
13
  'node_modules/**',
11
14
  'build/**',
12
15
  'dist/**',
13
- 'eslint-plugin-rpc/**',
14
16
  'postcss.config.js',
15
17
  'tailwind.config.js',
16
18
  ],
@@ -19,8 +21,19 @@ export default tseslint.config(
19
21
  files: ['src/**/*.{ts,tsx}'],
20
22
  plugins: {
21
23
  rpc: rpcPlugin,
24
+ 'react-hooks': reactHooks,
25
+ 'react-refresh': reactRefresh,
26
+ 'jsx-a11y': jsxA11y,
22
27
  },
23
28
  rules: {
29
+ ...reactHooks.configs.recommended.rules,
30
+ 'react-hooks/rules-of-hooks': 'warn',
31
+ 'react-hooks/exhaustive-deps': 'warn',
32
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
33
+ ...jsxA11y.configs.recommended.rules,
34
+ 'jsx-a11y/click-events-have-key-events': 'warn',
35
+ 'jsx-a11y/no-static-element-interactions': 'warn',
36
+ 'jsx-a11y/no-autofocus': 'warn',
24
37
  // TS 严格规则
25
38
  '@typescript-eslint/no-explicit-any': 'error',
26
39
  '@typescript-eslint/no-empty-object-type': ['error', { allowObjectTypes: 'always', allowInterfaces: 'with-single-extends' }],
@@ -9,6 +9,9 @@
9
9
  "dev:server": "bun src/server.ts",
10
10
  "build": "vite build",
11
11
  "build:canary": "vite build && electrobun build --env=canary",
12
+ "build:mac": "vite build && electrobun build --env=canary",
13
+ "build:win": "vite build && electrobun build --env=canary",
14
+ "build:linux": "vite build && electrobun build --env=canary",
12
15
  "lint": "eslint .",
13
16
  "lint:fix": "eslint . --fix",
14
17
  "test": "vitest run",
@@ -19,7 +22,7 @@
19
22
  "dependencies": {
20
23
  "@dyyz1993/rpc-core": "^1.2.0",
21
24
  "@tanstack/react-virtual": "^3.13.24",
22
- "electrobun": "1.13.1",
25
+ "electrobun": "1.18.1",
23
26
  "lucide-react": "^1.9.0",
24
27
  "prism-react-renderer": "^2.4.1",
25
28
  "react": "^18.3.1",
@@ -33,7 +36,8 @@
33
36
  "zustand": "^5.0.12"
34
37
  },
35
38
  "devDependencies": {
36
- "@eslint/js": "^9.0.0",
39
+ "@dyyz1993/eslint-plugin-rpc": "workspace:*",
40
+ "@eslint/js": "^9.0.0",
37
41
  "@testing-library/jest-dom": "^6.9.1",
38
42
  "@testing-library/react": "^16.3.2",
39
43
  "@testing-library/user-event": "^14.6.1",
@@ -41,10 +45,14 @@
41
45
  "@types/react": "^18.3.12",
42
46
  "@types/react-dom": "^18.3.1",
43
47
  "@types/ws": "^8.18.1",
48
+ "ws": "^8.18.0",
44
49
  "@vitejs/plugin-react": "^4.3.4",
45
50
  "autoprefixer": "^10.4.20",
46
51
  "concurrently": "^9.1.0",
47
52
  "eslint": "^9.0.0",
53
+ "eslint-plugin-jsx-a11y": "^6.10.0",
54
+ "eslint-plugin-react-hooks": "^5.2.0",
55
+ "eslint-plugin-react-refresh": "^0.4.20",
48
56
  "happy-dom": "^20.9.0",
49
57
  "husky": "^9.1.0",
50
58
  "jsdom": "^29.1.1",
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ vi.mock("electrobun/bun", () => ({
4
+ BrowserWindow: {
5
+ defineRPC: vi.fn(() => ({})),
6
+ },
7
+ BrowserView: {
8
+ defineRPC: vi.fn(() => ({
9
+ requests: {},
10
+ messages: {},
11
+ })),
12
+ },
13
+ Updater: {
14
+ localInfo: vi.fn(() => ({ channel: vi.fn(() => "dev") })),
15
+ },
16
+ ApplicationMenu: {
17
+ setApplicationMenu: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ vi.mock("electrobun", () => ({}));
22
+
23
+ describe("混合模式", () => {
24
+ beforeEach(() => {
25
+ vi.resetModules();
26
+ delete process.env.ENABLE_WEB_SERVICE;
27
+ delete process.env.AUTH_TOKEN;
28
+ process.env.NODE_ENV = "test";
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ it("默认不启动 Web 服务(enableWebService 为 false)", async () => {
36
+ const { config } = await import("../server-config");
37
+ expect(config.enableWebService).toBe(false);
38
+ });
39
+
40
+ it("enableWebService=true 时启动 HTTP + WS 服务", async () => {
41
+ process.env.ENABLE_WEB_SERVICE = "true";
42
+ const { config } = await import("../server-config");
43
+ expect(config.enableWebService).toBe(true);
44
+ });
45
+
46
+ it("IPC 和 WS 服务同时工作,状态隔离", async () => {
47
+ const { ElectrobunTransport } = await import("../gateway/ipc-transport");
48
+ const ipcTransport = new ElectrobunTransport();
49
+ expect(ipcTransport.isConnected()).toBe(false);
50
+
51
+ ipcTransport.setBrowserView({ executeJavascript: vi.fn() });
52
+ expect(ipcTransport.isConnected()).toBe(true);
53
+
54
+ const { createWebServer } = await import("../shared/lib/web-server");
55
+ const webServer = await createWebServer({
56
+ port: 0,
57
+ authToken: "test-token",
58
+ maxUploadSize: 1024,
59
+ corsOrigin: "*",
60
+ });
61
+ expect(webServer.port).toBeGreaterThan(0);
62
+ expect(ipcTransport.isConnected()).toBe(true);
63
+ await webServer.close();
64
+ });
65
+
66
+ it("WS 客户端需要 token 才能连接", async () => {
67
+ const { createWebServer } = await import("../shared/lib/web-server");
68
+ const webServer = await createWebServer({
69
+ port: 0,
70
+ authToken: "test-token",
71
+ maxUploadSize: 1024,
72
+ corsOrigin: "*",
73
+ });
74
+ expect(webServer.authToken).toBeTruthy();
75
+ expect(webServer.authToken.length).toBeGreaterThan(0);
76
+ await webServer.close();
77
+ });
78
+
79
+ it("IPC 客户端不需要 token", async () => {
80
+ const { ElectrobunTransport } = await import("../gateway/ipc-transport");
81
+ const transport = new ElectrobunTransport();
82
+ transport.setBrowserView({ executeJavascript: vi.fn() });
83
+ expect(transport.isConnected()).toBe(true);
84
+ });
85
+
86
+ it("动态生成的 token 每次启动不同", async () => {
87
+ const { createWebServer } = await import("../shared/lib/web-server");
88
+ const ws1 = await createWebServer({
89
+ port: 0,
90
+ authToken: `token-${Date.now()}-1`,
91
+ maxUploadSize: 1024,
92
+ corsOrigin: "*",
93
+ });
94
+ const ws2 = await createWebServer({
95
+ port: 0,
96
+ authToken: `token-${Date.now()}-2`,
97
+ maxUploadSize: 1024,
98
+ corsOrigin: "*",
99
+ });
100
+ expect(ws1.authToken).not.toBe(ws2.authToken);
101
+ await ws1.close();
102
+ await ws2.close();
103
+ });
104
+
105
+ it("关闭桌面端时 Web 服务也关闭", async () => {
106
+ const { createWebServer } = await import("../shared/lib/web-server");
107
+ const webServer = await createWebServer({
108
+ port: 0,
109
+ authToken: "test-token",
110
+ maxUploadSize: 1024,
111
+ corsOrigin: "*",
112
+ });
113
+ expect(webServer.httpServer.listening).toBe(true);
114
+ await webServer.close();
115
+ await new Promise((r) => setTimeout(r, 50));
116
+ expect(webServer.httpServer.listening).toBe(false);
117
+ });
118
+
119
+ it("getLocalIP 返回有效 IP 地址", async () => {
120
+ const { getLocalIP } = await import("../shared/lib/web-server");
121
+ const ip = getLocalIP();
122
+ expect(ip).toBeTruthy();
123
+ // IPv4 pattern
124
+ expect(ip).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
125
+ });
126
+ });
@@ -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
+ });
@@ -9,18 +9,18 @@ configureLogDir("logs");
9
9
  const log = createLogger("server");
10
10
 
11
11
  async function getMainViewUrl(): Promise<string> {
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";
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";
24
24
  }
25
25
 
26
26
  const url = await getMainViewUrl();
@@ -34,40 +34,95 @@ registerAllHandlers(server, { platform: "desktop", enableBash: config.enableBash
34
34
  // --- 创建窗口 ---
35
35
 
36
36
  const mainWindow = new BrowserWindow({
37
- title: "Pi Agent",
38
- url,
39
- frame: {
40
- width: 1200,
41
- height: 800,
42
- x: 200,
43
- y: 200,
44
- },
45
- rpc: BrowserView.defineRPC({
46
- maxRequestTime: 60000,
47
- handlers: {
48
- requests: {},
49
- messages: {
50
- // @ts-expect-error Electrobun's messages schema doesn't support custom keys at compile time
51
- "rpc-message": (data: unknown) => {
52
- try {
53
- const message = typeof data === "string" ? JSON.parse(data) : data;
54
- transport.handleMessage(message);
55
- } catch (error) {
56
- log.error("Failed to parse RPC message", { error });
57
- }
58
- },
59
- },
60
- },
61
- }),
37
+ title: "Pi Agent",
38
+ url,
39
+ frame: {
40
+ width: 1200,
41
+ height: 800,
42
+ x: 200,
43
+ y: 200,
44
+ },
45
+ rpc: BrowserView.defineRPC({
46
+ maxRequestTime: 60000,
47
+ handlers: {
48
+ requests: {},
49
+ messages: {
50
+ // @ts-expect-error Electrobun's messages schema doesn't support custom keys at compile time
51
+ "rpc-message": (data: unknown) => {
52
+ try {
53
+ const message = typeof data === "string" ? JSON.parse(data) : data;
54
+ transport.handleMessage(message);
55
+ } catch (error) {
56
+ log.error("Failed to parse RPC message", { error });
57
+ }
58
+ },
59
+ },
60
+ },
61
+ }),
62
62
  });
63
63
 
64
64
  transport.setBrowserView(mainWindow.webview);
65
65
 
66
66
  log.info("Pi Agent desktop app started!");
67
67
 
68
+ // ==============================
69
+ // Optional: Web service for LAN access (hybrid mode)
70
+ // ==============================
71
+ if (config.enableWebService) {
72
+ try {
73
+ const { createWebServer, getLocalIP } = await import("../shared/lib/web-server");
74
+
75
+ const webServer = await createWebServer({
76
+ port: config.port,
77
+ authToken: config.authToken,
78
+ maxUploadSize: config.maxUploadSize,
79
+ corsOrigin: "*",
80
+ });
81
+
82
+ const localIp = getLocalIP();
83
+ log.info(`[Hybrid] Web service started: http://${localIp}:${webServer.port}`);
84
+ log.info(`[Hybrid] WebSocket endpoint: ws://${localIp}:${webServer.port}/ws`);
85
+ log.info(
86
+ `[Hybrid] Share URL: http://${localIp}:${webServer.port}?token=${webServer.authToken}`
87
+ );
88
+
89
+ server.emitEvent("hybrid.ready", {
90
+ url: `http://${localIp}:${webServer.port}`,
91
+ wsUrl: `ws://${localIp}:${webServer.port}/ws`,
92
+ token: webServer.authToken,
93
+ });
94
+ } catch (err) {
95
+ log.error("[Hybrid] Failed to start web service", {
96
+ error: err instanceof Error ? err.message : String(err),
97
+ });
98
+ }
99
+ }
100
+
68
101
  ApplicationMenu.setApplicationMenu([
69
- { label: "Pi Agent", submenu: [{ role: "about" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "showAll" }, { type: "separator" }, { role: "quit" }] },
70
- { label: "Edit", submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }] },
71
- { label: "View", submenu: [{ role: "toggleFullScreen" }] },
72
- { label: "Window", submenu: [{ role: "minimize" }, { role: "zoom" }] },
102
+ {
103
+ label: "Pi Agent",
104
+ submenu: [
105
+ { role: "about" },
106
+ { type: "separator" },
107
+ { role: "hide" },
108
+ { role: "hideOthers" },
109
+ { role: "showAll" },
110
+ { type: "separator" },
111
+ { role: "quit" },
112
+ ],
113
+ },
114
+ {
115
+ label: "Edit",
116
+ submenu: [
117
+ { role: "undo" },
118
+ { role: "redo" },
119
+ { type: "separator" },
120
+ { role: "cut" },
121
+ { role: "copy" },
122
+ { role: "paste" },
123
+ { role: "selectAll" },
124
+ ],
125
+ },
126
+ { label: "View", submenu: [{ role: "toggleFullScreen" }] },
127
+ { label: "Window", submenu: [{ role: "minimize" }, { role: "zoom" }] },
73
128
  ]);
@@ -0,0 +1 @@
1
+ declare module "three";
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { EventEmitter } from "events";
3
+
4
+ vi.mock("../../shared/register-all-handlers", () => ({
5
+ registerAllHandlers: vi.fn(),
6
+ }));
7
+
8
+ vi.mock("../../shared/lib/logger", () => ({
9
+ createLogger: () => ({
10
+ info: vi.fn(),
11
+ error: vi.fn(),
12
+ warn: vi.fn(),
13
+ debug: vi.fn(),
14
+ }),
15
+ }));
16
+
17
+ const MOCK_AUTH_TOKEN = "test-secure-token-123456";
18
+
19
+ function createMockWs() {
20
+ const ws = new EventEmitter();
21
+ (ws as any).readyState = 1;
22
+ (ws as any).close = vi.fn();
23
+ (ws as any).send = vi.fn();
24
+ (ws as any).ping = vi.fn();
25
+ return ws;
26
+ }
27
+
28
+ function createMockReq(url: string, headers: Record<string, string> = {}) {
29
+ return { url, headers };
30
+ }
31
+
32
+ describe("WS Handler Token 安全", () => {
33
+ let _wssInstance: EventEmitter & { clients: Set<any> };
34
+
35
+ beforeEach(async () => {
36
+ vi.resetModules();
37
+
38
+ const { WebSocketServer } = await import("ws");
39
+ const _originalWSS = WebSocketServer;
40
+
41
+ vi.doMock("ws", () => {
42
+ class MockWSS extends EventEmitter {
43
+ clients = new Set();
44
+ constructor(_opts: any) {
45
+ super();
46
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
47
+ _wssInstance = this;
48
+ }
49
+ }
50
+ return { WebSocketServer: MockWSS, WebSocket: { OPEN: 1 } };
51
+ });
52
+ });
53
+
54
+ async function setupTest() {
55
+ const { createWsHandler } = await import("../ws-handler");
56
+ const httpServer = new EventEmitter();
57
+ const wss = createWsHandler(httpServer as any, {
58
+ config: { port: 3100, authToken: MOCK_AUTH_TOKEN, maxUploadSize: 50 * 1024 * 1024 },
59
+ });
60
+ return wss;
61
+ }
62
+
63
+ function emitConnection(wss: any, ws: any, req: any) {
64
+ wss.emit("connection", ws, req);
65
+ }
66
+
67
+ it("无 token 应被拒绝 (4001)", async () => {
68
+ const wss = await setupTest();
69
+ const ws = createMockWs();
70
+ const req = createMockReq("/ws");
71
+ emitConnection(wss, ws, req);
72
+ expect((ws as any).close).toHaveBeenCalledWith(4001, "Unauthorized");
73
+ });
74
+
75
+ it("错误 token 应被拒绝 (4001)", async () => {
76
+ const wss = await setupTest();
77
+ const ws = createMockWs();
78
+ const req = createMockReq("/ws?token=wrong-token");
79
+ emitConnection(wss, ws, req);
80
+ expect((ws as any).close).toHaveBeenCalledWith(4001, "Unauthorized");
81
+ });
82
+
83
+ it("正确 token 通过 URL query 应被接受", async () => {
84
+ const wss = await setupTest();
85
+ const ws = createMockWs();
86
+ const req = createMockReq(`/ws?token=${MOCK_AUTH_TOKEN}`);
87
+ emitConnection(wss, ws, req);
88
+ expect((ws as any).close).not.toHaveBeenCalled();
89
+ });
90
+
91
+ it("正确 token 通过 Sec-WebSocket-Protocol header 应被接受", async () => {
92
+ const wss = await setupTest();
93
+ const ws = createMockWs();
94
+ const req = createMockReq("/ws", {
95
+ "sec-websocket-protocol": MOCK_AUTH_TOKEN,
96
+ });
97
+ emitConnection(wss, ws, req);
98
+ expect((ws as any).close).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it("header 方式优先于 query 方式 (header 正确, query 错误)", async () => {
102
+ const wss = await setupTest();
103
+ const ws = createMockWs();
104
+ const req = createMockReq("/ws?token=wrong-token", {
105
+ "sec-websocket-protocol": MOCK_AUTH_TOKEN,
106
+ });
107
+ emitConnection(wss, ws, req);
108
+ expect((ws as any).close).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it("token 验证使用 timingSafeEqual(长度不等返回 false)", async () => {
112
+ const wss = await setupTest();
113
+ const ws = createMockWs();
114
+ const req = createMockReq(`/ws?token=short`);
115
+ emitConnection(wss, ws, req);
116
+ expect((ws as any).close).toHaveBeenCalledWith(4001, "Unauthorized");
117
+ });
118
+ });
@@ -1 +1 @@
1
- export { createHttpHandler, type HttpRouteDeps } from "@shared/http-routes";
1
+ export { createHttpHandler, type HttpRouteDeps } from "../shared/http-routes";