@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.
Files changed (165) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +24 -1
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. 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 { TypedClient, MethodParams, MethodResult, EventPayload, EventMetadata } from "@dyyz1993/rpc-core";
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,183 @@ import { rpcCache, CACHEABLE_METHODS } from "./rpc-cache";
10
16
  * 3. 默认值(开发用)
11
17
  */
12
18
  function resolveAuthToken(): string {
13
- if (typeof window !== "undefined") {
14
- const fromQuery = new URLSearchParams(window.location.search).get("token");
15
- if (fromQuery) return fromQuery;
16
- const fromStorage = localStorage.getItem("rpc-auth-token");
17
- if (fromStorage) return fromStorage;
18
- }
19
- return "pi-agent-template-token";
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
- private client: TypedClient<RPCMethods, RPCEvents> | null = null;
26
- private initPromise: Promise<void> | null = null;
27
- private _transport: "ipc" | "websocket" = "websocket";
28
- private _baseUrl: string | null = null;
29
- private wsTransport: WebSocketTransport | null = null;
30
-
31
- /**
32
- * 桌面端同步初始化:通过 executeJavascript 接收 + __electrobunBunBridge 发送
33
- */
34
- initSyncForDesktop(): void {
35
- if (this.client) return;
36
-
37
- const ipcTransport = new IPCTransport();
38
- this._transport = "ipc";
39
- this._baseUrl = null; // 桌面端不走 HTTP
40
- this.client = createTypedClient<RPCMethods, RPCEvents>(ipcTransport);
41
- this.setupElectrobunBridge(ipcTransport);
42
- if (import.meta.env.DEV) {
43
- console.warn("[APIClient] Desktop (IPC) initialized synchronously");
44
- }
45
- }
46
-
47
- /**
48
- * 异步初始化:Web 端使用(连接 WebSocket)
49
- */
50
- async initialize(): Promise<void> {
51
- // 已连接且 transport 正常 → 直接返回
52
- if (this.client && (this._transport === "ipc" || this.wsTransport?.isConnected())) {
53
- return;
54
- }
55
-
56
- // client 存在但 WS 断开了 → 清理后重连
57
- if (this.client && this.wsTransport && !this.wsTransport.isConnected()) {
58
- this.wsTransport.close();
59
- this.wsTransport = null;
60
- this.client = null;
61
- this.initPromise = null;
62
- }
63
-
64
- if (this.initPromise) return this.initPromise;
65
-
66
- this.initPromise = (async () => {
67
- const env = this.detectEnvironment();
68
-
69
- if (env === "electrobun") {
70
- // 不应走到这里,桌面端应通过 initSyncForDesktop() 初始化
71
- this.initSyncForDesktop();
72
- } else {
73
- this._transport = "websocket";
74
- const wsUrl = this.getWebSocketUrl();
75
- this.wsTransport = new WebSocketTransport(wsUrl);
76
- await this.wsTransport.connect();
77
- this.client = createTypedClient<RPCMethods, RPCEvents>(this.wsTransport);
78
-
79
- const wsUrlObj = new URL(wsUrl);
80
- this._baseUrl = `http://${wsUrlObj.host}`;
81
- }
82
- })();
83
-
84
- return this.initPromise;
85
- }
86
-
87
- private detectEnvironment(): "electrobun" | "browser" {
88
- if (typeof window === "undefined") return "browser";
89
- if (window.__electrobunBunBridge) return "electrobun";
90
- return "browser";
91
- }
92
-
93
- private getWebSocketUrl(): string {
94
- if (typeof window === "undefined") return `ws://localhost:3100?token=${AUTH_TOKEN}`;
95
- const customUrl = (
96
- new URLSearchParams(window.location.search).get("ws") ||
97
- localStorage.getItem("rpc-websocket-url")
98
- );
99
- if (customUrl) return customUrl.includes("token=") ? customUrl : `${customUrl}?token=${AUTH_TOKEN}`;
100
- return `ws://${window.location.host}/ws?token=${AUTH_TOKEN}`;
101
- }
102
-
103
- /**
104
- * 桌面端 IPC 桥接:
105
- * - Bun → Browser: 通过 executeJavascript 调用 window.__piAgentIPC()
106
- * - BrowserBun: 通过 __electrobunBunBridge.postMessage 发送 Electrobun 消息格式
107
- */
108
- private setupElectrobunBridge(ipcTransport: IPCTransport): void {
109
- if (typeof window === "undefined") return;
110
-
111
- const win = window;
112
-
113
- // 1. 注册接收函数:Bun 通过 executeJavascript 调用此函数发送消息到 Browser
114
- win.__piAgentIPC = (msg: unknown) => {
115
- ipcTransport.simulateMessage(msg);
116
- };
117
-
118
- // 2. 覆写 send:将 RPC-core 消息包装成 Electrobun 消息格式,通过原生桥接发送
119
- const bridge = win.__electrobunBunBridge;
120
-
121
- if (bridge) {
122
- ipcTransport.send = async (message: unknown) => {
123
- // 包装成 Electrobun message packet,bun defineRPC 注册了 "rpc-message" handler
124
- const electrobunPacket = {
125
- type: "message",
126
- id: "rpc-message",
127
- payload: JSON.stringify(message),
128
- };
129
- bridge.postMessage(JSON.stringify(electrobunPacket));
130
- };
131
- }
132
- }
133
-
134
- getTransport(): "ipc" | "websocket" {
135
- return this._transport;
136
- }
137
-
138
- /** Web 端 HTTP 基础 URL(如 http://localhost:3100),桌面端返回 null */
139
- getBaseUrl(): string | null {
140
- return this._baseUrl;
141
- }
142
-
143
- /** 获取当前 auth token(用于需要直接调 HTTP 的场景) */
144
- getAuthToken(): string {
145
- return AUTH_TOKEN;
146
- }
147
-
148
- async call<K extends keyof RPCMethods>(
149
- method: K,
150
- params: MethodParams<RPCMethods, K>
151
- ): Promise<MethodResult<RPCMethods, K>> {
152
- const methodStr = method as string;
153
- const ttl = CACHEABLE_METHODS[methodStr];
154
- if (ttl !== undefined) {
155
- const cached = rpcCache.get<MethodResult<RPCMethods, K>>(methodStr, params, ttl);
156
- if (cached !== null) return cached;
157
- }
158
-
159
- await this.initialize();
160
- const result = await this.client!.call(method, params);
161
-
162
- if (ttl !== undefined) {
163
- rpcCache.set(methodStr, params, result);
164
- }
165
-
166
- return result;
167
- }
168
-
169
- async subscribe<K extends keyof RPCEvents>(
170
- eventType: K,
171
- handler: (payload: EventPayload<RPCEvents[K]>, metadata: EventMetadata<RPCEvents[K]>) => void,
172
- filter?: Record<string, unknown>
173
- ): Promise<string> {
174
- await this.initialize();
175
- return this.client!.subscribe(eventType, handler, filter);
176
- }
177
-
178
- unsubscribe(subscriptionId: string): void {
179
- this.client?.unsubscribe(subscriptionId);
180
- }
181
-
182
- isConnected(): boolean {
183
- return this.client !== null;
184
- }
185
-
186
- close(): void {
187
- this.client?.close();
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;
46
+ this.client = createTypedClient<RPCMethods, RPCEvents>(ipcTransport);
47
+ this.setupElectrobunBridge(ipcTransport);
48
+ }
49
+
50
+ /**
51
+ * 异步初始化:Web 端使用(连接 WebSocket)
52
+ */
53
+ async initialize(): Promise<void> {
54
+ // 已连接且 transport 正常 → 直接返回
55
+ if (this.client && (this._transport === "ipc" || this.wsTransport?.isConnected())) {
56
+ return;
57
+ }
58
+
59
+ // client 存在但 WS 断开了 → 清理后重连
60
+ if (this.client && this.wsTransport && !this.wsTransport.isConnected()) {
61
+ this.wsTransport.close();
62
+ this.wsTransport = null;
63
+ this.client = null;
64
+ this.initPromise = null;
65
+ }
66
+
67
+ if (this.initPromise) return this.initPromise;
68
+
69
+ this.initPromise = (async () => {
70
+ const env = this.detectEnvironment();
71
+
72
+ if (env === "electrobun") {
73
+ // 不应走到这里,桌面端应通过 initSyncForDesktop() 初始化
74
+ this.initSyncForDesktop();
75
+ } else {
76
+ this._transport = "websocket";
77
+ const wsUrl = this.getWebSocketUrl();
78
+ this.wsTransport = new WebSocketTransport({
79
+ url: wsUrl,
80
+ authToken: AUTH_TOKEN,
81
+ reconnect: true,
82
+ });
83
+ await this.wsTransport.connect();
84
+ this.client = createTypedClient<RPCMethods, RPCEvents>(this.wsTransport);
85
+
86
+ const wsUrlObj = new URL(wsUrl);
87
+ this._baseUrl = `http://${wsUrlObj.host}`;
88
+ }
89
+ })();
90
+
91
+ return this.initPromise;
92
+ }
93
+
94
+ private detectEnvironment(): "electrobun" | "browser" {
95
+ if (typeof window === "undefined") return "browser";
96
+ if (window.__electrobunBunBridge) return "electrobun";
97
+ return "browser";
98
+ }
99
+
100
+ private getWebSocketUrl(): string {
101
+ if (typeof window === "undefined") return `ws://localhost:3100?token=${AUTH_TOKEN}`;
102
+ const customUrl =
103
+ new URLSearchParams(window.location.search).get("ws") ||
104
+ localStorage.getItem("rpc-websocket-url");
105
+ if (customUrl)
106
+ return customUrl.includes("token=") ? customUrl : `${customUrl}?token=${AUTH_TOKEN}`;
107
+ return `ws://${window.location.host}/ws?token=${AUTH_TOKEN}`;
108
+ }
109
+
110
+ /**
111
+ * 桌面端 IPC 桥接:
112
+ * - BunBrowser: 通过 executeJavascript 调用 window.__piAgentIPC()
113
+ * - Browser → Bun: 通过 __electrobunBunBridge.postMessage 发送 Electrobun 消息格式
114
+ */
115
+ private setupElectrobunBridge(ipcTransport: IPCTransport): void {
116
+ if (typeof window === "undefined") return;
117
+
118
+ const win = window;
119
+
120
+ // 1. 注册接收函数:Bun 通过 executeJavascript 调用此函数发送消息到 Browser
121
+ win.__piAgentIPC = (msg: unknown) => {
122
+ ipcTransport.simulateMessage(msg);
123
+ };
124
+
125
+ // 2. 覆写 send:将 RPC-core 消息包装成 Electrobun 消息格式,通过原生桥接发送
126
+ const bridge = win.__electrobunBunBridge;
127
+
128
+ if (bridge) {
129
+ ipcTransport.send = async (message: unknown) => {
130
+ // 包装成 Electrobun message packet,bun 端 defineRPC 注册了 "rpc-message" handler
131
+ const electrobunPacket = {
132
+ type: "message",
133
+ id: "rpc-message",
134
+ payload: JSON.stringify(message),
135
+ };
136
+ bridge.postMessage(JSON.stringify(electrobunPacket));
137
+ };
138
+ }
139
+ }
140
+
141
+ getTransport(): "ipc" | "websocket" {
142
+ return this._transport;
143
+ }
144
+
145
+ /** Web HTTP 基础 URL(如 http://localhost:3100),桌面端返回 null */
146
+ getBaseUrl(): string | null {
147
+ return this._baseUrl;
148
+ }
149
+
150
+ /** 获取当前 auth token(用于需要直接调 HTTP 的场景) */
151
+ getAuthToken(): string {
152
+ return AUTH_TOKEN;
153
+ }
154
+
155
+ async call<K extends keyof RPCMethods>(
156
+ method: K,
157
+ params: MethodParams<RPCMethods, K>
158
+ ): Promise<MethodResult<RPCMethods, K>> {
159
+ const methodStr = method as string;
160
+ const ttl = CACHEABLE_METHODS[methodStr];
161
+ if (ttl !== undefined) {
162
+ const cached = rpcCache.get<MethodResult<RPCMethods, K>>(methodStr, params, ttl);
163
+ if (cached !== null) return cached;
164
+ }
165
+
166
+ await this.initialize();
167
+ const result = await this.client!.call(method, params);
168
+
169
+ if (ttl !== undefined) {
170
+ rpcCache.set(methodStr, params, result);
171
+ }
172
+
173
+ return result;
174
+ }
175
+
176
+ async subscribe<K extends keyof RPCEvents>(
177
+ eventType: K,
178
+ handler: (payload: EventPayload<RPCEvents[K]>, metadata: EventMetadata<RPCEvents[K]>) => void,
179
+ filter?: Record<string, unknown>
180
+ ): Promise<string> {
181
+ await this.initialize();
182
+ return this.client!.subscribe(eventType, handler, filter);
183
+ }
184
+
185
+ unsubscribe(subscriptionId: string): void {
186
+ this.client?.unsubscribe(subscriptionId);
187
+ }
188
+
189
+ isConnected(): boolean {
190
+ return this.client !== null;
191
+ }
192
+
193
+ close(): void {
194
+ this.client?.close();
195
+ }
189
196
  }
