hypercore-cli 1.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/dist/api-XGC7D5AW.js +162 -0
  4. package/dist/auth-DNQWYQKT.js +21 -0
  5. package/dist/background-2EGCAAQH.js +14 -0
  6. package/dist/backlog-Q2NZCLNY.js +24 -0
  7. package/dist/chunk-2CMSCWQW.js +162 -0
  8. package/dist/chunk-2LJ2DVEB.js +167 -0
  9. package/dist/chunk-3RPFCQKJ.js +288 -0
  10. package/dist/chunk-43OLRXM5.js +263 -0
  11. package/dist/chunk-4DVYJAJL.js +57 -0
  12. package/dist/chunk-6OL3GA3P.js +173 -0
  13. package/dist/chunk-AUHU7ALH.js +2023 -0
  14. package/dist/chunk-B6A2AKLN.js +139 -0
  15. package/dist/chunk-BE46C7JW.js +46 -0
  16. package/dist/chunk-CUVAUOXL.js +58 -0
  17. package/dist/chunk-GH7E2OJE.js +223 -0
  18. package/dist/chunk-GOOTEPBK.js +271 -0
  19. package/dist/chunk-GPPMJYSM.js +133 -0
  20. package/dist/chunk-GU2FZQ6A.js +69 -0
  21. package/dist/chunk-IOPKN5GD.js +190 -0
  22. package/dist/chunk-IXOIOGR5.js +1505 -0
  23. package/dist/chunk-KRPOPWGA.js +251 -0
  24. package/dist/chunk-MGLJ53QN.js +219 -0
  25. package/dist/chunk-MV4TTRYX.js +533 -0
  26. package/dist/chunk-OPZYEVYR.js +150 -0
  27. package/dist/chunk-QTSLP47C.js +166 -0
  28. package/dist/chunk-R3GPQC7I.js +393 -0
  29. package/dist/chunk-RKB2JOV2.js +43 -0
  30. package/dist/chunk-RNG3K465.js +80 -0
  31. package/dist/chunk-TGTYKBGC.js +86 -0
  32. package/dist/chunk-U5SGAIMM.js +681 -0
  33. package/dist/chunk-V5UHPPSY.js +140 -0
  34. package/dist/chunk-WHLVZCQY.js +245 -0
  35. package/dist/chunk-XDRCBMZZ.js +66 -0
  36. package/dist/chunk-XOS6HPEF.js +134 -0
  37. package/dist/chunk-ZSBHUGWR.js +262 -0
  38. package/dist/claude-NSQ442XD.js +12 -0
  39. package/dist/commands-CK3WFAGI.js +128 -0
  40. package/dist/commands-U63OEO5J.js +1044 -0
  41. package/dist/commands-ZE6GD3WC.js +232 -0
  42. package/dist/config-4EW42BSF.js +8 -0
  43. package/dist/config-loader-SXO674TF.js +24 -0
  44. package/dist/diagnose-AFW3ZTZ4.js +12 -0
  45. package/dist/display-IIUBEYWN.js +58 -0
  46. package/dist/extractor-QV53W2YJ.js +129 -0
  47. package/dist/history-WMSCHERZ.js +180 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +406 -0
  50. package/dist/instance-registry-YSIJXSO7.js +15 -0
  51. package/dist/keybindings-JAAMLH3G.js +15 -0
  52. package/dist/loader-WHNTZTLP.js +58 -0
  53. package/dist/network-MM6YWPGO.js +279 -0
  54. package/dist/notify-HPTALZDC.js +14 -0
  55. package/dist/openai-compat-UQWJXBEK.js +12 -0
  56. package/dist/permissions-JUKXMNDH.js +10 -0
  57. package/dist/prompt-QV45TXRL.js +166 -0
  58. package/dist/quality-ST7PPNFR.js +16 -0
  59. package/dist/repl-RT3AHL7M.js +3375 -0
  60. package/dist/roadmap-5OBEKROY.js +17 -0
  61. package/dist/server-PORT7OEG.js +57 -0
  62. package/dist/session-4VUNDWLH.js +21 -0
  63. package/dist/skills-V4A35XKG.js +175 -0
  64. package/dist/store-Y4LU5QTO.js +25 -0
  65. package/dist/team-HO7Z4SIM.js +385 -0
  66. package/dist/telemetry-6R4EIE6O.js +30 -0
  67. package/dist/test-runner-ZQH5Y6OJ.js +619 -0
  68. package/dist/theme-3SYJ3UQA.js +14 -0
  69. package/dist/upgrade-7TGI3SXO.js +83 -0
  70. package/dist/verify-JUDKTPKZ.js +14 -0
  71. package/dist/web/static/app.js +562 -0
  72. package/dist/web/static/index.html +132 -0
  73. package/dist/web/static/mirror.css +1001 -0
  74. package/dist/web/static/mirror.html +184 -0
  75. package/dist/web/static/mirror.js +1125 -0
  76. package/dist/web/static/onboard.css +302 -0
  77. package/dist/web/static/onboard.html +140 -0
  78. package/dist/web/static/onboard.js +260 -0
  79. package/dist/web/static/style.css +602 -0
  80. package/dist/web/static/workspace.css +1568 -0
  81. package/dist/web/static/workspace.html +408 -0
  82. package/dist/web/static/workspace.js +1683 -0
  83. package/dist/web-Z5HSCQHW.js +39 -0
  84. package/package.json +67 -0
