feihong-code 0.6.1 → 7.0.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.
@@ -48,6 +48,7 @@
48
48
  toast('✅ ' + (d.message || '总结完成'));
49
49
  loadMemoryStats();
50
50
  loadSummaryHistory();
51
+ loadLongTermMemory();
51
52
  } catch (e) {
52
53
  toast('总结失败:' + e.message);
53
54
  } finally {
@@ -55,6 +56,133 @@
55
56
  btn.textContent = '✨ 立即总结';
56
57
  }
57
58
  });
59
+
60
+ // 短期记忆:添加记录按钮(展开文本框)
61
+ document.getElementById('memAddShort')?.addEventListener('click', () => {
62
+ const editor = document.getElementById('memShortEditor');
63
+ const input = document.getElementById('memShortInput');
64
+ if (editor) {
65
+ editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
66
+ if (editor.style.display !== 'none' && input) {
67
+ input.value = '';
68
+ input.focus();
69
+ }
70
+ }
71
+ });
72
+ // 短期记忆:取消
73
+ document.getElementById('memShortCancel')?.addEventListener('click', () => {
74
+ const editor = document.getElementById('memShortEditor');
75
+ if (editor) editor.style.display = 'none';
76
+ });
77
+ // 短期记忆:保存
78
+ document.getElementById('memShortSave')?.addEventListener('click', async () => {
79
+ const input = document.getElementById('memShortInput');
80
+ const content = input ? input.value.trim() : '';
81
+ if (!content) { toast('请输入记录内容'); return; }
82
+ try {
83
+ await api('/api/memory/short', 'POST', {
84
+ title: content.slice(0, 30) + (content.length > 30 ? '...' : ''),
85
+ type: 'note',
86
+ content: content,
87
+ });
88
+ toast('已添加到短期记忆');
89
+ const editor = document.getElementById('memShortEditor');
90
+ if (editor) editor.style.display = 'none';
91
+ const dateStr = document.getElementById('memDateInput').value;
92
+ if (dateStr) loadMemoryContent(dateStr);
93
+ loadMemoryStats();
94
+ } catch (e) {
95
+ toast('添加失败:' + e.message);
96
+ }
97
+ });
98
+
99
+ // 长期记忆:编辑按钮
100
+ document.getElementById('memEditLong')?.addEventListener('click', () => {
101
+ const content = document.getElementById('memLongContent');
102
+ const editor = document.getElementById('memLongEditor');
103
+ const editBtn = document.getElementById('memEditLong');
104
+ const saveBtn = document.getElementById('memSaveLong');
105
+ const cancelBtn = document.getElementById('memCancelLong');
106
+ const appendBtn = document.getElementById('memAppendLong');
107
+ const appendEditor = document.getElementById('memLongAppendEditor');
108
+ // 把当前内容放到编辑器
109
+ if (editor && content) {
110
+ editor.value = content.innerText || content.textContent || '';
111
+ }
112
+ if (content) content.style.display = 'none';
113
+ if (editor) editor.style.display = 'block';
114
+ if (editBtn) editBtn.style.display = 'none';
115
+ if (appendBtn) appendBtn.style.display = 'none';
116
+ if (appendEditor) appendEditor.style.display = 'none';
117
+ if (saveBtn) saveBtn.style.display = '';
118
+ if (cancelBtn) cancelBtn.style.display = '';
119
+ });
120
+ // 长期记忆:取消编辑
121
+ document.getElementById('memCancelLong')?.addEventListener('click', () => {
122
+ const content = document.getElementById('memLongContent');
123
+ const editor = document.getElementById('memLongEditor');
124
+ const editBtn = document.getElementById('memEditLong');
125
+ const saveBtn = document.getElementById('memSaveLong');
126
+ const cancelBtn = document.getElementById('memCancelLong');
127
+ const appendBtn = document.getElementById('memAppendLong');
128
+ if (content) content.style.display = '';
129
+ if (editor) editor.style.display = 'none';
130
+ if (editBtn) editBtn.style.display = '';
131
+ if (appendBtn) appendBtn.style.display = '';
132
+ if (saveBtn) saveBtn.style.display = 'none';
133
+ if (cancelBtn) cancelBtn.style.display = 'none';
134
+ });
135
+ // 长期记忆:保存编辑
136
+ document.getElementById('memSaveLong')?.addEventListener('click', async () => {
137
+ const editor = document.getElementById('memLongEditor');
138
+ const content = editor ? editor.value : '';
139
+ try {
140
+ await api('/api/memory/long', 'POST', { content });
141
+ toast('长期记忆已保存');
142
+ document.getElementById('memCancelLong')?.click();
143
+ loadLongTermMemory();
144
+ loadMemoryStats();
145
+ } catch (e) {
146
+ toast('保存失败:' + e.message);
147
+ }
148
+ });
149
+ // 长期记忆:追加按钮(展开文本框)
150
+ document.getElementById('memAppendLong')?.addEventListener('click', () => {
151
+ const editor = document.getElementById('memLongAppendEditor');
152
+ const titleInput = document.getElementById('memAppendTitle');
153
+ const contentInput = document.getElementById('memAppendContent');
154
+ if (editor) {
155
+ editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
156
+ if (editor.style.display !== 'none') {
157
+ if (titleInput) titleInput.value = '';
158
+ if (contentInput) contentInput.value = '';
159
+ if (titleInput) titleInput.focus();
160
+ }
161
+ }
162
+ });
163
+ // 长期记忆:取消追加
164
+ document.getElementById('memAppendCancel')?.addEventListener('click', () => {
165
+ const editor = document.getElementById('memLongAppendEditor');
166
+ if (editor) editor.style.display = 'none';
167
+ });
168
+ // 长期记忆:确认追加
169
+ document.getElementById('memAppendSave')?.addEventListener('click', async () => {
170
+ const titleInput = document.getElementById('memAppendTitle');
171
+ const contentInput = document.getElementById('memAppendContent');
172
+ const title = titleInput ? titleInput.value.trim() : '';
173
+ const content = contentInput ? contentInput.value.trim() : '';
174
+ if (!title || !content) { toast('请输入标题和内容'); return; }
175
+ try {
176
+ await api('/api/memory/long/append', 'POST', { title, category: '自定义', content });
177
+ toast('已追加到长期记忆');
178
+ const editor = document.getElementById('memLongAppendEditor');
179
+ if (editor) editor.style.display = 'none';
180
+ loadLongTermMemory();
181
+ loadMemoryStats();
182
+ } catch (e) {
183
+ toast('追加失败:' + e.message);
184
+ }
185
+ });
58
186
  }