190
197
 
191
198
  export const apiClient = new APIClientImpl();
@@ -8,17 +8,11 @@ import App from "./App";
8
8
  const isElectrobun = typeof window !== "undefined" && !!window.__electrobunBunBridge;
9
9
 
10
10
  if (isElectrobun) {
11
- // 桌面端:同步初始化 IPC(Electrobun 要求桥接在页面加载时同步设置)
12
- apiClient.initSyncForDesktop();
13
- }
14
- // Web 端 WS 初始化由 App.tsx 统一管理,避免竞争
15
-
16
- if (import.meta.env.DEV) {
17
- console.warn("[Main] Application starting, isElectrobun:", isElectrobun);
11
+ apiClient.initSyncForDesktop();
18
12
  }
19
13
 
20
14
  createRoot(document.getElementById("root")!).render(
21
- <StrictMode>
22
- <App />
23
- </StrictMode>
15
+ <StrictMode>
16
+ <App />
17
+ </StrictMode>
24
18
  );
@@ -3,53 +3,53 @@ import { apiClient } from "../lib/api-client";
3
3
  import type { FeedCategory, FeedPost } from "../../shared/modules/feed";
4
4
 
5
5
  interface FeedState {
6
- posts: FeedPost[];
7
- author: string;
8
- category: FeedCategory;
9
- content: string;
10
- loading: boolean;
11
-
12
- setAuthor: (author: string) => void;
13
- setCategory: (category: FeedCategory) => void;
14
- setContent: (content: string) => void;
15
- createPost: () => Promise<void>;
16
- loadPosts: () => Promise<void>;
17
- addPost: (post: FeedPost) => void;
6
+ posts: FeedPost[];
7
+ author: string;
8
+ category: FeedCategory;
9
+ content: string;
10
+ loading: boolean;
11
+
12
+ setAuthor: (author: string) => void;
13
+ setCategory: (category: FeedCategory) => void;
14
+ setContent: (content: string) => void;
15
+ createPost: () => Promise<void>;
16
+ loadPosts: () => Promise<void>;
17
+ addPost: (post: FeedPost) => void;
18
18
  }