@@ -0,0 +1,1683 @@
1
+ /**
2
+ * Hypercore Workbench — 客户端逻辑
3
+ * WebSocket 驱动,与 CLI REPL 共享会话
4
+ */
5
+
6
+ class HypercoreWorkspace {
7
+ constructor() {
8
+ this.ws = null;
9
+ this.state = {};
10
+ this.isStreaming = false;
11
+ this.streamBuffer = '';
12
+ this.reconnectDelay = 1000;
13
+ this.maxReconnectDelay = 30000;
14
+ this.heartbeatInterval = null;
15
+
16
+ this.els = {
17
+ connStatus: document.getElementById('connStatus'),
18
+ modelBadge: document.getElementById('modelBadge'),
19
+ chatMessages: document.getElementById('chatMessages'),
20
+ chatInput: document.getElementById('chatInput'),
21
+ btnSend: document.getElementById('btnSend'),
22
+ sessionInfo: document.getElementById('sessionInfo'),
23
+ // Context panel
24
+ ctxSession: document.getElementById('ctxSession'),
25
+ ctxModel: document.getElementById('ctxModel'),
26
+ ctxProvider: document.getElementById('ctxProvider'),
27
+ ctxInputTokens: document.getElementById('ctxInputTokens'),
28
+ ctxOutputTokens: document.getElementById('ctxOutputTokens'),
29
+ ctxTotalTokens: document.getElementById('ctxTotalTokens'),
30
+ ctxTokenBar: document.getElementById('ctxTokenBar'),
31
+ ctxToolCount: document.getElementById('ctxToolCount'),
32
+ ctxCwd: document.getElementById('ctxCwd'),
33
+ ctxGitBranch: document.getElementById('ctxGitBranch'),
34
+ ctxGitStatus: document.getElementById('ctxGitStatus'),
35
+ // Footer
36
+ footerConn: document.getElementById('footerConn'),
37
+ footerSession: document.getElementById('footerSession'),
38
+ footerModel: document.getElementById('footerModel'),
39
+ footerTokens: document.getElementById('footerTokens'),
40
+ // Panels
41
+ fileList: document.getElementById('fileList'),
42
+ memoryStats: document.getElementById('memoryStats'),
43
+ memoryList: document.getElementById('memoryList'),
44
+ memorySearch: document.getElementById('memorySearch'),
45
+ toolList: document.getElementById('toolList'),
46
+ currentPath: document.getElementById('currentPath'),
47
+ // Modal
48
+ filePreviewModal: document.getElementById('filePreviewModal'),
49
+ previewFileName: document.getElementById('previewFileName'),
50
+ previewContent: document.getElementById('previewContent'),
51
+ };
52
+
53
+ this.init();
54
+ }
55
+
56
+ init() {
57
+ this.bindEvents();
58
+ this.connect();
59
+ this.restoreActiveTab();
60
+ this.initTeam();
61
+ this.loadWorkspaceProfile();
62
+ }
63
+
64
+ // ===== WebSocket =====
65
+
66
+ connect() {
67
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
68
+ const wsUrl = `${protocol}//${location.host}/ws/repl`;
69
+ console.log(`[HyperWS] Connecting to ${wsUrl} (attempt #${++this._connectAttempts || (this._connectAttempts = 1)})`);
70
+ this.setStatus('connecting');
71
+
72
+ try {
73
+ this.ws = new WebSocket(wsUrl);
74
+ } catch (e) {
75
+ console.error('[HyperWS] Failed to create WebSocket:', e);
76
+ this.fallbackHTTPState();
77
+ this.scheduleReconnect();
78
+ return;
79
+ }
80
+
81
+ // Connection timeout: if not open within 5s, force close and retry
82
+ const connectTimeout = setTimeout(() => {
83
+ if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
84
+ console.warn('[HyperWS] Connection timeout after 5s, retrying...');
85
+ this.ws.close();
86
+ }
87
+ }, 5000);
88
+
89
+ this.ws.onopen = () => {
90
+ clearTimeout(connectTimeout);
91
+ console.log('[HyperWS] ✅ WebSocket OPEN');
92
+ this.reconnectDelay = 1000;
93
+ this.startHeartbeat();
94
+ // Request fresh state after connection
95
+ this.send('state_request');
96
+ };
97
+
98
+ this.ws.onmessage = (event) => {
99
+ try {
100
+ const msg = JSON.parse(event.data);
101
+ console.log(`[HyperWS] ← ${msg.type}`, msg.payload ? Object.keys(msg.payload) : '');
102
+ this.handleMessage(msg);
103
+ } catch { /* ignore parse errors */ }
104
+ };
105
+
106
+ this.ws.onclose = (event) => {
107
+ clearTimeout(connectTimeout);
108
+ console.warn(`[HyperWS] WebSocket closed: code=${event.code} reason=${event.reason || 'none'} wasClean=${event.wasClean}`);
109
+ this.setStatus('disconnected');
110
+ this.stopHeartbeat();
111
+ this.scheduleReconnect();
112
+ };
113
+
114
+ this.ws.onerror = (e) => {
115
+ console.error('[HyperWS] WebSocket error event fired');
116
+ // onclose will fire after this — try HTTP fallback for state
117
+ this.fallbackHTTPState();
118
+ };
119
+ }
120
+
121
+ /** When WebSocket is down, try REST API to at least get state info */
122
+ async fallbackHTTPState() {
123
+ try {
124
+ const res = await fetch('/api/repl/state');
125
+ if (res.ok) {
126
+ const data = await res.json();
127
+ if (data && !data.error) {
128
+ this.updateState(data);
129
+ }
130
+ }
131
+ } catch { /* REST also unavailable */ }
132
+ // Also try loading side panels via REST
133
+ this.loadSidePanels();
134
+ }
135
+
136
+ scheduleReconnect() {
137
+ setTimeout(() => {
138
+ this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxReconnectDelay);
139
+ this.connect();
140
+ }, this.reconnectDelay);
141
+ }
142
+
143
+ send(type, payload = {}) {
144
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
145
+ this.ws.send(JSON.stringify({
146
+ type,
147
+ payload,
148
+ timestamp: new Date().toISOString(),
149
+ source: 'gui',
150
+ }));
151
+ }
152
+ }
153
+
154
+ startHeartbeat() {
155
+ this.stopHeartbeat();
156
+ this.heartbeatInterval = setInterval(() => {
157
+ this.send('heartbeat');
158
+ }, 30000);
159
+ }
160
+
161
+ stopHeartbeat() {
162
+ if (this.heartbeatInterval) {
163
+ clearInterval(this.heartbeatInterval);
164
+ this.heartbeatInterval = null;
165
+ }
166
+ }
167
+
168
+ // ===== Message Router =====
169
+
170
+ handleMessage(msg) {
171
+ switch (msg.type) {
172
+ case 'connected':
173
+ this.setStatus('connected');
174
+ if (msg.payload.state) {
175
+ this.updateState(msg.payload.state);
176
+ this.restoreHistory(msg.payload.state.chatHistory || []);
177
+ }
178
+ this.loadSidePanels();
179
+ break;
180
+
181
+ case 'session_sync':
182
+ case 'state_sync':
183
+ this.updateState(msg.payload);
184
+ break;
185
+
186
+ case 'user_input_cli':
187
+ this.renderUserMessage(msg.payload.content, 'cli');
188
+ break;
189
+
190
+ case 'assistant_text':
191
+ this.appendStreamText(msg.payload.content);
192
+ break;
193
+
194
+ case 'assistant_done':
195
+ this.finishStream(msg.payload);
196
+ break;
197
+
198
+ case 'tool_call':
199
+ this.renderToolCall(msg.payload);
200
+ break;
201
+
202
+ case 'tool_result':
203
+ this.renderToolResult(msg.payload);
204
+ break;
205
+
206
+ case 'command_output':
207
+ this.renderCommandOutput(msg.payload.output);
208
+ break;
209
+
210
+ case 'error':
211
+ this.renderError(msg.payload.message || 'Unknown error');
212
+ break;
213
+
214
+ case 'heartbeat':
215
+ // pong received
216
+ break;
217
+
218
+ default:
219
+ break;
220
+ }
221
+ }
222
+
223
+ // ===== State Management =====
224
+
225
+ updateState(state) {
226
+ this.state = { ...this.state, ...state };
227
+
228
+ const s = this.state;
229
+ this.els.ctxSession.textContent = s.sessionId ? s.sessionId.slice(0, 12) : '--';
230
+ this.els.ctxModel.textContent = s.model || '--';
231
+ this.els.ctxProvider.textContent = s.provider || '--';
232
+
233
+ const inTok = s.tokens?.inputTokens || s.tokens?.input || 0;
234
+ const outTok = s.tokens?.outputTokens || s.tokens?.output || 0;
235
+ const totalTok = inTok + outTok;
236
+ this.els.ctxInputTokens.textContent = this.formatNumber(inTok);
237
+ this.els.ctxOutputTokens.textContent = this.formatNumber(outTok);
238
+ this.els.ctxTotalTokens.textContent = this.formatNumber(totalTok);
239
+
240
+ // Token bar (assume 128K context window)
241
+ const maxTokens = 128000;
242
+ const pct = Math.min((totalTok / maxTokens) * 100, 100);
243
+ this.els.ctxTokenBar.style.width = pct + '%';
244
+
245
+ this.els.ctxToolCount.textContent = `${s.toolCount || 0} 个注册工具`;
246
+ this.els.ctxCwd.textContent = s.cwd || '--';
247
+
248
+ this.els.modelBadge.textContent = this.shortModel(s.model || '');
249
+ this.els.sessionInfo.querySelector('.wb-session-id').textContent =
250
+ s.sessionId ? s.sessionId.slice(0, 16) : '--';
251
+
252
+ // Footer
253
+ this.els.footerSession.textContent = s.sessionId ? s.sessionId.slice(0, 12) : '--';
254
+ this.els.footerModel.textContent = this.shortModel(s.model || '');
255
+ this.els.footerTokens.textContent = `${this.formatNumber(inTok)} / ${this.formatNumber(outTok)}`;
256
+ }
257
+
258
+ setStatus(status) {
259
+ const dot = this.els.connStatus.querySelector('.wb-dot');
260
+ dot.className = 'wb-dot';
261
+
262
+ if (status === 'connected') {
263
+ dot.classList.add('wb-dot-connected');
264
+ this.els.connStatus.lastChild.textContent = ' 已连接';
265
+ this.els.footerConn.textContent = '🟢 已连接';
266
+ } else if (status === 'connecting') {
267
+ dot.classList.add('wb-dot-connecting');
268
+ this.els.connStatus.lastChild.textContent = ' 连接中…';
269
+ this.els.footerConn.textContent = '🟡 连接中…';
270
+ } else {
271
+ dot.classList.add('wb-dot-disconnected');
272
+ this.els.connStatus.lastChild.textContent = ' 未连接';
273
+ this.els.footerConn.textContent = '🔴 未连接';
274
+ }
275
+ }
276
+
277
+ // ===== Chat Rendering =====
278
+
279
+ renderUserMessage(content, source = 'gui') {
280
+ const msgEl = document.createElement('div');
281
+ msgEl.className = 'wb-msg wb-msg-user';
282
+
283
+ const header = document.createElement('div');
284
+ header.className = 'wb-msg-header';
285
+
286
+ const sourceTag = document.createElement('span');
287
+ sourceTag.className = `wb-msg-source wb-msg-source-${source}`;
288
+ sourceTag.textContent = source.toUpperCase();
289
+ header.appendChild(sourceTag);
290
+
291
+ const label = document.createElement('span');
292
+ label.textContent = '你';
293
+ header.appendChild(label);
294
+
295
+ const body = document.createElement('div');
296
+ body.className = 'wb-msg-body';
297
+ body.textContent = content;
298
+
299
+ msgEl.appendChild(header);
300
+ msgEl.appendChild(body);
301
+
302
+ this.removeWelcome();
303
+ this.els.chatMessages.appendChild(msgEl);
304
+ this.scrollToBottom();
305
+ }
306
+
307
+ appendStreamText(text) {
308
+ if (!this.isStreaming) {
309
+ this.isStreaming = true;
310
+ this.streamBuffer = '';
311
+
312
+ const msgEl = document.createElement('div');
313
+ msgEl.className = 'wb-msg wb-msg-assistant';
314
+ msgEl.id = 'streaming-msg';
315
+
316
+ const header = document.createElement('div');
317
+ header.className = 'wb-msg-header';
318
+ header.innerHTML = '<span>AI</span>';
319
+
320
+ const body = document.createElement('div');
321
+ body.className = 'wb-msg-body';
322
+
323
+ const cursor = document.createElement('span');
324
+ cursor.className = 'wb-streaming';
325
+
326
+ body.appendChild(document.createTextNode(''));
327
+ body.appendChild(cursor);
328
+
329
+ msgEl.appendChild(header);
330
+ msgEl.appendChild(body);
331
+
332
+ this.removeWelcome();
333
+ this.els.chatMessages.appendChild(msgEl);
334
+ }
335
+
336
+ this.streamBuffer += text;
337
+ const streamMsg = document.getElementById('streaming-msg');
338
+ if (streamMsg) {
339
+ const body = streamMsg.querySelector('.wb-msg-body');
340
+ const cursor = body.querySelector('.wb-streaming');
341
+ // Update text before cursor
342
+ if (cursor) {
343
+ cursor.previousSibling.textContent = this.streamBuffer;
344
+ } else {
345
+ body.textContent = this.streamBuffer;
346
+ }
347
+ }
348
+ this.scrollToBottom();
349
+ }
350
+
351
+ finishStream(payload) {
352
+ this.isStreaming = false;
353
+ const streamMsg = document.getElementById('streaming-msg');
354
+ if (streamMsg) {
355
+ streamMsg.removeAttribute('id');
356
+ const body = streamMsg.querySelector('.wb-msg-body');
357
+ const cursor = body.querySelector('.wb-streaming');
358
+ if (cursor) cursor.remove();
359
+
360
+ // Use final content from server if available
361
+ const finalContent = payload.content || this.streamBuffer;
362
+ body.innerHTML = this.renderMarkdown(finalContent);
363
+ }
364
+ this.streamBuffer = '';
365
+
366
+ // Update tokens
367
+ if (payload.tokens) {
368
+ const current = this.state.tokens || {};
369
+ this.updateState({
370
+ tokens: {
371
+ inputTokens: (current.inputTokens || current.input || 0) + (payload.tokens.input || 0),
372
+ outputTokens: (current.outputTokens || current.output || 0) + (payload.tokens.output || 0),
373
+ }
374
+ });
375
+ }
376
+
377
+ this.scrollToBottom();
378
+ }
379
+
380
+ renderToolCall(payload) {
381
+ const el = document.createElement('div');
382
+ el.className = 'wb-msg wb-tool-detail';
383
+
384
+ const toolName = this.escapeHtml(payload.name || 'tool');
385
+ const inputStr = payload.input ? JSON.stringify(payload.input, null, 2) : '';
386
+
387
+ el.innerHTML = `
388
+ <div class="wb-tool-header" onclick="this.classList.toggle('expanded');this.nextElementSibling.classList.toggle('visible')">
389
+ <span>🔧</span>
390
+ <span class="wb-tool-name">${toolName}</span>
391
+ ${inputStr ? '<span class="wb-tool-chevron">▶</span>' : ''}
392
+ </div>
393
+ ${inputStr ? `<div class="wb-tool-params">${this.escapeHtml(inputStr)}</div>` : ''}
394
+ `;
395
+
396
+ this.els.chatMessages.appendChild(el);
397
+ this.scrollToBottom();
398
+ }
399
+
400
+ renderToolResult(payload) {
401
+ const el = document.createElement('div');
402
+ el.className = 'wb-msg wb-tool-detail';
403
+ const isError = !!payload.error;
404
+ const statusClass = isError ? 'wb-tool-status-err' : 'wb-tool-status-ok';
405
+ const icon = isError ? '❌' : '✅';
406
+ el.innerHTML = `<div class="wb-tool-status ${statusClass}">${icon} ${this.escapeHtml(payload.name || 'tool')} 完成</div>`;
407
+ this.els.chatMessages.appendChild(el);
408
+ this.scrollToBottom();
409
+ }
410
+
411
+ renderCommandOutput(output) {
412
+ const el = document.createElement('div');
413
+ el.className = 'wb-msg wb-msg-assistant';
414
+ const body = document.createElement('div');
415
+ body.className = 'wb-msg-body';
416
+ body.style.fontFamily = "'SF Mono', Monaco, monospace";
417
+ body.style.fontSize = '12px';
418
+ body.textContent = output;
419
+ el.appendChild(body);
420
+ this.els.chatMessages.appendChild(el);
421
+ this.scrollToBottom();
422
+ }
423
+
424
+ renderError(message) {
425
+ const el = document.createElement('div');
426
+ el.className = 'wb-msg wb-msg-assistant';
427
+ const body = document.createElement('div');
428
+ body.className = 'wb-msg-body';
429
+ body.style.borderColor = 'var(--wb-red)';
430
+ body.style.color = 'var(--wb-red)';
431
+ body.textContent = `❌ ${message}`;
432
+ el.appendChild(body);
433
+ this.els.chatMessages.appendChild(el);
434
+ this.scrollToBottom();
435
+ }
436
+
437
+ restoreHistory(history) {
438
+ // Render existing chat history from state snapshot
439
+ if (!history || history.length === 0) return;
440
+
441
+ this.removeWelcome();
442
+ for (const msg of history) {
443
+ if (msg.role === 'user') {
444
+ this.renderUserMessage(msg.content, 'cli');
445
+ } else if (msg.role === 'assistant') {
446
+ const el = document.createElement('div');
447
+ el.className = 'wb-msg wb-msg-assistant';
448
+ const header = document.createElement('div');
449
+ header.className = 'wb-msg-header';
450
+ header.innerHTML = '<span>AI</span>';
451
+ const body = document.createElement('div');
452
+ body.className = 'wb-msg-body';
453
+ body.innerHTML = this.renderMarkdown(msg.content);
454
+ el.appendChild(header);
455
+ el.appendChild(body);
456
+ this.els.chatMessages.appendChild(el);
457
+ }
458
+ }
459
+ this.scrollToBottom();
460
+ }
461
+
462
+ // ===== Side Panels =====
463
+
464
+ async loadSidePanels() {
465
+ this.loadFiles('.');
466
+ this.loadMemory();
467
+ this.loadTools();
468
+ this.loadGitStatus();
469
+ }
470
+
471
+ async loadFiles(dirPath) {
472
+ try {
473
+ const res = await fetch(`/api/files?path=${encodeURIComponent(dirPath)}`);
474
+ const data = await res.json();
475
+ this.els.currentPath.textContent = dirPath;
476
+
477
+ if (data.error) {
478
+ this.els.fileList.innerHTML = `<div class="wb-empty">${this.escapeHtml(data.error)}</div>`;
479
+ return;
480
+ }
481
+
482
+ const items = data.entries || [];
483
+ if (items.length === 0) {
484
+ this.els.fileList.innerHTML = '<div class="wb-empty">空目录</div>';
485
+ return;
486
+ }
487
+
488
+ this.els.fileList.innerHTML = '';
489
+
490
+ // Parent directory
491
+ if (dirPath !== '.') {
492
+ const parent = document.createElement('div');
493
+ parent.className = 'wb-file-item';
494
+ parent.innerHTML = `<span class="wb-file-icon">📁</span><span class="wb-file-name">..</span>`;
495
+ parent.onclick = () => {
496
+ const parts = dirPath.split('/');
497
+ parts.pop();
498
+ this.loadFiles(parts.join('/') || '.');
499
+ };
500
+ this.els.fileList.appendChild(parent);
501
+ }
502
+
503
+ for (const item of items) {
504
+ const el = document.createElement('div');
505
+ el.className = 'wb-file-item';
506
+ const icon = item.type === 'directory' ? '📁' : '📄';
507
+ const size = item.type === 'file' && item.size ? this.formatSize(item.size) : '';
508
+ el.innerHTML = `
509
+ <span class="wb-file-icon">${icon}</span>
510
+ <span class="wb-file-name">${this.escapeHtml(item.name)}</span>
511
+ <span class="wb-file-size">${size}</span>
512
+ `;
513
+ if (item.type === 'directory') {
514
+ el.onclick = () => this.loadFiles(dirPath === '.' ? item.name : `${dirPath}/${item.name}`);
515
+ } else {
516
+ el.onclick = () => this.openFilePreview(dirPath === '.' ? item.name : `${dirPath}/${item.name}`);
517
+ }
518
+ this.els.fileList.appendChild(el);
519
+ }
520
+ } catch {
521
+ this.els.fileList.innerHTML = '<div class="wb-empty">无法加载文件</div>';
522
+ }
523
+ }
524
+
525
+ async loadMemory() {
526
+ try {
527
+ const res = await fetch('/api/memory');
528
+ const data = await res.json();
529
+
530
+ if (data.error) {
531
+ this.els.memoryStats.innerHTML = `<div class="wb-empty">${this.escapeHtml(data.error)}</div>`;
532
+ return;
533
+ }
534
+
535
+ const layers = data.layers || {};
536
+ let statsHtml = '';
537
+ let totalCount = 0;
538
+
539
+ for (const [layer, info] of Object.entries(layers)) {
540
+ const count = info.count || 0;
541
+ totalCount += count;
542
+ statsHtml += `
543
+ <div class="wb-memory-stat">
544
+ <span class="wb-memory-stat-label">${this.escapeHtml(layer)}</span>
545
+ <span class="wb-memory-stat-value">${count} 条</span>
546
+ </div>`;
547
+ }
548
+
549
+ if (totalCount === 0) {
550
+ statsHtml = '<div class="wb-empty">暂无记忆</div>';
551
+ }
552
+
553
+ this.els.memoryStats.innerHTML = statsHtml;
554
+
555
+ // Load records
556
+ if (data.records && data.records.length > 0) {
557
+ this.els.memoryList.innerHTML = '';
558
+ for (const r of data.records.slice(0, 30)) {
559
+ const el = document.createElement('div');
560
+ el.className = 'wb-memory-item';
561
+ const tags = (r.tags || []).map(t => `#${t}`).join(' ');
562
+ el.innerHTML = `
563
+ <span class="wb-memory-category">${this.escapeHtml(r.category)}</span>
564
+ <div class="wb-memory-content">${this.escapeHtml(r.content)}</div>
565
+ ${tags ? `<div class="wb-memory-tags">${this.escapeHtml(tags)}</div>` : ''}
566
+ `;
567
+ this.els.memoryList.appendChild(el);
568
+ }
569
+ }
570
+ } catch {
571
+ this.els.memoryStats.innerHTML = '<div class="wb-empty">无法加载记忆</div>';
572
+ }
573
+ }
574
+
575
+ async loadTools() {
576
+ try {
577
+ const res = await fetch('/api/tools');
578
+ const data = await res.json();
579
+
580
+ if (!data.tools || data.tools.length === 0) {
581
+ this.els.toolList.innerHTML = '<div class="wb-empty">无注册工具</div>';
582
+ return;
583
+ }
584
+
585
+ this.els.toolList.innerHTML = '';
586
+ for (const tool of data.tools) {
587
+ const el = document.createElement('div');
588
+ el.className = 'wb-tool-item';
589
+ el.innerHTML = `
590
+ <div class="wb-tool-name">${this.escapeHtml(tool.name)}</div>
591
+ <div class="wb-tool-desc">${this.escapeHtml((tool.description || '').slice(0, 100))}</div>
592
+ `;
593
+ this.els.toolList.appendChild(el);
594
+ }
595
+ } catch {
596
+ this.els.toolList.innerHTML = '<div class="wb-empty">无法加载工具</div>';
597
+ }
598
+ }
599
+
600
+ // ===== Events =====
601
+
602
+ bindEvents() {
603
+ // Send button
604
+ this.els.btnSend.addEventListener('click', () => this.sendMessage());
605
+
606
+ // Enter to send, Shift+Enter for newline
607
+ this.els.chatInput.addEventListener('keydown', (e) => {
608
+ if (e.key === 'Enter' && !e.shiftKey) {
609
+ e.preventDefault();
610
+ this.sendMessage();
611
+ }
612
+ });
613
+
614
+ // Auto-resize textarea
615
+ this.els.chatInput.addEventListener('input', () => {
616
+ const el = this.els.chatInput;
617
+ el.style.height = 'auto';
618
+ el.style.height = Math.min(el.scrollHeight, 150) + 'px';
619
+ });
620
+
621
+ // Tab navigation
622
+ document.querySelectorAll('.wb-nav-item').forEach(btn => {
623
+ btn.addEventListener('click', () => {
624
+ const tab = btn.dataset.tab;
625
+ this.switchTab(tab);
626
+ });
627
+ });
628
+
629
+ // Shortcut commands
630
+ document.querySelectorAll('.wb-shortcut').forEach(btn => {
631
+ btn.addEventListener('click', () => {
632
+ const cmd = btn.dataset.cmd;
633
+ if (cmd) {
634
+ this.sendCommand(cmd);
635
+ }
636
+ });
637
+ });
638
+
639
+ // Memory search
640
+ const searchBtn = document.getElementById('btnMemorySearch');
641
+ if (searchBtn) {
642
+ searchBtn.addEventListener('click', () => this.searchMemory());
643
+ }
644
+ if (this.els.memorySearch) {
645
+ this.els.memorySearch.addEventListener('keydown', (e) => {
646
+ if (e.key === 'Enter') this.searchMemory();
647
+ });
648
+ this.els.memorySearch.addEventListener('input', () => {
649
+ clearTimeout(this._memSearchTimer);
650
+ this._memSearchTimer = setTimeout(() => this.searchMemory(), 300);
651
+ });
652
+ }
653
+
654
+ // File preview modal close
655
+ const closeBtn = document.getElementById('btnClosePreview');
656
+ if (closeBtn) {
657
+ closeBtn.addEventListener('click', () => this.closeFilePreview());
658
+ }
659
+ if (this.els.filePreviewModal) {
660
+ this.els.filePreviewModal.addEventListener('click', (e) => {
661
+ if (e.target === this.els.filePreviewModal) this.closeFilePreview();
662
+ });
663
+ }
664
+
665
+ // Global Escape key
666
+ document.addEventListener('keydown', (e) => {
667
+ if (e.key === 'Escape') this.closeFilePreview();
668
+ });
669
+ }
670
+
671
+ sendMessage() {
672
+ const text = this.els.chatInput.value.trim();
673
+ if (!text) return;
674
+
675
+ this.els.chatInput.value = '';
676
+ this.els.chatInput.style.height = 'auto';
677
+
678
+ // Render in chat
679
+ this.renderUserMessage(text, 'gui');
680
+
681
+ // Send via WebSocket
682
+ this.send('user_input', { content: text });
683
+ }
684
+
685
+ sendCommand(cmd) {
686
+ this.send('slash_command', { command: cmd });
687
+ // Show in chat
688
+ const el = document.createElement('div');
689
+ el.className = 'wb-msg wb-msg-user';
690
+ const body = document.createElement('div');
691
+ body.className = 'wb-msg-body';
692
+ body.style.fontFamily = "'SF Mono', Monaco, monospace";
693
+ body.style.fontSize = '12px';
694
+ body.textContent = cmd;
695
+ el.appendChild(body);
696
+ this.els.chatMessages.appendChild(el);
697
+ this.scrollToBottom();
698
+ }
699
+
700
+ switchTab(tab) {
701
+ // Update nav
702
+ document.querySelectorAll('.wb-nav-item').forEach(btn => {
703
+ btn.classList.toggle('active', btn.dataset.tab === tab);
704
+ });
705
+
706
+ // Update panels
707
+ document.querySelectorAll('.wb-tab-panel').forEach(panel => {
708
+ panel.classList.toggle('active', panel.id === `panel-${tab}`);
709
+ });
710
+
711
+ // Save preference
712
+ try { localStorage.setItem('wb-active-tab', tab); } catch {}
713
+ }
714
+
715
+ restoreActiveTab() {
716
+ try {
717
+ const saved = localStorage.getItem('wb-active-tab');
718
+ if (saved) {
719
+ // 验证 tab 是否存在于 DOM 中
720
+ const validTab = document.querySelector(`.wb-nav-item[data-tab="${saved}"]`);
721
+ if (validTab) {
722
+ this.switchTab(saved);
723
+ }
724
+ }
725
+ } catch {}
726
+ }
727
+
728
+ // ===== Git Status =====
729
+
730
+ async loadGitStatus() {
731
+ try {
732
+ const res = await fetch('/api/git/status');
733
+ const data = await res.json();
734
+ if (data.available) {
735
+ this.els.ctxGitBranch.textContent = data.branch || '--';
736
+ if (data.uncommitted) {
737
+ this.els.ctxGitStatus.textContent = data.summary || '有未提交更改';
738
+ this.els.ctxGitStatus.style.color = 'var(--wb-orange)';
739
+ } else {
740
+ this.els.ctxGitStatus.textContent = 'Clean';
741
+ this.els.ctxGitStatus.style.color = 'var(--wb-green)';
742
+ }
743
+ } else {
744
+ this.els.ctxGitBranch.textContent = 'N/A';
745
+ this.els.ctxGitStatus.textContent = '非 Git 仓库';
746
+ }
747
+ } catch {
748
+ this.els.ctxGitBranch.textContent = '--';
749
+ this.els.ctxGitStatus.textContent = '--';
750
+ }
751
+ }
752
+
753
+ // ===== File Preview =====
754
+
755
+ async openFilePreview(filePath) {
756
+ try {
757
+ const res = await fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`);
758
+ const data = await res.json();
759
+ if (data.error) {
760
+ this.els.previewContent.textContent = `Error: ${data.error}`;
761
+ } else {
762
+ const content = data.content || '';
763
+ const MAX_PREVIEW = 5000;
764
+ if (content.length > MAX_PREVIEW) {
765
+ this.els.previewContent.textContent = content.slice(0, MAX_PREVIEW) + '\n\n--- 文件过大,仅显示前 5000 字符 ---';
766
+ } else {
767
+ this.els.previewContent.textContent = content;
768
+ }
769
+ }
770
+ this.els.previewFileName.textContent = filePath;
771
+ this.els.filePreviewModal.classList.add('visible');
772
+ } catch {
773
+ this.els.previewContent.textContent = '无法加载文件';
774
+ this.els.previewFileName.textContent = filePath;
775
+ this.els.filePreviewModal.classList.add('visible');
776
+ }
777
+ }
778
+
779
+ closeFilePreview() {
780
+ this.els.filePreviewModal.classList.remove('visible');
781
+ }
782
+
783
+ // ===== Memory Search =====
784
+
785
+ async searchMemory() {
786
+ const query = this.els.memorySearch?.value?.trim();
787
+ if (!query) return;
788
+
789
+ this.els.memoryList.innerHTML = '<div class="wb-empty">搜索中…</div>';
790
+
791
+ try {
792
+ const res = await fetch('/api/memory/search', {
793
+ method: 'POST',
794
+ headers: { 'Content-Type': 'application/json' },
795
+ body: JSON.stringify({ query }),
796
+ });
797
+ const data = await res.json();
798
+
799
+ if (!data.results || data.results.length === 0) {
800
+ this.els.memoryList.innerHTML = '<div class="wb-empty">未找到匹配记忆</div>';
801
+ return;
802
+ }
803
+
804
+ this.els.memoryList.innerHTML = '';
805
+ for (const r of data.results) {
806
+ const el = document.createElement('div');
807
+ el.className = 'wb-memory-item';
808
+ const tags = (r.tags || []).map(t => `#${t}`).join(' ');
809
+ const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : '';
810
+ el.innerHTML = `
811
+ <span class="wb-memory-category">${this.escapeHtml(r.category)}${score}</span>
812
+ <div class="wb-memory-content">${this.escapeHtml(r.content)}</div>
813
+ ${tags ? `<div class="wb-memory-tags">${this.escapeHtml(tags)}</div>` : ''}
814
+ `;
815
+ this.els.memoryList.appendChild(el);
816
+ }
817
+ } catch {
818
+ this.els.memoryList.innerHTML = '<div class="wb-empty">搜索失败</div>';
819
+ }
820
+ }
821
+
822
+ // ===== Helpers =====
823
+
824
+ removeWelcome() {
825
+ const welcome = this.els.chatMessages.querySelector('.wb-welcome-msg');
826
+ if (welcome) welcome.remove();
827
+ }
828
+
829
+ scrollToBottom() {
830
+ requestAnimationFrame(() => {
831
+ this.els.chatMessages.scrollTop = this.els.chatMessages.scrollHeight;
832
+ });
833
+ }
834
+
835
+ escapeHtml(str) {
836
+ const div = document.createElement('div');
837
+ div.textContent = str;
838
+ return div.innerHTML;
839
+ }
840
+
841
+ renderMarkdown(text) {
842
+ if (!text) return '';
843
+ // Enhanced markdown rendering
844
+ let html = this.escapeHtml(text);
845
+
846
+ // Code blocks (must be first)
847
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
848
+
849
+ // Horizontal rules
850
+ html = html.replace(/^(-{3,}|_{3,}|\*{3,})$/gm, '<hr>');
851
+
852
+ // Headings
853
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
854
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
855
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
856
+
857
+ // Blockquotes
858
+ html = html.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
859
+
860
+ // Unordered lists
861
+ html = html.replace(/^[*\-+] (.+)$/gm, '<li>$1</li>');
862
+ html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
863
+
864
+ // Ordered lists
865
+ html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
866
+
867
+ // Links
868
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
869
+
870
+ // Inline code
871
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
872
+
873
+ // Bold
874
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
875
+
876
+ // Italic
877
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
878
+
879
+ // Strikethrough
880
+ html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
881
+
882
+ // Line breaks (but not inside pre blocks)
883
+ html = html.replace(/\n/g, '<br>');
884
+
885
+ // Clean up: remove <br> after block elements
886
+ html = html.replace(/<\/(h[1-3]|hr|pre|ul|ol|blockquote|li)><br>/g, '</$1>');
887
+ html = html.replace(/<br><(h[1-3]|hr|pre|ul|ol|blockquote)/g, '<$1');
888
+
889
+ return html;
890
+ }
891
+
892
+ shortModel(model) {
893
+ if (!model) return '--';
894
+ // Extract short name
895
+ const parts = model.split('/');
896
+ const name = parts[parts.length - 1];
897
+ if (name.length > 20) {
898
+ return name.slice(0, 18) + '…';
899
+ }
900
+ return name;
901
+ }
902
+
903
+ formatNumber(n) {
904
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
905
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
906
+ return String(n);
907
+ }
908
+
909
+ formatSize(bytes) {
910
+ if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
911
+ if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
912
+ return bytes + ' B';
913
+ }
914
+
915
+ // ===== Toast Notification =====
916
+
917
+ showToast(message, type = 'info') {
918
+ let container = document.getElementById('toastContainer');
919
+ if (!container) {
920
+ container = document.createElement('div');
921
+ container.id = 'toastContainer';
922
+ container.className = 'wb-toast-container';
923
+ document.body.appendChild(container);
924
+ }
925
+ const toast = document.createElement('div');
926
+ toast.className = `wb-toast wb-toast-${type}`;
927
+ toast.textContent = message;
928
+ container.appendChild(toast);
929
+ setTimeout(() => { toast.classList.add('wb-toast-out'); }, 3000);
930
+ setTimeout(() => { toast.remove(); }, 3500);
931
+ }
932
+
933
+ // ===== Team Module =====
934
+
935
+ initTeam() {
936
+ this.teamWs = null;
937
+ this.teamAuth = null;
938
+ this.teamMembers = [];
939
+ this.teamTasks = [];
940
+ this.teamTypingTimer = null;
941
+
942
+ // Try restore saved team session
943
+ try {
944
+ const saved = localStorage.getItem('wb-team-auth');
945
+ if (saved) {
946
+ this.teamAuth = JSON.parse(saved);
947
+ this.onTeamJoined();
948
+ }
949
+ } catch {}
950
+
951
+ this.bindTeamEvents();
952
+ }
953
+
954
+ bindTeamEvents() {
955
+ // Create team
956
+ const btnCreate = document.getElementById('btnTeamCreate');
957
+ if (btnCreate) btnCreate.addEventListener('click', () => this.createTeam());
958
+
959
+ // Join team
960
+ const btnJoin = document.getElementById('btnTeamJoin');
961
+ if (btnJoin) btnJoin.addEventListener('click', () => this.joinTeam());
962
+
963
+ // Sub-tabs
964
+ document.querySelectorAll('.wb-subtab').forEach(btn => {
965
+ btn.addEventListener('click', () => {
966
+ const subtab = btn.dataset.subtab;
967
+ document.querySelectorAll('.wb-subtab').forEach(b => b.classList.toggle('active', b.dataset.subtab === subtab));
968
+ document.querySelectorAll('.wb-subtab-panel').forEach(p => p.classList.toggle('active', p.id === `subtab-${subtab}`));
969
+ });
970
+ });
971
+
972
+ // Add task
973
+ const btnAddTask = document.getElementById('btnAddTask');
974
+ if (btnAddTask) btnAddTask.addEventListener('click', () => {
975
+ const form = document.getElementById('taskAddForm');
976
+ form.style.display = form.style.display === 'none' ? 'flex' : 'none';
977
+ });
978
+
979
+ const btnSubmit = document.getElementById('btnTaskSubmit');
980
+ if (btnSubmit) btnSubmit.addEventListener('click', () => this.createTask());
981
+
982
+ // Team chat send
983
+ const btnChatSend = document.getElementById('btnTeamChatSend');
984
+ if (btnChatSend) btnChatSend.addEventListener('click', () => this.sendTeamChat());
985
+ const chatInput = document.getElementById('teamChatInput');
986
+ if (chatInput) {
987
+ chatInput.addEventListener('keydown', (e) => {
988
+ if (e.key === 'Enter') this.sendTeamChat();
989
+ else this.sendTeamTyping();
990
+ });
991
+ }
992
+
993
+ // Copy join code
994
+ const codeDisplay = document.getElementById('teamJoinCodeDisplay');
995
+ if (codeDisplay) codeDisplay.addEventListener('click', () => {
996
+ navigator.clipboard.writeText(codeDisplay.textContent).catch(() => {});
997
+ });
998
+
999
+ // Task edit modal
1000
+ const btnTaskSave = document.getElementById('btnTaskSave');
1001
+ if (btnTaskSave) btnTaskSave.addEventListener('click', () => this.saveTaskEdit());
1002
+ const btnTaskDelete = document.getElementById('btnTaskDelete');
1003
+ if (btnTaskDelete) btnTaskDelete.addEventListener('click', () => this.deleteTask());
1004
+
1005
+ // Run network
1006
+ const btnRun = document.getElementById('btnRunNetwork');
1007
+ if (btnRun) btnRun.addEventListener('click', () => this.runNetworkRound());
1008
+ }
1009
+
1010
+ async createTeam() {
1011
+ const name = document.getElementById('teamCreateName')?.value?.trim();
1012
+ const ownerName = document.getElementById('teamCreateOwner')?.value?.trim() || 'owner';
1013
+ if (!name) return;
1014
+ const btn = document.getElementById('btnTeamCreate');
1015
+ if (btn) { btn.disabled = true; btn.textContent = '创建中…'; }
1016
+ try {
1017
+ const res = await fetch('/api/team/create', {
1018
+ method: 'POST',
1019
+ headers: { 'Content-Type': 'application/json' },
1020
+ body: JSON.stringify({ name, ownerName }),
1021
+ });
1022
+ const data = await res.json();
1023
+ if (data.error) { alert(data.error); return; }
1024
+ this.teamAuth = { teamId: data.team.id, token: data.ownerToken, memberName: ownerName };
1025
+ try { localStorage.setItem('wb-team-auth', JSON.stringify(this.teamAuth)); } catch {}
1026
+ document.getElementById('teamCreateName').value = '';
1027
+ document.getElementById('teamCreateOwner').value = '';
1028
+ this.onTeamJoined();
1029
+ } catch (e) { this.showToast('创建失败: ' + e.message, 'error'); } finally {
1030
+ if (btn) { btn.disabled = false; btn.textContent = '创建'; }
1031
+ }
1032
+ }
1033
+
1034
+ async joinTeam() {
1035
+ const joinCode = document.getElementById('teamJoinCode')?.value?.trim();
1036
+ const memberName = document.getElementById('teamJoinName')?.value?.trim();
1037
+ if (!joinCode || !memberName) return;
1038
+ const btn = document.getElementById('btnTeamJoin');
1039
+ if (btn) { btn.disabled = true; btn.textContent = '加入中…'; }
1040
+ try {
1041
+ const res = await fetch('/api/team/join', {
1042
+ method: 'POST',
1043
+ headers: { 'Content-Type': 'application/json' },
1044
+ body: JSON.stringify({ joinCode, memberName }),
1045
+ });
1046
+ const data = await res.json();
1047
+ if (data.error) { alert(data.error); return; }
1048
+ this.teamAuth = { teamId: data.team.id, token: data.token, memberName: data.member.name };
1049
+ try { localStorage.setItem('wb-team-auth', JSON.stringify(this.teamAuth)); } catch {}
1050
+ document.getElementById('teamJoinCode').value = '';
1051
+ document.getElementById('teamJoinName').value = '';
1052
+ this.onTeamJoined();
1053
+ } catch (e) { this.showToast('加入失败: ' + e.message, 'error'); } finally {
1054
+ if (btn) { btn.disabled = false; btn.textContent = '加入'; }
1055
+ }
1056
+ }
1057
+
1058
+ async onTeamJoined() {
1059
+ document.getElementById('teamJoinSection').style.display = 'none';
1060
+ document.getElementById('teamContent').style.display = '';
1061
+ document.getElementById('networkNeedTeam').style.display = 'none';
1062
+ document.getElementById('networkContent').style.display = '';
1063
+
1064
+ const safeCall = (fn, label) => fn.call(this).catch(e => console.warn(`[${label}]`, e));
1065
+ await safeCall(this.loadTeamStatus, 'TeamStatus');
1066
+ await safeCall(this.loadTeamTasks, 'TeamTasks');
1067
+ this.connectTeamWS();
1068
+ safeCall(this.loadNetworkProfile, 'NetworkProfile');
1069
+ safeCall(this.loadNetworkRounds, 'NetworkRounds');
1070
+ safeCall(this.loadNetworkDigest, 'NetworkDigest');
1071
+ }
1072
+
1073
+ async loadTeamStatus() {
1074
+ if (!this.teamAuth) return;
1075
+ try {
1076
+ const res = await fetch(`/api/team/${this.teamAuth.teamId}/status`, {
1077
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1078
+ });
1079
+ const data = await res.json();
1080
+ if (data.error) return;
1081
+
1082
+ document.getElementById('teamName').textContent = data.team?.name || '--';
1083
+ document.getElementById('teamJoinCodeDisplay').textContent = data.team?.joinCode || '--';
1084
+
1085
+ this.teamMembers = data.members || [];
1086
+ const online = this.teamMembers.filter(m => m.status === 'online').length;
1087
+ document.getElementById('teamOnlineCount').textContent = `${online} 在线`;
1088
+
1089
+ this.renderMembers();
1090
+ } catch (e) { console.warn('[Team] loadStatus', e); }
1091
+ }
1092
+
1093
+ renderMembers() {
1094
+ const list = document.getElementById('memberList');
1095
+ if (!list) return;
1096
+ list.innerHTML = '';
1097
+ for (const m of this.teamMembers) {
1098
+ const el = document.createElement('div');
1099
+ el.className = 'wb-member-item';
1100
+ const dotClass = m.status === 'online' ? 'wb-member-dot-online' : 'wb-member-dot-offline';
1101
+ el.innerHTML = `
1102
+ <span class="wb-member-dot ${dotClass}"></span>
1103
+ <span class="wb-member-name">${this.escapeHtml(m.name)}</span>
1104
+ <span class="wb-member-role">${this.escapeHtml(m.role)}</span>
1105
+ `;
1106
+ list.appendChild(el);
1107
+ }
1108
+ }
1109
+
1110
+ // ===== Team Tasks =====
1111
+
1112
+ async loadTeamTasks() {
1113
+ if (!this.teamAuth) return;
1114
+ try {
1115
+ const res = await fetch(`/api/team/${this.teamAuth.teamId}/tasks`, {
1116
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1117
+ });
1118
+ const data = await res.json();
1119
+ this.teamTasks = data.tasks || [];
1120
+ this.renderKanban();
1121
+ } catch (e) { console.warn('[Team] loadTasks', e); }
1122
+ }
1123
+
1124
+ renderKanban() {
1125
+ const cols = { todo: [], doing: [], review: [], done: [] };
1126
+ for (const t of this.teamTasks) {
1127
+ const status = t.status || 'todo';
1128
+ if (cols[status]) cols[status].push(t);
1129
+ }
1130
+
1131
+ for (const [status, tasks] of Object.entries(cols)) {
1132
+ const container = document.getElementById(`col-${status}`);
1133
+ if (!container) continue;
1134
+ container.innerHTML = '';
1135
+ for (const t of tasks) {
1136
+ const card = document.createElement('div');
1137
+ card.className = 'wb-task-card';
1138
+ card.dataset.priority = t.priority || 'B';
1139
+ card.onclick = () => this.openTaskEdit(t);
1140
+ card.innerHTML = `
1141
+ <div class="wb-task-title">${this.escapeHtml(t.title)}</div>
1142
+ <div class="wb-task-meta">
1143
+ <span class="wb-task-priority">${t.priority || 'B'}</span>
1144
+ ${t.assignee ? `<span class="wb-task-assignee">${this.escapeHtml(t.assignee)}</span>` : ''}
1145
+ </div>
1146
+ `;
1147
+ container.appendChild(card);
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ async createTask() {
1153
+ const title = document.getElementById('taskTitle')?.value?.trim();
1154
+ const priority = document.getElementById('taskPriority')?.value || 'A';
1155
+ if (!title || !this.teamAuth) return;
1156
+
1157
+ try {
1158
+ const res = await fetch(`/api/team/${this.teamAuth.teamId}/tasks`, {
1159
+ method: 'POST',
1160
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.teamAuth.token}` },
1161
+ body: JSON.stringify({ title, priority }),
1162
+ });
1163
+ const data = await res.json();
1164
+ if (data.error) { this.showToast(data.error, 'error'); return; }
1165
+ document.getElementById('taskTitle').value = '';
1166
+ document.getElementById('taskAddForm').style.display = 'none';
1167
+ await this.loadTeamTasks();
1168
+ } catch (e) { this.showToast('添加任务失败: ' + e.message, 'error'); }
1169
+ }
1170
+
1171
+ openTaskEdit(task) {
1172
+ document.getElementById('editTaskId').value = task.id;
1173
+ document.getElementById('editTaskTitle').value = task.title;
1174
+ document.getElementById('editTaskStatus').value = task.status || 'todo';
1175
+ document.getElementById('editTaskPriority').value = task.priority || 'B';
1176
+ document.getElementById('editTaskAssignee').value = task.assignee || '';
1177
+ document.getElementById('taskEditModal').classList.add('visible');
1178
+ }
1179
+
1180
+ async saveTaskEdit() {
1181
+ const id = document.getElementById('editTaskId').value;
1182
+ if (!id || !this.teamAuth) return;
1183
+
1184
+ const updates = {
1185
+ title: document.getElementById('editTaskTitle').value,
1186
+ status: document.getElementById('editTaskStatus').value,
1187
+ priority: document.getElementById('editTaskPriority').value,
1188
+ assignee: document.getElementById('editTaskAssignee').value || undefined,
1189
+ };
1190
+
1191
+ try {
1192
+ await fetch(`/api/team/${this.teamAuth.teamId}/tasks/${id}`, {
1193
+ method: 'PUT',
1194
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.teamAuth.token}` },
1195
+ body: JSON.stringify(updates),
1196
+ });
1197
+ document.getElementById('taskEditModal').classList.remove('visible');
1198
+ await this.loadTeamTasks();
1199
+ } catch (e) { this.showToast('保存失败: ' + e.message, 'error'); }
1200
+ }
1201
+
1202
+ async deleteTask() {
1203
+ const id = document.getElementById('editTaskId').value;
1204
+ if (!id || !this.teamAuth) return;
1205
+ try {
1206
+ await fetch(`/api/team/${this.teamAuth.teamId}/tasks/${id}`, {
1207
+ method: 'DELETE',
1208
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1209
+ });
1210
+ document.getElementById('taskEditModal').classList.remove('visible');
1211
+ await this.loadTeamTasks();
1212
+ } catch (e) { this.showToast('删除失败: ' + e.message, 'error'); }
1213
+ }
1214
+
1215
+ // ===== Team WebSocket =====
1216
+
1217
+ connectTeamWS() {
1218
+ if (!this.teamAuth || this.teamWs) return;
1219
+
1220
+ const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1221
+ const wsUrl = `${wsProto}//${location.host}/ws/team`;
1222
+ this.teamWs = new WebSocket(wsUrl);
1223
+
1224
+ this.teamWs.onopen = () => {
1225
+ this.teamWs.send(JSON.stringify({
1226
+ type: 'auth',
1227
+ payload: { teamId: this.teamAuth.teamId, token: this.teamAuth.token },
1228
+ }));
1229
+ };
1230
+
1231
+ this.teamWs.onmessage = (event) => {
1232
+ try {
1233
+ const msg = JSON.parse(event.data);
1234
+ this.handleTeamMessage(msg);
1235
+ } catch (e) { console.warn('[TeamWS] message parse error', e); }
1236
+ };
1237
+
1238
+ this.teamWs.onclose = () => {
1239
+ this.teamWs = null;
1240
+ // Reconnect after 3s
1241
+ setTimeout(() => this.connectTeamWS(), 3000);
1242
+ };
1243
+ }
1244
+
1245
+ handleTeamMessage(msg) {
1246
+ switch (msg.type) {
1247
+ case 'auth_ok':
1248
+ break;
1249
+ case 'auth_fail':
1250
+ this.teamAuth = null;
1251
+ try { localStorage.removeItem('wb-team-auth'); } catch {}
1252
+ document.getElementById('teamJoinSection').style.display = '';
1253
+ document.getElementById('teamContent').style.display = 'none';
1254
+ break;
1255
+ case 'member_joined':
1256
+ case 'member_left':
1257
+ this.loadTeamStatus();
1258
+ break;
1259
+ case 'task_created':
1260
+ case 'task_updated':
1261
+ case 'task_deleted':
1262
+ this.loadTeamTasks();
1263
+ break;
1264
+ case 'chat_message':
1265
+ this.appendTeamChat(msg.payload);
1266
+ break;
1267
+ case 'chat_typing':
1268
+ this.showTeamTyping(msg.payload);
1269
+ break;
1270
+ // Network events
1271
+ case 'network_round_start':
1272
+ this.loadNetworkRounds();
1273
+ break;
1274
+ case 'network_digest_ready':
1275
+ this.loadNetworkDigest();
1276
+ break;
1277
+ case 'network_dialogue_complete':
1278
+ case 'network_match_found':
1279
+ this.loadNetworkRounds();
1280
+ break;
1281
+ }
1282
+ }
1283
+
1284
+ // ===== Team Chat =====
1285
+
1286
+ sendTeamChat() {
1287
+ const input = document.getElementById('teamChatInput');
1288
+ const content = input?.value?.trim();
1289
+ if (!content || !this.teamWs) return;
1290
+ input.value = '';
1291
+ this.teamWs.send(JSON.stringify({
1292
+ type: 'chat_message',
1293
+ payload: { content },
1294
+ }));
1295
+ }
1296
+
1297
+ sendTeamTyping() {
1298
+ if (!this.teamWs || this.teamTypingTimer) return;
1299
+ this.teamWs.send(JSON.stringify({ type: 'chat_typing', payload: {} }));
1300
+ this.teamTypingTimer = setTimeout(() => { this.teamTypingTimer = null; }, 2000);
1301
+ }
1302
+
1303
+ appendTeamChat(payload) {
1304
+ const container = document.getElementById('teamChatMessages');
1305
+ if (!container) return;
1306
+ // Remove empty placeholder
1307
+ const empty = container.querySelector('.wb-empty');
1308
+ if (empty) empty.remove();
1309
+
1310
+ const el = document.createElement('div');
1311
+ el.className = 'wb-team-chat-msg';
1312
+ const time = payload.timestamp ? new Date(payload.timestamp).toLocaleTimeString() : '';
1313
+ el.innerHTML = `
1314
+ <span class="wb-team-chat-sender">${this.escapeHtml(payload.senderName || '???')}</span>
1315
+ <span class="wb-team-chat-text">${this.escapeHtml(payload.content)}</span>
1316
+ <span class="wb-team-chat-time">${time}</span>
1317
+ `;
1318
+ container.appendChild(el);
1319
+ container.scrollTop = container.scrollHeight;
1320
+ }
1321
+
1322
+ showTeamTyping(payload) {
1323
+ const el = document.getElementById('teamTyping');
1324
+ if (!el) return;
1325
+ el.textContent = `${payload.memberName || '???'} 正在输入…`;
1326
+ clearTimeout(this._typingClear);
1327
+ this._typingClear = setTimeout(() => { el.textContent = ''; }, 3000);
1328
+ }
1329
+
1330
+ // ===== Network Module =====
1331
+
1332
+ async loadNetworkProfile() {
1333
+ if (!this.teamAuth) return;
1334
+ try {
1335
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/profile`, {
1336
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1337
+ });
1338
+ const data = await res.json();
1339
+ if (data.error) return;
1340
+
1341
+ const profile = data.profile || {};
1342
+ this.renderTagList('profileSkills', profile.skills || [], 'skills');
1343
+ this.renderTagList('profileNeeds', profile.needs || [], 'needs');
1344
+ this.renderTagList('profileOfferings', profile.offerings || [], 'offerings');
1345
+ } catch (e) { console.warn('[Network] loadProfile', e); }
1346
+ }
1347
+
1348
+ renderTagList(containerId, tags, field) {
1349
+ const el = document.getElementById(containerId);
1350
+ if (!el) return;
1351
+ el.innerHTML = '';
1352
+ for (const tag of tags) {
1353
+ const span = document.createElement('span');
1354
+ span.className = 'wb-tag';
1355
+ span.textContent = tag + ' ';
1356
+ const removeBtn = document.createElement('span');
1357
+ removeBtn.className = 'wb-tag-remove';
1358
+ removeBtn.textContent = '\u00d7';
1359
+ removeBtn.addEventListener('click', () => this.removeProfileTag(field, tag));
1360
+ span.appendChild(removeBtn);
1361
+ el.appendChild(span);
1362
+ }
1363
+ }
1364
+
1365
+ async addProfileTag(field, inputId) {
1366
+ const input = document.getElementById(inputId);
1367
+ const value = input?.value?.trim();
1368
+ if (!value || !this.teamAuth) return;
1369
+ input.value = '';
1370
+
1371
+ try {
1372
+ // Get current profile
1373
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/profile`, {
1374
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1375
+ });
1376
+ const data = await res.json();
1377
+ const current = data.profile?.[field] || [];
1378
+ if (current.includes(value)) return;
1379
+
1380
+ await fetch(`/api/network/${this.teamAuth.teamId}/profile`, {
1381
+ method: 'PUT',
1382
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.teamAuth.token}` },
1383
+ body: JSON.stringify({ field, value: [...current, value] }),
1384
+ });
1385
+ this.loadNetworkProfile();
1386
+ } catch (e) { console.warn('[Network] addProfileTag', e); }
1387
+ }
1388
+
1389
+ async removeProfileTag(field, tag) {
1390
+ if (!this.teamAuth) return;
1391
+ try {
1392
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/profile`, {
1393
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1394
+ });
1395
+ const data = await res.json();
1396
+ const current = (data.profile?.[field] || []).filter(t => t !== tag);
1397
+
1398
+ await fetch(`/api/network/${this.teamAuth.teamId}/profile`, {
1399
+ method: 'PUT',
1400
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.teamAuth.token}` },
1401
+ body: JSON.stringify({ field, value: current }),
1402
+ });
1403
+ this.loadNetworkProfile();
1404
+ } catch (e) { console.warn('[Network] removeProfileTag', e); }
1405
+ }
1406
+
1407
+ async runNetworkRound() {
1408
+ if (!this.teamAuth) return;
1409
+ const btn = document.getElementById('btnRunNetwork');
1410
+ if (btn) { btn.disabled = true; btn.textContent = '匹配中…'; }
1411
+ try {
1412
+ await fetch(`/api/network/${this.teamAuth.teamId}/run`, {
1413
+ method: 'POST',
1414
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1415
+ });
1416
+ await this.loadNetworkRounds();
1417
+ await this.loadNetworkDigest();
1418
+ } catch {} finally {
1419
+ if (btn) { btn.disabled = false; btn.textContent = '运行匹配'; }
1420
+ }
1421
+ }
1422
+
1423
+ async loadNetworkRounds() {
1424
+ if (!this.teamAuth) return;
1425
+ try {
1426
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/rounds`, {
1427
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1428
+ });
1429
+ const data = await res.json();
1430
+ const rounds = data.rounds || [];
1431
+ const container = document.getElementById('networkRoundList');
1432
+ if (!container) return;
1433
+
1434
+ if (rounds.length === 0) {
1435
+ container.innerHTML = '<div class="wb-empty">暂无轮次</div>';
1436
+ return;
1437
+ }
1438
+
1439
+ container.innerHTML = '';
1440
+ for (const r of rounds.slice(0, 20)) {
1441
+ const card = document.createElement('div');
1442
+ card.className = 'wb-round-card';
1443
+ const time = r.startedAt ? new Date(r.startedAt).toLocaleString() : '--';
1444
+ const matchCount = r.matchCount || r.matches?.length || 0;
1445
+ const statusClass = `wb-round-status-${r.status || 'completed'}`;
1446
+ card.innerHTML = `
1447
+ <div class="wb-round-header" onclick="this.nextElementSibling.classList.toggle('visible')">
1448
+ <span class="wb-round-time">${time}</span>
1449
+ <span class="wb-round-matches">${matchCount} 对匹配</span>
1450
+ <span class="wb-round-status ${statusClass}">${r.status || '--'}</span>
1451
+ </div>
1452
+ <div class="wb-round-detail" id="round-${r.id}">
1453
+ <div class="wb-empty">点击加载详情…</div>
1454
+ </div>
1455
+ `;
1456
+ // Lazy load detail
1457
+ card.querySelector('.wb-round-header').addEventListener('click', () => this.loadRoundDetail(r.id));
1458
+ container.appendChild(card);
1459
+ }
1460
+ } catch {}
1461
+ }
1462
+
1463
+ async loadRoundDetail(roundId) {
1464
+ if (!this.teamAuth) return;
1465
+ const container = document.getElementById(`round-${roundId}`);
1466
+ if (!container || container.dataset.loaded) return;
1467
+ container.dataset.loaded = 'true';
1468
+
1469
+ try {
1470
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/rounds/${roundId}`, {
1471
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1472
+ });
1473
+ const data = await res.json();
1474
+ const matches = data.matches || [];
1475
+
1476
+ if (matches.length === 0) {
1477
+ container.innerHTML = '<div class="wb-empty">无匹配</div>';
1478
+ return;
1479
+ }
1480
+
1481
+ container.innerHTML = '';
1482
+ for (const m of matches) {
1483
+ const el = document.createElement('div');
1484
+ el.className = 'wb-match-pair';
1485
+ el.innerHTML = `
1486
+ <div class="wb-match-names">${this.escapeHtml(m.memberA?.name || '?')} ↔ ${this.escapeHtml(m.memberB?.name || '?')}</div>
1487
+ <div class="wb-match-reason">${this.escapeHtml(m.reason || m.suggestedTopic || '')}</div>
1488
+ <span class="wb-match-score">匹配度: ${m.score || 0}%</span>
1489
+ `;
1490
+ container.appendChild(el);
1491
+ }
1492
+ } catch {
1493
+ container.innerHTML = '<div class="wb-empty">加载失败</div>';
1494
+ }
1495
+ }
1496
+
1497
+ async loadNetworkDigest() {
1498
+ if (!this.teamAuth) return;
1499
+ try {
1500
+ const res = await fetch(`/api/network/${this.teamAuth.teamId}/digest`, {
1501
+ headers: { 'Authorization': `Bearer ${this.teamAuth.token}` },
1502
+ });
1503
+ if (!res.ok) return;
1504
+ const data = await res.json();
1505
+ const container = document.getElementById('networkDigest');
1506
+ if (!container) return;
1507
+
1508
+ const digest = data.digest;
1509
+ if (!digest) {
1510
+ container.innerHTML = '<div class="wb-empty">暂无摘要 — 运行一次匹配后将生成</div>';
1511
+ return;
1512
+ }
1513
+
1514
+ const entries = digest.matches || [];
1515
+ if (entries.length === 0) {
1516
+ container.innerHTML = '<div class="wb-empty">暂无匹配结果</div>';
1517
+ return;
1518
+ }
1519
+
1520
+ container.innerHTML = '';
1521
+ for (const m of entries) {
1522
+ const card = document.createElement('div');
1523
+ card.className = 'wb-digest-card';
1524
+ const actions = (m.actionItems || []).map(a => `<li>${this.escapeHtml(a)}</li>`).join('');
1525
+ const insights = (m.keyInsights || []).map(i => this.escapeHtml(i)).join(' | ');
1526
+ card.innerHTML = `
1527
+ <div class="wb-digest-partner">${this.escapeHtml(m.partnerName || '?')}</div>
1528
+ <div class="wb-digest-value">${this.escapeHtml(m.valueForMe || '')}</div>
1529
+ ${actions ? `<ul class="wb-digest-actions">${actions}</ul>` : ''}
1530
+ ${insights ? `<div class="wb-digest-insights">${insights}</div>` : ''}
1531
+ `;
1532
+ container.appendChild(card);
1533
+ }
1534
+ } catch (e) { console.warn('[Network] loadDigest', e); }
1535
+ }
1536
+
1537
+ // ===== Workspace Profile =====
1538
+
1539
+ async loadWorkspaceProfile() {
1540
+ try {
1541
+ const res = await fetch('/api/workspace-profile');
1542
+ if (!res.ok) return;
1543
+ const data = await res.json();
1544
+ if (data.profile) {
1545
+ this.applyProfile(data.profile);
1546
+ }
1547
+ } catch (e) {
1548
+ console.warn('[Profile] loadWorkspaceProfile', e);
1549
+ }
1550
+ }
1551
+
1552
+ applyProfile(profile) {
1553
+ if (!profile) return;
1554
+ this.workspaceProfile = profile;
1555
+
1556
+ // 1. 显示/隐藏导航 Tab(team 和 network 始终保留)
1557
+ const ALWAYS_ENABLED = ['chat', 'files', 'memory', 'tools', 'team', 'network'];
1558
+ const profileModules = Array.isArray(profile.enabledModules) ? profile.enabledModules : [];
1559
+ const enabledModules = new Set([...ALWAYS_ENABLED, ...profileModules]);
1560
+ document.querySelectorAll('.wb-nav-item[data-tab]').forEach(btn => {
1561
+ const tab = btn.dataset.tab;
1562
+ if (tab && !enabledModules.has(tab)) {
1563
+ btn.style.display = 'none';
1564
+ } else {
1565
+ btn.style.display = '';
1566
+ }
1567
+ });
1568
+
1569
+ // 确保当前 tab 仍可见,否则切到 chat
1570
+ try {
1571
+ const saved = localStorage.getItem('wb-active-tab');
1572
+ if (saved && !enabledModules.has(saved)) {
1573
+ this.switchTab('chat');
1574
+ }
1575
+ } catch {}
1576
+
1577
+ // 2. 替换欢迎文案
1578
+ const welcomeText = document.querySelector('.wb-welcome-text');
1579
+ const welcomeSub = document.querySelector('.wb-welcome-sub');
1580
+ if (welcomeText && profile.workspaceLabel) {
1581
+ const label = profile.workspaceLabel;
1582
+ welcomeText.textContent = label.length > 20 ? label.slice(0, 18) + '…' : label;
1583
+ }
1584
+ if (welcomeSub && profile.welcomeMessage) {
1585
+ welcomeSub.textContent = profile.welcomeMessage;
1586
+ }
1587
+
1588
+ // 3. 渲染个性化快捷命令
1589
+ const shortcutsEl = document.getElementById('shortcuts');
1590
+ const quickCommands = Array.isArray(profile.quickCommands) ? profile.quickCommands : [];
1591
+ if (shortcutsEl && quickCommands.length > 0) {
1592
+ shortcutsEl.innerHTML = '';
1593
+ for (const cmd of quickCommands) {
1594
+ if (!cmd || !cmd.cmd) continue;
1595
+ const btn = document.createElement('button');
1596
+ btn.className = 'wb-shortcut';
1597
+ btn.dataset.cmd = cmd.cmd;
1598
+ btn.textContent = `${cmd.icon || '⚡'} ${cmd.label || cmd.cmd}`;
1599
+ btn.addEventListener('click', () => this.sendShortcut(cmd.cmd));
1600
+ shortcutsEl.appendChild(btn);
1601
+ }
1602
+ }
1603
+
1604
+ // 4. 渲染推荐生产线
1605
+ const linesSection = document.getElementById('recommendedLinesSection');
1606
+ const linesContainer = document.getElementById('recommendedLines');
1607
+ const recommendedLines = Array.isArray(profile.recommendedLines) ? profile.recommendedLines : [];
1608
+ if (linesSection && linesContainer && recommendedLines.length > 0) {
1609
+ linesSection.style.display = '';
1610
+ linesContainer.innerHTML = '';
1611
+ for (const lineName of recommendedLines) {
1612
+ if (!lineName) continue;
1613
+ const btn = document.createElement('button');
1614
+ btn.className = 'wb-shortcut';
1615
+ btn.textContent = `🏭 ${lineName}`;
1616
+ btn.addEventListener('click', () => this.sendShortcut(`/run ${lineName}`));
1617
+ linesContainer.appendChild(btn);
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ sendShortcut(cmd) {
1623
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1624
+ this.send('user_input', { content: cmd });
1625
+ this.renderUserMessage(cmd, 'gui');
1626
+ }
1627
+ }
1628
+
1629
+ // ===== Theme Switching =====
1630
+
1631
+ setTheme(theme) {
1632
+ document.documentElement.setAttribute('data-theme', theme);
1633
+ localStorage.setItem('hypercore-theme', theme);
1634
+ // 更新主题选择器(如果存在)
1635
+ const themeBtn = document.getElementById('themeToggle');
1636
+ if (themeBtn) {
1637
+ const labels = { dark: '🌙', light: '☀️', 'high-contrast': '🔲' };
1638
+ themeBtn.textContent = labels[theme] || '🌙';
1639
+ }
1640
+ }
1641
+
1642
+ cycleTheme() {
1643
+ const themes = ['dark', 'light', 'high-contrast'];
1644
+ const current = document.documentElement.getAttribute('data-theme') || 'dark';
1645
+ const idx = themes.indexOf(current);
1646
+ const next = themes[(idx + 1) % themes.length];
1647
+ this.setTheme(next);
1648
+ }
1649
+
1650
+ loadSavedTheme() {
1651
+ const saved = localStorage.getItem('hypercore-theme');
1652
+ if (saved) {
1653
+ this.setTheme(saved);
1654
+ }
1655
+ }
1656
+
1657
+ // ===== Browser Notifications =====
1658
+
1659
+ async requestNotificationPermission() {
1660
+ if ('Notification' in window && Notification.permission === 'default') {
1661
+ await Notification.requestPermission();
1662
+ }
1663
+ }
1664
+
1665
+ sendBrowserNotification(title, body) {
1666
+ if ('Notification' in window && Notification.permission === 'granted') {
1667
+ new Notification(title, {
1668
+ body,
1669
+ icon: '/static/favicon.ico',
1670
+ tag: 'hypercore-notify',
1671
+ });
1672
+ }
1673
+ }
1674
+ }
1675
+
1676
+ // ===== Launch =====
1677
+ document.addEventListener('DOMContentLoaded', () => {
1678
+ window.workspace = new HypercoreWorkspace();
1679
+ // 加载保存的主题
1680
+ window.workspace.loadSavedTheme();
1681
+ // 请求通知权限
1682
+ window.workspace.requestNotificationPermission();
1683
+ });