59
187
 
60
188
  // 每次进入都刷新数据(事件不重复绑定,数据实时刷新)
@@ -113,14 +241,125 @@
113
241
  return '<div class="tile" data-goal="' + encodeURIComponent(t.goal) + '"><div class="icon">' + (t.icon || '📄') + '</div><div class="title">' + escapeHtml(t.title) + '</div><div class="desc">' + escapeHtml(t.category || '') + ' · ' + escapeHtml(t.goal).slice(0, 40) + '…</div><div class="ops"><button class="use" data-goal="' + encodeURIComponent(t.goal) + '">填入输入框</button>' + (deletable ? '<button class="ghost del" data-id="' + t.id + '">删除</button>' : '') + '</div></div>';
114
242
  }
115
243
 
244
+ function renderBuiltinAutomations(list) {
245
+ const grid = document.getElementById('builtinAutoGrid');
246
+ if (!grid) return;
247
+ if (!list.length) { renderEmpty(grid, '暂无预置指令'); return; }
248
+ grid.innerHTML = list.map((a) => {
249
+ const icon = a.icon || '⚡';
250
+ const category = a.category || '常用';
251
+ const goalPreview = (a.goal || '').slice(0, 50) + ((a.goal || '').length > 50 ? '…' : '');
252
+ return '<div class="tile builtin-tile" data-id="' + escapeHtml(a.id) + '">'
253
+ + '<div class="icon">' + icon + '</div>'
254
+ + '<div class="title">' + escapeHtml(a.name) + '</div>'
255
+ + '<div class="desc"><span style="color:var(--brand);font-weight:500;">' + escapeHtml(category) + '</span> · ' + escapeHtml(goalPreview) + '</div>'
256
+ + '<div class="ops">'
257
+ + '<button class="run builtin-run" data-id="' + escapeHtml(a.id) + '">▶ 运行</button>'
258
+ + '<button class="ghost builtin-save" data-id="' + escapeHtml(a.id) + '" data-name="' + escapeHtml(a.name) + '" data-goal="' + encodeURIComponent(a.goal || '') + '">+ 保存为我的</button>'
259
+ + '</div></div>';
260
+ }).join('');
261
+ grid.querySelectorAll('button.builtin-run').forEach((b) => b.addEventListener('click', (e) => {
262
+ e.stopPropagation();
263
+ runAuto(b.getAttribute('data-id'));
264
+ }));
265
+ grid.querySelectorAll('button.builtin-save').forEach((b) => b.addEventListener('click', (e) => {
266
+ e.stopPropagation();
267
+ const name = b.getAttribute('data-name') || '';
268
+ const goal = decodeURIComponent(b.getAttribute('data-goal') || '');
269
+ saveBuiltinAsMine(name, goal);
270
+ }));
271
+ }
272
+
273
+ async function saveBuiltinAsMine(name, goal) {
274
+ try {
275
+ await api('/api/automations', 'POST', { name, goal });
276
+ await loadAutomations();
277
+ toast('已保存到「我的指令」,可在那里编辑');
278
+ } catch (e) { toast('保存失败:' + e.message); }
279
+ }
280
+
116
281
  function renderAutoGrid(list) {
117
282
  const grid = document.getElementById('autoGrid');
118
283
  if (!list.length) { renderEmpty(grid, t('empty.no_automations')); return; }
119
- grid.innerHTML = list.map((a) => '<div class="tile" data-id="' + a.id + '"><div class="icon">⚡</div><div class="title">' + escapeHtml(a.name) + '</div><div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div><div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>').join('');
284
+ grid.innerHTML = list.map((a) => {
285
+ const icon = a.icon || '⚡';
286
+ const category = a.category ? ' · ' + escapeHtml(a.category) : '';
287
+ return '<div class="tile" data-id="' + a.id + '">'
288
+ + '<div class="icon">' + icon + '</div>'
289
+ + '<div class="title">' + escapeHtml(a.name) + category + '</div>'
290
+ + '<div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div>'
291
+ + '<div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>';
292
+ }).join('');
120
293
  grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
121
294
  grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
122
295
  }
