@yuhere/js-jsonrpc2 2.1.12 → 2.1.14

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 (181) hide show
  1. package/dist/cli/cli.d.ts +0 -1
  2. package/dist/cli/cli.js +0 -1
  3. package/dist/cli/demo-methods.d.ts +0 -1
  4. package/dist/cli/demo-methods.js +0 -1
  5. package/dist/cli/express.d.ts +0 -1
  6. package/dist/cli/express.js +0 -1
  7. package/dist/cli/stdio.d.ts +0 -1
  8. package/dist/cli/stdio.js +0 -1
  9. package/dist/core/Repository.d.ts +0 -1
  10. package/dist/core/Repository.js +0 -1
  11. package/dist/core/convert.d.ts +0 -1
  12. package/dist/core/convert.js +0 -1
  13. package/dist/core/descriptor.d.ts +0 -1
  14. package/dist/core/descriptor.js +0 -1
  15. package/dist/core/index.d.ts +0 -1
  16. package/dist/core/index.js +0 -1
  17. package/dist/core/llm-tools.d.ts +0 -1
  18. package/dist/core/llm-tools.js +41 -43
  19. package/dist/core/permission.d.ts +0 -1
  20. package/dist/core/permission.js +0 -1
  21. package/dist/core/spec-check.d.ts +0 -1
  22. package/dist/core/spec-check.js +0 -1
  23. package/dist/core/system-methods.d.ts +0 -1
  24. package/dist/core/system-methods.js +0 -1
  25. package/dist/errors/RPCError.d.ts +0 -1
  26. package/dist/errors/RPCError.js +0 -1
  27. package/dist/errors/codes.d.ts +0 -1
  28. package/dist/errors/codes.js +0 -1
  29. package/dist/errors/index.d.ts +0 -1
  30. package/dist/errors/index.js +0 -1
  31. package/dist/index.d.ts +0 -1
  32. package/dist/index.js +0 -1
  33. package/dist/metrics/memory.d.ts +0 -1
  34. package/dist/metrics/memory.js +0 -1
  35. package/dist/metrics/prom.d.ts +0 -1
  36. package/dist/metrics/prom.js +0 -1
  37. package/dist/schema/json/index.d.ts +1 -2
  38. package/dist/schema/json/index.js +0 -1
  39. package/dist/schema/json/marker.d.ts +0 -1
  40. package/dist/schema/json/marker.js +0 -1
  41. package/dist/schema/json/primitives.d.ts +49 -70
  42. package/dist/schema/json/primitives.js +70 -77
  43. package/dist/schema/z/protocol.d.ts +1 -2
  44. package/dist/schema/z/protocol.js +0 -1
  45. package/dist/schema/z/register.d.ts +0 -1
  46. package/dist/schema/z/register.js +0 -1
  47. package/dist/stream/stringify-stream.d.ts +0 -1
  48. package/dist/stream/stringify-stream.js +94 -70
  49. package/dist/transport/express/body.d.ts +0 -1
  50. package/dist/transport/express/body.js +0 -1
  51. package/dist/transport/express/index.d.ts +0 -1
  52. package/dist/transport/express/index.js +0 -1
  53. package/dist/transport/express/middleware.d.ts +0 -1
  54. package/dist/transport/express/middleware.js +0 -1
  55. package/dist/transport/express/respond.d.ts +0 -1
  56. package/dist/transport/express/respond.js +0 -1
  57. package/dist/transport/stdio/index.d.ts +0 -1
  58. package/dist/transport/stdio/index.js +0 -1
  59. package/dist/transport/stdio/middleware.d.ts +0 -1
  60. package/dist/transport/stdio/middleware.js +0 -1
  61. package/dist/transport/ws/index.d.ts +0 -1
  62. package/dist/transport/ws/index.js +0 -1
  63. package/dist/transport/ws/middleware.d.ts +0 -1
  64. package/dist/transport/ws/middleware.js +0 -1
  65. package/dist/transport/ws/standalone.d.ts +0 -1
  66. package/dist/transport/ws/standalone.js +0 -1
  67. package/dist/types.d.ts +0 -1
  68. package/dist/types.js +0 -1
  69. package/dist/webclient/webui/src/client/httpTransport.d.ts +8 -0
  70. package/dist/webclient/webui/src/client/httpTransport.js +114 -0
  71. package/dist/webclient/webui/src/client/index.d.ts +4 -0
  72. package/dist/webclient/webui/src/client/index.js +2 -0
  73. package/dist/webclient/webui/src/client/rpc.d.ts +24 -0
  74. package/dist/webclient/webui/src/client/rpc.js +32 -0
  75. package/dist/webclient/webui/src/client/types.d.ts +14 -0
  76. package/dist/webclient/webui/src/client/types.js +1 -0
  77. package/dist/webclient/webui/src/client/wsTransport.d.ts +8 -0
  78. package/{webui/src/api/wsTransport.ts → dist/webclient/webui/src/client/wsTransport.js} +263 -305
  79. package/htdoc/assets/favicon-CXdFuLlv.png +0 -0
  80. package/{webui/src/styles/geek.css → htdoc/assets/index-Rq1yIwo2.css} +58 -0
  81. package/htdoc/assets/{index-Cr0Jnrgb.js → index-mYqtDOwe.js} +1344 -398
  82. package/htdoc/assets/index-mYqtDOwe.js.map +1 -0
  83. package/htdoc/index.html +4 -2
  84. package/package.json +10 -7
  85. package/dist/cli/cli.d.ts.map +0 -1
  86. package/dist/cli/cli.js.map +0 -1
  87. package/dist/cli/demo-methods.d.ts.map +0 -1
  88. package/dist/cli/demo-methods.js.map +0 -1
  89. package/dist/cli/express.d.ts.map +0 -1
  90. package/dist/cli/express.js.map +0 -1
  91. package/dist/cli/stdio.d.ts.map +0 -1
  92. package/dist/cli/stdio.js.map +0 -1
  93. package/dist/core/Repository.d.ts.map +0 -1
  94. package/dist/core/Repository.js.map +0 -1
  95. package/dist/core/convert.d.ts.map +0 -1
  96. package/dist/core/convert.js.map +0 -1
  97. package/dist/core/descriptor.d.ts.map +0 -1
  98. package/dist/core/descriptor.js.map +0 -1
  99. package/dist/core/index.d.ts.map +0 -1
  100. package/dist/core/index.js.map +0 -1
  101. package/dist/core/llm-tools.d.ts.map +0 -1
  102. package/dist/core/llm-tools.js.map +0 -1
  103. package/dist/core/permission.d.ts.map +0 -1
  104. package/dist/core/permission.js.map +0 -1
  105. package/dist/core/spec-check.d.ts.map +0 -1
  106. package/dist/core/spec-check.js.map +0 -1
  107. package/dist/core/system-methods.d.ts.map +0 -1
  108. package/dist/core/system-methods.js.map +0 -1
  109. package/dist/errors/RPCError.d.ts.map +0 -1
  110. package/dist/errors/RPCError.js.map +0 -1
  111. package/dist/errors/codes.d.ts.map +0 -1
  112. package/dist/errors/codes.js.map +0 -1
  113. package/dist/errors/index.d.ts.map +0 -1
  114. package/dist/errors/index.js.map +0 -1
  115. package/dist/index.d.ts.map +0 -1
  116. package/dist/index.js.map +0 -1
  117. package/dist/metrics/memory.d.ts.map +0 -1
  118. package/dist/metrics/memory.js.map +0 -1
  119. package/dist/metrics/prom.d.ts.map +0 -1
  120. package/dist/metrics/prom.js.map +0 -1
  121. package/dist/schema/json/index.d.ts.map +0 -1
  122. package/dist/schema/json/index.js.map +0 -1
  123. package/dist/schema/json/marker.d.ts.map +0 -1
  124. package/dist/schema/json/marker.js.map +0 -1
  125. package/dist/schema/json/primitives.d.ts.map +0 -1
  126. package/dist/schema/json/primitives.js.map +0 -1
  127. package/dist/schema/z/protocol.d.ts.map +0 -1
  128. package/dist/schema/z/protocol.js.map +0 -1
  129. package/dist/schema/z/register.d.ts.map +0 -1
  130. package/dist/schema/z/register.js.map +0 -1
  131. package/dist/stream/stringify-stream.d.ts.map +0 -1
  132. package/dist/stream/stringify-stream.js.map +0 -1
  133. package/dist/transport/express/body.d.ts.map +0 -1
  134. package/dist/transport/express/body.js.map +0 -1
  135. package/dist/transport/express/index.d.ts.map +0 -1
  136. package/dist/transport/express/index.js.map +0 -1
  137. package/dist/transport/express/middleware.d.ts.map +0 -1
  138. package/dist/transport/express/middleware.js.map +0 -1
  139. package/dist/transport/express/respond.d.ts.map +0 -1
  140. package/dist/transport/express/respond.js.map +0 -1
  141. package/dist/transport/stdio/index.d.ts.map +0 -1
  142. package/dist/transport/stdio/index.js.map +0 -1
  143. package/dist/transport/stdio/middleware.d.ts.map +0 -1
  144. package/dist/transport/stdio/middleware.js.map +0 -1
  145. package/dist/transport/ws/index.d.ts.map +0 -1
  146. package/dist/transport/ws/index.js.map +0 -1
  147. package/dist/transport/ws/middleware.d.ts.map +0 -1
  148. package/dist/transport/ws/middleware.js.map +0 -1
  149. package/dist/transport/ws/standalone.d.ts.map +0 -1
  150. package/dist/transport/ws/standalone.js.map +0 -1
  151. package/dist/types.d.ts.map +0 -1
  152. package/dist/types.js.map +0 -1
  153. package/htdoc/assets/index-Ck-39Igh.css +0 -963
  154. package/htdoc/assets/index-Cr0Jnrgb.js.map +0 -1
  155. package/webui/index.html +0 -12
  156. package/webui/src/App.tsx +0 -164
  157. package/webui/src/api/docGen.ts +0 -188
  158. package/webui/src/api/glob.ts +0 -90
  159. package/webui/src/api/httpTransport.ts +0 -148
  160. package/webui/src/api/metrics.ts +0 -135
  161. package/webui/src/api/paramForm.ts +0 -80
  162. package/webui/src/api/rpc.ts +0 -57
  163. package/webui/src/api/schema.ts +0 -12
  164. package/webui/src/api/types.ts +0 -16
  165. package/webui/src/components/MethodDetail.tsx +0 -43
  166. package/webui/src/components/MethodTree.tsx +0 -118
  167. package/webui/src/components/MetricsPanel.md +0 -126
  168. package/webui/src/components/MetricsPanel.tsx +0 -290
  169. package/webui/src/components/SchemaForm.tsx +0 -697
  170. package/webui/src/components/SchemaView.tsx +0 -151
  171. package/webui/src/components/TestPanel.tsx +0 -322
  172. package/webui/src/components/Toolbar.tsx +0 -82
  173. package/webui/src/hooks/useMethodDetail.ts +0 -61
  174. package/webui/src/hooks/useMethods.ts +0 -27
  175. package/webui/src/hooks/useMetrics.ts +0 -91
  176. package/webui/src/main.tsx +0 -13
  177. package/webui/src/theme/ThemeProvider.tsx +0 -79
  178. package/webui/src/vite-env.d.ts +0 -1
  179. package/webui/tsconfig.json +0 -34
  180. package/webui/tsconfig.node.json +0 -13
  181. package/webui/vite.config.ts +0 -15