19
19
 
20
20
  export const useFeedStore = create<FeedState>((set, get) => ({
21
- posts: [],
22
- author: "alice",
23
- category: "tech",
24
- content: "",
25
- loading: false,
26
-
27
- setAuthor: (author) => set({ author }),
28
- setCategory: (category) => set({ category }),
29
- setContent: (content) => set({ content }),
30
-
31
- createPost: async () => {
32
- const { content, category, author } = get();
33
- if (!content.trim()) return;
34
- set({ loading: true });
35
- try {
36
- await apiClient.call("feed.post", { content: content.trim(), category, author });
37
- set({ content: "", loading: false });
38
- } catch {
39
- set({ loading: false });
40
- }
41
- },
42
-
43
- loadPosts: async () => {
44
- try {
45
- const res = await apiClient.call("feed.list", { limit: 100 });
46
- set({ posts: res.posts });
47
- } catch {
48
- // ignore
49
- }
50
- },
51
-
52
- addPost: (post) => set((s) => ({ posts: [...s.posts, post] })),
21
+ posts: [],
22
+ author: "alice",
23
+ category: "tech",
24
+ content: "",
25
+ loading: false,
26
+
27
+ setAuthor: (author) => set({ author }),
28
+ setCategory: (category) => set({ category }),
29
+ setContent: (content) => set({ content }),
30
+
31
+ createPost: async () => {
32
+ const { content, category, author } = get();
33
+ if (!content.trim()) return;
34
+ set({ loading: true });
35
+ try {
36
+ await apiClient.call("feed.post", { content: content.trim(), category, author });
37
+ set({ content: "", loading: false });
38
+ } catch {
39
+ set({ loading: false });
40
+ }
41
+ },
42
+
43
+ loadPosts: async () => {
44
+ try {
45
+ const res = await apiClient.call("feed.list", { limit: 100 });
46
+ set({ posts: res.posts });
47
+ } catch {
48
+ // ignore
49
+ }
50
+ },
51
+
52
+ addPost: (post) => set((s) => ({ posts: [...s.posts, post] })),
53
53
  }));