123
296
 
297
+ /* ========== 节点模板 & 自定义来源模板 ========== */
298
+ function renderNodeTpl(list) {
299
+ const grid = document.getElementById('nodeTplGrid');
300
+ const title = document.getElementById('nodeTplTitle');
301
+ if (!grid) return;
302
+ if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
303
+ if (title) title.style.display = '';
304
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
305
+ bindTplCards(grid);
306
+ }
307
+ function renderCustomTpl(list) {
308
+ const grid = document.getElementById('customTplGrid');
309
+ const title = document.getElementById('customTplTitle');
310
+ if (!grid) return;
311
+ if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
312
+ if (title) title.style.display = '';
313
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
314
+ bindTplCards(grid);
315
+ }
316
+
317
+ /* ========== 节点管理渲染 ========== */
318
+ function renderNodesGrid(list) {
319
+ const grid = document.getElementById('nodesGrid');
320
+ if (!grid) return;
321
+ window._nodesCache = list;
322
+ if (!list.length) { grid.innerHTML = '<div class="empty">还没有添加节点,点击上方「添加节点」连接外部服务</div>'; return; }
323
+ grid.innerHTML = list.map((n) => {
324
+ const statusColor = n.status === 'connected' ? 'var(--ok)' : n.status === 'error' ? 'var(--err)' : 'var(--muted)';
325
+ const statusText = n.status === 'connected' ? '已连接' : n.status === 'error' ? '连接失败' : '未连接';
326
+ const caps = (n.capabilities || []).map((c) => c === 'templates' ? '模板' : c === 'skills' ? '插件' : '办公').join('、');
327
+ return '<div class="tile node-tile" data-id="' + n.id + '">'
328
+ + '<div class="icon">' + (n.type === 'http' ? '🌐' : n.type === 'local' ? '📁' : '📦') + '</div>'
329
+ + '<div class="title">' + escapeHtml(n.name) + ' <span style="font-size:11px;color:' + statusColor + ';">● ' + statusText + '</span></div>'
330
+ + '<div class="desc">' + escapeHtml(n.url).slice(0, 50) + '…<br>能力:' + caps + (n.lastSyncAt ? '<br>上次同步:' + fmtTime(n.lastSyncAt) : '') + (n.lastError ? '<br style="color:var(--err);">错误:' + escapeHtml(n.lastError) : '') + '</div>'
331
+ + '<div class="ops" style="flex-wrap:wrap;">'
332
+ + '<button class="run node-sync" data-id="' + n.id + '">🔄 同步</button>'
333
+ + '<button class="ghost node-test" data-id="' + n.id + '">测试</button>'
334
+ + '<button class="ghost node-edit" data-id="' + n.id + '">编辑</button>'
335
+ + '<button class="ghost del node-del" data-id="' + n.id + '">删除</button>'
336
+ + '</div></div>';
337
+ }).join('');
338
+ grid.querySelectorAll('button.node-sync').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); syncNode(b.getAttribute('data-id')); }));
339
+ grid.querySelectorAll('button.node-test').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); testNode(b.getAttribute('data-id')); }));
340
+ grid.querySelectorAll('button.node-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editNode(b.getAttribute('data-id')); }));
341
+ grid.querySelectorAll('button.node-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此节点?')) deleteNode(b.getAttribute('data-id')); }));
342
+ }
343
+
344
+ /* ========== 自定义来源渲染 ========== */
345
+ function renderSourcesGrid(list) {
346
+ const grid = document.getElementById('sourcesGrid');
347
+ if (!grid) return;
348
+ window._sourcesCache = list;
349
+ if (!list.length) { grid.innerHTML = '<div class="empty">还没有自定义来源,点击上方「自定义来源」添加远程数据源</div>'; return; }
350
+ const typeMap = { templates: '模板', skills: '插件', office: '办公' };
351
+ grid.innerHTML = list.map((s) => '<div class="tile" data-id="' + s.id + '">'
352
+ + '<div class="icon">🔗</div>'
353
+ + '<div class="title">' + escapeHtml(s.name) + ' <span style="font-size:11px;color:var(--brand);">' + (typeMap[s.type] || s.type) + '</span></div>'
354
+ + '<div class="desc">' + escapeHtml(s.url).slice(0, 60) + '…<br>状态:' + (s.enabled ? '已启用' : '已禁用') + '</div>'
355
+ + '<div class="ops">'
356
+ + '<button class="ghost source-edit" data-id="' + s.id + '">编辑</button>'
357
+ + '<button class="ghost del source-del" data-id="' + s.id + '">删除</button>'
358
+ + '</div></div>').join('');
359
+ grid.querySelectorAll('button.source-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editSource(b.getAttribute('data-id')); }));
360
+ grid.querySelectorAll('button.source-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此来源?')) deleteSource(b.getAttribute('data-id')); }));
361
+ }
362
+
124
363
  function renderWorkspaceBar() {
125
364
  const el = document.getElementById('workspaceCwdBottom');
126
365
  if (el) el.textContent = state.workspaceDir || '(未设置)';
@@ -313,7 +552,18 @@
313
552
  function toggleDirectMode() {
314
553
  state.directMode = !state.directMode;
315
554
  document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
316
- toast(state.directMode ? '已开启「直接操作电脑」模式' : '已关闭「直接操作电脑」模式');
555
+ const computerTab = document.getElementById('computerTab');
556
+ if (computerTab) {
557
+ computerTab.style.display = state.directMode ? '' : 'none';
558
+ if (state.directMode) {
559
+ // 切换到电脑操作标签页
560
+ document.querySelectorAll('.right-tab').forEach(t => t.classList.remove('active'));
561
+ computerTab.classList.add('active');
562
+ document.querySelectorAll('.preview-panel').forEach(p => p.classList.remove('active'));
563
+ document.getElementById('computerPanel')?.classList.add('active');
564
+ }
565
+ }
566
+ toast(state.directMode ? '已开启「电脑操作」模式,可以用语言控制电脑' : '已关闭「电脑操作」模式');
317
567
  }
318
568
 
319
569
  function previewImage(path) {
@@ -448,6 +698,14 @@
448
698
  return '';
449
699
  }
450
700
 
701
+ // 消息操作按钮(复制、创建文档)
702
+ function renderMsgActions() {
703
+ return '<div class="msg-actions">'
704
+ + '<button class="msg-action-btn" data-action="copy" title="复制整条消息">📋 复制</button>'
705
+ + '<button class="msg-action-btn" data-action="create-doc" title="创建文档">📄 建文档</button>'
706
+ + '</div>';
707
+ }
708
+
451
709
  function renderTaskThread(task) {
452
710
  const box = document.getElementById('messages');
453
711
  const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
@@ -461,82 +719,95 @@
461
719
  const conv = Array.isArray(task.conversation) ? task.conversation : [];
462
720
  const steps = Array.isArray(task.steps) ? task.steps : [];
463
721
 
464
- if (conv.length > 0) {
465
- let i = 0;
466
- while (i < conv.length) {
467
- const m = conv[i];
468
- if (!m) { i++; continue; }
469
- // 过滤 tool 消息和 system 消息,用户只看得到人和助手的对话
470
- if (m.role === 'tool' || m.role === 'system') { i++; continue; }
471
-
472
- if (m.role === 'user') {
473
- html += '<div class="msg user">' + linkifyArtifacts(m.content || '') + '</div>';
474
- i++;
475
- continue;
476
- }
722
+ // 统一处理:先显示用户消息,再从 steps 提取思考过程(实时更新),最后显示最终回复
723
+ // 这样确保思考过程能实时显示,不会因为 conversation 有内容就跳过 steps
477
724
 
478
- if (m.role === 'assistant') {
479
- // 收集连续 assistant 消息
480
- const assistantMsgs = [];
481
- while (i < conv.length && conv[i] && conv[i].role === 'assistant') {
482
- assistantMsgs.push(conv[i]);
483
- i++;
484
- }
485
- const hasToolCalls = assistantMsgs.some(msg => (msg.toolCalls || []).length > 0);
486
- const allText = assistantMsgs.map(msg => (msg.content || '').trim()).filter(Boolean).join('\n\n');
487
- if (hasToolCalls && allText) {
488
- // 有工具调用且有思考文本 → 展示思考过程(直接显示,不折叠)
489
- html += renderThinkingProcess(assistantMsgs);
490
- } else if (allText) {
491
- // 纯文本回复 → 普通气泡,这就是最终回复
492
- html += '<div class="msg assistant">'
493
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(allText) + '</div>'
494
- + '</div>';
495
- }
496
- } else {
497
- i++;
725
+ // 1. 显示用户消息
726
+ let userMsgShown = false;
727
+ if (conv.length > 0) {
728
+ for (const m of conv) {
729
+ if (m && m.role === 'user' && m.content) {
730
+ html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(m.content) + '</div>';
731
+ userMsgShown = true;
732
+ break; // 只显示第一条用户消息,后续的在多轮对话中处理
498
733
  }
499
734
  }
500
- } else if (steps.length > 0) {
501
- // 任务运行中 conversation 还没生成,从 steps 里提取思考文本实时显示
502
- if (task.goal) {
503
- html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
735
+ }
736
+ if (!userMsgShown && task.goal) {
737
+ html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(task.goal) + '</div>';
738
+ }
739
+
740
+ // 2. 从 steps 提取思考过程(实时更新,这是关键)
741
+ const thinkingTexts = [];
742
+ for (const s of steps) {
743
+ if (s.type === 'model.response' && s.data && s.data.content && s.data.content.trim()) {
744
+ thinkingTexts.push(s.data.content.trim());
745
+ } else if (s.type === 'self-heal') {
746
+ thinkingTexts.push('刚才遇到点小问题,我调整一下思路再试试。');
504
747
  }
505
- const thinkingTexts = [];
506
- for (const s of steps) {
507
- if (s.type === 'model.response' && s.data && s.data.content && s.data.content.trim()) {
508
- thinkingTexts.push(s.data.content.trim());
509
- } else if (s.type === 'self-heal') {
510
- thinkingTexts.push('刚才遇到点小问题,我调整一下思路再试试。');
748
+ }
749
+ if (thinkingTexts.length > 0) {
750
+ html += '<div class="msg assistant">'
751
+ + renderMsgActions()
752
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(thinkingTexts.join('\n\n')) + '</div>'
753
+ + '</div>';
754
+ }
755
+
756
+ // 3. 如果 conversation 中有完整的 assistant 文本回复(任务完成后),也显示出来
757
+ if (conv.length > 0) {
758
+ const assistantTexts = [];
759
+ for (const m of conv) {
760
+ if (m && m.role === 'assistant' && m.content && m.content.trim() && !(m.toolCalls && m.toolCalls.length > 0)) {
761
+ assistantTexts.push(m.content.trim());
511
762
  }
512
763
  }
513
- if (thinkingTexts.length > 0) {
514
- html += '<div class="msg assistant" style="opacity:0.9;">'
515
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(thinkingTexts.join('\n\n')) + '</div>'
516
- + '</div>';
764
+ // 避免和 steps 重复:只显示 steps 中没有的最终回复
765
+ if (assistantTexts.length > 0) {
766
+ const lastAssistantText = assistantTexts[assistantTexts.length - 1];
767
+ const alreadyInSteps = thinkingTexts.some(t => t === lastAssistantText || lastAssistantText.includes(t) || t.includes(lastAssistantText));
768
+ if (!alreadyInSteps && lastAssistantText !== finalAnswer) {
769
+ html += '<div class="msg assistant">'
770
+ + renderMsgActions()
771
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(lastAssistantText) + '</div>'
772
+ + '</div>';
773
+ }
517
774
  }
518
- } else if (task.goal) {
519
- // 首轮尚未产生对话流时,至少呈现用户原始指令
520
- html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
521
775
  }
522
776
 
523
777
  // 终态:如果对话流里没有最终回复(旧任务或被中断),再显示 finalAnswer
524
778
  const hasFinalInConv = conv.some(m => m.role === 'assistant' && !(m.toolCalls || []).length && (m.content || '').trim());
525
779
  if (task.status === 'done' && finalAnswer && !hasFinalInConv) {
526
780
  html += '<div class="msg assistant">'
781
+ + renderMsgActions()
527
782
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
528
783
  + '</div>';
529
784
  } else if (task.status === 'failed') {
530
785
  if (finalAnswer && !hasFinalInConv) {
531
786
  html += '<div class="msg assistant">'
787
+ + renderMsgActions()
532
788
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
533
789
  + '</div>';
534
790
  }
535
791
  html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
536
792
  } else if (task.status === 'running') {
793
+ // 计算已运行时间,让用户知道等了多久
794
+ let waitTip = '';
795
+ if (task.createdAt) {
796
+ const elapsed = Math.floor((Date.now() - new Date(task.createdAt).getTime()) / 1000);
797
+ if (elapsed > 5) {
798
+ const mins = Math.floor(elapsed / 60);
799
+ const secs = elapsed % 60;
800
+ const timeStr = mins > 0 ? `${mins}分${secs}秒` : `${secs}秒`;
801
+ waitTip = `<span style="color:var(--muted);font-size:12px;margin-left:8px;">已等待 ${timeStr}</span>`;
802
+ }
803
+ if (elapsed > 60) {
804
+ waitTip += '<div style="color:var(--muted);font-size:12px;margin-top:6px;line-height:1.5;">模型正在深度思考或执行复杂操作,请耐心等待...<br>如果等待超过3分钟,可以尝试点击「停止」后重新提交。</div>';
805
+ }
806
+ }
537
807
  html += '<div class="thinking-indicator">'
538
808
  + '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
539
809
  + '<span>思考中...</span>'
810
+ + waitTip
540
811
  + '</div>';
541
812
  } else if (task.status === 'queued') {
542
813
  html += '<div class="msg sys">任务已入队,等待执行…</div>';
@@ -578,6 +849,7 @@
578
849
  if (!textContent) return '';
579
850
  // 用普通 assistant 气泡样式展示思考过程,让用户看到模型在想什么
580
851
  return '<div class="msg assistant thinking-msg" style="opacity:0.85;">'
852
+ + renderMsgActions()
581
853
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(textContent) + '</div>'
582
854
  + '</div>';
583
855
  }
@@ -714,6 +986,29 @@
714
986
  // 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
715
987
  const isChat = nav === 'chat';
716
988
  document.getElementById('chatFootbar').style.display = isChat ? 'flex' : 'none';
989
+ // 切换到对话视图时,如果当前任务已完成或不存在,自动开启新对话
990
+ if (isChat) {
991
+ const cur = state.tasks.find((t) => t.id === state.currentTaskId);
992
+ if (!cur || cur.status === 'done' || cur.status === 'failed') {
993
+ startNewChat();
994
+ }
995
+ }
996
+ }
997
+
998
+ // 开启新对话:清空当前任务选中,显示空状态,聚焦输入框
999
+ function startNewChat() {
1000
+ state.currentTaskId = null;
1001
+ renderTaskThread(null);
1002
+ renderTaskDetail(null);
1003
+ renderSidebarTaskList();
1004
+ // 显示任务列表区域
1005
+ const taskListSection = document.getElementById('taskListSection');
1006
+ if (taskListSection) taskListSection.style.display = 'block';
1007
+ // 聚焦输入框
1008
+ setTimeout(() => {
1009
+ const input = document.getElementById('goalInput');
1010
+ if (input) input.focus();
1011
+ }, 100);
717
1012
  }
718
1013
 
719
1014
  function updateUserBar() {