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.
- package/CHANGELOG.md +46 -467
- package/dist/agent/orchestrator.js +33 -6
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/prompts.js +20 -0
- 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/version.js +1 -1
- 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 +850 -123
- package/dist/web/public/js/ui.js +347 -52
- package/dist/web/server.js +897 -53
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +3 -3
- 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 +1 -1
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;
|
|
@@ -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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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() {
|