@@ -1,305 +1,263 @@
1
- import type { RpcResponse } from './types';
2
- import {
3
- RPCError,
4
- isStreamResult,
5
- type CallResult,
6
- type RpcClient,
7
- } from './rpc';
8
-
9
- export interface WsRpcOptions {
10
- /** 默认:根据当前页面 URL 推导(http->ws / https->wss,同路径) */
11
- endpoint?: string;
12
- connectTimeoutMs?: number;
13
- callTimeoutMs?: number;
14
- }
15
-
16
- interface Pending {
17
- resolve: (v: unknown) => void;
18
- reject: (e: unknown) => void;
19
- timer: ReturnType<typeof setTimeout>;
20
- }
21
-
22
- interface StreamSub {
23
- push: (v: unknown) => void;
24
- finish: () => void;
25
- fail: (e: unknown) => void;
26
- }
27
-
28
- interface StreamFrame {
29
- jsonrpc: '2.0';
30
- id: number | string;
31
- done: boolean;
32
- index?: number;
33
- value?: unknown;
34
- error?: { code: number; message: string };
35
- }
36
-
37
- /** 首帧等待器:resolve 到原始消息对象,由 call 决定走哪条路。 */
38
- type FirstWaiter = (msg: RpcResponse | StreamFrame) => void;
39
-
40
- let idCounter = 0;
41
-
42
- function deriveWsEndpoint(): string {
43
- const loc = window.location;
44
- const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
45
- return `${proto}//${loc.host}${loc.pathname}`;
46
- }
47
-
48
- function isStreamFrame(v: unknown): v is StreamFrame {
49
- return !!v && typeof v === 'object' && typeof (v as StreamFrame).done === 'boolean';
50
- }
51
-
52
- export function createWsRpc(options: WsRpcOptions = {}): RpcClient {
53
- const {
54
- endpoint = deriveWsEndpoint(),
55
- connectTimeoutMs = 5000,
56
- callTimeoutMs = 15000,
57
- } = options;
58
-
59
- const pending = new Map<number | string, Pending>();
60
- const streams = new Map<number | string, StreamSub>();
61
- const firstWaiters = new Map<number | string, FirstWaiter>();
62
- let socket: WebSocket | null = null;
63
- let openPromise: Promise<void> | null = null;
64
- let closedByUser = false;
65
-
66
- function rejectAll(reason: string) {
67
- for (const [, p] of pending) {
68
- clearTimeout(p.timer);
69
- p.reject(new Error(reason));
70
- }
71
- pending.clear();
72
- for (const [, sub] of streams) {
73
- sub.fail(new Error(reason));
74
- }
75
- streams.clear();
76
- for (const [, w] of firstWaiters) {
77
- // 首帧没等到连接就关了:用一个假的 failure 消息唤醒,让它走普通失败分支
78
- w({
79
- jsonrpc: '2.0',
80
- id: 0,
81
- error: { code: -32603, message: reason },
82
- } as RpcResponse);
83
- }
84
- firstWaiters.clear();
85
- }
86
-
87
- function attach(ws: WebSocket) {
88
- ws.addEventListener('message', (ev) => {
89
- let data: RpcResponse | RpcResponse[] | StreamFrame | StreamFrame[] | null = null;
90
- try {
91
- data = JSON.parse(ev.data as string);
92
- } catch {
93
- return;
94
- }
95
- const list = Array.isArray(data) ? data : [data];
96
- for (const msg of list) {
97
- if (msg.id == null) continue;
98
-
99
- // 1) 首帧等待者
100
- const fw = firstWaiters.get(msg.id);
101
- if (fw) {
102
- firstWaiters.delete(msg.id);
103
- fw(msg as RpcResponse | StreamFrame);
104
- continue;
105
- }
106
-
107
- // 2) 流帧
108
- if (isStreamFrame(msg)) {
109
- const sub = streams.get(msg.id);
110
- if (!sub) continue;
111
- if (msg.error) {
112
- streams.delete(msg.id);
113
- sub.fail(new RPCError(msg.error));
114
- continue;
115
- }
116
- if (msg.done) {
117
- streams.delete(msg.id);
118
- sub.finish();
119
- continue;
120
- }
121
- if (msg.value !== undefined) sub.push(msg.value);
122
- continue;
123
- }
124
-
125
- // 3) 普通响应
126
- const p = pending.get(msg.id);
127
- if (!p) continue;
128
- pending.delete(msg.id);
129
- clearTimeout(p.timer);
130
- const plain = msg as RpcResponse;
131
- if (plain.error) p.reject(new RPCError(plain.error));
132
- else p.resolve(plain.result);
133
- }
134
- });
135
-
136
- ws.addEventListener('close', () => {
137
- if (!closedByUser) rejectAll('WebSocket 已关闭');
138
- socket = null;
139
- openPromise = null;
140
- });
141
- }
142
-
143
- function ensureOpen(): Promise<void> {
144
- if (socket && socket.readyState === WebSocket.OPEN) return Promise.resolve();
145
- if (openPromise) return openPromise;
146
-
147
- openPromise = new Promise<void>((resolve, reject) => {
148
- let settled = false;
149
- const ws = new WebSocket(endpoint);
150
- const timer = setTimeout(() => {
151
- if (settled) return;
152
- settled = true;
153
- try { ws.close(); } catch { /* ignore */ }
154
- reject(new Error(`WebSocket 连接超时(${connectTimeoutMs}ms)`));
155
- }, connectTimeoutMs);
156
-
157
- attach(ws);
158
-
159
- ws.addEventListener('open', () => {
160
- if (settled) return;
161
- settled = true;
162
- clearTimeout(timer);
163
- socket = ws;
164
- resolve();
165
- }, { once: true });
166
-
167
- ws.addEventListener('close', () => {
168
- if (settled) return;
169
- settled = true;
170
- clearTimeout(timer);
171
- reject(new Error('WebSocket 连接被拒绝'));
172
- }, { once: true });
173
-
174
- ws.addEventListener('error', () => {
175
- if (settled) return;
176
- settled = true;
177
- clearTimeout(timer);
178
- reject(new Error('WebSocket 连接失败'));
179
- }, { once: true });
180
- }).finally(() => {
181
- if (!socket) openPromise = null;
182
- });
183
-
184
- return openPromise;
185
- }
186
-
187
- function call<T = unknown>(method: string, ...params: unknown[]): CallResult<T> {
188
- return (async () => {
189
- await ensureOpen();
190
- const ws = socket;
191
- if (!ws || ws.readyState !== WebSocket.OPEN) {
192
- throw new Error('WebSocket 未连接');
193
- }
194
-
195
- const id = ++idCounter;
196
- const payload = { jsonrpc: '2.0', method, params, id };
197
-
198
- // 等第一帧
199
- const first = new Promise<RpcResponse | StreamFrame>((resolve, reject) => {
200
- const timer = setTimeout(() => {
201
- firstWaiters.delete(id);
202
- reject(new Error(`调用 ${method} 超时(${callTimeoutMs}ms)`));
203
- }, callTimeoutMs);
204
-
205
- firstWaiters.set(id, (msg) => {
206
- clearTimeout(timer);
207
- resolve(msg);
208
- });
209
- });
210
-
211
- try {
212
- ws.send(JSON.stringify(payload));
213
- } catch (e) {
214
- firstWaiters.delete(id);
215
- throw e;
216
- }
217
-
218
- let firstMsg: RpcResponse | StreamFrame;
219
- try {
220
- firstMsg = await first;
221
- } catch (e) {
222
- firstWaiters.delete(id);
223
- throw e;
224
- }
225
-
226
- // 流式
227
- if (isStreamFrame(firstMsg)) {
228
- // 桥接到 AsyncGenerator
229
- const queue: T[] = [];
230
- let finished = false;
231
- let failure: unknown = null;
232
- let waiter: (() => void) | null = null;
233
- const wake = () => {
234
- const w = waiter;
235
- waiter = null;
236
- w?.();
237
- };
238
-
239
- // 先处理首帧
240
- if (firstMsg.error) {
241
- throw new RPCError(firstMsg.error);
242
- }
243
- if (firstMsg.done) {
244
- // 空流:直接返回空 generator
245
- return (async function* (): AsyncGenerator<T, void, unknown> {})();
246
- }
247
- if (firstMsg.value !== undefined) queue.push(firstMsg.value as T);
248
-
249
- const sub: StreamSub = {
250
- push: (v) => {
251
- queue.push(v as T);
252
- wake();
253
- },
254
- finish: () => {
255
- finished = true;
256
- wake();
257
- },
258
- fail: (e) => {
259
- failure = e;
260
- finished = true;
261
- wake();
262
- },
263
- };
264
- streams.set(id, sub);
265
-
266
- return (async function* (): AsyncGenerator<T, void, unknown> {
267
- try {
268
- while (true) {
269
- if (failure) throw failure;
270
- if (queue.length > 0) {
271
- yield queue.shift() as T;
272
- continue;
273
- }
274
- if (finished) return;
275
- await new Promise<void>((resolve) => {
276
- waiter = resolve;
277
- });
278
- }
279
- } finally {
280
- streams.delete(id);
281
- }
282
- })();
283
- }
284
-
285
- // 普通
286
- if (firstMsg.error) {
287
- throw new RPCError(firstMsg.error);
288
- }
289
- return (firstMsg as RpcResponse).result as T;
290
- })();
291
- }
292
-
293
- return {
294
- call,
295
- close() {
296
- closedByUser = true;
297
- rejectAll('客户端已关闭');
298
- if (socket) {
299
- try { socket.close(1000, 'client closed'); } catch { /* ignore */ }
300
- }
301
- socket = null;
302
- openPromise = null;
303
- },
304
- };
305
- }
1
+ import { RPCError, } from './rpc.js';
2
+ let idCounter = 0;
3
+ function deriveWsEndpoint() {
4
+ const loc = window.location;
5
+ const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
6
+ return `${proto}//${loc.host}${loc.pathname}`;
7
+ }
8
+ function isStreamFrame(v) {
9
+ return !!v && typeof v === 'object' && typeof v.done === 'boolean';
10
+ }
11
+ export function createWsRPC(options = {}) {
12
+ const { endpoint = deriveWsEndpoint(), connectTimeoutMs = 5000, callTimeoutMs = 15000, } = options;
13
+ const pending = new Map();
14
+ const streams = new Map();
15
+ const firstWaiters = new Map();
16
+ let socket = null;
17
+ let openPromise = null;
18
+ let closedByUser = false;
19
+ function rejectAll(reason) {
20
+ for (const [, p] of pending) {
21
+ clearTimeout(p.timer);
22
+ p.reject(new Error(reason));
23
+ }
24
+ pending.clear();
25
+ for (const [, sub] of streams) {
26
+ sub.fail(new Error(reason));
27
+ }
28
+ streams.clear();
29
+ for (const [, w] of firstWaiters) {
30
+ // 首帧没等到连接就关了:用一个假的 failure 消息唤醒,让它走普通失败分支
31
+ w({
32
+ jsonrpc: '2.0',
33
+ id: 0,
34
+ error: { code: -32603, message: reason },
35
+ });
36
+ }
37
+ firstWaiters.clear();
38
+ }
39
+ function attach(ws) {
40
+ ws.addEventListener('message', (ev) => {
41
+ let data = null;
42
+ try {
43
+ data = JSON.parse(ev.data);
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ const list = Array.isArray(data) ? data : [data];
49
+ for (const msg of list) {
50
+ if (!msg || typeof msg !== 'object')
51
+ continue;
52
+ if (msg.id == null)
53
+ continue;
54
+ // 1) 首帧等待者
55
+ const fw = firstWaiters.get(msg.id);
56
+ if (fw) {
57
+ firstWaiters.delete(msg.id);
58
+ fw(msg);
59
+ continue;
60
+ }
61
+ // 2) 流帧
62
+ if (isStreamFrame(msg)) {
63
+ const sub = streams.get(msg.id);
64
+ if (!sub)
65
+ continue;
66
+ if (msg.error) {
67
+ streams.delete(msg.id);
68
+ sub.fail(new RPCError(msg.error));
69
+ continue;
70
+ }
71
+ if (msg.done) {
72
+ streams.delete(msg.id);
73
+ sub.finish();
74
+ continue;
75
+ }
76
+ if (msg.value !== undefined)
77
+ sub.push(msg.value);
78
+ continue;
79
+ }
80
+ // 3) 普通响应
81
+ const p = pending.get(msg.id);
82
+ if (!p)
83
+ continue;
84
+ pending.delete(msg.id);
85
+ clearTimeout(p.timer);
86
+ const plain = msg;
87
+ if (plain.error)
88
+ p.reject(new RPCError(plain.error));
89
+ else
90
+ p.resolve(plain.result);
91
+ }
92
+ });
93
+ ws.addEventListener('close', () => {
94
+ if (!closedByUser)
95
+ rejectAll('WebSocket 已关闭');
96
+ socket = null;
97
+ openPromise = null;
98
+ });
99
+ }
100
+ function ensureOpen() {
101
+ if (socket && socket.readyState === WebSocket.OPEN)
102
+ return Promise.resolve();
103
+ if (openPromise)
104
+ return openPromise;
105
+ openPromise = new Promise((resolve, reject) => {
106
+ let settled = false;
107
+ const ws = new WebSocket(endpoint);
108
+ const timer = setTimeout(() => {
109
+ if (settled)
110
+ return;
111
+ settled = true;
112
+ try {
113
+ ws.close();
114
+ }
115
+ catch { /* ignore */ }
116
+ reject(new Error(`WebSocket 连接超时(${connectTimeoutMs}ms)`));
117
+ }, connectTimeoutMs);
118
+ attach(ws);
119
+ ws.addEventListener('open', () => {
120
+ if (settled)
121
+ return;
122
+ settled = true;
123
+ clearTimeout(timer);
124
+ socket = ws;
125
+ resolve();
126
+ }, { once: true });
127
+ ws.addEventListener('close', () => {
128
+ if (settled)
129
+ return;
130
+ settled = true;
131
+ clearTimeout(timer);
132
+ reject(new Error('WebSocket 连接被拒绝'));
133
+ }, { once: true });
134
+ ws.addEventListener('error', () => {
135
+ if (settled)
136
+ return;
137
+ settled = true;
138
+ clearTimeout(timer);
139
+ reject(new Error('WebSocket 连接失败'));
140
+ }, { once: true });
141
+ }).finally(() => {
142
+ if (!socket)
143
+ openPromise = null;
144
+ });
145
+ return openPromise;
146
+ }
147
+ function call(method, ...params) {
148
+ return (async () => {
149
+ await ensureOpen();
150
+ const ws = socket;
151
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
152
+ throw new Error('WebSocket 未连接');
153
+ }
154
+ const id = ++idCounter;
155
+ const payload = { jsonrpc: '2.0', method, params, id };
156
+ // 等第一帧
157
+ const first = new Promise((resolve, reject) => {
158
+ const timer = setTimeout(() => {
159
+ firstWaiters.delete(id);
160
+ reject(new Error(`调用 ${method} 超时(${callTimeoutMs}ms)`));
161
+ }, callTimeoutMs);
162
+ firstWaiters.set(id, (msg) => {
163
+ clearTimeout(timer);
164
+ resolve(msg);
165
+ });
166
+ });
167
+ try {
168
+ ws.send(JSON.stringify(payload));
169
+ }
170
+ catch (e) {
171
+ firstWaiters.delete(id);
172
+ throw e;
173
+ }
174
+ let firstMsg;
175
+ try {
176
+ firstMsg = await first;
177
+ }
178
+ catch (e) {
179
+ firstWaiters.delete(id);
180
+ throw e;
181
+ }
182
+ // 流式
183
+ if (isStreamFrame(firstMsg)) {
184
+ // 桥接到 AsyncGenerator
185
+ const queue = [];
186
+ let finished = false;
187
+ let failure = null;
188
+ let waiter = null;
189
+ const wake = () => {
190
+ const w = waiter;
191
+ waiter = null;
192
+ w?.();
193
+ };
194
+ // 先处理首帧
195
+ if (firstMsg.error) {
196
+ throw new RPCError(firstMsg.error);
197
+ }
198
+ if (firstMsg.done) {
199
+ // 空流:直接返回空 generator
200
+ return (async function* () { })();
201
+ }
202
+ if (firstMsg.value !== undefined)
203
+ queue.push(firstMsg.value);
204
+ const sub = {
205
+ push: (v) => {
206
+ queue.push(v);
207
+ wake();
208
+ },
209
+ finish: () => {
210
+ finished = true;
211
+ wake();
212
+ },
213
+ fail: (e) => {
214
+ failure = e;
215
+ finished = true;
216
+ wake();
217
+ },
218
+ };
219
+ streams.set(id, sub);
220
+ return (async function* () {
221
+ try {
222
+ while (true) {
223
+ if (failure)
224
+ throw failure;
225
+ if (queue.length > 0) {
226
+ yield queue.shift();
227
+ continue;
228
+ }
229
+ if (finished)
230
+ return;
231
+ await new Promise((resolve) => {
232
+ waiter = resolve;
233
+ });
234
+ }
235
+ }
236
+ finally {
237
+ streams.delete(id);
238
+ }
239
+ })();
240
+ }
241
+ // 普通
242
+ if (firstMsg.error) {
243
+ throw new RPCError(firstMsg.error);
244
+ }
245
+ return firstMsg.result;
246
+ })();
247
+ }
248
+ return {
249
+ call,
250
+ close() {
251
+ closedByUser = true;
252
+ rejectAll('客户端已关闭');
253
+ if (socket) {
254
+ try {
255
+ socket.close(1000, 'client closed');
256
+ }
257
+ catch { /* ignore */ }
258
+ }
259
+ socket = null;
260
+ openPromise = null;
261
+ },
262
+ };
263
+ }
@@ -623,6 +623,13 @@ textarea:focus {
623
623
  white-space: nowrap;
624
624
  }
