feihong-code 0.6.0 → 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.
- package/CHANGELOG.md +46 -446
- package/dist/agent/orchestrator.js +33 -6
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parallel-orchestrator.js +6 -3
- package/dist/agent/parallel-orchestrator.js.map +1 -1
- package/dist/agent/prompts.js +46 -16
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/self-heal.js +2 -2
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/cli/repl.js +10 -9
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.js +26 -23
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/shared/concurrency.js +80 -0
- package/dist/shared/concurrency.js.map +1 -0
- package/dist/web/public/css/style.css +132 -7
- package/dist/web/public/index.html +193 -9
- package/dist/web/public/js/api.js +76 -3
- package/dist/web/public/js/app.js +858 -120
- package/dist/web/public/js/ui.js +367 -140
- package/dist/web/public/js/utils.js +44 -0
- package/dist/web/server.js +897 -53
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +14 -6
- package/dist/web/task-queue.js.map +1 -1
- package/docs/BENCHMARK_REPORT_en.md +329 -0
- package/docs/BENCHMARK_REPORT_zh.md +329 -0
- package/docs/TECHNICAL_SPEC_en.md +292 -0
- package/docs/TECHNICAL_SPEC_zh.md +292 -0
- package/docs/UPDATE_NOTES_en.md +159 -0
- package/docs/UPDATE_NOTES_zh.md +159 -0
- package/electron/main.js +404 -0
- package/package.json +62 -3
- package/tool-schema.json +2 -2
package/dist/web/public/js/ui.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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;
|
|
@@ -456,81 +714,103 @@
|
|
|
456
714
|
box.innerHTML = '<div class="msg sys">从左侧「任务列表」选择历史任务查看对话,或在下方输入指令发起新任务。刷新页面会自动清空当前对话视图,历史对话已保存在任务列表中。</div>';
|
|
457
715
|
return;
|
|
458
716
|
}
|
|
459
|
-
const steps = task.steps || [];
|
|
460
717
|
const finalAnswer = (task.result && task.result.finalAnswer || '').trim();
|
|
461
718
|
let html = '';
|
|
462
719
|
const conv = Array.isArray(task.conversation) ? task.conversation : [];
|
|
720
|
+
const steps = Array.isArray(task.steps) ? task.steps : [];
|
|
721
|
+
|
|
722
|
+
// 统一处理:先显示用户消息,再从 steps 提取思考过程(实时更新),最后显示最终回复
|
|
723
|
+
// 这样确保思考过程能实时显示,不会因为 conversation 有内容就跳过 steps
|
|
463
724
|
|
|
725
|
+
// 1. 显示用户消息
|
|
726
|
+
let userMsgShown = false;
|
|
464
727
|
if (conv.length > 0) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// 过滤 tool 消息
|
|
471
|
-
if (m.role === 'tool') { i++; continue; }
|
|
472
|
-
|
|
473
|
-
if (m.role === 'user') {
|
|
474
|
-
html += '<div class="msg user">' + linkifyArtifacts(m.content || '') + '</div>';
|
|
475
|
-
i++;
|
|
476
|
-
continue;
|
|
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; // 只显示第一条用户消息,后续的在多轮对话中处理
|
|
477
733
|
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!userMsgShown && task.goal) {
|
|
737
|
+
html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(task.goal) + '</div>';
|
|
738
|
+
}
|
|
478
739
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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('刚才遇到点小问题,我调整一下思路再试试。');
|
|
747
|
+
}
|
|
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
|
+
}
|
|
489
755
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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());
|
|
762
|
+
}
|
|
763
|
+
}
|
|
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>';
|
|
499
773
|
}
|
|
500
774
|
}
|
|
501
|
-
} else if (task.goal) {
|
|
502
|
-
// 首轮尚未产生对话流时,至少呈现用户原始指令
|
|
503
|
-
html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
|
|
504
775
|
}
|
|
505
776
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
+ '<button onclick="bubbleActions(event, ' + safeText + ')" title="复制/编辑/分享/创建文档">⚙️ 操作</button>'
|
|
514
|
-
+ '</div>'
|
|
515
|
-
+ '<div style="margin-top:4px;cursor:text;user-select:text;">' + renderMarkdown(task.result.finalAnswer) + '</div>'
|
|
516
|
-
+ '</div>';
|
|
517
|
-
}
|
|
777
|
+
// 终态:如果对话流里没有最终回复(旧任务或被中断),再显示 finalAnswer
|
|
778
|
+
const hasFinalInConv = conv.some(m => m.role === 'assistant' && !(m.toolCalls || []).length && (m.content || '').trim());
|
|
779
|
+
if (task.status === 'done' && finalAnswer && !hasFinalInConv) {
|
|
780
|
+
html += '<div class="msg assistant">'
|
|
781
|
+
+ renderMsgActions()
|
|
782
|
+
+ '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
|
|
783
|
+
+ '</div>';
|
|
518
784
|
} else if (task.status === 'failed') {
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
+ '<div
|
|
523
|
-
+ '
|
|
785
|
+
if (finalAnswer && !hasFinalInConv) {
|
|
786
|
+
html += '<div class="msg assistant">'
|
|
787
|
+
+ renderMsgActions()
|
|
788
|
+
+ '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
|
|
789
|
+
+ '</div>';
|
|
524
790
|
}
|
|
525
|
-
html += '<div class="msg assistant error-msg"
|
|
791
|
+
html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
|
|
526
792
|
} else if (task.status === 'running') {
|
|
527
|
-
//
|
|
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
|
+
}
|
|
528
807
|
html += '<div class="thinking-indicator">'
|
|
529
808
|
+ '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
|
|
530
809
|
+ '<span>思考中...</span>'
|
|
810
|
+
+ waitTip
|
|
531
811
|
+ '</div>';
|
|
532
812
|
} else if (task.status === 'queued') {
|
|
533
|
-
html += '<div class="msg sys"
|
|
813
|
+
html += '<div class="msg sys">任务已入队,等待执行…</div>';
|
|
534
814
|
}
|
|
535
815
|
box.innerHTML = html;
|
|
536
816
|
if (nearBottom) box.scrollTop = box.scrollHeight;
|
|
@@ -559,95 +839,19 @@
|
|
|
559
839
|
}
|
|
560
840
|
|
|
561
841
|
function renderThinkingProcess(msgs) {
|
|
562
|
-
//
|
|
563
|
-
const allToolCalls = [];
|
|
842
|
+
// 收集所有思考文本,直接展示,不折叠、不显示工具调用
|
|
564
843
|
const textParts = [];
|
|
565
844
|
for (const m of msgs) {
|
|
566
|
-
const calls = m.toolCalls || [];
|
|
567
|
-
allToolCalls.push(...calls);
|
|
568
845
|
const text = (m.content || '').trim();
|
|
569
846
|
if (text) textParts.push(text);
|
|
570
847
|
}
|
|
571
848
|
const textContent = textParts.join('\n\n');
|
|
572
|
-
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
// 工具名称 → 友好中文描述
|
|
580
|
-
const toolLabels = {
|
|
581
|
-
'read': '读取文件', 'write': '写入文件', 'edit': '编辑文件',
|
|
582
|
-
'search': '搜索文件', 'grep': '搜索文件', 'glob': '搜索文件',
|
|
583
|
-
'run': '执行命令', 'execute': '执行命令', 'command': '执行命令',
|
|
584
|
-
'web_search': '搜索网络', 'web_fetch': '访问网页',
|
|
585
|
-
'list': '列出目录', 'ls': '列出目录',
|
|
586
|
-
'delete': '删除文件', 'remove': '删除文件',
|
|
587
|
-
};
|
|
588
|
-
const summaryParts = Object.entries(toolCounts).map(([name, count]) => {
|
|
589
|
-
const label = toolLabels[name] || name;
|
|
590
|
-
return label + ' ' + count + ' 次';
|
|
591
|
-
});
|
|
592
|
-
const summaryText = summaryParts.join('、');
|
|
593
|
-
|
|
594
|
-
// 工具调用 → 带图标的一行描述
|
|
595
|
-
const stepIcons = {
|
|
596
|
-
'read': '📖', 'write': '✏️', 'edit': '✏️',
|
|
597
|
-
'search': '🔍', 'grep': '🔍', 'glob': '🔍',
|
|
598
|
-
'run': '⚡', 'execute': '⚡', 'command': '⚡',
|
|
599
|
-
'web_search': '🌐', 'web_fetch': '🌐',
|
|
600
|
-
'list': '📂', 'ls': '📂',
|
|
601
|
-
'delete': '🗑️', 'remove': '🗑️',
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
let html = '<div class="msg assistant thinking-msg">';
|
|
605
|
-
|
|
606
|
-
// 可折叠的思考过程头部
|
|
607
|
-
html += '<div class="thinking-header" onclick="toggleThinking(this)">';
|
|
608
|
-
html += '<span class="thinking-arrow">▶</span>';
|
|
609
|
-
html += '<span class="thinking-label">思考过程</span>';
|
|
610
|
-
if (summaryText) {
|
|
611
|
-
html += '<span class="thinking-summary"> · ' + escapeHtml(summaryText) + '</span>';
|
|
612
|
-
}
|
|
613
|
-
html += '</div>';
|
|
614
|
-
|
|
615
|
-
// 可折叠的思考过程主体
|
|
616
|
-
html += '<div class="thinking-body" style="display:none;">';
|
|
617
|
-
for (const tc of allToolCalls) {
|
|
618
|
-
const name = tc.name || 'unknown';
|
|
619
|
-
const icon = stepIcons[name] || '🔧';
|
|
620
|
-
const label = toolLabels[name] || name;
|
|
621
|
-
const args = tc.arguments ? JSON.stringify(tc.arguments, null, 2) : '';
|
|
622
|
-
// 从参数中提取关键信息显示
|
|
623
|
-
let brief = escapeHtml(label);
|
|
624
|
-
if (tc.arguments) {
|
|
625
|
-
const arg = tc.arguments;
|
|
626
|
-
if (arg.path) brief += ' <code>' + escapeHtml(arg.path) + '</code>';
|
|
627
|
-
else if (arg.query) brief += ' <code>' + escapeHtml(String(arg.query).slice(0, 60)) + '</code>';
|
|
628
|
-
else if (arg.url) brief += ' <code>' + escapeHtml(arg.url) + '</code>';
|
|
629
|
-
else if (arg.command) brief += ' <code>' + escapeHtml(String(arg.command).slice(0, 60)) + '</code>';
|
|
630
|
-
}
|
|
631
|
-
html += '<div class="thinking-step">';
|
|
632
|
-
html += '<span class="thinking-step-icon">' + icon + '</span>';
|
|
633
|
-
html += '<span class="thinking-step-text">' + brief + '</span>';
|
|
634
|
-
if (args) {
|
|
635
|
-
html += '<span class="thinking-step-detail" onclick="toggleArgs(this)">查看参数</span>';
|
|
636
|
-
}
|
|
637
|
-
html += '</div>';
|
|
638
|
-
if (args) {
|
|
639
|
-
html += '<pre class="thinking-args">' + escapeHtml(args) + '</pre>';
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
html += '</div>';
|
|
643
|
-
|
|
644
|
-
// 回复内容
|
|
645
|
-
if (textContent) {
|
|
646
|
-
html += '<div class="thinking-reply">' + renderMarkdown(textContent) + '</div>';
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
html += '</div>';
|
|
650
|
-
return html;
|
|
849
|
+
if (!textContent) return '';
|
|
850
|
+
// 用普通 assistant 气泡样式展示思考过程,让用户看到模型在想什么
|
|
851
|
+
return '<div class="msg assistant thinking-msg" style="opacity:0.85;">'
|
|
852
|
+
+ renderMsgActions()
|
|
853
|
+
+ '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(textContent) + '</div>'
|
|
854
|
+
+ '</div>';
|
|
651
855
|
}
|
|
652
856
|
|
|
653
857
|
function toggleArgs(el) {
|
|
@@ -782,6 +986,29 @@
|
|
|
782
986
|
// 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
|
|
783
987
|
const isChat = nav === 'chat';
|
|
784
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);
|
|
785
1012
|
}
|
|
786
1013
|
|
|
787
1014
|
function updateUserBar() {
|