54
54
 
55
55
  // --- Event Stream (订阅面板) ---
@@ -57,73 +57,73 @@ export const useFeedStore = create<FeedState>((set, get) => ({
57
57
  export type SubEventType = "feed.update" | "chat.message" | "timer.tick";
58
58
 
59
59
  interface EventEntry {
60
- id: string;
61
- eventType: SubEventType;
62
- payload: unknown;
63
- filter: Record<string, unknown>;
64
- timestamp: number;
60
+ id: string;
61
+ eventType: SubEventType;
62
+ payload: unknown;
63
+ filter: Record<string, unknown>;
64
+ timestamp: number;
65
65
  }
66
66
 
67
67
  interface EventStreamState {
68
- entries: EventEntry[];
69
- activeEventType: SubEventType;
70
- activeFilter: string; // JSON string for the filter
71
- subscriptionId: string | null;
72
- subscribed: boolean;
73
-
74
- setActiveEventType: (t: SubEventType) => void;
75
- setActiveFilter: (f: string) => void;
76
- handleSubscribe: () => Promise<void>;
77
- handleUnsubscribe: () => void;
78
- addEntry: (eventType: SubEventType, payload: unknown, filter: Record<string, unknown>) => void;
68
+ entries: EventEntry[];
69
+ activeEventType: SubEventType;
70
+ activeFilter: string; // JSON string for the filter
71
+ subscriptionId: string | null;
72
+ subscribed: boolean;
73
+
74
+ setActiveEventType: (t: SubEventType) => void;
75
+ setActiveFilter: (f: string) => void;
76
+ handleSubscribe: () => Promise<void>;
77
+ handleUnsubscribe: () => void;
78
+ addEntry: (eventType: SubEventType, payload: unknown, filter: Record<string, unknown>) => void;
79
79
  }
80
80
 
81
81
  export const useEventStreamStore = create<EventStreamState>((set, get) => ({
82
- entries: [],
83
- activeEventType: "feed.update",
84
- activeFilter: "",
85
- subscriptionId: null,
86
- subscribed: false,
87
-
88
- setActiveEventType: (t) => set({ activeEventType: t }),
89
- setActiveFilter: (f) => set({ activeFilter: f }),
90
-
91
- handleSubscribe: async () => {
92
- const { activeEventType, activeFilter } = get();
93
- try {
94
- const filter = activeFilter.trim() ? JSON.parse(activeFilter) : {};
95
- const subId = await apiClient.subscribe(
96
- activeEventType,
97
- (payload: unknown) => {
98
- get().addEntry(activeEventType, payload, filter);
99
- },
100
- filter,
101
- );
102
- set({ subscriptionId: subId, subscribed: true });
103
- } catch (err) {
104
- console.error("Subscribe error:", err);
105
- }
106
- },
107
-
108
- handleUnsubscribe: () => {
109
- const { subscriptionId } = get();
110
- if (subscriptionId) {
111
- apiClient.unsubscribe(subscriptionId);
112
- }
113
- set({ subscriptionId: null, subscribed: false });
114
- },
115
-
116
- addEntry: (eventType, payload, filter) =>
117
- set((s) => ({
118
- entries: [
119
- ...s.entries.slice(-49),
120
- {
121
- id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
122
- eventType,
123
- payload,
124
- filter,
125
- timestamp: Date.now(),
126
- },
127
- ],
128
- })),
82
+ entries: [],
83
+ activeEventType: "feed.update",
84
+ activeFilter: "",
85
+ subscriptionId: null,
86
+ subscribed: false,
87
+
88
+ setActiveEventType: (t) => set({ activeEventType: t }),
89
+ setActiveFilter: (f) => set({ activeFilter: f }),
90
+
91
+ handleSubscribe: async () => {
92
+ const { activeEventType, activeFilter } = get();
93
+ try {
94
+ const filter = activeFilter.trim() ? JSON.parse(activeFilter) : {};
95
+ const subId = await apiClient.subscribe(
96
+ activeEventType,
97
+ (payload: unknown) => {
98
+ get().addEntry(activeEventType, payload, filter);
99
+ },
100
+ filter
101
+ );
102
+ set({ subscriptionId: subId, subscribed: true });
103
+ } catch {
104
+ // subscription failed — UI stays unsubscribed
105
+ }
106
+ },
107
+
108
+ handleUnsubscribe: () => {
109
+ const { subscriptionId } = get();
110
+ if (subscriptionId) {
111
+ apiClient.unsubscribe(subscriptionId);
112
+ }
113
+ set({ subscriptionId: null, subscribed: false });
114
+ },
115
+
116
+ addEntry: (eventType, payload, filter) =>
117
+ set((s) => ({
118
+ entries: [
119
+ ...s.entries.slice(-49),
120
+ {
121
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
122
+ eventType,
123
+ payload,
124
+ filter,
125
+ timestamp: Date.now(),
126
+ },
127
+ ],
128
+ })),
129
129
  }));