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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 模块:HTTP 请求与数据获取,依赖 utils
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
async function loadSummaryHistory() {
|
|
6
|
+
try {
|
|
7
|
+
const d = await api('/api/memory/history?limit=20');
|
|
8
|
+
const content = document.getElementById('memHistoryContent');
|
|
9
|
+
const list = d.history || [];
|
|
10
|
+
if (!list.length) {
|
|
11
|
+
content.innerHTML = '<div class="empty">暂无总结历史</div>';
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
content.innerHTML = list.map(h =>
|
|
15
|
+
'<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:13px;">' +
|
|
16
|
+
'<strong>' + h.date + '</strong> → ' + h.newEntries + ' 条新增, ' + h.updatedEntries + ' 条更新<br>' +
|
|
17
|
+
'<span class="muted">' + h.timestamp + '</span>' +
|
|
18
|
+
'</div>'
|
|
19
|
+
).join('');
|
|
20
|
+
} catch (e) { toast('加载失败:' + e.message); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadLongTermMemory() {
|
|
24
|
+
try {
|
|
25
|
+
const d = await api('/api/memory/long');
|
|
26
|
+
const content = document.getElementById('memLongContent');
|
|
27
|
+
if (!d || !d.content) {
|
|
28
|
+
content.innerHTML = '<div class="empty">长期记忆为空</div>';
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
content.innerHTML = formatMarkdown(d.content);
|
|
32
|
+
} catch (e) { toast('加载失败:' + e.message); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadMemoryContent(dateStr) {
|
|
36
|
+
try {
|
|
37
|
+
const d = await api('/api/memory/short?date=' + dateStr);
|
|
38
|
+
const content = document.getElementById('memShortContent');
|
|
39
|
+
if (!d || !d.content) {
|
|
40
|
+
content.innerHTML = '<div class="empty">当天暂无记录</div>';
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
content.innerHTML = formatMarkdown(d.content);
|
|
44
|
+
document.getElementById('memDateLabel').textContent = '📅 ' + dateStr;
|
|
45
|
+
} catch (e) { toast('加载失败:' + e.message); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadMemoryStats() {
|
|
49
|
+
try {
|
|
50
|
+
const d = await api('/api/memory/stats');
|
|
51
|
+
document.getElementById('statShortDays').textContent = d.shortTermFiles + ' 天';
|
|
52
|
+
document.getElementById('statLongNotes').textContent = d.longTermNotes + ' 条';
|
|
53
|
+
document.getElementById('statLastSum').textContent = d.lastSummarize || '暂无';
|
|
54
|
+
// 计算下次自动总结时间(今晚24:00)
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0);
|
|
57
|
+
document.getElementById('statNextSum').textContent = next.toLocaleString('zh-CN', { hour12: false });
|
|
58
|
+
} catch (e) { console.warn('加载记忆统计失败', e); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadOffice() {
|
|
62
|
+
try {
|
|
63
|
+
const d = await api('/api/office/capabilities');
|
|
64
|
+
const grid = document.getElementById('officeGrid');
|
|
65
|
+
grid.innerHTML = (d.capabilities || []).map((c) => '<div class="tile" data-prompt="' + encodeURIComponent(c.prompt) + '"><div class="icon">' + c.icon + '</div><div class="title">' + escapeHtml(c.title) + '</div><div class="desc">' + escapeHtml(c.desc) + '</div><div class="ops"><button class="use" data-prompt="' + encodeURIComponent(c.prompt) + '">生成提示词</button></div></div>').join('');
|
|
66
|
+
grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
|
|
67
|
+
e.stopPropagation();
|
|
68
|
+
const inp = document.getElementById('goalInput');
|
|
69
|
+
if (inp) inp.value = decodeURIComponent(b.getAttribute('data-prompt'));
|
|
70
|
+
switchView('chat');
|
|
71
|
+
if (inp) inp.focus();
|
|
72
|
+
toast('已填入提示词');
|
|
73
|
+
}));
|
|
74
|
+
} catch (e) { document.getElementById('officeGrid').innerHTML = '<div class="empty" style="color:var(--err)">加载失败:' + escapeHtml(e.message) + '</div>'; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadInstalled() {
|
|
78
|
+
try {
|
|
79
|
+
const d = await api('/api/skills/installed');
|
|
80
|
+
state.installed = new Set((d.skills || []).map((s) => s.id));
|
|
81
|
+
const grid = document.getElementById('installedGrid');
|
|
82
|
+
if (!d.skills || !d.skills.length) { grid.innerHTML = '<div class="empty">暂无已安装技能</div>'; return; }
|
|
83
|
+
grid.innerHTML = d.skills.map((s) => '<div class="tile"><div class="icon">✅</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description || '').slice(0, 60) + '…</div></div>').join('');
|
|
84
|
+
} catch (e) { console.warn('加载已安装失败', e); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function installSkill(idEnc, nameEnc, source) {
|
|
88
|
+
const id = decodeURIComponent(idEnc);
|
|
89
|
+
const name = decodeURIComponent(nameEnc);
|
|
90
|
+
try { await api('/api/skills/install', 'POST', { id, name, source }); toast('已安装:' + name); await loadInstalled(); }
|
|
91
|
+
catch (e) { toast('安装失败:' + e.message); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function loadMarket() {
|
|
95
|
+
const q = document.getElementById('marketSearch').value.trim();
|
|
96
|
+
const source = document.getElementById('marketSource').value;
|
|
97
|
+
const grid = document.getElementById('marketGrid');
|
|
98
|
+
grid.innerHTML = '<div class="empty">加载中…</div>';
|
|
99
|
+
try {
|
|
100
|
+
const params = new URLSearchParams({ source });
|
|
101
|
+
if (q) params.set('q', q);
|
|
102
|
+
const d = await api('/api/skills/market?' + params.toString());
|
|
103
|
+
state.market = d.skills || [];
|
|
104
|
+
await loadInstalled(); // 先加载已安装集合,市场按钮才能正确显示「已安装」态
|
|
105
|
+
renderMarketGrid();
|
|
106
|
+
} catch (e) { grid.innerHTML = '<div class="empty" style="color:var(--err)">加载失败:' + escapeHtml(e.message) + '</div>'; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadTemplates() {
|
|
110
|
+
try { const d = await api('/api/templates'); renderBuiltin(d.builtin || []); renderUserTpl(d.user || []); }
|
|
111
|
+
catch (e) { console.warn('加载模板失败', e); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function delAuto(id) {
|
|
115
|
+
try { await api('/api/automations/' + id, 'DELETE'); await loadAutomations(); toast('已删除'); }
|
|
116
|
+
catch (e) { toast('删除失败:' + e.message); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runAuto(id) {
|
|
120
|
+
try { const d = await api('/api/automations/' + id + '/run', 'POST'); toast('已发起任务,运行次数 ' + d.runCount); await loadTasks(); }
|
|
121
|
+
catch (e) { toast('运行失败:' + e.message); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadAutomations() {
|
|
125
|
+
try { const d = await api('/api/automations'); renderAutoGrid(d.automations || []); }
|
|
126
|
+
catch (e) { console.warn('加载自动化失败', e); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getDrivesList() {
|
|
130
|
+
if (_drivesCache) return _drivesCache;
|
|
131
|
+
try {
|
|
132
|
+
const d = await api('/api/drives');
|
|
133
|
+
_drivesCache = d.drives || [];
|
|
134
|
+
return _drivesCache;
|
|
135
|
+
} catch {
|
|
136
|
+
return ['C:\\', 'D:\\', 'E:\\', 'F:\\', 'H:\\'];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadWorkspaceTree(dir) {
|
|
141
|
+
// 已废弃:左侧工作区树已移除,保留此函数仅供兼容
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function loadWorkspace() {
|
|
146
|
+
if (!state.token) {
|
|
147
|
+
console.warn('[workspace] 未登录,跳过加载');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const d = await api('/api/workspace');
|
|
152
|
+
state.workspaceDir = d.cwd;
|
|
153
|
+
// 持久化到 localStorage
|
|
154
|
+
localStorage.setItem('fhcode.workspaceDir', d.cwd);
|
|
155
|
+
renderWorkspaceBar();
|
|
156
|
+
console.log('[workspace] 加载成功:', d.cwd);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error('[workspace] 加载失败:', e.message);
|
|
159
|
+
toast('工作区加载失败,请检查登录状态');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function setDefaultModel(id) {
|
|
164
|
+
try {
|
|
165
|
+
const d = await api('/api/models/' + encodeURIComponent(id) + '/default', 'POST');
|
|
166
|
+
state.defaultModelId = d.defaultId;
|
|
167
|
+
state.modelId = d.defaultId;
|
|
168
|
+
localStorage.setItem('fhcode.model', '');
|
|
169
|
+
renderModelSelect();
|
|
170
|
+
renderModelList();
|
|
171
|
+
renderModelListInline();
|
|
172
|
+
toast('已设为默认大模型');
|
|
173
|
+
} catch (e) { toast('设置失败:' + e.message); }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function deleteModel(id) {
|
|
177
|
+
try {
|
|
178
|
+
await api('/api/models/' + encodeURIComponent(id), 'DELETE');
|
|
179
|
+
toast('已删除配置');
|
|
180
|
+
await loadModels();
|
|
181
|
+
renderModelList();
|
|
182
|
+
} catch (e) { toast('删除失败:' + e.message); }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function saveModel() {
|
|
186
|
+
const name = document.getElementById('modelName').value.trim();
|
|
187
|
+
const apiBase = document.getElementById('modelApiBase').value.trim();
|
|
188
|
+
const apiKey = document.getElementById('modelApiKey').value;
|
|
189
|
+
const reasoning = document.getElementById('modelReasoning').value;
|
|
190
|
+
const msg = document.getElementById('modelFormMsg');
|
|
191
|
+
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
192
|
+
const editing = document.getElementById('modelSaveBtn').dataset.editing;
|
|
193
|
+
try {
|
|
194
|
+
const body = { name, apiBase, reasoning };
|
|
195
|
+
// 三重加密·通信层:密钥经 RSA 公钥加密传输,杜绝明文出网(编辑留空则服务端保留原密钥)
|
|
196
|
+
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
197
|
+
if (editing) body.id = editing;
|
|
198
|
+
const d = await api('/api/models', 'POST', body);
|
|
199
|
+
msg.textContent = '已保存:' + d.model.name;
|
|
200
|
+
msg.classList.remove('err');
|
|
201
|
+
await loadModels();
|
|
202
|
+
renderModelList();
|
|
203
|
+
resetModelForm();
|
|
204
|
+
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function loadModels() {
|
|
208
|
+
try {
|
|
209
|
+
const d = await api('/api/models');
|
|
210
|
+
state.models = d.models || [];
|
|
211
|
+
state.defaultModelId = d.defaultId || null;
|
|
212
|
+
const manual = localStorage.getItem('fhcode.model');
|
|
213
|
+
if (manual !== null && manual !== '') {
|
|
214
|
+
state.modelId = manual;
|
|
215
|
+
} else {
|
|
216
|
+
state.modelId = state.defaultModelId || '';
|
|
217
|
+
}
|
|
218
|
+
renderModelSelect();
|
|
219
|
+
} catch (e) { console.warn('加载大模型失败', e); }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function rsaEncryptText(text) {
|
|
223
|
+
if (!text) return '';
|
|
224
|
+
const d = await api('/api/security/public-key');
|
|
225
|
+
const b64 = String(d.publicKey || '').replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
|
|
226
|
+
if (!b64) throw new Error('未获取到加密公钥');
|
|
227
|
+
const bin = atob(b64);
|
|
228
|
+
const buf = new Uint8Array(bin.length);
|
|
229
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
230
|
+
const key = await crypto.subtle.importKey('spki', buf.buffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
|
231
|
+
const enc = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, new TextEncoder().encode(text));
|
|
232
|
+
return btoa(String.fromCharCode.apply(null, new Uint8Array(enc)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function loadTasks() {
|
|
236
|
+
if (!state.token) return;
|
|
237
|
+
try {
|
|
238
|
+
const d = await api('/api/tasks');
|
|
239
|
+
state.tasks = d.tasks || [];
|
|
240
|
+
renderSidebarTaskList();
|
|
241
|
+
// 刷新页面:不自动选中旧任务,聊天区保持清空;
|
|
242
|
+
// 历史任务全部保留在左侧「任务列表」,点击即可回看对话。
|
|
243
|
+
if (state.currentTaskId) {
|
|
244
|
+
await refreshCurrentThread();
|
|
245
|
+
} else {
|
|
246
|
+
renderTaskThread(null);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (e) { console.warn('加载任务失败', e); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function doLogin() {
|
|
253
|
+
const input = document.getElementById('loginPhone');
|
|
254
|
+
const status = document.getElementById('loginStatus');
|
|
255
|
+
const btn = document.getElementById('loginBtn');
|
|
256
|
+
const phone = (input.value || '').trim();
|
|
257
|
+
if (!/^\d{6,20}$/.test(phone)) { status.textContent = '请输入有效的手机号码'; status.classList.add('err'); return; }
|
|
258
|
+
btn.disabled = true;
|
|
259
|
+
status.textContent = '登录中…';
|
|
260
|
+
status.classList.remove('err');
|
|
261
|
+
try {
|
|
262
|
+
const d = await api('/api/auth/login', 'POST', { phone });
|
|
263
|
+
if (!d || !d.token) throw new Error('服务端未返回令牌');
|
|
264
|
+
state.token = d.token;
|
|
265
|
+
state.phone = d.phone || phone;
|
|
266
|
+
localStorage.setItem('fhcode.token', state.token);
|
|
267
|
+
localStorage.setItem('fhcode.phone', state.phone);
|
|
268
|
+
// 保存引导任务(供首次登录后展示)
|
|
269
|
+
if (d.isFirstLogin && d.welcomeTasks) {
|
|
270
|
+
await saveWelcomeTasks(d.welcomeTasks);
|
|
271
|
+
}
|
|
272
|
+
status.classList.remove('err');
|
|
273
|
+
document.getElementById('loginOverlay').style.display = 'none';
|
|
274
|
+
document.getElementById('appLayout').classList.add('show');
|
|
275
|
+
await afterLogin();
|
|
276
|
+
} catch (e) {
|
|
277
|
+
status.textContent = '登录失败:' + e.message;
|
|
278
|
+
status.classList.add('err');
|
|
279
|
+
} finally {
|
|
280
|
+
btn.disabled = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function api(path, method = 'GET', body) {
|
|
285
|
+
const res = await fetch(path, { method, headers: authHeaders(), body: body ? JSON.stringify(body) : undefined });
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
const d = await res.json().catch(() => ({}));
|
|
288
|
+
// 401:令牌确实被服务端拒绝,清除并回到登录页。
|
|
289
|
+
// 会话已落盘(web-sessions.json),正常重启不会再走到这里;
|
|
290
|
+
// 走到这里说明令牌真的过期或被吊销,此时给出明确提示,避免用户以为是"莫名跳回登录"。
|
|
291
|
+
if (res.status === 401 && d.error === 'unauthorized') {
|
|
292
|
+
if (state.token) {
|
|
293
|
+
console.warn('[api] token 已被服务端拒绝,清除并回到登录页:', path);
|
|
294
|
+
localStorage.removeItem('fhcode.token');
|
|
295
|
+
state.token = '';
|
|
296
|
+
try { toast('登录已过期,请重新登录'); } catch {}
|
|
297
|
+
}
|
|
298
|
+
const overlay = document.getElementById('loginOverlay');
|
|
299
|
+
if (overlay) overlay.style.display = 'flex';
|
|
300
|
+
throw new Error('登录已过期,请重新登录');
|
|
301
|
+
}
|
|
302
|
+
throw new Error(d.error || ('HTTP ' + res.status));
|
|
303
|
+
}
|
|
304
|
+
return res.json();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function authHeaders() {
|
|
308
|
+
return state.token ? { 'Authorization': 'Bearer ' + state.token, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
309
|
+
}
|