@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.
- package/package.json +1 -1
- package/src/lib/copy.ts +23 -1
- package/templates/agent/eslint.config.mjs +15 -2
- package/templates/agent/package.json +10 -2
- 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/__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/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/todo/TodoPanel.tsx +149 -122
- package/templates/agent/src/mainview/lib/api-client.ts +182 -172
- package/templates/agent/src/server-config.ts +34 -32
- package/templates/agent/src/server.ts +43 -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 +8 -0
- package/templates/agent/src/shared/handlers/feed.ts +31 -32
- 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/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 +10 -0
- package/templates/agent/tsconfig.json +10 -2
- package/templates/chat/eslint.config.mjs +15 -2
- package/templates/chat/package.json +10 -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/lib/api-client.ts +183 -156
- package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/chat/src/mainview/main.tsx +4 -3
- package/templates/chat/src/server-config.ts +33 -31
- package/templates/chat/src/server.ts +43 -60
- package/templates/chat/src/shared/handlers/chat.ts +196 -162
- package/templates/chat/src/shared/handlers/debug.ts +8 -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 +10 -0
- package/templates/general/eslint.config.mjs +15 -2
- package/templates/general/package.json +10 -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/layout/AppLayout.tsx +6 -0
- package/templates/general/src/mainview/lib/api-client.ts +183 -156
- package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
- package/templates/general/src/mainview/main.tsx +4 -3
- package/templates/general/src/server-config.ts +33 -31
- package/templates/general/src/server.ts +43 -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 +8 -0
- package/templates/general/src/shared/handlers/feed.ts +38 -40
- 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 +10 -0
- 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 +58 -0
- package/templates/shared/env.d.ts +11 -0
- package/templates/shared/http-routes.ts +230 -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,5 +1,11 @@
|
|
|
1
1
|
import { createTypedClient, WebSocketTransport, IPCTransport } from "@dyyz1993/rpc-core";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypedClient,
|
|
4
|
+
MethodParams,
|
|
5
|
+
MethodResult,
|
|
6
|
+
EventPayload,
|
|
7
|
+
EventMetadata,
|
|
8
|
+
} from "@dyyz1993/rpc-core";
|
|
3
9
|
import type { RPCMethods, RPCEvents } from "../../shared/rpc-schema";
|
|
4
10
|
import { rpcCache, CACHEABLE_METHODS } from "./rpc-cache";
|
|
5
11
|
|
|
@@ -10,182 +16,186 @@ import { rpcCache, CACHEABLE_METHODS } from "./rpc-cache";
|
|
|
10
16
|
* 3. 默认值(开发用)
|
|
11
17
|
*/
|
|
12
18
|
function resolveAuthToken(): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
const fromQuery = new URLSearchParams(window.location.search).get("token");
|
|
21
|
+
if (fromQuery) return fromQuery;
|
|
22
|
+
const fromStorage = localStorage.getItem("rpc-auth-token");
|
|
23
|
+
if (fromStorage) return fromStorage;
|
|
24
|
+
}
|
|
25
|
+
return "pi-agent-template-token";
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const AUTH_TOKEN = resolveAuthToken();
|
|
23
29
|
|
|
24
30
|
class APIClientImpl {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
31
|
+
private client: TypedClient<RPCMethods, RPCEvents> | null = null;
|
|
32
|
+
private initPromise: Promise<void> | null = null;
|
|
33
|
+
private _transport: "ipc" | "websocket" = "websocket";
|
|
34
|
+
private _baseUrl: string | null = null;
|
|
35
|
+
private wsTransport: WebSocketTransport | null = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 桌面端同步初始化:通过 executeJavascript 接收 + __electrobunBunBridge 发送
|
|
39
|
+
*/
|
|
40
|
+
initSyncForDesktop(): void {
|
|
41
|
+
if (this.client) return;
|
|
42
|
+
|
|
43
|
+
const ipcTransport = new IPCTransport();
|
|
44
|
+
this._transport = "ipc";
|
|
45
|
+
this._baseUrl = null; // 桌面端不走 HTTP
|
|
46
|
+
this.client = createTypedClient<RPCMethods, RPCEvents>(ipcTransport);
|
|
47
|
+
this.setupElectrobunBridge(ipcTransport);
|
|
48
|
+
if (import.meta.env.DEV) {
|
|
49
|
+
console.warn("[APIClient] Desktop (IPC) initialized synchronously");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 异步初始化:Web 端使用(连接 WebSocket)
|
|
55
|
+
*/
|
|
56
|
+
async initialize(): Promise<void> {
|
|
57
|
+
// 已连接且 transport 正常 → 直接返回
|
|
58
|
+
if (this.client && (this._transport === "ipc" || this.wsTransport?.isConnected())) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// client 存在但 WS 断开了 → 清理后重连
|
|
63
|
+
if (this.client && this.wsTransport && !this.wsTransport.isConnected()) {
|
|
64
|
+
this.wsTransport.close();
|
|
65
|
+
this.wsTransport = null;
|
|
66
|
+
this.client = null;
|
|
67
|
+
this.initPromise = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.initPromise) return this.initPromise;
|
|
71
|
+
|
|
72
|
+
this.initPromise = (async () => {
|
|
73
|
+
const env = this.detectEnvironment();
|
|
74
|
+
|
|
75
|
+
if (env === "electrobun") {
|
|
76
|
+
// 不应走到这里,桌面端应通过 initSyncForDesktop() 初始化
|
|
77
|
+
this.initSyncForDesktop();
|
|
78
|
+
} else {
|
|
79
|
+
this._transport = "websocket";
|
|
80
|
+
const wsUrl = this.getWebSocketUrl();
|
|
81
|
+
this.wsTransport = new WebSocketTransport({
|
|
82
|
+
url: wsUrl,
|
|
83
|
+
authToken: AUTH_TOKEN,
|
|
84
|
+
reconnect: true,
|
|
85
|
+
});
|
|
86
|
+
await this.wsTransport.connect();
|
|
87
|
+
this.client = createTypedClient<RPCMethods, RPCEvents>(this.wsTransport);
|
|
88
|
+
|
|
89
|
+
const wsUrlObj = new URL(wsUrl);
|
|
90
|
+
this._baseUrl = `http://${wsUrlObj.host}`;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
return this.initPromise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private detectEnvironment(): "electrobun" | "browser" {
|
|
98
|
+
if (typeof window === "undefined") return "browser";
|
|
99
|
+
if (window.__electrobunBunBridge) return "electrobun";
|
|
100
|
+
return "browser";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getWebSocketUrl(): string {
|
|
104
|
+
if (typeof window === "undefined") return `ws://localhost:3100?token=${AUTH_TOKEN}`;
|
|
105
|
+
const customUrl =
|
|
106
|
+
new URLSearchParams(window.location.search).get("ws") ||
|
|
107
|
+
localStorage.getItem("rpc-websocket-url");
|
|
108
|
+
if (customUrl)
|
|
109
|
+
return customUrl.includes("token=") ? customUrl : `${customUrl}?token=${AUTH_TOKEN}`;
|
|
110
|
+
return `ws://${window.location.host}/ws?token=${AUTH_TOKEN}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 桌面端 IPC 桥接:
|
|
115
|
+
* - Bun → Browser: 通过 executeJavascript 调用 window.__piAgentIPC()
|
|
116
|
+
* - Browser → Bun: 通过 __electrobunBunBridge.postMessage 发送 Electrobun 消息格式
|
|
117
|
+
*/
|
|
118
|
+
private setupElectrobunBridge(ipcTransport: IPCTransport): void {
|
|
119
|
+
if (typeof window === "undefined") return;
|
|
120
|
+
|
|
121
|
+
const win = window;
|
|
122
|
+
|
|
123
|
+
// 1. 注册接收函数:Bun 通过 executeJavascript 调用此函数发送消息到 Browser
|
|
124
|
+
win.__piAgentIPC = (msg: unknown) => {
|
|
125
|
+
ipcTransport.simulateMessage(msg);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// 2. 覆写 send:将 RPC-core 消息包装成 Electrobun 消息格式,通过原生桥接发送
|
|
129
|
+
const bridge = win.__electrobunBunBridge;
|
|
130
|
+
|
|
131
|
+
if (bridge) {
|
|
132
|
+
ipcTransport.send = async (message: unknown) => {
|
|
133
|
+
// 包装成 Electrobun message packet,bun 端 defineRPC 注册了 "rpc-message" handler
|
|
134
|
+
const electrobunPacket = {
|
|
135
|
+
type: "message",
|
|
136
|
+
id: "rpc-message",
|
|
137
|
+
payload: JSON.stringify(message),
|
|
138
|
+
};
|
|
139
|
+
bridge.postMessage(JSON.stringify(electrobunPacket));
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getTransport(): "ipc" | "websocket" {
|
|
145
|
+
return this._transport;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Web 端 HTTP 基础 URL(如 http://localhost:3100),桌面端返回 null */
|
|
149
|
+
getBaseUrl(): string | null {
|
|
150
|
+
return this._baseUrl;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 获取当前 auth token(用于需要直接调 HTTP 的场景) */
|
|
154
|
+
getAuthToken(): string {
|
|
155
|
+
return AUTH_TOKEN;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async call<K extends keyof RPCMethods>(
|
|
159
|
+
method: K,
|
|
160
|
+
params: MethodParams<RPCMethods, K>
|
|
161
|
+
): Promise<MethodResult<RPCMethods, K>> {
|
|
162
|
+
const methodStr = method as string;
|
|
163
|
+
const ttl = CACHEABLE_METHODS[methodStr];
|
|
164
|
+
if (ttl !== undefined) {
|
|
165
|
+
const cached = rpcCache.get<MethodResult<RPCMethods, K>>(methodStr, params, ttl);
|
|
166
|
+
if (cached !== null) return cached;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await this.initialize();
|
|
170
|
+
const result = await this.client!.call(method, params);
|
|
171
|
+
|
|
172
|
+
if (ttl !== undefined) {
|
|
173
|
+
rpcCache.set(methodStr, params, result);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async subscribe<K extends keyof RPCEvents>(
|
|
180
|
+
eventType: K,
|
|
181
|
+
handler: (payload: EventPayload<RPCEvents[K]>, metadata: EventMetadata<RPCEvents[K]>) => void,
|
|
182
|
+
filter?: Record<string, unknown>
|
|
183
|
+
): Promise<string> {
|
|
184
|
+
await this.initialize();
|
|
185
|
+
return this.client!.subscribe(eventType, handler, filter);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
unsubscribe(subscriptionId: string): void {
|
|
189
|
+
this.client?.unsubscribe(subscriptionId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
isConnected(): boolean {
|
|
193
|
+
return this.client !== null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
close(): void {
|
|
197
|
+
this.client?.close();
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
export const apiClient = new APIClientImpl();
|
|
@@ -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
|
+
console.warn(`[config] Invalid ${key}: "${value}", using default: ${defaultValue}`);
|
|
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,59 @@ 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
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.log("\n" + formatRegistryForOutput() + "\n");
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
start().catch((err) => {
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
log.error("Server failed to start", { error: err.message });
|
|
77
|
+
process.exit(1);
|
|
89
78
|
});
|