625
625
 
626
+ .toolbar .theme-select:first-of-type {
627
+ margin-left: auto;
628
+ }
629
+ .toolbar .theme-select + .theme-select {
630
+ margin-left: 0;
631
+ }
632
+
626
633
  .theme-select-label {
627
634
  color: var(--fg-dim);
628
635
  }
@@ -960,4 +967,55 @@ textarea:focus {
960
967
  font-size: 11px;
961
968
  margin: 4px 0 0;
962
969
  color: var(--accent);
970
+ }
971
+
972
+ /* ================================================================
973
+ * Footer
974
+ * ================================================================ */
975
+ .footer {
976
+ display: flex;
977
+ align-items: center;
978
+ justify-content: space-between;
979
+ /* 版本左、链接右 */
980
+ gap: 16px;
981
+ padding: 8px 16px;
982
+ border-top: 1px solid var(--border);
983
+ background: var(--bg-elev);
984
+ flex-shrink: 0;
985
+ }
986
+
987
+ .footer-version {
988
+ font-size: 12px;
989
+ color: var(--fg-dim);
990
+ letter-spacing: .3px;
991
+ font-variant-numeric: tabular-nums;
992
+ }
993
+
994
+ .footer-links {
995
+ display: flex;
996
+ align-items: center;
997
+ gap: 12px;
998
+ }
999
+
1000
+ /* footer 内的 npm / github 链接(保持之前那段) */
1001
+ .footer .npm-link,
1002
+ .footer .github-link {
1003
+ display: inline-flex;
1004
+ align-items: center;
1005
+ line-height: 0;
1006
+ color: inherit;
1007
+ text-decoration: none;
1008
+ opacity: .8;
1009
+ transition: opacity .15s, transform .15s;
1010
+ }
1011
+
1012
+ .footer .npm-link:hover,
1013
+ .footer .github-link:hover {
1014
+ opacity: 1;
1015
+ transform: translateY(-1px);
1016
+ }
1017
+
1018
+ .footer .npm-link svg,
1019
+ .footer .github-link svg {
1020
+ display: block;
963
1021
  }