feihong-code 0.2.3 → 0.6.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/.github/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -37
- package/.github/ISSUE_TEMPLATE/config.yml +14 -14
- package/.github/ISSUE_TEMPLATE/feature_request.md +28 -28
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -46
- package/.github/SECURITY.md +67 -67
- package/.github/dependabot.yml +16 -0
- package/.github/workflows/ci.yml +1 -1
- package/AGENT-GUIDE.md +382 -237
- package/CHANGELOG.md +392 -0
- package/CODE_OF_CONDUCT.md +56 -56
- package/CONTRIBUTING.md +68 -68
- package/LICENSE +22 -22
- package/README.md +586 -522
- package/README.self-evolve.md +274 -0
- package/dist/agent/code-review.js +2 -0
- package/dist/agent/code-review.js.map +1 -1
- package/dist/agent/code-writer.js +74 -18
- package/dist/agent/code-writer.js.map +1 -1
- package/dist/agent/context-compactor.js +13 -4
- package/dist/agent/context-compactor.js.map +1 -1
- package/dist/agent/experience.js +319 -84
- package/dist/agent/experience.js.map +1 -1
- package/dist/agent/orchestrator.js +233 -71
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/planner.js +30 -7
- package/dist/agent/planner.js.map +1 -1
- package/dist/agent/prompts.js +9 -0
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/quality-gate.js +10 -4
- package/dist/agent/quality-gate.js.map +1 -1
- package/dist/agent/repo-context.js +181 -0
- package/dist/agent/repo-context.js.map +1 -0
- package/dist/agent/repo-reader.js +92 -99
- package/dist/agent/repo-reader.js.map +1 -1
- package/dist/agent/self-heal.js +80 -60
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/agent/self-improver.js +65 -61
- package/dist/agent/self-improver.js.map +1 -1
- package/dist/agent/subagent-summary.js +31 -0
- package/dist/agent/subagent-summary.js.map +1 -0
- package/dist/agent/subagent.js +52 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/symbol-index.js +160 -0
- package/dist/agent/symbol-index.js.map +1 -0
- package/dist/agent/team.js +193 -0
- package/dist/agent/team.js.map +1 -0
- package/dist/cli/commands.js +124 -143
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/index.js +113 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.js +82 -7
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.js +600 -95
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui.js +145 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/version.js +1 -1
- package/dist/enterprise/audit.js +145 -23
- package/dist/enterprise/audit.js.map +1 -1
- package/dist/enterprise/index.js +14 -11
- package/dist/enterprise/index.js.map +1 -1
- package/dist/enterprise/policy.js +22 -10
- package/dist/enterprise/policy.js.map +1 -1
- package/dist/harness/executor.js +127 -0
- package/dist/harness/executor.js.map +1 -0
- package/dist/harness/harness.js +87 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.js +31 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/loader.js +138 -0
- package/dist/harness/loader.js.map +1 -0
- package/dist/harness/reporter.js +34 -0
- package/dist/harness/reporter.js.map +1 -0
- package/dist/harness/types.js +10 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/verifier.js +48 -0
- package/dist/harness/verifier.js.map +1 -0
- package/dist/hello.js +14 -0
- package/dist/hello.js.map +1 -0
- package/dist/memory/auto-summarize.js +208 -0
- package/dist/memory/auto-summarize.js.map +1 -0
- package/dist/memory/index.js +228 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/models/model-router.js +100 -27
- package/dist/models/model-router.js.map +1 -1
- package/dist/models/model.dto.js +25 -3
- package/dist/models/model.dto.js.map +1 -1
- package/dist/models/providers/ollama.provider.js +12 -0
- package/dist/models/providers/ollama.provider.js.map +1 -1
- package/dist/models/providers/openai-compatible.provider.js +14 -1
- package/dist/models/providers/openai-compatible.provider.js.map +1 -1
- package/dist/plugins/plugin-loader.js +179 -0
- package/dist/plugins/plugin-loader.js.map +1 -0
- package/dist/runtime/event-log.js.map +1 -1
- package/dist/runtime/hooks.js +80 -0
- package/dist/runtime/hooks.js.map +1 -0
- package/dist/self-evolve/hook.js +60 -0
- package/dist/self-evolve/hook.js.map +1 -0
- package/dist/self-evolve/hook.ts +80 -0
- package/dist/self-evolve/manager.d.ts +8 -0
- package/dist/self-evolve/manager.js +401 -0
- package/dist/shared/config.js +35 -3
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/errors.js +6 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/i18n.js +535 -0
- package/dist/shared/i18n.js.map +1 -0
- package/dist/shared/secure-store.js +117 -0
- package/dist/shared/secure-store.js.map +1 -0
- package/dist/skills/grill.js +2 -1
- package/dist/skills/grill.js.map +1 -1
- package/dist/skills/self-heal.js +73 -0
- package/dist/skills/self-heal.js.map +1 -0
- package/dist/skills/skill-loader.js +131 -0
- package/dist/skills/skill-loader.js.map +1 -0
- package/dist/skills/skill-market.js +195 -0
- package/dist/skills/skill-market.js.map +1 -0
- package/dist/tools/analysis/code-analyzer.js +45 -18
- package/dist/tools/analysis/code-analyzer.js.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp/index.js +84 -0
- package/dist/tools/mcp/index.js.map +1 -0
- package/dist/tools/mcp/mcp-client.js +194 -0
- package/dist/tools/mcp/mcp-client.js.map +1 -0
- package/dist/tools/sandbox.js +126 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/shell/exec.js +72 -3
- package/dist/tools/shell/exec.js.map +1 -1
- package/dist/tools/shell/run-shell.tool.js +37 -7
- package/dist/tools/shell/run-shell.tool.js.map +1 -1
- package/dist/tools/skills/load-skill.tool.js +36 -0
- package/dist/tools/skills/load-skill.tool.js.map +1 -0
- package/dist/tools/tool.interface.js.map +1 -1
- package/dist/tools/tool.registry.js +69 -1
- package/dist/tools/tool.registry.js.map +1 -1
- package/dist/tools/web/web.tool.js +139 -0
- package/dist/tools/web/web.tool.js.map +1 -0
- package/dist/web/auth.js +178 -3
- package/dist/web/auth.js.map +1 -1
- package/dist/web/channels.js +171 -0
- package/dist/web/channels.js.map +1 -0
- package/dist/web/public/css/style.css +1546 -0
- package/dist/web/public/index.html +804 -37
- package/dist/web/public/js/api.js +309 -0
- package/dist/web/public/js/app.js +1472 -0
- package/dist/web/public/js/ui.js +832 -0
- package/dist/web/public/js/utils.js +170 -0
- package/dist/web/server.js +937 -11
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +470 -0
- package/dist/web/task-queue.js.map +1 -0
- package/dist/web/web-config.js +143 -0
- package/dist/web/web-config.js.map +1 -0
- package/docs/App/344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +299 -0
- package/docs/App/346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +554 -0
- package/docs/Deployment_Guide_EN.md +288 -0
- package/docs/SELF-EVOLVE-GUIDE.md +313 -0
- package/docs/Technical_Manual_EN.md +216 -0
- package/docs/User_Manual_EN.md +314 -0
- package/docs/error-codes.md +198 -0
- package/docs/screenshots/cli-demo.png +0 -0
- package/docs/screenshots/feature-comparison.png +0 -0
- package/docs/screenshots/web-console.png +0 -0
- package/docs/self-evolve-implementation.md +165 -0
- package/docs/self-evolve.md +166 -0
- package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +614 -614
- package/docs//344/274/201/344/270/232/351/203/250/347/275/262/344/270/216/345/220/210/350/247/204.md +267 -267
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +318 -358
- package/docs//345/270/270/350/247/201/351/227/256/351/242/230/344/270/216/346/225/205/351/232/234/346/216/222/346/237/245.md +130 -130
- package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +216 -303
- package/docs//346/236/266/346/236/204/344/270/216API.md +238 -238
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +196 -196
- package/docs//351/203/250/347/275/262/346/214/207/345/215/227.md +165 -165
- package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +288 -0
- package/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +127 -127
- package/docs//351/241/265/351/235/242/345/212/237/350/203/275/345/244/215/347/233/230/344/270/216/345/206/222/347/203/237/346/265/213/350/257/225/346/212/245/345/221/212.html +117 -0
- package/package.json +108 -109
- package/tool-schema.json +117 -46
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI 渲染模块:DOM 渲染与交互,依赖 utils 和 api
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function initMemoryUI() {
|
|
6
|
+
// 设置默认日期为今天
|
|
7
|
+
const today = new Date().toISOString().split('T')[0];
|
|
8
|
+
document.getElementById('memDateInput').value = today;
|
|
9
|
+
loadMemoryContent(today);
|
|
10
|
+
|
|
11
|
+
// 事件只绑定一次(多次进出记忆页会累积监听器,导致一次点击多次请求)
|
|
12
|
+
if (!window._memUIInited) {
|
|
13
|
+
window._memUIInited = true;
|
|
14
|
+
|
|
15
|
+
// Tab 切换
|
|
16
|
+
document.querySelectorAll('.mem-tab').forEach(tab => {
|
|
17
|
+
tab.addEventListener('click', () => {
|
|
18
|
+
document.querySelectorAll('.mem-tab').forEach(t => t.classList.remove('active'));
|
|
19
|
+
document.querySelectorAll('.mem-panel').forEach(p => p.classList.toggle('active', false));
|
|
20
|
+
tab.classList.add('active');
|
|
21
|
+
const panelId = tab.getAttribute('data-memtab') + 'Panel';
|
|
22
|
+
document.getElementById(panelId)?.classList.add('active');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 查看按钮
|
|
27
|
+
document.getElementById('memLoadDay')?.addEventListener('click', () => {
|
|
28
|
+
const dateStr = document.getElementById('memDateInput').value;
|
|
29
|
+
if (dateStr) loadMemoryContent(dateStr);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 刷新按钮
|
|
33
|
+
document.getElementById('memRefreshBtn')?.addEventListener('click', () => {
|
|
34
|
+
loadMemoryStats();
|
|
35
|
+
const dateStr = document.getElementById('memDateInput').value;
|
|
36
|
+
if (dateStr) loadMemoryContent(dateStr);
|
|
37
|
+
loadLongTermMemory();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 立即总结按钮
|
|
41
|
+
document.getElementById('memSummarizeBtn')?.addEventListener('click', async () => {
|
|
42
|
+
const btn = document.getElementById('memSummarizeBtn');
|
|
43
|
+
btn.disabled = true;
|
|
44
|
+
btn.textContent = '⏳ 总结中...';
|
|
45
|
+
try {
|
|
46
|
+
const dateStr = document.getElementById('memDateInput').value || today;
|
|
47
|
+
const d = await api('/api/memory/summarize', 'POST', { date: dateStr });
|
|
48
|
+
toast('✅ ' + (d.message || '总结完成'));
|
|
49
|
+
loadMemoryStats();
|
|
50
|
+
loadSummaryHistory();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
toast('总结失败:' + e.message);
|
|
53
|
+
} finally {
|
|
54
|
+
btn.disabled = false;
|
|
55
|
+
btn.textContent = '✨ 立即总结';
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 每次进入都刷新数据(事件不重复绑定,数据实时刷新)
|
|
61
|
+
loadMemoryStats();
|
|
62
|
+
loadLongTermMemory();
|
|
63
|
+
loadSummaryHistory();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderMarketGrid() {
|
|
67
|
+
const grid = document.getElementById('marketGrid');
|
|
68
|
+
if (!state.market.length) { renderEmpty(grid, t('empty.no_skills')); return; }
|
|
69
|
+
grid.innerHTML = state.market.map((s) => {
|
|
70
|
+
const installed = state.installed.has(s.id);
|
|
71
|
+
const btn = installed
|
|
72
|
+
? '<button class="install" disabled style="opacity:.55;cursor:default;">✅ 已安装</button>'
|
|
73
|
+
: '<button class="install" data-id="' + encodeURIComponent(s.id) + '" data-name="' + encodeURIComponent(s.name) + '" data-source="' + s.source + '">安装</button>';
|
|
74
|
+
return '<div class="tile"><div class="icon">' + (s.source === 'clawhub' ? '🧩' : '🛠') + '</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description).slice(0, 70) + '…<br><span class="muted">' + (s.source === 'clawhub' ? 'ClawHub' : 'Agent-Foundry') + ' · 下载 ' + (s.downloads || 0) + '</span></div><div class="ops">' + btn + '</div></div>';
|
|
75
|
+
}).join('');
|
|
76
|
+
grid.querySelectorAll('button.install').forEach((b) => b.addEventListener('click', (e) => {
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
installSkill(b.getAttribute('data-id'), b.getAttribute('data-name'), b.getAttribute('data-source'));
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bindTplCards(grid) {
|
|
83
|
+
grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
const inp = document.getElementById('goalInput');
|
|
86
|
+
if (inp) inp.value = decodeURIComponent(b.getAttribute('data-goal'));
|
|
87
|
+
switchView('chat');
|
|
88
|
+
if (inp) inp.focus();
|
|
89
|
+
toast('已填入输入框,去发送吧');
|
|
90
|
+
}));
|
|
91
|
+
grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', async (e) => {
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
try { await api('/api/templates/' + b.getAttribute('data-id'), 'DELETE'); await loadTemplates(); toast('已删除'); }
|
|
94
|
+
catch (err) { toast('删除失败:' + err.message); }
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderUserTpl(list) {
|
|
99
|
+
const grid = document.getElementById('userTplGrid');
|
|
100
|
+
if (!list.length) { renderEmpty(grid, t('empty.no_templates')); return; }
|
|
101
|
+
grid.innerHTML = list.map((t) => tplCard(t, true)).join('');
|
|
102
|
+
bindTplCards(grid);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderBuiltin(list) {
|
|
106
|
+
const grid = document.getElementById('builtinGrid');
|
|
107
|
+
if (!list.length) { renderEmpty(grid, t('empty.no_builtin_templates')); return; }
|
|
108
|
+
grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
|
|
109
|
+
bindTplCards(grid);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function tplCard(t, deletable) {
|
|
113
|
+
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
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderAutoGrid(list) {
|
|
117
|
+
const grid = document.getElementById('autoGrid');
|
|
118
|
+
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('');
|
|
120
|
+
grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
|
|
121
|
+
grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderWorkspaceBar() {
|
|
125
|
+
const el = document.getElementById('workspaceCwdBottom');
|
|
126
|
+
if (el) el.textContent = state.workspaceDir || '(未设置)';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function openPermissions() {
|
|
130
|
+
const p = state.permissions;
|
|
131
|
+
document.querySelector('input[name="readScope"][value="' + p.readScope + '"]').checked = true;
|
|
132
|
+
document.getElementById('permReadPath').value = p.readPath || '';
|
|
133
|
+
document.getElementById('permRead').checked = p.allowRead;
|
|
134
|
+
document.getElementById('permWrite').checked = p.allowWrite;
|
|
135
|
+
document.getElementById('permShell').checked = p.allowShell;
|
|
136
|
+
document.getElementById('permNetwork').checked = p.allowNetwork;
|
|
137
|
+
document.getElementById('permBrowser').checked = p.allowBrowser;
|
|
138
|
+
openModal('permModal');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function openAgentSelector() {
|
|
142
|
+
const grid = document.getElementById('agentGrid');
|
|
143
|
+
grid.innerHTML = Object.entries(AGENT_TYPES).map(([k, v]) =>
|
|
144
|
+
'<div class="agent-card ' + (state.agentType === k ? 'selected' : '') + '" data-type="' + k + '"><div class="icon">' + v.icon + '</div><div class="title">' + v.label + '</div></div>'
|
|
145
|
+
).join('');
|
|
146
|
+
grid.querySelectorAll('.agent-card').forEach((c) => c.addEventListener('click', () => {
|
|
147
|
+
state.agentType = c.getAttribute('data-type');
|
|
148
|
+
updateUserBar();
|
|
149
|
+
closeModal('agentModal');
|
|
150
|
+
toast('已切换:' + AGENT_TYPES[state.agentType].label);
|
|
151
|
+
}));
|
|
152
|
+
openModal('agentModal');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fillInlineForm(id) {
|
|
156
|
+
const m = state.models.find((x) => x.id === id);
|
|
157
|
+
if (!m) return;
|
|
158
|
+
document.getElementById('imName').value = m.name;
|
|
159
|
+
document.getElementById('imBase').value = m.apiBase || '';
|
|
160
|
+
document.getElementById('imKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
|
|
161
|
+
document.getElementById('imReasoning').value = m.reasoning || '';
|
|
162
|
+
document.getElementById('imMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
|
|
163
|
+
document.getElementById('imMsg').classList.remove('err');
|
|
164
|
+
document.getElementById('imSave').dataset.editing = id;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resetInlineForm() {
|
|
168
|
+
document.getElementById('imName').value = '';
|
|
169
|
+
document.getElementById('imBase').value = '';
|
|
170
|
+
document.getElementById('imKey').value = '';
|
|
171
|
+
document.getElementById('imReasoning').value = '';
|
|
172
|
+
document.getElementById('imMsg').textContent = '';
|
|
173
|
+
document.getElementById('imMsg').classList.remove('err');
|
|
174
|
+
document.getElementById('imSave').dataset.editing = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderModelListInline() {
|
|
178
|
+
const list = document.getElementById('modelListInline');
|
|
179
|
+
if (!state.models.length) {
|
|
180
|
+
list.innerHTML =
|
|
181
|
+
'<div class="mm-empty">' +
|
|
182
|
+
'<div class="mm-empty-icon">🧠</div>' +
|
|
183
|
+
'<div>还没有模型配置</div>' +
|
|
184
|
+
'<div class="mm-empty-hint">在下方添加第一个模型,保存后即可在对话中使用</div>' +
|
|
185
|
+
'</div>';
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
list.innerHTML = state.models.map((m) => {
|
|
189
|
+
const url = (m.apiBase || '').trim() || '(未填写 API 地址)';
|
|
190
|
+
const meta = m.reasoning ? '已配置推理中间环节' : '未配置推理中间环节';
|
|
191
|
+
const tag = m.default ? '<span class="mm-default-tag">默认</span>' : '';
|
|
192
|
+
const defaultBtn = m.default ? '' : '<button class="mm-act mm-act-primary" data-act="default">设为默认</button>';
|
|
193
|
+
return (
|
|
194
|
+
'<div class="mm-card" data-id="' + escapeHtml(m.id) + '">' +
|
|
195
|
+
'<div class="mm-card-head">' +
|
|
196
|
+
'<span class="mm-card-name">' + escapeHtml(m.name) + '</span>' +
|
|
197
|
+
tag +
|
|
198
|
+
'</div>' +
|
|
199
|
+
'<div class="mm-card-url">' + escapeHtml(url) + '</div>' +
|
|
200
|
+
'<div class="mm-card-meta">' + meta + '</div>' +
|
|
201
|
+
'<div class="mm-card-actions">' +
|
|
202
|
+
defaultBtn +
|
|
203
|
+
'<button class="mm-act" data-act="edit">编辑</button>' +
|
|
204
|
+
'<button class="mm-act mm-act-danger" data-act="del">删除</button>' +
|
|
205
|
+
'</div>' +
|
|
206
|
+
'</div>'
|
|
207
|
+
);
|
|
208
|
+
}).join('');
|
|
209
|
+
list.querySelectorAll('.mm-card').forEach((el) => {
|
|
210
|
+
const id = el.getAttribute('data-id');
|
|
211
|
+
el.querySelectorAll('button').forEach((b) => {
|
|
212
|
+
b.addEventListener('click', (e) => {
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
const act = b.getAttribute('data-act');
|
|
215
|
+
if (act === 'default') setDefaultModel(id);
|
|
216
|
+
else if (act === 'edit') fillInlineForm(id);
|
|
217
|
+
else if (act === 'del') deleteModel(id);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toggleModelManage() {
|
|
224
|
+
const box = document.getElementById('modelManage');
|
|
225
|
+
const open = box.style.display === 'none' || !box.style.display;
|
|
226
|
+
box.style.display = open ? '' : 'none';
|
|
227
|
+
if (open) { renderModelListInline(); resetInlineForm(); }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function openSettings() {
|
|
231
|
+
// 打开设置时同步会话开关状态,避免界面显示与真实配置不一致
|
|
232
|
+
document.querySelectorAll('.session-toggle').forEach((el) => {
|
|
233
|
+
const key = el.dataset.key;
|
|
234
|
+
if (key && key in state.sessionConfig) el.checked = !!state.sessionConfig[key];
|
|
235
|
+
});
|
|
236
|
+
openModal('settingsModal');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function persistSessionConfig() {
|
|
240
|
+
localStorage.setItem('fhcode.sessionConfig', JSON.stringify(state.sessionConfig));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function editModel(id) {
|
|
244
|
+
const m = state.models.find((x) => x.id === id);
|
|
245
|
+
if (!m) return;
|
|
246
|
+
document.getElementById('modelName').value = m.name;
|
|
247
|
+
document.getElementById('modelApiBase').value = m.apiBase || '';
|
|
248
|
+
document.getElementById('modelApiKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
|
|
249
|
+
document.getElementById('modelReasoning').value = m.reasoning || '';
|
|
250
|
+
document.getElementById('modelFormMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
|
|
251
|
+
document.getElementById('modelFormMsg').classList.remove('err');
|
|
252
|
+
document.getElementById('modelSaveBtn').dataset.editing = id;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resetModelForm() {
|
|
256
|
+
document.getElementById('modelName').value = '';
|
|
257
|
+
document.getElementById('modelApiBase').value = '';
|
|
258
|
+
document.getElementById('modelApiKey').value = '';
|
|
259
|
+
document.getElementById('modelReasoning').value = '';
|
|
260
|
+
document.getElementById('modelFormMsg').textContent = '';
|
|
261
|
+
document.getElementById('modelFormMsg').classList.remove('err');
|
|
262
|
+
document.getElementById('modelSaveBtn').dataset.editing = '';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderModelList() {
|
|
266
|
+
const list = document.getElementById('modelList');
|
|
267
|
+
if (!state.models.length) {
|
|
268
|
+
renderEmpty(list, t('empty.no_models'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
list.innerHTML = state.models.map((m) => (
|
|
272
|
+
'<div class="model-item" data-id="' + escapeHtml(m.id) + '">' +
|
|
273
|
+
'<div><div class="mi-name">' + escapeHtml(m.name) + '</div>' +
|
|
274
|
+
'<div class="mi-base">' + escapeHtml(m.apiBase || '(未填写 API 地址)') + (m.reasoning ? ' · 已配推理内容' : '') + '</div></div>' +
|
|
275
|
+
(m.default ? '<span class="mi-default">默认</span>' : '') +
|
|
276
|
+
'<div class="mi-actions">' +
|
|
277
|
+
(m.default ? '' : '<button data-act="default">设为默认</button>') +
|
|
278
|
+
'<button data-act="edit">编辑</button>' +
|
|
279
|
+
'<button class="danger" data-act="del">删除</button>' +
|
|
280
|
+
'</div>' +
|
|
281
|
+
'</div>'
|
|
282
|
+
)).join('');
|
|
283
|
+
list.querySelectorAll('.model-item').forEach((el) => {
|
|
284
|
+
const id = el.getAttribute('data-id');
|
|
285
|
+
el.querySelectorAll('button').forEach((b) => {
|
|
286
|
+
b.addEventListener('click', (e) => {
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
const act = b.getAttribute('data-act');
|
|
289
|
+
if (act === 'default') setDefaultModel(id);
|
|
290
|
+
else if (act === 'edit') editModel(id);
|
|
291
|
+
else if (act === 'del') deleteModel(id);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function openModels() {
|
|
298
|
+
renderModelList();
|
|
299
|
+
resetModelForm();
|
|
300
|
+
openModal('modelModal');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderModelSelect() {
|
|
304
|
+
const sel = document.getElementById('modelSelect');
|
|
305
|
+
const opts = ['<option value="">默认(系统路由)</option>'];
|
|
306
|
+
state.models.forEach((m) => {
|
|
307
|
+
opts.push('<option value="' + escapeHtml(m.id) + '">' + escapeHtml(m.name) + (m.default ? ' ★' : '') + '</option>');
|
|
308
|
+
});
|
|
309
|
+
sel.innerHTML = opts.join('');
|
|
310
|
+
sel.value = state.modelId || '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toggleDirectMode() {
|
|
314
|
+
state.directMode = !state.directMode;
|
|
315
|
+
document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
|
|
316
|
+
toast(state.directMode ? '已开启「直接操作电脑」模式' : '已关闭「直接操作电脑」模式');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function previewImage(path) {
|
|
320
|
+
document.getElementById('previewTitle').textContent = '图片: ' + path;
|
|
321
|
+
const box = document.getElementById('previewContent');
|
|
322
|
+
const img = document.createElement('img');
|
|
323
|
+
img.src = 'file://' + path;
|
|
324
|
+
img.style.maxWidth = '100%';
|
|
325
|
+
img.style.borderRadius = '8px';
|
|
326
|
+
img.addEventListener('error', () => {
|
|
327
|
+
box.innerHTML = '<div class="muted">无法直接显示本地图片,请用系统相册打开</div>';
|
|
328
|
+
});
|
|
329
|
+
box.innerHTML = '';
|
|
330
|
+
box.appendChild(img);
|
|
331
|
+
switchRightTab('preview');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function previewFile(path) {
|
|
335
|
+
try {
|
|
336
|
+
const d = await api('/api/files/read', 'POST', { path });
|
|
337
|
+
showPreview('文件: ' + path, d.content, 'file');
|
|
338
|
+
switchRightTab('preview');
|
|
339
|
+
} catch (e) { toast('预览失败:' + e.message); }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function bindArtifacts(container) {
|
|
343
|
+
container.querySelectorAll('.artifact').forEach((el) => {
|
|
344
|
+
el.addEventListener('click', () => {
|
|
345
|
+
const kind = el.getAttribute('data-kind');
|
|
346
|
+
const path = el.getAttribute('data-path');
|
|
347
|
+
if (kind === 'url') openBrowser(path);
|
|
348
|
+
else previewFile(path);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
container.querySelectorAll('.file-chip').forEach((el) => {
|
|
352
|
+
el.addEventListener('click', () => {
|
|
353
|
+
const path = el.getAttribute('data-path');
|
|
354
|
+
const mime = el.getAttribute('data-type') || '';
|
|
355
|
+
if (mime.startsWith('image/')) previewImage(path);
|
|
356
|
+
else previewFile(path);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function renderStagedFiles() {
|
|
362
|
+
const box = document.getElementById('stagedArea');
|
|
363
|
+
if (!box) return;
|
|
364
|
+
if (!state.stagedFiles.length) {
|
|
365
|
+
box.classList.remove('show');
|
|
366
|
+
box.innerHTML = '';
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
box.classList.add('show');
|
|
370
|
+
box.innerHTML = state.stagedFiles.map((f) => {
|
|
371
|
+
const isImg = (f.mime || '').startsWith('image/');
|
|
372
|
+
const icon = isImg ? '🖼' : '📎';
|
|
373
|
+
return (
|
|
374
|
+
'<div class="staged-chip" data-id="' + escapeHtml(f.id) + '">' +
|
|
375
|
+
'<span>' + icon + '</span>' +
|
|
376
|
+
'<span class="staged-name">' + escapeHtml(f.name) + '</span>' +
|
|
377
|
+
'<span class="staged-size">' + formatSize(f.size) + '</span>' +
|
|
378
|
+
'<button class="staged-remove" data-act="remove" title="移除">×</button>' +
|
|
379
|
+
'</div>'
|
|
380
|
+
);
|
|
381
|
+
}).join('') + '<span class="staged-hint">💡 点发送时一起上传</span>';
|
|
382
|
+
box.querySelectorAll('.staged-remove').forEach((b) => {
|
|
383
|
+
b.addEventListener('click', (e) => {
|
|
384
|
+
e.stopPropagation();
|
|
385
|
+
const chip = b.closest('.staged-chip');
|
|
386
|
+
if (chip) removeStagedFile(chip.getAttribute('data-id'));
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function showPreview(title, content, mode) {
|
|
392
|
+
document.getElementById('previewTitle').textContent = title;
|
|
393
|
+
const box = document.getElementById('previewContent');
|
|
394
|
+
if (mode === 'code' || mode === 'file') {
|
|
395
|
+
box.innerHTML = '<pre>' + escapeHtml(content) + '</pre>';
|
|
396
|
+
} else {
|
|
397
|
+
box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function switchRightTab(name) {
|
|
402
|
+
state.rightTab = name;
|
|
403
|
+
document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
|
|
404
|
+
document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
|
|
405
|
+
document.getElementById('previewPanel').classList.toggle('active', name === 'preview');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function showArtifacts(dir) {
|
|
409
|
+
try {
|
|
410
|
+
const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
|
|
411
|
+
const files = (d.entries || []).filter((e) => e.type !== 'dir');
|
|
412
|
+
const box = document.getElementById('taskDetail');
|
|
413
|
+
const listHtml = files.length
|
|
414
|
+
? files.map((f) => '<div class="artifact-file" data-path="' + escapeHtml(f.path) + '">📄 ' + escapeHtml(f.name) + '</div>').join('')
|
|
415
|
+
: '<div class="muted">该目录暂无文件产物。</div>';
|
|
416
|
+
const hint = '<div class="muted" style="margin-bottom:8px;">产物文件(来自:' + escapeHtml(dir) + '),点击在右侧预览:</div>';
|
|
417
|
+
const cur = box.innerHTML;
|
|
418
|
+
box.innerHTML = cur + '<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">' + hint + '<div class="artifact-list">' + listHtml + '</div>';
|
|
419
|
+
box.querySelectorAll('.artifact-file').forEach((el) => el.addEventListener('click', () => {
|
|
420
|
+
previewFile(el.getAttribute('data-path'));
|
|
421
|
+
toast('已打开:' + el.getAttribute('data-path'));
|
|
422
|
+
}));
|
|
423
|
+
switchRightTab('detail');
|
|
424
|
+
} catch (e) { toast('读取产物失败:' + e.message); }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderStepHtml(step) {
|
|
428
|
+
const d = step.data || {};
|
|
429
|
+
if (step.type === 'model.response') {
|
|
430
|
+
const content = (d.content || '').toString();
|
|
431
|
+
const toolCalls = Array.isArray(d.toolCalls) ? d.toolCalls : [];
|
|
432
|
+
let inner = '<div class="reasoning-head">🧠 模型推理 · ' + escapeHtml(d.model || '') + '</div>';
|
|
433
|
+
if (content) inner += '<div style="white-space:pre-wrap;word-break:break-word;">' + renderMarkdown(content) + '</div>';
|
|
434
|
+
if (toolCalls.length) inner += '<div class="muted" style="margin-top:6px;">↳ 计划调用工具:' + toolCalls.map((t) => '<code>' + escapeHtml(t) + '</code>').join('、') + '</div>';
|
|
435
|
+
return '<div class="step reasoning">' + inner + '</div>';
|
|
436
|
+
} else if (step.type === 'tool.call') {
|
|
437
|
+
const argsHtml = formatToolArgs(d.args);
|
|
438
|
+
return '<div class="step toolcall"><div class="tc-name">🔧 调用 ' + escapeHtml(d.name || '') + '</div>' + argsHtml + '</div>';
|
|
439
|
+
} else if (step.type === 'tool.result') {
|
|
440
|
+
const ok = !!d.ok;
|
|
441
|
+
const out = (d.output || '').toString();
|
|
442
|
+
return '<div class="step result ' + (ok ? 'ok' : 'fail') + '"><div class="res-head">' + (ok ? '✅' : '❌') + ' ' + escapeHtml(d.name || '') + ' ' + (ok ? '成功' : '失败') + '</div>' + (out.trim() ? '<div class="res-out">' + escapeHtml(out).slice(0, 500) + '</div>' : '') + '</div>';
|
|
443
|
+
} else if (step.type === 'self-heal') {
|
|
444
|
+
return '<div class="step note">🩹 进入自愈:检测到 <b>' + escapeHtml(d.category || '') + '</b> 类错误,已注入反思重试(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
|
|
445
|
+
} else if (step.type === 'context.compact') {
|
|
446
|
+
return '<div class="step note">📦 上下文压缩:从 ' + (d.originalLength != null ? d.originalLength : '?') + ' 条压缩至 ' + (d.compressedLength != null ? d.compressedLength : '?') + ' 条,保留首条目标与近期对话</div>';
|
|
447
|
+
}
|
|
448
|
+
return '';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function renderTaskThread(task) {
|
|
452
|
+
const box = document.getElementById('messages');
|
|
453
|
+
const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
|
|
454
|
+
renderTaskHeader(task);
|
|
455
|
+
if (!task) {
|
|
456
|
+
box.innerHTML = '<div class="msg sys">从左侧「任务列表」选择历史任务查看对话,或在下方输入指令发起新任务。刷新页面会自动清空当前对话视图,历史对话已保存在任务列表中。</div>';
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const steps = task.steps || [];
|
|
460
|
+
const finalAnswer = (task.result && task.result.finalAnswer || '').trim();
|
|
461
|
+
let html = '';
|
|
462
|
+
const conv = Array.isArray(task.conversation) ? task.conversation : [];
|
|
463
|
+
|
|
464
|
+
if (conv.length > 0) {
|
|
465
|
+
// 分组处理:连续 assistant 消息合并为一个思考过程
|
|
466
|
+
let i = 0;
|
|
467
|
+
while (i < conv.length) {
|
|
468
|
+
const m = conv[i];
|
|
469
|
+
if (!m) { i++; continue; }
|
|
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;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (m.role === 'assistant') {
|
|
480
|
+
// 收集连续 assistant 消息
|
|
481
|
+
const assistantMsgs = [];
|
|
482
|
+
while (i < conv.length && conv[i] && conv[i].role === 'assistant') {
|
|
483
|
+
assistantMsgs.push(conv[i]);
|
|
484
|
+
i++;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const hasToolCalls = assistantMsgs.some(msg => (msg.toolCalls || []).length > 0);
|
|
488
|
+
const allText = assistantMsgs.map(msg => (msg.content || '').trim()).filter(Boolean).join('\n\n');
|
|
489
|
+
|
|
490
|
+
if (hasToolCalls) {
|
|
491
|
+
// 有工具调用 → 渲染为豆包风格思考过程(默认折叠)
|
|
492
|
+
html += renderThinkingProcess(assistantMsgs);
|
|
493
|
+
} else if (allText) {
|
|
494
|
+
// 纯文本回复 → 普通气泡,markdown 渲染
|
|
495
|
+
html += '<div class="msg assistant">' + renderMarkdown(allText) + '</div>';
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
i++;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (task.goal) {
|
|
502
|
+
// 首轮尚未产生对话流时,至少呈现用户原始指令
|
|
503
|
+
html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 终态:最终回复(干净气泡)/ 失败提示
|
|
507
|
+
if (task.status === 'done') {
|
|
508
|
+
if (task.result && task.result.finalAnswer) {
|
|
509
|
+
const safeText = JSON.stringify(task.result.finalAnswer);
|
|
510
|
+
html += '<div class="msg assistant final-msg">'
|
|
511
|
+
+ '<div class="final-head">📬 最终回复</div>'
|
|
512
|
+
+ '<div class="final-actions">'
|
|
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
|
+
}
|
|
518
|
+
} else if (task.status === 'failed') {
|
|
519
|
+
if (task.result && task.result.finalAnswer) {
|
|
520
|
+
const safeText = JSON.stringify(task.result.finalAnswer);
|
|
521
|
+
html += '<div class="msg assistant final-msg"><div class="final-head">📬 部分回复</div>'
|
|
522
|
+
+ '<div class="final-actions"><button onclick="bubbleActions(event, ' + safeText + ')" title="操作">⚙️</button></div>'
|
|
523
|
+
+ '<div style="margin-top:4px;cursor:text;user-select:text;">' + renderMarkdown(task.result.finalAnswer) + '</div></div>';
|
|
524
|
+
}
|
|
525
|
+
html += '<div class="msg assistant error-msg">⛔ 任务失败:' + escapeHtml(task.error || '未知错误') + '</div>';
|
|
526
|
+
} else if (task.status === 'running') {
|
|
527
|
+
// 运行中:在最后添加思考中指示
|
|
528
|
+
html += '<div class="thinking-indicator">'
|
|
529
|
+
+ '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
|
|
530
|
+
+ '<span>思考中...</span>'
|
|
531
|
+
+ '</div>';
|
|
532
|
+
} else if (task.status === 'queued') {
|
|
533
|
+
html += '<div class="msg sys">📥 任务已入队,等待执行…</div>';
|
|
534
|
+
}
|
|
535
|
+
box.innerHTML = html;
|
|
536
|
+
if (nearBottom) box.scrollTop = box.scrollHeight;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function renderTaskHeader(task) {
|
|
540
|
+
const titleEl = document.getElementById('tchTitle');
|
|
541
|
+
const statusEl = document.getElementById('tchStatus');
|
|
542
|
+
const timeEl = document.getElementById('tchTime');
|
|
543
|
+
const subEl = document.getElementById('tchSub');
|
|
544
|
+
if (!task) {
|
|
545
|
+
titleEl.textContent = '未选择任务';
|
|
546
|
+
statusEl.className = 'badge'; statusEl.textContent = '—';
|
|
547
|
+
timeEl.textContent = '';
|
|
548
|
+
subEl.textContent = '从左侧任务列表选择,或在下方输入指令发起新任务';
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
titleEl.textContent = task.goal.length > 60 ? task.goal.slice(0, 60) + '…' : task.goal;
|
|
552
|
+
statusEl.className = 'badge ' + task.status;
|
|
553
|
+
statusEl.textContent = task.status;
|
|
554
|
+
timeEl.textContent = '创建于 ' + fmtTime(task.createdAt);
|
|
555
|
+
subEl.textContent = '对话流 + 内部执行过程追踪 · 共 ' + (task.steps ? task.steps.length : 0) + ' 个步骤';
|
|
556
|
+
// 仅当任务正在运行/排队时显示「停止」按钮(解救卡死、无法继续的任务)
|
|
557
|
+
const stopEl = document.getElementById('tchStop');
|
|
558
|
+
if (stopEl) stopEl.style.display = (task && (task.status === 'running' || task.status === 'queued')) ? '' : 'none';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function renderThinkingProcess(msgs) {
|
|
562
|
+
// 收集所有工具调用和文本内容
|
|
563
|
+
const allToolCalls = [];
|
|
564
|
+
const textParts = [];
|
|
565
|
+
for (const m of msgs) {
|
|
566
|
+
const calls = m.toolCalls || [];
|
|
567
|
+
allToolCalls.push(...calls);
|
|
568
|
+
const text = (m.content || '').trim();
|
|
569
|
+
if (text) textParts.push(text);
|
|
570
|
+
}
|
|
571
|
+
const textContent = textParts.join('\n\n');
|
|
572
|
+
|
|
573
|
+
// 构建工具调用摘要(按工具名称分组统计)
|
|
574
|
+
const toolCounts = {};
|
|
575
|
+
for (const tc of allToolCalls) {
|
|
576
|
+
const name = tc.name || 'unknown';
|
|
577
|
+
toolCounts[name] = (toolCounts[name] || 0) + 1;
|
|
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;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function toggleArgs(el) {
|
|
654
|
+
const args = el.parentElement.querySelector('.thinking-args');
|
|
655
|
+
if (!args) return;
|
|
656
|
+
args.classList.toggle('show');
|
|
657
|
+
el.textContent = args.classList.contains('show') ? '收起参数' : '查看参数';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function toggleThinking(header) {
|
|
661
|
+
const body = header.parentElement.querySelector('.thinking-body');
|
|
662
|
+
if (!body) return;
|
|
663
|
+
const arrow = header.querySelector('.thinking-arrow');
|
|
664
|
+
if (body.style.display === 'none' || body.style.display === '') {
|
|
665
|
+
body.style.display = 'block';
|
|
666
|
+
if (arrow) arrow.textContent = '▼';
|
|
667
|
+
} else {
|
|
668
|
+
body.style.display = 'none';
|
|
669
|
+
if (arrow) arrow.textContent = '▶';
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function renderTaskDetail(id) {
|
|
674
|
+
const t = state.tasks.find((x) => x.id === id);
|
|
675
|
+
const box = document.getElementById('taskDetail');
|
|
676
|
+
if (!t) { box.innerHTML = '<div class="muted">任务不存在。</div>'; return; }
|
|
677
|
+
const r = t.result;
|
|
678
|
+
let ops = '';
|
|
679
|
+
if (t.workspaceDir) {
|
|
680
|
+
ops = '<div class="detail-ops">' +
|
|
681
|
+
'<button class="secondary" data-op="browser">🌐 打开浏览器</button>' +
|
|
682
|
+
'<button class="secondary" data-op="folder" data-path="' + escapeHtml(t.workspaceDir) + '">📂 打开资源管理器</button>' +
|
|
683
|
+
'<button class="secondary" data-op="artifacts" data-path="' + escapeHtml(t.workspaceDir) + '">📑 查看产物文件</button>' +
|
|
684
|
+
'</div>';
|
|
685
|
+
}
|
|
686
|
+
const rows = [
|
|
687
|
+
ops,
|
|
688
|
+
'<div><b>目标</b><br>' + escapeHtml(t.goal) + '</div>',
|
|
689
|
+
'<div><b>状态</b> ' + statusBadge(t.status) + '</div>',
|
|
690
|
+
t.agentType ? '<div><b>智能体</b> ' + escapeHtml(AGENT_TYPES[t.agentType]?.label || t.agentType) + '</div>' : '',
|
|
691
|
+
r ? '<div><b>迭代</b> ' + r.iterations + ' · <b>成本</b> $' + r.costUsd.toFixed(6) + '</div>' : '',
|
|
692
|
+
r && r.logFile ? '<div><b>日志</b> <code>' + escapeHtml(r.logFile) + '</code></div>' : '',
|
|
693
|
+
r && r.finalAnswer ? '<div><b>结果</b><br><pre style="white-space:pre-wrap;word-break:break-word;">' + escapeHtml(r.finalAnswer) + '</pre></div>' : '',
|
|
694
|
+
t.error ? '<div style="color:var(--err)"><b>错误</b><br>' + escapeHtml(t.error) + '</div>' : '',
|
|
695
|
+
].filter(Boolean).join('<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">');
|
|
696
|
+
box.innerHTML = rows || '<div class="muted">无详情</div>';
|
|
697
|
+
box.querySelectorAll('[data-op]').forEach((b) => b.addEventListener('click', () => handleDetailOp(t, b.getAttribute('data-op'), b.getAttribute('data-path'))));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderSidebarTaskList() {
|
|
701
|
+
const container = document.getElementById('taskListContainer');
|
|
702
|
+
const section = document.getElementById('taskListSection');
|
|
703
|
+
const badge = document.getElementById('taskCountBadge');
|
|
704
|
+
if (!container || !section) return;
|
|
705
|
+
|
|
706
|
+
// 置顶任务排在前面,其余按时间倒序
|
|
707
|
+
const sortedTasks = [...state.tasks].sort((a, b) => {
|
|
708
|
+
if (state.pinnedTasks.has(a.id) && !state.pinnedTasks.has(b.id)) return -1;
|
|
709
|
+
if (!state.pinnedTasks.has(a.id) && state.pinnedTasks.has(b.id)) return 1;
|
|
710
|
+
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const activeTasks = sortedTasks.filter(t => t.status === 'queued' || t.status === 'running' || t.status === 'done' || t.status === 'failed');
|
|
714
|
+
|
|
715
|
+
if (activeTasks.length === 0) {
|
|
716
|
+
section.style.display = 'none';
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
section.style.display = 'block';
|
|
721
|
+
badge.textContent = activeTasks.length;
|
|
722
|
+
|
|
723
|
+
let html = '';
|
|
724
|
+
activeTasks.slice(0, 15).forEach(t => {
|
|
725
|
+
const icon = t.status === 'running' ? '⏳' : t.status === 'queued' ? '📥' : t.error ? '❌' : '✅';
|
|
726
|
+
const title = t.goal.slice(0, 18) + (t.goal.length > 18 ? '…' : '');
|
|
727
|
+
const isPinned = state.pinnedTasks.has(t.id);
|
|
728
|
+
const activeClass = state.currentTaskId === t.id ? 'active' : '';
|
|
729
|
+
html += `<div class="task-item ${activeClass}" data-task-id="${t.id}" data-pinned="${isPinned}">
|
|
730
|
+
<span class="task-icon">${icon}</span>
|
|
731
|
+
<span class="task-title">${title}</span>
|
|
732
|
+
${isPinned ? '<span class="task-pinned">📌</span>' : ''}
|
|
733
|
+
<div class="task-actions">
|
|
734
|
+
<button class="pin-btn" data-id="${t.id}" data-pinned="${isPinned}" title="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '📌' : '📍'}</button>
|
|
735
|
+
${t.status === 'done' || t.status === 'failed' ? '<button class="delete-btn" data-id="' + t.id + '" title="删除任务">🗑️</button>' : ''}
|
|
736
|
+
</div>
|
|
737
|
+
</div>`;
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
container.innerHTML = html;
|
|
741
|
+
|
|
742
|
+
// 绑定点击事件
|
|
743
|
+
container.querySelectorAll('.task-item[data-task-id]').forEach(el => {
|
|
744
|
+
el.addEventListener('click', (e) => {
|
|
745
|
+
// 如果是点击pin按钮,不切换任务
|
|
746
|
+
if (e.target.classList.contains('pin-btn')) return;
|
|
747
|
+
const id = el.getAttribute('data-task-id');
|
|
748
|
+
selectTask(id);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// 右键菜单 - 置顶/取消置顶
|
|
752
|
+
el.addEventListener('contextmenu', (e) => {
|
|
753
|
+
e.preventDefault();
|
|
754
|
+
const id = el.getAttribute('data-task-id');
|
|
755
|
+
togglePinTask(id);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// 绑定pin按钮
|
|
760
|
+
container.querySelectorAll('.pin-btn').forEach(btn => {
|
|
761
|
+
btn.addEventListener('click', (e) => {
|
|
762
|
+
e.stopPropagation();
|
|
763
|
+
const id = btn.getAttribute('data-id');
|
|
764
|
+
togglePinTask(id);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// 绑定删除按钮
|
|
769
|
+
container.querySelectorAll('.delete-btn').forEach(btn => {
|
|
770
|
+
btn.addEventListener('click', (e) => {
|
|
771
|
+
e.stopPropagation();
|
|
772
|
+
const id = btn.getAttribute('data-id');
|
|
773
|
+
deleteTask(id);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function switchView(nav) {
|
|
779
|
+
document.querySelectorAll('.topbar-nav-item').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
|
|
780
|
+
document.querySelectorAll('.sidebar-icon').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
|
|
781
|
+
document.querySelectorAll('.view, .page-view').forEach((v) => v.classList.toggle('active', v.getAttribute('data-view') === nav));
|
|
782
|
+
// 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
|
|
783
|
+
const isChat = nav === 'chat';
|
|
784
|
+
document.getElementById('chatFootbar').style.display = isChat ? 'flex' : 'none';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function updateUserBar() {
|
|
788
|
+
document.getElementById('topbarPhone').textContent = state.phone || '未登录';
|
|
789
|
+
document.getElementById('sheetPhone').textContent = state.phone || '未登录';
|
|
790
|
+
const a = AGENT_TYPES[state.agentType];
|
|
791
|
+
document.getElementById('skillPill').textContent = (a?.icon || '🤖') + ' ' + (a?.label || '通用助手');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function showWelcomeModal(welcomeTasks) {
|
|
795
|
+
const modal = document.getElementById('welcomeGuideModal');
|
|
796
|
+
if (!modal) return;
|
|
797
|
+
|
|
798
|
+
const listEl = document.getElementById('welcomeTaskList');
|
|
799
|
+
if (listEl) {
|
|
800
|
+
if (welcomeTasks.length > 0) {
|
|
801
|
+
listEl.innerHTML = welcomeTasks.map((t, i) => `
|
|
802
|
+
<div class="welcome-task-item" data-task-id="${t.taskId}" data-goal="${encodeURIComponent(t.goal)}">
|
|
803
|
+
<div class="welcome-task-index">${i + 1}</div>
|
|
804
|
+
<div class="welcome-task-content">
|
|
805
|
+
<div class="welcome-task-title">${escapeHtml(t.title || '任务 ' + (i + 1))}</div>
|
|
806
|
+
<div class="welcome-task-desc">${escapeHtml(t.description || t.goal.slice(0, 50) + '…')}</div>
|
|
807
|
+
</div>
|
|
808
|
+
<button class="welcome-task-run" data-id="${t.taskId}">▶ 运行</button>
|
|
809
|
+
</div>
|
|
810
|
+
`).join('');
|
|
811
|
+
|
|
812
|
+
// 绑定运行按钮
|
|
813
|
+
listEl.querySelectorAll('.welcome-task-run').forEach(btn => {
|
|
814
|
+
btn.addEventListener('click', (e) => {
|
|
815
|
+
e.stopPropagation();
|
|
816
|
+
const taskId = btn.getAttribute('data-id');
|
|
817
|
+
runWelcomeTask(taskId);
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
} else {
|
|
821
|
+
listEl.innerHTML = '<div class="empty-welcome">' + t('empty.preparing_welcome') + '</div>';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
modal.classList.add('show');
|
|
826
|
+
toast('🎉 欢迎!系统已为您准备了三个引导任务');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function showWelcomeGuide(welcomeTasks = []) {
|
|
830
|
+
// 显示欢迎引导弹窗
|
|
831
|
+
showWelcomeModal(welcomeTasks);
|
|
832
|
+
}
|