@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
|
@@ -1,150 +1,149 @@
|
|
|
1
1
|
import { apiClient } from "../lib/api-client";
|
|
2
2
|
|
|
3
3
|
export interface DropEntry {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
name: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
file?: File;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
children?: DropEntry[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 递归读取 webkitGetAsEntry,返回扁平文件列表 + 目录结构
|
|
13
13
|
*/
|
|
14
|
-
function readEntry(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return new Promise((resolve) => {
|
|
27
|
-
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
|
28
|
-
const children: DropEntry[] = [];
|
|
14
|
+
function readEntry(entry: FileSystemEntry, path: string): Promise<DropEntry> {
|
|
15
|
+
if (entry.isFile) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
(entry as FileSystemFileEntry).file((file) => {
|
|
18
|
+
resolve({ name: entry.name, relativePath: path, file, isDirectory: false });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Directory
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
|
25
|
+
const children: DropEntry[] = [];
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
const readBatch = () => {
|
|
28
|
+
reader.readEntries(async (entries) => {
|
|
29
|
+
if (entries.length === 0) {
|
|
30
|
+
resolve({ name: entry.name, relativePath: path, isDirectory: true, children });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
children.push(await readEntry(e, `${path}/${e.name}`));
|
|
35
|
+
}
|
|
36
|
+
readBatch(); // readEntries may not return all entries in one call
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
readBatch();
|
|
40
|
+
});
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/**
|
|
47
44
|
* 从 DataTransfer 递归读取所有文件和目录
|
|
48
45
|
*/
|
|
49
46
|
export async function readDropItems(dataTransfer: DataTransfer): Promise<DropEntry[]> {
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const items = dataTransfer.items;
|
|
48
|
+
if (!items) return [];
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const entries: DropEntry[] = [];
|
|
51
|
+
const tasks: Promise<void>[] = [];
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
const item = items[i]!;
|
|
55
|
+
// Try webkitGetAsEntry (Chrome, Edge, Safari)
|
|
56
|
+
const entry = item.webkitGetAsEntry?.();
|
|
57
|
+
if (entry) {
|
|
58
|
+
tasks.push(
|
|
59
|
+
readEntry(entry, entry.name).then<void>((e) => {
|
|
60
|
+
entries.push(e);
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
await Promise.all(tasks);
|
|
67
|
+
return entries;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
/**
|
|
72
71
|
* Web 端:递归上传 entries 到目标目录
|
|
73
72
|
*/
|
|
74
73
|
export async function uploadEntriesWeb(entries: DropEntry[], destDir: string): Promise<number> {
|
|
75
|
-
|
|
74
|
+
let count = 0;
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
async function process(entry: DropEntry, currentDir: string): Promise<void> {
|
|
77
|
+
if (entry.isDirectory) {
|
|
78
|
+
// Create directory
|
|
79
|
+
const dirPath = `${currentDir}/${entry.name}`;
|
|
80
|
+
await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
|
|
81
|
+
count++;
|
|
82
|
+
if (entry.children) {
|
|
83
|
+
for (const child of entry.children) {
|
|
84
|
+
await process(child, dirPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else if (entry.file) {
|
|
88
|
+
// Upload file via HTTP
|
|
89
|
+
const filePath = `${currentDir}/${entry.name}`;
|
|
90
|
+
const arrayBuffer = await entry.file.arrayBuffer();
|
|
91
|
+
const baseUrl = apiClient.getBaseUrl();
|
|
92
|
+
const token = apiClient.getAuthToken();
|
|
93
|
+
const res = await fetch(
|
|
94
|
+
`${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
|
|
95
|
+
{ method: "POST", body: arrayBuffer }
|
|
96
|
+
);
|
|
97
|
+
if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
|
|
98
|
+
count++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
await process(entry, destDir);
|
|
104
|
+
}
|
|
105
|
+
return count;
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
/**
|
|
110
109
|
* 桌面端:通过 RPC file.copy 直接复制
|
|
111
110
|
*/
|
|
112
111
|
export async function importFilesDesktop(entries: DropEntry[], destDir: string): Promise<number> {
|
|
113
|
-
|
|
112
|
+
let count = 0;
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
114
|
+
async function process(entry: DropEntry, currentDir: string): Promise<void> {
|
|
115
|
+
if (entry.isDirectory) {
|
|
116
|
+
const dirPath = `${currentDir}/${entry.name}`;
|
|
117
|
+
await apiClient.call("file.createDir", { dirPath: currentDir, name: entry.name });
|
|
118
|
+
count++;
|
|
119
|
+
if (entry.children) {
|
|
120
|
+
for (const child of entry.children) {
|
|
121
|
+
await process(child, dirPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else if (entry.file) {
|
|
125
|
+
// Desktop: File object has .path property (Electron/Electrobun)
|
|
126
|
+
const srcPath = (entry.file as File & { path?: string }).path;
|
|
127
|
+
if (srcPath) {
|
|
128
|
+
await apiClient.call("file.copy", { srcPath, destDir: currentDir });
|
|
129
|
+
} else {
|
|
130
|
+
// Fallback: no path available, upload via HTTP
|
|
131
|
+
const filePath = `${currentDir}/${entry.name}`;
|
|
132
|
+
const arrayBuffer = await entry.file.arrayBuffer();
|
|
133
|
+
const baseUrl = apiClient.getBaseUrl();
|
|
134
|
+
const token = apiClient.getAuthToken();
|
|
135
|
+
const res = await fetch(
|
|
136
|
+
`${baseUrl}/file/upload?path=${encodeURIComponent(filePath)}&token=${token}`,
|
|
137
|
+
{ method: "POST", body: arrayBuffer }
|
|
138
|
+
);
|
|
139
|
+
if (!res.ok) throw new Error(`Upload failed: ${entry.name}`);
|
|
140
|
+
}
|
|
141
|
+
count++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
await process(entry, destDir);
|
|
147
|
+
}
|
|
148
|
+
return count;
|
|
150
149
|
}
|
|
@@ -3,41 +3,43 @@
|
|
|
3
3
|
* Values are read from environment variables with sensible defaults.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
|
|
8
|
+
if (process.env.NODE_ENV === "production" && !process.env.AUTH_TOKEN) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"[FATAL] AUTH_TOKEN environment variable must be set in production mode. " +
|
|
11
|
+
"Example: AUTH_TOKEN=$(openssl rand -hex 32) bun src/server.ts"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export function parseEnvInt(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
key: string,
|
|
17
|
+
value: string | undefined,
|
|
18
|
+
defaultValue: number,
|
|
19
|
+
min: number,
|
|
20
|
+
max: number
|
|
12
21
|
): number {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
return parsed;
|
|
22
|
+
if (value === undefined || value === "") return defaultValue;
|
|
23
|
+
const parsed = parseInt(value, 10);
|
|
24
|
+
if (isNaN(parsed) || parsed < min || parsed > max) {
|
|
25
|
+
process.stderr.write(`[config] Invalid ${key}: "${value}", using default: ${defaultValue}\n`);
|
|
26
|
+
return defaultValue;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export const config = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
port: parseEnvInt("PORT", process.env.PORT, 3100, 1024, 65535),
|
|
33
|
+
authToken: process.env.AUTH_TOKEN || `dev-${randomUUID()}`,
|
|
34
|
+
maxUploadSize: parseEnvInt(
|
|
35
|
+
"MAX_UPLOAD_SIZE",
|
|
36
|
+
process.env.MAX_UPLOAD_SIZE,
|
|
37
|
+
50 * 1024 * 1024,
|
|
38
|
+
0,
|
|
39
|
+
1024 * 1024 * 1024
|
|
40
|
+
),
|
|
41
|
+
logDir: process.env.LOG_DIR || "logs",
|
|
42
|
+
enableBash: process.env.ENABLE_BASH !== "false",
|
|
43
|
+
enableWebService: process.env.ENABLE_WEB_SERVICE === "true",
|
|
44
|
+
corsOrigin: process.env.CORS_ORIGIN || "http://localhost:5173",
|
|
37
45
|
} as const;
|
|
38
|
-
|
|
39
|
-
if (process.env.NODE_ENV === "production" && !process.env.AUTH_TOKEN) {
|
|
40
|
-
console.warn(
|
|
41
|
-
"[security] WARNING: Using default AUTH_TOKEN in production. Set AUTH_TOKEN environment variable.",
|
|
42
|
-
);
|
|
43
|
-
}
|
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
* Writes actual port to .server-port for dev orchestration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createServer } from "http";
|
|
8
7
|
import { writeFileSync, unlinkSync, existsSync } from "fs";
|
|
9
8
|
import { join, resolve, basename } from "path";
|
|
10
9
|
import { config } from "./server-config";
|
|
11
|
-
import {
|
|
12
|
-
import { createWsHandler } from "./gateway/ws-handler";
|
|
10
|
+
import { createWebServer, getLocalIP } from "./shared/lib/web-server";
|
|
13
11
|
import { createLogger, configureLogDir } from "./shared/lib/logger";
|
|
14
12
|
import { registerPort, unregisterPort, formatRegistryForOutput } from "./shared/lib/port-registry";
|
|
15
13
|
import { discoverMethodNames } from "./shared/register-all-handlers";
|
|
@@ -22,68 +20,58 @@ const PROJECT_ROOT = resolve(import.meta.dir, "..");
|
|
|
22
20
|
const PROJECT_NAME = basename(PROJECT_ROOT);
|
|
23
21
|
|
|
24
22
|
function cleanupPortFile() {
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(PORT_FILE)) unlinkSync(PORT_FILE);
|
|
25
|
+
} catch {}
|
|
26
|
+
unregisterPort(PROJECT_ROOT);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function writePortFile(port: number) {
|
|
30
|
-
|
|
30
|
+
writeFileSync(PORT_FILE, String(port), "utf-8");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
process.on("exit", cleanupPortFile);
|
|
34
|
-
process.on("SIGINT", () => {
|
|
35
|
-
|
|
34
|
+
process.on("SIGINT", () => {
|
|
35
|
+
cleanupPortFile();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
});
|
|
38
|
+
process.on("SIGTERM", () => {
|
|
39
|
+
cleanupPortFile();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
const
|
|
43
|
+
async function start() {
|
|
44
|
+
const { port, close } = await createWebServer({
|
|
45
|
+
port: config.port,
|
|
46
|
+
authToken: config.authToken,
|
|
47
|
+
maxUploadSize: config.maxUploadSize,
|
|
48
|
+
corsOrigin: config.corsOrigin,
|
|
49
|
+
});
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
process.on("SIGINT", () => {
|
|
52
|
+
close();
|
|
53
|
+
cleanupPortFile();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
});
|
|
56
|
+
process.on("SIGTERM", () => {
|
|
57
|
+
close();
|
|
58
|
+
cleanupPortFile();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
});
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const testServer = createServer();
|
|
48
|
-
testServer.once("error", (err: NodeJS.ErrnoException) => {
|
|
49
|
-
testServer.close();
|
|
50
|
-
reject(err);
|
|
51
|
-
});
|
|
52
|
-
testServer.once("listening", () => {
|
|
53
|
-
testServer.close();
|
|
54
|
-
resolve(port);
|
|
55
|
-
});
|
|
56
|
-
testServer.listen(port);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
62
|
+
writePortFile(port);
|
|
63
|
+
registerPort(PROJECT_ROOT, port, PROJECT_NAME);
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`No available port found after ${maxRetries} retries`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function start() {
|
|
73
|
-
const port = await findAvailablePort(config.port);
|
|
74
|
-
httpServer.listen(port, () => {
|
|
75
|
-
writePortFile(port);
|
|
76
|
-
registerPort(PROJECT_ROOT, port, PROJECT_NAME);
|
|
77
|
-
log.info(`HTTP + WebSocket server running on http://localhost:${port}`);
|
|
78
|
-
log.info(`WebSocket: ws://localhost:${port}/ws (auth required)`);
|
|
79
|
-
log.info(`Available RPC methods: ${discoverMethodNames().join(", ")}`);
|
|
80
|
-
log.info("File endpoints: GET /file/{path}, GET /info/{path}");
|
|
81
|
-
// eslint-disable-next-line no-console
|
|
82
|
-
console.log("\n" + formatRegistryForOutput() + "\n");
|
|
83
|
-
});
|
|
65
|
+
const localIp = getLocalIP();
|
|
66
|
+
log.info(`HTTP + WebSocket server running on http://localhost:${port}`);
|
|
67
|
+
log.info(`Local network access: http://${localIp}:${port}`);
|
|
68
|
+
log.info(`WebSocket: ws://localhost:${port}/ws (auth required)`);
|
|
69
|
+
log.info(`Available RPC methods: ${discoverMethodNames().join(", ")}`);
|
|
70
|
+
log.info("File endpoints: GET /file/{path}, GET /info/{path}");
|
|
71
|
+
log.info(formatRegistryForOutput());
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
start().catch((err) => {
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
log.error("Server failed to start", { error: err.message });
|
|
76
|
+
process.exit(1);
|
|
89
77
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { unlink } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
|
|
7
|
+
vi.mock("../../lib/logger", () => ({
|
|
8
|
+
createLogger: () => ({
|
|
9
|
+
info: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
debug: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
function createMockServer() {
|
|
17
|
+
const handlers = new Map<string, (params: unknown) => Promise<unknown>>();
|
|
18
|
+
return {
|
|
19
|
+
register: vi.fn((method: string, handler: (params: unknown) => Promise<unknown>) => {
|
|
20
|
+
handlers.set(method, handler);
|
|
21
|
+
}),
|
|
22
|
+
emitEvent: vi.fn(),
|
|
23
|
+
handlers,
|
|
24
|
+
async call(method: string, params: unknown) {
|
|
25
|
+
const h = handlers.get(method);
|
|
26
|
+
if (!h) throw new Error(`No handler for ${method}`);
|
|
27
|
+
return h(params);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DESKTOP_HISTORY_PATH = join(homedir(), ".pi-agent", "chat-history-desktop.json");
|
|
33
|
+
|
|
34
|
+
describe("Chat Handler 并发安全", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.resetModules();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
if (existsSync(DESKTOP_HISTORY_PATH)) {
|
|
41
|
+
await unlink(DESKTOP_HISTORY_PATH).catch(() => {});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("两个独立的 RPCServer 应注册各自的 handler", async () => {
|
|
46
|
+
const { register } = await import("../chat");
|
|
47
|
+
const serverA = createMockServer();
|
|
48
|
+
const serverB = createMockServer();
|
|
49
|
+
|
|
50
|
+
register(serverA as any, { platform: "desktop" });
|
|
51
|
+
register(serverB as any, { platform: "web" });
|
|
52
|
+
|
|
53
|
+
expect(serverA.register).toHaveBeenCalledWith("chat.list", expect.any(Function));
|
|
54
|
+
expect(serverA.register).toHaveBeenCalledWith("chat.send", expect.any(Function));
|
|
55
|
+
expect(serverB.register).toHaveBeenCalledWith("chat.list", expect.any(Function));
|
|
56
|
+
expect(serverB.register).toHaveBeenCalledWith("chat.send", expect.any(Function));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("server A 的消息不影响 server B 的消息列表", async () => {
|
|
60
|
+
const { register } = await import("../chat");
|
|
61
|
+
const serverA = createMockServer();
|
|
62
|
+
const serverB = createMockServer();
|
|
63
|
+
|
|
64
|
+
register(serverA as any, { platform: "desktop" });
|
|
65
|
+
register(serverB as any, { platform: "web" });
|
|
66
|
+
|
|
67
|
+
await serverA.call("chat.send", { content: "hello from desktop" });
|
|
68
|
+
await serverB.call("chat.send", { content: "hello from web" });
|
|
69
|
+
|
|
70
|
+
const resultA = (await serverA.call("chat.list", { limit: 50 })) as {
|
|
71
|
+
messages: { content: string }[];
|
|
72
|
+
};
|
|
73
|
+
const resultB = (await serverB.call("chat.list", { limit: 50 })) as {
|
|
74
|
+
messages: { content: string }[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const desktopContents = resultA.messages.map((m) => m.content);
|
|
78
|
+
const webContents = resultB.messages.map((m) => m.content);
|
|
79
|
+
|
|
80
|
+
expect(desktopContents).toContain("hello from desktop");
|
|
81
|
+
expect(webContents).toContain("hello from web");
|
|
82
|
+
expect(desktopContents).not.toContain("hello from web");
|
|
83
|
+
expect(webContents).not.toContain("hello from desktop");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("同一 server 内的消息按顺序写入", async () => {
|
|
87
|
+
const { register } = await import("../chat");
|
|
88
|
+
const server = createMockServer();
|
|
89
|
+
register(server as any, { platform: "desktop" });
|
|
90
|
+
|
|
91
|
+
await server.call("chat.send", { content: "msg1" });
|
|
92
|
+
await server.call("chat.send", { content: "msg2" });
|
|
93
|
+
await server.call("chat.send", { content: "msg3" });
|
|
94
|
+
|
|
95
|
+
const result = (await server.call("chat.list", { limit: 50 })) as {
|
|
96
|
+
messages: { role: string; content: string }[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const userMsgs = result.messages.filter((m) => m.role === "user").map((m) => m.content);
|
|
100
|
+
expect(userMsgs).toEqual(["msg1", "msg2", "msg3"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("存储路径应包含平台标识(desktop/web)或唯一 ID", async () => {
|
|
104
|
+
const { getStoragePathFor } = await import("../chat");
|
|
105
|
+
const desktopPath = getStoragePathFor("desktop");
|
|
106
|
+
const webPath = getStoragePathFor("web");
|
|
107
|
+
|
|
108
|
+
expect(desktopPath).toContain("desktop");
|
|
109
|
+
expect(webPath).not.toBe(desktopPath);
|
|
110
|
+
expect(webPath).toMatch(/web/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("desktop 使用固定路径保留历史", async () => {
|
|
114
|
+
const { getStoragePathFor } = await import("../chat");
|
|
115
|
+
const path1 = getStoragePathFor("desktop");
|
|
116
|
+
const path2 = getStoragePathFor("desktop");
|
|
117
|
+
expect(path1).toBe(path2);
|
|
118
|
+
expect(path1).toContain("chat-history-desktop.json");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("web 每次生成不同的路径", async () => {
|
|
122
|
+
const { getStoragePathFor } = await import("../chat");
|
|
123
|
+
const path1 = getStoragePathFor("web");
|
|
124
|
+
const path2 = getStoragePathFor("web");
|
|
125
|
+
expect(path1).not.toBe(path2);
|
|
126
|
+
});
|
|
127
|
+
});
|