feihong-code 7.2.0 → 7.5.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/workflows/ci.yml +180 -140
- package/.github/workflows/release.yml +200 -173
- package/CHANGELOG.md +94 -115
- package/README.md +609 -586
- package/dist/agent/completion-engine.js +76 -45
- package/dist/agent/completion-engine.js.map +1 -1
- package/dist/agent/completion-postprocess.js +109 -0
- package/dist/agent/completion-postprocess.js.map +1 -0
- package/dist/agent/context-budget.js +121 -0
- package/dist/agent/context-budget.js.map +1 -0
- package/dist/agent/context-compactor.js +47 -0
- package/dist/agent/context-compactor.js.map +1 -1
- package/dist/agent/custom-agent.js +52 -52
- package/dist/agent/design-to-code.js +40 -40
- package/dist/agent/lint.js +121 -0
- package/dist/agent/lint.js.map +1 -0
- package/dist/agent/multi-agent.js +44 -44
- package/dist/agent/orchestrator.js +20 -20
- package/dist/agent/prompts.js +54 -54
- package/dist/agent/self-correction.js +24 -24
- package/dist/agent/self-heal.js +27 -27
- package/dist/cli/commands.js +9 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/index.js +9 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/multi-pane-tui.js +217 -0
- package/dist/cli/multi-pane-tui.js.map +1 -0
- package/dist/cli/run.js +129 -40
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui-run.js +79 -0
- package/dist/cli/tui-run.js.map +1 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/lsp/lsp-client.js +183 -0
- package/dist/lsp/lsp-client.js.map +1 -0
- package/dist/lsp/lsp-service.js +159 -0
- package/dist/lsp/lsp-service.js.map +1 -0
- package/dist/plugins/manager.js +50 -2
- package/dist/plugins/manager.js.map +1 -1
- package/dist/shared/i18n.js +108 -100
- package/dist/shared/i18n.js.map +1 -1
- package/dist/tools/generator/code-generator.js +40 -40
- package/dist/tools/generator/test-generator.js +9 -9
- package/dist/tools/shell/exec.js +23 -1
- package/dist/tools/shell/exec.js.map +1 -1
- package/dist/web/extra-apis.js +440 -0
- package/dist/web/extra-apis.js.map +1 -0
- package/dist/web/public/css/style.css +1715 -1715
- package/dist/web/public/index.html +1242 -1200
- package/dist/web/public/js/api.js +555 -496
- package/dist/web/public/js/app.js +2210 -2210
- package/dist/web/public/js/mock-api.js +234 -234
- package/dist/web/public/js/monaco-editor.js +363 -203
- package/dist/web/public/js/ui.js +2251 -1981
- package/dist/web/public/js/utils.js +214 -214
- package/dist/web/public/manifest.json +59 -59
- package/dist/web/public/service-worker.js +173 -173
- package/dist/web/server.js +97 -45
- package/dist/web/server.js.map +1 -1
- package/docs/BENCHMARK_REPORT_en.md +386 -329
- package/docs/BENCHMARK_REPORT_zh.md +386 -329
- package/docs/DPA.md +63 -0
- package/docs/DPA/346/225/260/346/215/256/345/244/204/347/220/206/345/215/217/350/256/256/346/250/241/346/235/277_v1.0.md +67 -0
- package/docs/SECURITY_WHITEPAPER.md +74 -0
- package/docs/SWE_BENCH_REPORT.md +55 -0
- package/docs/TECHNICAL_SPEC_en.md +292 -292
- package/docs/TECHNICAL_SPEC_zh.md +292 -292
- package/docs/UPDATE_NOTES_en.md +159 -159
- package/docs/UPDATE_NOTES_zh.md +159 -159
- package/docs/UPGRADE_7.2.0.md +309 -0
- package/docs/UPGRADE_GUIDE_7_5.md +86 -0
- package/docs/geo-feihong-code.html +391 -0
- package/docs//345/205/250/351/235/242/345/244/215/347/233/230_50/350/275/256/346/267/261/345/272/246/345/256/241/350/256/241_v7.2.0.md +208 -0
- package/docs//345/205/250/351/235/242/345/244/215/347/233/230_6/351/241/271/351/207/215/347/202/271/346/240/270/346/237/245_v7.2.0.md +152 -0
- package/docs//345/205/250/351/235/242/345/244/215/347/233/230_/350/277/220/350/241/214/346/227/266/345/256/236/346/265/213/347/211/210_v7.2.0.md +271 -0
- package/docs//345/220/210/350/247/204/350/256/244/350/257/201/345/207/206/345/244/207/346/270/205/345/215/225_v7.2.0.md +59 -0
- package/docs//345/256/211/345/205/250/347/231/275/347/232/256/344/271/246_v7.2.0.md +82 -0
- package/electron/main.js +404 -602
- package/package.json +175 -172
- package/templates/skills/api-design/SKILL.md +27 -0
- package/templates/skills/code-review/SKILL.md +35 -0
- package/templates/skills/dependency-upgrade/SKILL.md +29 -0
- package/templates/skills/doc-gen/SKILL.md +29 -0
- package/templates/skills/git-flow/SKILL.md +26 -0
- package/templates/skills/onboarding/SKILL.md +27 -0
- package/templates/skills/performance/SKILL.md +29 -0
- package/templates/skills/refactor/SKILL.md +28 -0
- package/templates/skills/security-audit/SKILL.md +28 -0
- package/templates/skills/test-gen/SKILL.md +30 -0
- package/tool-schema.json +197 -197
- package/dist/hello.js +0 -14
- package/dist/hello.js.map +0 -1
- package/dist/self-evolve/hook.ts +0 -80
- package/dist/self-evolve/manager.d.ts +0 -8
- package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +0 -1
- package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +0 -1
- package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +0 -97
- package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +0 -30
- package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +0 -30
- package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +0 -474
- package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +0 -62
- package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +0 -67722
- package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +0 -1
- package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +0 -1
- package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +0 -1
- package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +0 -2
- package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +0 -1
- package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +0 -1
- package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +0 -1
- package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +0 -1
- package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +0 -1
- package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +0 -1
- package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +0 -3
- package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +0 -1
- package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +0 -1
- package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +0 -1
- package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +0 -1
- package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +0 -1
- package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +0 -1
- package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +0 -1
- package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +0 -1
- package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +0 -1
- package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +0 -831
- package/dist/web/public/vendor/monaco/vs/editor.js +0 -1
- package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +0 -35
- package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +0 -1
- package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +0 -1
- package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +0 -3
- package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +0 -1
- package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +0 -1
- package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +0 -1
- package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +0 -1
- package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +0 -1
- package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +0 -1
- package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +0 -1
- package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +0 -1
- package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +0 -1
- package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +0 -5
- package/dist/web/public/vendor/monaco/vs/index.js +0 -1
- package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +0 -1
- package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +0 -1
- package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +0 -1
- package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +0 -1
- package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +0 -1
- package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +0 -1
- package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +0 -1
- package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +0 -1
- package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +0 -66
- package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +0 -1
- package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +0 -443
- package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +0 -1
- package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +0 -17
- package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +0 -1
- package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +0 -1
- package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +0 -67693
- package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +0 -2
- package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +0 -1
- package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +0 -1
- package/dist/web/public/vendor/monaco/vs/loader.js +0 -1368
- package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +0 -2
- package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +0 -1
- package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +0 -1
- package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +0 -15
- package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +0 -3
- package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +0 -1
- package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +0 -1
- package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +0 -1
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +0 -1
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +0 -1
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +0 -1
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +0 -1
- package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +0 -1
- package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +0 -1
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +0 -9
- package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +0 -1
- package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +0 -1
- package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +0 -1
- package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +0 -1
- package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +0 -1
- package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +0 -1
- package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +0 -1
- package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +0 -1
- package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +0 -1
- package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +0 -1
- package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +0 -1
- package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +0 -2
- package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +0 -1
- package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +0 -1
- package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +0 -1
- package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +0 -1
- package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +0 -1
- package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +0 -1
- package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +0 -1
- package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +0 -1
- package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +0 -1
- package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +0 -1
- package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +0 -1
- package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +0 -1
- package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +0 -1
- package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +0 -3
- package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +0 -1
- package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +0 -1
- package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +0 -1
- package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +0 -1
- package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +0 -1
- package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +0 -1
- package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +0 -1
- package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +0 -1
- package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +0 -1
- package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +0 -61
- package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +0 -1
- package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +0 -11
- package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +0 -1
- package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +0 -1
- package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +0 -1
- package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +0 -1
- package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +0 -298
- package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +0 -1
- package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +0 -1
- package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +0 -1
- package/dist/web/web-config.js +0 -143
- package/dist/web/web-config.js.map +0 -1
- package/docs/DEPLOYMENT.md +0 -382
- package/docs/MODEL_FINETUNING.md +0 -405
|
@@ -1,496 +1,555 @@
|
|
|
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 {
|
|
111
|
-
const d = await api('/api/templates');
|
|
112
|
-
renderBuiltin(d.builtin || []);
|
|
113
|
-
renderNodeTpl(d.node || []);
|
|
114
|
-
renderCustomTpl(d.custom || []);
|
|
115
|
-
renderUserTpl(d.user || []);
|
|
116
|
-
} catch (e) { console.warn('加载模板失败', e); }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* ========== 节点管理 ========== */
|
|
120
|
-
async function loadNodes() {
|
|
121
|
-
try {
|
|
122
|
-
const d = await api('/api/nodes');
|
|
123
|
-
renderNodesGrid(d.nodes || []);
|
|
124
|
-
} catch (e) { console.warn('加载节点失败', e); }
|
|
125
|
-
}
|
|
126
|
-
async function saveNode(data) {
|
|
127
|
-
try {
|
|
128
|
-
const d = data.id
|
|
129
|
-
? await api('/api/nodes/' + data.id, 'PUT', data)
|
|
130
|
-
: await api('/api/nodes', 'POST', data);
|
|
131
|
-
await loadNodes();
|
|
132
|
-
toast(data.id ? '节点已更新' : '节点已添加,连接状态:' + (d.connection?.ok ? '成功' : '失败'));
|
|
133
|
-
return d;
|
|
134
|
-
} catch (e) { toast('保存失败:' + e.message); }
|
|
135
|
-
}
|
|
136
|
-
async function deleteNode(id) {
|
|
137
|
-
try { await api('/api/nodes/' + id, 'DELETE'); await loadNodes(); toast('已删除节点'); }
|
|
138
|
-
catch (e) { toast('删除失败:' + e.message); }
|
|
139
|
-
}
|
|
140
|
-
async function testNode(id) {
|
|
141
|
-
try {
|
|
142
|
-
const d = await api('/api/nodes/' + id + '/test', 'POST');
|
|
143
|
-
toast(d.ok ? '连接成功' : '连接失败:' + (d.error || ''));
|
|
144
|
-
await loadNodes();
|
|
145
|
-
return d;
|
|
146
|
-
} catch (e) { toast('测试失败:' + e.message); }
|
|
147
|
-
}
|
|
148
|
-
async function syncNode(id) {
|
|
149
|
-
try {
|
|
150
|
-
const d = await api('/api/nodes/' + id + '/sync', 'POST');
|
|
151
|
-
toast(d.ok ? '同步完成:' + (d.synced || []).join('、') : '同步失败:' + (d.error || ''));
|
|
152
|
-
await loadNodes();
|
|
153
|
-
await loadTemplates();
|
|
154
|
-
await loadMarket();
|
|
155
|
-
await loadOffice();
|
|
156
|
-
return d;
|
|
157
|
-
} catch (e) { toast('同步失败:' + e.message); }
|
|
158
|
-
}
|
|
159
|
-
async function toggleNode(id, enabled) {
|
|
160
|
-
try { await api('/api/nodes/' + id, 'PUT', { enabled }); await loadNodes(); }
|
|
161
|
-
catch (e) { toast('操作失败:' + e.message); }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* ========== 自定义来源 ========== */
|
|
165
|
-
async function loadSources() {
|
|
166
|
-
try {
|
|
167
|
-
const d = await api('/api/sources');
|
|
168
|
-
renderSourcesGrid(d.sources || []);
|
|
169
|
-
} catch (e) { console.warn('加载来源失败', e); }
|
|
170
|
-
}
|
|
171
|
-
async function saveSource(data) {
|
|
172
|
-
try {
|
|
173
|
-
const d = data.id
|
|
174
|
-
? await api('/api/sources/' + data.id, 'PUT', data)
|
|
175
|
-
: await api('/api/sources', 'POST', data);
|
|
176
|
-
await loadSources();
|
|
177
|
-
toast(data.id ? '来源已更新' : '来源已添加');
|
|
178
|
-
return d;
|
|
179
|
-
} catch (e) { toast('保存失败:' + e.message); }
|
|
180
|
-
}
|
|
181
|
-
async function deleteSource(id) {
|
|
182
|
-
try { await api('/api/sources/' + id, 'DELETE'); await loadSources(); toast('已删除来源'); }
|
|
183
|
-
catch (e) { toast('删除失败:' + e.message); }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function delAuto(id) {
|
|
187
|
-
try { await api('/api/automations/' + id, 'DELETE'); await loadAutomations(); toast('已删除'); }
|
|
188
|
-
catch (e) { toast('删除失败:' + e.message); }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function runAuto(id) {
|
|
192
|
-
try { const d = await api('/api/automations/' + id + '/run', 'POST'); toast('已发起任务,运行次数 ' + d.runCount); await loadTasks(); }
|
|
193
|
-
catch (e) { toast('运行失败:' + e.message); }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function loadAutomations() {
|
|
197
|
-
try { const d = await api('/api/automations'); renderBuiltinAutomations(d.builtin || []); renderAutoGrid(d.automations || []); }
|
|
198
|
-
catch (e) { console.warn('加载自动化失败', e); }
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function getDrivesList() {
|
|
202
|
-
if (_drivesCache) return _drivesCache;
|
|
203
|
-
try {
|
|
204
|
-
const d = await api('/api/drives');
|
|
205
|
-
_drivesCache = d.drives || [];
|
|
206
|
-
return _drivesCache;
|
|
207
|
-
} catch {
|
|
208
|
-
return ['C:\\', 'D:\\', 'E:\\', 'F:\\', 'H:\\'];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function loadWorkspaceTree(dir) {
|
|
213
|
-
// 已废弃:左侧工作区树已移除,保留此函数仅供兼容
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function loadWorkspace() {
|
|
218
|
-
if (!state.token) {
|
|
219
|
-
console.warn('[workspace] 未登录,跳过加载');
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
const d = await api('/api/workspace');
|
|
224
|
-
state.workspaceDir = d.cwd;
|
|
225
|
-
// 持久化到 localStorage
|
|
226
|
-
localStorage.setItem('fhcode.workspaceDir', d.cwd);
|
|
227
|
-
renderWorkspaceBar();
|
|
228
|
-
console.log('[workspace] 加载成功:', d.cwd);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
console.error('[workspace] 加载失败:', e.message);
|
|
231
|
-
toast('工作区加载失败,请检查登录状态');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function setDefaultModel(id) {
|
|
236
|
-
try {
|
|
237
|
-
const d = await api('/api/models/' + encodeURIComponent(id) + '/default', 'POST');
|
|
238
|
-
state.defaultModelId = d.defaultId;
|
|
239
|
-
state.modelId = d.defaultId;
|
|
240
|
-
localStorage.setItem('fhcode.model', '');
|
|
241
|
-
renderModelSelect();
|
|
242
|
-
renderModelList();
|
|
243
|
-
renderModelListInline();
|
|
244
|
-
toast('已设为默认大模型');
|
|
245
|
-
} catch (e) { toast('设置失败:' + e.message); }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function deleteModel(id) {
|
|
249
|
-
try {
|
|
250
|
-
await api('/api/models/' + encodeURIComponent(id), 'DELETE');
|
|
251
|
-
toast('已删除配置');
|
|
252
|
-
await loadModels();
|
|
253
|
-
renderModelList();
|
|
254
|
-
} catch (e) { toast('删除失败:' + e.message); }
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function saveModel() {
|
|
258
|
-
const name = document.getElementById('modelName').value.trim();
|
|
259
|
-
const apiBase = document.getElementById('modelApiBase').value.trim();
|
|
260
|
-
const apiKey = document.getElementById('modelApiKey').value;
|
|
261
|
-
const reasoning = document.getElementById('modelReasoning').value;
|
|
262
|
-
const msg = document.getElementById('modelFormMsg');
|
|
263
|
-
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
264
|
-
const editing = document.getElementById('modelSaveBtn').dataset.editing;
|
|
265
|
-
try {
|
|
266
|
-
const body = { name, apiBase, reasoning };
|
|
267
|
-
// 三重加密·通信层:密钥经 RSA 公钥加密传输,杜绝明文出网(编辑留空则服务端保留原密钥)
|
|
268
|
-
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
269
|
-
if (editing) body.id = editing;
|
|
270
|
-
const d = await api('/api/models', 'POST', body);
|
|
271
|
-
msg.textContent = '已保存:' + d.model.name;
|
|
272
|
-
msg.classList.remove('err');
|
|
273
|
-
await loadModels();
|
|
274
|
-
renderModelList();
|
|
275
|
-
resetModelForm();
|
|
276
|
-
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function loadModels() {
|
|
280
|
-
try {
|
|
281
|
-
const d = await api('/api/models');
|
|
282
|
-
state.models = d.models || [];
|
|
283
|
-
state.defaultModelId = d.defaultId || null;
|
|
284
|
-
const manual = localStorage.getItem('fhcode.model');
|
|
285
|
-
if (manual !== null && manual !== '') {
|
|
286
|
-
state.modelId = manual;
|
|
287
|
-
} else {
|
|
288
|
-
state.modelId = state.defaultModelId || '';
|
|
289
|
-
}
|
|
290
|
-
renderModelSelect();
|
|
291
|
-
} catch (e) { console.warn('加载大模型失败', e); }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function rsaEncryptText(text) {
|
|
295
|
-
if (!text) return '';
|
|
296
|
-
const d = await api('/api/security/public-key');
|
|
297
|
-
const b64 = String(d.publicKey || '').replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
|
|
298
|
-
if (!b64) throw new Error('未获取到加密公钥');
|
|
299
|
-
const bin = atob(b64);
|
|
300
|
-
const buf = new Uint8Array(bin.length);
|
|
301
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
302
|
-
const key = await crypto.subtle.importKey('spki', buf.buffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
|
303
|
-
const enc = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, new TextEncoder().encode(text));
|
|
304
|
-
return btoa(String.fromCharCode.apply(null, new Uint8Array(enc)));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
async function loadTasks() {
|
|
308
|
-
if (!state.token) return;
|
|
309
|
-
try {
|
|
310
|
-
const d = await api('/api/tasks');
|
|
311
|
-
state.tasks = d.tasks || [];
|
|
312
|
-
renderSidebarTaskList();
|
|
313
|
-
// 刷新页面:不自动选中旧任务,聊天区保持清空;
|
|
314
|
-
// 历史任务全部保留在左侧「任务列表」,点击即可回看对话。
|
|
315
|
-
if (state.currentTaskId) {
|
|
316
|
-
await refreshCurrentThread();
|
|
317
|
-
} else {
|
|
318
|
-
renderTaskThread(null);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
catch (e) { console.warn('加载任务失败', e); }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function doLogin() {
|
|
325
|
-
const input = document.getElementById('loginPhone');
|
|
326
|
-
const status = document.getElementById('loginStatus');
|
|
327
|
-
const btn = document.getElementById('loginBtn');
|
|
328
|
-
const phone = (input.value || '').trim();
|
|
329
|
-
if (!/^\d{6,20}$/.test(phone)) { status.textContent = '请输入有效的手机号码'; status.classList.add('err'); return; }
|
|
330
|
-
btn.disabled = true;
|
|
331
|
-
status.textContent = '登录中…';
|
|
332
|
-
status.classList.remove('err');
|
|
333
|
-
try {
|
|
334
|
-
const d = await api('/api/auth/login', 'POST', { phone });
|
|
335
|
-
if (!d || !d.token) throw new Error('服务端未返回令牌');
|
|
336
|
-
state.token = d.token;
|
|
337
|
-
state.phone = d.phone || phone;
|
|
338
|
-
localStorage.setItem('fhcode.token', state.token);
|
|
339
|
-
localStorage.setItem('fhcode.phone', state.phone);
|
|
340
|
-
// 保存引导任务(供首次登录后展示)
|
|
341
|
-
if (d.isFirstLogin && d.welcomeTasks) {
|
|
342
|
-
await saveWelcomeTasks(d.welcomeTasks);
|
|
343
|
-
}
|
|
344
|
-
status.classList.remove('err');
|
|
345
|
-
document.getElementById('loginOverlay').style.display = 'none';
|
|
346
|
-
document.getElementById('appLayout').classList.add('show');
|
|
347
|
-
document.documentElement.classList.add('logged-in');
|
|
348
|
-
await afterLogin();
|
|
349
|
-
} catch (e) {
|
|
350
|
-
status.textContent = '登录失败:' + e.message;
|
|
351
|
-
status.classList.add('err');
|
|
352
|
-
} finally {
|
|
353
|
-
btn.disabled = false;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function api(path, method = 'GET', body) {
|
|
358
|
-
let res;
|
|
359
|
-
try {
|
|
360
|
-
res = await fetch(path, { method, headers: authHeaders(), body: body ? JSON.stringify(body) : undefined });
|
|
361
|
-
} catch (e) {
|
|
362
|
-
// 网络层失败(Failed to fetch / 连接拒绝 / 超时中断):标记后端可能已断开
|
|
363
|
-
if (typeof BackendConn !== 'undefined') BackendConn.markFailure();
|
|
364
|
-
throw e;
|
|
365
|
-
}
|
|
366
|
-
// 请求到达后端并返回响应:若之前处于断连状态,立即恢复
|
|
367
|
-
if (typeof BackendConn !== 'undefined' && !BackendConn.online) BackendConn.setOnline(true);
|
|
368
|
-
if (!res.ok) {
|
|
369
|
-
const d = await res.json().catch(() => ({}));
|
|
370
|
-
// 401:令牌确实被服务端拒绝,清除并回到登录页。
|
|
371
|
-
// 会话已落盘(web-sessions.json),正常重启不会再走到这里;
|
|
372
|
-
// 走到这里说明令牌真的过期或被吊销,此时给出明确提示,避免用户以为是"莫名跳回登录"。
|
|
373
|
-
if (res.status === 401 && d.error === 'unauthorized') {
|
|
374
|
-
if (state.token) {
|
|
375
|
-
console.warn('[api] token 已被服务端拒绝,清除并回到登录页:', path);
|
|
376
|
-
localStorage.removeItem('fhcode.token');
|
|
377
|
-
state.token = '';
|
|
378
|
-
try { toast('登录已过期,请重新登录'); } catch {}
|
|
379
|
-
}
|
|
380
|
-
const overlay = document.getElementById('loginOverlay');
|
|
381
|
-
if (overlay) overlay.style.display = 'flex';
|
|
382
|
-
throw new Error('登录已过期,请重新登录');
|
|
383
|
-
}
|
|
384
|
-
throw new Error(d.error || ('HTTP ' + res.status));
|
|
385
|
-
}
|
|
386
|
-
return res.json();
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function authHeaders() {
|
|
390
|
-
return state.token ? { 'Authorization': 'Bearer ' + state.token, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/* ========== 后端连接状态监测(心跳 + 断连横幅 + 自动重连) ==========
|
|
394
|
-
* 解决问题:后端进程崩溃后,前端无感知,用户操作均报 "Failed to fetch" 且不知原因。
|
|
395
|
-
* 机制:
|
|
396
|
-
* 1. 每 5s 轮询 /api/health,连续失败 2 次判定后端断开,顶部显示红色横幅。
|
|
397
|
-
* 2. 断连后自动加快到每 2s 重试,恢复后自动切回 5s 并 toast 提示。
|
|
398
|
-
* 3. 任意 api() 调用网络失败也会累计失败计数(双保险,不必等下一轮心跳)。
|
|
399
|
-
* 4. 页面从后台切回 / 网络恢复事件时立即检测一次。
|
|
400
|
-
*/
|
|
401
|
-
const BackendConn = {
|
|
402
|
-
online: true,
|
|
403
|
-
heartbeatTimer: null,
|
|
404
|
-
reconnectTimer: null,
|
|
405
|
-
intervalMs: 5000, // 正常心跳间隔
|
|
406
|
-
reconnectMs: 2000, // 断连后重连检测间隔
|
|
407
|
-
consecutiveFailures: 0,
|
|
408
|
-
threshold: 2, // 连续失败 N 次才判定断开(避免瞬时抖动误报)
|
|
409
|
-
healthTimeoutMs: 3000, // 单次健康检查超时
|
|
410
|
-
|
|
411
|
-
init() {
|
|
412
|
-
this.startHeartbeat();
|
|
413
|
-
// 重连按钮
|
|
414
|
-
const btn = document.getElementById('backendReconnectBtn');
|
|
415
|
-
if (btn) btn.addEventListener('click', () => this.reconnect());
|
|
416
|
-
// 页面从后台切回前台时立即检测
|
|
417
|
-
document.addEventListener('visibilitychange', () => {
|
|
418
|
-
if (!document.hidden) this.check();
|
|
419
|
-
});
|
|
420
|
-
// 浏览器网络恢复事件
|
|
421
|
-
window.addEventListener('online', () => this.check());
|
|
422
|
-
},
|
|
423
|
-
|
|
424
|
-
startHeartbeat() {
|
|
425
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
426
|
-
this.heartbeatTimer = setInterval(() => this.check(), this.intervalMs);
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
async check() {
|
|
430
|
-
try {
|
|
431
|
-
const ctrl = new AbortController();
|
|
432
|
-
const timeout = setTimeout(() => ctrl.abort(), this.healthTimeoutMs);
|
|
433
|
-
const res = await fetch('/api/health', {
|
|
434
|
-
method: 'GET',
|
|
435
|
-
signal: ctrl.signal,
|
|
436
|
-
cache: 'no-store',
|
|
437
|
-
headers: { 'Cache-Control': 'no-cache' },
|
|
438
|
-
});
|
|
439
|
-
clearTimeout(timeout);
|
|
440
|
-
if (res.ok) {
|
|
441
|
-
this.consecutiveFailures = 0;
|
|
442
|
-
this.setOnline(true);
|
|
443
|
-
} else {
|
|
444
|
-
this.markFailure();
|
|
445
|
-
}
|
|
446
|
-
} catch (e) {
|
|
447
|
-
this.markFailure();
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
|
|
451
|
-
/** 累计失败次数,达到阈值则判定断开 */
|
|
452
|
-
markFailure() {
|
|
453
|
-
this.consecutiveFailures++;
|
|
454
|
-
if (this.consecutiveFailures >= this.threshold) {
|
|
455
|
-
this.setOnline(false);
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
setOnline(online) {
|
|
460
|
-
if (this.online === online) return;
|
|
461
|
-
this.online = online;
|
|
462
|
-
const banner = document.getElementById('backendOfflineBanner');
|
|
463
|
-
const text = document.getElementById('backendOfflineText');
|
|
464
|
-
if (banner) {
|
|
465
|
-
banner.style.display = online ? 'none' : 'flex';
|
|
466
|
-
}
|
|
467
|
-
if (online) {
|
|
468
|
-
// 恢复:切回正常心跳间隔
|
|
469
|
-
if (this.reconnectTimer) { clearInterval(this.reconnectTimer); this.reconnectTimer = null; }
|
|
470
|
-
this.startHeartbeat();
|
|
471
|
-
try { toast('后端服务已恢复连接'); } catch (e) {}
|
|
472
|
-
} else {
|
|
473
|
-
// 断开:停止正常心跳,加快重连检测
|
|
474
|
-
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
475
|
-
if (!this.reconnectTimer) {
|
|
476
|
-
this.reconnectTimer = setInterval(() => this.check(), this.reconnectMs);
|
|
477
|
-
}
|
|
478
|
-
if (text) text.textContent = '后端服务已断开,正在自动重连…';
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
|
|
482
|
-
/** 手动触发重连:重置失败计数并立即检测 */
|
|
483
|
-
reconnect() {
|
|
484
|
-
const text = document.getElementById('backendOfflineText');
|
|
485
|
-
if (text) text.textContent = '正在重连后端服务…';
|
|
486
|
-
this.consecutiveFailures = 0;
|
|
487
|
-
this.check();
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
// 页面加载完成后启动心跳监测
|
|
492
|
-
if (document.readyState === 'loading') {
|
|
493
|
-
document.addEventListener('DOMContentLoaded', () => BackendConn.init());
|
|
494
|
-
} else {
|
|
495
|
-
BackendConn.init();
|
|
496
|
-
}
|
|
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 {
|
|
111
|
+
const d = await api('/api/templates');
|
|
112
|
+
renderBuiltin(d.builtin || []);
|
|
113
|
+
renderNodeTpl(d.node || []);
|
|
114
|
+
renderCustomTpl(d.custom || []);
|
|
115
|
+
renderUserTpl(d.user || []);
|
|
116
|
+
} catch (e) { console.warn('加载模板失败', e); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ========== 节点管理 ========== */
|
|
120
|
+
async function loadNodes() {
|
|
121
|
+
try {
|
|
122
|
+
const d = await api('/api/nodes');
|
|
123
|
+
renderNodesGrid(d.nodes || []);
|
|
124
|
+
} catch (e) { console.warn('加载节点失败', e); }
|
|
125
|
+
}
|
|
126
|
+
async function saveNode(data) {
|
|
127
|
+
try {
|
|
128
|
+
const d = data.id
|
|
129
|
+
? await api('/api/nodes/' + data.id, 'PUT', data)
|
|
130
|
+
: await api('/api/nodes', 'POST', data);
|
|
131
|
+
await loadNodes();
|
|
132
|
+
toast(data.id ? '节点已更新' : '节点已添加,连接状态:' + (d.connection?.ok ? '成功' : '失败'));
|
|
133
|
+
return d;
|
|
134
|
+
} catch (e) { toast('保存失败:' + e.message); }
|
|
135
|
+
}
|
|
136
|
+
async function deleteNode(id) {
|
|
137
|
+
try { await api('/api/nodes/' + id, 'DELETE'); await loadNodes(); toast('已删除节点'); }
|
|
138
|
+
catch (e) { toast('删除失败:' + e.message); }
|
|
139
|
+
}
|
|
140
|
+
async function testNode(id) {
|
|
141
|
+
try {
|
|
142
|
+
const d = await api('/api/nodes/' + id + '/test', 'POST');
|
|
143
|
+
toast(d.ok ? '连接成功' : '连接失败:' + (d.error || ''));
|
|
144
|
+
await loadNodes();
|
|
145
|
+
return d;
|
|
146
|
+
} catch (e) { toast('测试失败:' + e.message); }
|
|
147
|
+
}
|
|
148
|
+
async function syncNode(id) {
|
|
149
|
+
try {
|
|
150
|
+
const d = await api('/api/nodes/' + id + '/sync', 'POST');
|
|
151
|
+
toast(d.ok ? '同步完成:' + (d.synced || []).join('、') : '同步失败:' + (d.error || ''));
|
|
152
|
+
await loadNodes();
|
|
153
|
+
await loadTemplates();
|
|
154
|
+
await loadMarket();
|
|
155
|
+
await loadOffice();
|
|
156
|
+
return d;
|
|
157
|
+
} catch (e) { toast('同步失败:' + e.message); }
|
|
158
|
+
}
|
|
159
|
+
async function toggleNode(id, enabled) {
|
|
160
|
+
try { await api('/api/nodes/' + id, 'PUT', { enabled }); await loadNodes(); }
|
|
161
|
+
catch (e) { toast('操作失败:' + e.message); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ========== 自定义来源 ========== */
|
|
165
|
+
async function loadSources() {
|
|
166
|
+
try {
|
|
167
|
+
const d = await api('/api/sources');
|
|
168
|
+
renderSourcesGrid(d.sources || []);
|
|
169
|
+
} catch (e) { console.warn('加载来源失败', e); }
|
|
170
|
+
}
|
|
171
|
+
async function saveSource(data) {
|
|
172
|
+
try {
|
|
173
|
+
const d = data.id
|
|
174
|
+
? await api('/api/sources/' + data.id, 'PUT', data)
|
|
175
|
+
: await api('/api/sources', 'POST', data);
|
|
176
|
+
await loadSources();
|
|
177
|
+
toast(data.id ? '来源已更新' : '来源已添加');
|
|
178
|
+
return d;
|
|
179
|
+
} catch (e) { toast('保存失败:' + e.message); }
|
|
180
|
+
}
|
|
181
|
+
async function deleteSource(id) {
|
|
182
|
+
try { await api('/api/sources/' + id, 'DELETE'); await loadSources(); toast('已删除来源'); }
|
|
183
|
+
catch (e) { toast('删除失败:' + e.message); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function delAuto(id) {
|
|
187
|
+
try { await api('/api/automations/' + id, 'DELETE'); await loadAutomations(); toast('已删除'); }
|
|
188
|
+
catch (e) { toast('删除失败:' + e.message); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runAuto(id) {
|
|
192
|
+
try { const d = await api('/api/automations/' + id + '/run', 'POST'); toast('已发起任务,运行次数 ' + d.runCount); await loadTasks(); }
|
|
193
|
+
catch (e) { toast('运行失败:' + e.message); }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function loadAutomations() {
|
|
197
|
+
try { const d = await api('/api/automations'); renderBuiltinAutomations(d.builtin || []); renderAutoGrid(d.automations || []); }
|
|
198
|
+
catch (e) { console.warn('加载自动化失败', e); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function getDrivesList() {
|
|
202
|
+
if (_drivesCache) return _drivesCache;
|
|
203
|
+
try {
|
|
204
|
+
const d = await api('/api/drives');
|
|
205
|
+
_drivesCache = d.drives || [];
|
|
206
|
+
return _drivesCache;
|
|
207
|
+
} catch {
|
|
208
|
+
return ['C:\\', 'D:\\', 'E:\\', 'F:\\', 'H:\\'];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function loadWorkspaceTree(dir) {
|
|
213
|
+
// 已废弃:左侧工作区树已移除,保留此函数仅供兼容
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function loadWorkspace() {
|
|
218
|
+
if (!state.token) {
|
|
219
|
+
console.warn('[workspace] 未登录,跳过加载');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const d = await api('/api/workspace');
|
|
224
|
+
state.workspaceDir = d.cwd;
|
|
225
|
+
// 持久化到 localStorage
|
|
226
|
+
localStorage.setItem('fhcode.workspaceDir', d.cwd);
|
|
227
|
+
renderWorkspaceBar();
|
|
228
|
+
console.log('[workspace] 加载成功:', d.cwd);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.error('[workspace] 加载失败:', e.message);
|
|
231
|
+
toast('工作区加载失败,请检查登录状态');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function setDefaultModel(id) {
|
|
236
|
+
try {
|
|
237
|
+
const d = await api('/api/models/' + encodeURIComponent(id) + '/default', 'POST');
|
|
238
|
+
state.defaultModelId = d.defaultId;
|
|
239
|
+
state.modelId = d.defaultId;
|
|
240
|
+
localStorage.setItem('fhcode.model', '');
|
|
241
|
+
renderModelSelect();
|
|
242
|
+
renderModelList();
|
|
243
|
+
renderModelListInline();
|
|
244
|
+
toast('已设为默认大模型');
|
|
245
|
+
} catch (e) { toast('设置失败:' + e.message); }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function deleteModel(id) {
|
|
249
|
+
try {
|
|
250
|
+
await api('/api/models/' + encodeURIComponent(id), 'DELETE');
|
|
251
|
+
toast('已删除配置');
|
|
252
|
+
await loadModels();
|
|
253
|
+
renderModelList();
|
|
254
|
+
} catch (e) { toast('删除失败:' + e.message); }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function saveModel() {
|
|
258
|
+
const name = document.getElementById('modelName').value.trim();
|
|
259
|
+
const apiBase = document.getElementById('modelApiBase').value.trim();
|
|
260
|
+
const apiKey = document.getElementById('modelApiKey').value;
|
|
261
|
+
const reasoning = document.getElementById('modelReasoning').value;
|
|
262
|
+
const msg = document.getElementById('modelFormMsg');
|
|
263
|
+
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
264
|
+
const editing = document.getElementById('modelSaveBtn').dataset.editing;
|
|
265
|
+
try {
|
|
266
|
+
const body = { name, apiBase, reasoning };
|
|
267
|
+
// 三重加密·通信层:密钥经 RSA 公钥加密传输,杜绝明文出网(编辑留空则服务端保留原密钥)
|
|
268
|
+
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
269
|
+
if (editing) body.id = editing;
|
|
270
|
+
const d = await api('/api/models', 'POST', body);
|
|
271
|
+
msg.textContent = '已保存:' + d.model.name;
|
|
272
|
+
msg.classList.remove('err');
|
|
273
|
+
await loadModels();
|
|
274
|
+
renderModelList();
|
|
275
|
+
resetModelForm();
|
|
276
|
+
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function loadModels() {
|
|
280
|
+
try {
|
|
281
|
+
const d = await api('/api/models');
|
|
282
|
+
state.models = d.models || [];
|
|
283
|
+
state.defaultModelId = d.defaultId || null;
|
|
284
|
+
const manual = localStorage.getItem('fhcode.model');
|
|
285
|
+
if (manual !== null && manual !== '') {
|
|
286
|
+
state.modelId = manual;
|
|
287
|
+
} else {
|
|
288
|
+
state.modelId = state.defaultModelId || '';
|
|
289
|
+
}
|
|
290
|
+
renderModelSelect();
|
|
291
|
+
} catch (e) { console.warn('加载大模型失败', e); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function rsaEncryptText(text) {
|
|
295
|
+
if (!text) return '';
|
|
296
|
+
const d = await api('/api/security/public-key');
|
|
297
|
+
const b64 = String(d.publicKey || '').replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
|
|
298
|
+
if (!b64) throw new Error('未获取到加密公钥');
|
|
299
|
+
const bin = atob(b64);
|
|
300
|
+
const buf = new Uint8Array(bin.length);
|
|
301
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
302
|
+
const key = await crypto.subtle.importKey('spki', buf.buffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
|
303
|
+
const enc = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, new TextEncoder().encode(text));
|
|
304
|
+
return btoa(String.fromCharCode.apply(null, new Uint8Array(enc)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function loadTasks() {
|
|
308
|
+
if (!state.token) return;
|
|
309
|
+
try {
|
|
310
|
+
const d = await api('/api/tasks');
|
|
311
|
+
state.tasks = d.tasks || [];
|
|
312
|
+
renderSidebarTaskList();
|
|
313
|
+
// 刷新页面:不自动选中旧任务,聊天区保持清空;
|
|
314
|
+
// 历史任务全部保留在左侧「任务列表」,点击即可回看对话。
|
|
315
|
+
if (state.currentTaskId) {
|
|
316
|
+
await refreshCurrentThread();
|
|
317
|
+
} else {
|
|
318
|
+
renderTaskThread(null);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (e) { console.warn('加载任务失败', e); }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function doLogin() {
|
|
325
|
+
const input = document.getElementById('loginPhone');
|
|
326
|
+
const status = document.getElementById('loginStatus');
|
|
327
|
+
const btn = document.getElementById('loginBtn');
|
|
328
|
+
const phone = (input.value || '').trim();
|
|
329
|
+
if (!/^\d{6,20}$/.test(phone)) { status.textContent = '请输入有效的手机号码'; status.classList.add('err'); return; }
|
|
330
|
+
btn.disabled = true;
|
|
331
|
+
status.textContent = '登录中…';
|
|
332
|
+
status.classList.remove('err');
|
|
333
|
+
try {
|
|
334
|
+
const d = await api('/api/auth/login', 'POST', { phone });
|
|
335
|
+
if (!d || !d.token) throw new Error('服务端未返回令牌');
|
|
336
|
+
state.token = d.token;
|
|
337
|
+
state.phone = d.phone || phone;
|
|
338
|
+
localStorage.setItem('fhcode.token', state.token);
|
|
339
|
+
localStorage.setItem('fhcode.phone', state.phone);
|
|
340
|
+
// 保存引导任务(供首次登录后展示)
|
|
341
|
+
if (d.isFirstLogin && d.welcomeTasks) {
|
|
342
|
+
await saveWelcomeTasks(d.welcomeTasks);
|
|
343
|
+
}
|
|
344
|
+
status.classList.remove('err');
|
|
345
|
+
document.getElementById('loginOverlay').style.display = 'none';
|
|
346
|
+
document.getElementById('appLayout').classList.add('show');
|
|
347
|
+
document.documentElement.classList.add('logged-in');
|
|
348
|
+
await afterLogin();
|
|
349
|
+
} catch (e) {
|
|
350
|
+
status.textContent = '登录失败:' + e.message;
|
|
351
|
+
status.classList.add('err');
|
|
352
|
+
} finally {
|
|
353
|
+
btn.disabled = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function api(path, method = 'GET', body) {
|
|
358
|
+
let res;
|
|
359
|
+
try {
|
|
360
|
+
res = await fetch(path, { method, headers: authHeaders(), body: body ? JSON.stringify(body) : undefined });
|
|
361
|
+
} catch (e) {
|
|
362
|
+
// 网络层失败(Failed to fetch / 连接拒绝 / 超时中断):标记后端可能已断开
|
|
363
|
+
if (typeof BackendConn !== 'undefined') BackendConn.markFailure();
|
|
364
|
+
throw e;
|
|
365
|
+
}
|
|
366
|
+
// 请求到达后端并返回响应:若之前处于断连状态,立即恢复
|
|
367
|
+
if (typeof BackendConn !== 'undefined' && !BackendConn.online) BackendConn.setOnline(true);
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
const d = await res.json().catch(() => ({}));
|
|
370
|
+
// 401:令牌确实被服务端拒绝,清除并回到登录页。
|
|
371
|
+
// 会话已落盘(web-sessions.json),正常重启不会再走到这里;
|
|
372
|
+
// 走到这里说明令牌真的过期或被吊销,此时给出明确提示,避免用户以为是"莫名跳回登录"。
|
|
373
|
+
if (res.status === 401 && d.error === 'unauthorized') {
|
|
374
|
+
if (state.token) {
|
|
375
|
+
console.warn('[api] token 已被服务端拒绝,清除并回到登录页:', path);
|
|
376
|
+
localStorage.removeItem('fhcode.token');
|
|
377
|
+
state.token = '';
|
|
378
|
+
try { toast('登录已过期,请重新登录'); } catch {}
|
|
379
|
+
}
|
|
380
|
+
const overlay = document.getElementById('loginOverlay');
|
|
381
|
+
if (overlay) overlay.style.display = 'flex';
|
|
382
|
+
throw new Error('登录已过期,请重新登录');
|
|
383
|
+
}
|
|
384
|
+
throw new Error(d.error || ('HTTP ' + res.status));
|
|
385
|
+
}
|
|
386
|
+
return res.json();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function authHeaders() {
|
|
390
|
+
return state.token ? { 'Authorization': 'Bearer ' + state.token, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ========== 后端连接状态监测(心跳 + 断连横幅 + 自动重连) ==========
|
|
394
|
+
* 解决问题:后端进程崩溃后,前端无感知,用户操作均报 "Failed to fetch" 且不知原因。
|
|
395
|
+
* 机制:
|
|
396
|
+
* 1. 每 5s 轮询 /api/health,连续失败 2 次判定后端断开,顶部显示红色横幅。
|
|
397
|
+
* 2. 断连后自动加快到每 2s 重试,恢复后自动切回 5s 并 toast 提示。
|
|
398
|
+
* 3. 任意 api() 调用网络失败也会累计失败计数(双保险,不必等下一轮心跳)。
|
|
399
|
+
* 4. 页面从后台切回 / 网络恢复事件时立即检测一次。
|
|
400
|
+
*/
|
|
401
|
+
const BackendConn = {
|
|
402
|
+
online: true,
|
|
403
|
+
heartbeatTimer: null,
|
|
404
|
+
reconnectTimer: null,
|
|
405
|
+
intervalMs: 5000, // 正常心跳间隔
|
|
406
|
+
reconnectMs: 2000, // 断连后重连检测间隔
|
|
407
|
+
consecutiveFailures: 0,
|
|
408
|
+
threshold: 2, // 连续失败 N 次才判定断开(避免瞬时抖动误报)
|
|
409
|
+
healthTimeoutMs: 3000, // 单次健康检查超时
|
|
410
|
+
|
|
411
|
+
init() {
|
|
412
|
+
this.startHeartbeat();
|
|
413
|
+
// 重连按钮
|
|
414
|
+
const btn = document.getElementById('backendReconnectBtn');
|
|
415
|
+
if (btn) btn.addEventListener('click', () => this.reconnect());
|
|
416
|
+
// 页面从后台切回前台时立即检测
|
|
417
|
+
document.addEventListener('visibilitychange', () => {
|
|
418
|
+
if (!document.hidden) this.check();
|
|
419
|
+
});
|
|
420
|
+
// 浏览器网络恢复事件
|
|
421
|
+
window.addEventListener('online', () => this.check());
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
startHeartbeat() {
|
|
425
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
426
|
+
this.heartbeatTimer = setInterval(() => this.check(), this.intervalMs);
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
async check() {
|
|
430
|
+
try {
|
|
431
|
+
const ctrl = new AbortController();
|
|
432
|
+
const timeout = setTimeout(() => ctrl.abort(), this.healthTimeoutMs);
|
|
433
|
+
const res = await fetch('/api/health', {
|
|
434
|
+
method: 'GET',
|
|
435
|
+
signal: ctrl.signal,
|
|
436
|
+
cache: 'no-store',
|
|
437
|
+
headers: { 'Cache-Control': 'no-cache' },
|
|
438
|
+
});
|
|
439
|
+
clearTimeout(timeout);
|
|
440
|
+
if (res.ok) {
|
|
441
|
+
this.consecutiveFailures = 0;
|
|
442
|
+
this.setOnline(true);
|
|
443
|
+
} else {
|
|
444
|
+
this.markFailure();
|
|
445
|
+
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
this.markFailure();
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/** 累计失败次数,达到阈值则判定断开 */
|
|
452
|
+
markFailure() {
|
|
453
|
+
this.consecutiveFailures++;
|
|
454
|
+
if (this.consecutiveFailures >= this.threshold) {
|
|
455
|
+
this.setOnline(false);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
setOnline(online) {
|
|
460
|
+
if (this.online === online) return;
|
|
461
|
+
this.online = online;
|
|
462
|
+
const banner = document.getElementById('backendOfflineBanner');
|
|
463
|
+
const text = document.getElementById('backendOfflineText');
|
|
464
|
+
if (banner) {
|
|
465
|
+
banner.style.display = online ? 'none' : 'flex';
|
|
466
|
+
}
|
|
467
|
+
if (online) {
|
|
468
|
+
// 恢复:切回正常心跳间隔
|
|
469
|
+
if (this.reconnectTimer) { clearInterval(this.reconnectTimer); this.reconnectTimer = null; }
|
|
470
|
+
this.startHeartbeat();
|
|
471
|
+
try { toast('后端服务已恢复连接'); } catch (e) {}
|
|
472
|
+
} else {
|
|
473
|
+
// 断开:停止正常心跳,加快重连检测
|
|
474
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
475
|
+
if (!this.reconnectTimer) {
|
|
476
|
+
this.reconnectTimer = setInterval(() => this.check(), this.reconnectMs);
|
|
477
|
+
}
|
|
478
|
+
if (text) text.textContent = '后端服务已断开,正在自动重连…';
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
/** 手动触发重连:重置失败计数并立即检测 */
|
|
483
|
+
reconnect() {
|
|
484
|
+
const text = document.getElementById('backendOfflineText');
|
|
485
|
+
if (text) text.textContent = '正在重连后端服务…';
|
|
486
|
+
this.consecutiveFailures = 0;
|
|
487
|
+
this.check();
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// 页面加载完成后启动心跳监测
|
|
492
|
+
if (document.readyState === 'loading') {
|
|
493
|
+
document.addEventListener('DOMContentLoaded', () => BackendConn.init());
|
|
494
|
+
} else {
|
|
495
|
+
BackendConn.init();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* ========== v7.2.0 新能力 API 封装(voice / knowledge / plugins / sso)==========
|
|
499
|
+
* 修复:v7.2 新增能力此前前端 0 调用。以下封装供能力中心面板使用。
|
|
500
|
+
*/
|
|
501
|
+
async function apiVoiceParse(text) {
|
|
502
|
+
return api('/api/voice/parse', 'POST', { text });
|
|
503
|
+
}
|
|
504
|
+
async function apiVoiceCommands() {
|
|
505
|
+
return api('/api/voice/commands');
|
|
506
|
+
}
|
|
507
|
+
async function apiVoiceToCode(description, language) {
|
|
508
|
+
return api('/api/voice/to-code', 'POST', { description, language: language || 'typescript' });
|
|
509
|
+
}
|
|
510
|
+
async function apiKnowledgeStats() {
|
|
511
|
+
return api('/api/knowledge/stats');
|
|
512
|
+
}
|
|
513
|
+
async function apiKnowledgeList(params) {
|
|
514
|
+
const qs = new URLSearchParams();
|
|
515
|
+
if (params) { for (const k of ['category', 'type', 'sortBy', 'order', 'limit', 'offset']) { if (params[k]) qs.set(k, params[k]); } }
|
|
516
|
+
const s = qs.toString();
|
|
517
|
+
return api('/api/knowledge/documents' + (s ? '?' + s : ''));
|
|
518
|
+
}
|
|
519
|
+
async function apiKnowledgeSearch(query, extra) {
|
|
520
|
+
return api('/api/knowledge/search', 'POST', { query, ...(extra || {}) });
|
|
521
|
+
}
|
|
522
|
+
async function apiKnowledgeAdd(data) {
|
|
523
|
+
return api('/api/knowledge/document', 'POST', data);
|
|
524
|
+
}
|
|
525
|
+
async function apiKnowledgeDelete(id) {
|
|
526
|
+
return api('/api/knowledge/document/' + encodeURIComponent(id), 'DELETE');
|
|
527
|
+
}
|
|
528
|
+
async function apiPluginsMarket(q, category) {
|
|
529
|
+
const qs = new URLSearchParams();
|
|
530
|
+
if (q) qs.set('q', q);
|
|
531
|
+
if (category) qs.set('category', category);
|
|
532
|
+
const s = qs.toString();
|
|
533
|
+
return api('/api/plugins/market' + (s ? '?' + s : ''));
|
|
534
|
+
}
|
|
535
|
+
async function apiPluginsList(status) {
|
|
536
|
+
const s = status ? '?status=' + encodeURIComponent(status) : '';
|
|
537
|
+
return api('/api/plugins' + s);
|
|
538
|
+
}
|
|
539
|
+
async function apiPluginsInstall(plugin) {
|
|
540
|
+
return api('/api/plugins/install', 'POST', { plugin });
|
|
541
|
+
}
|
|
542
|
+
async function apiSsoProviders() {
|
|
543
|
+
return api('/api/sso/providers');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* ========== P1-1: 代码补全(ghost text)========== */
|
|
547
|
+
async function apiCompletion(filePath, fileContent, opts) {
|
|
548
|
+
return api('/api/completion', 'POST', {
|
|
549
|
+
filePath,
|
|
550
|
+
fileContent,
|
|
551
|
+
cursorOffset: opts?.cursorOffset ?? fileContent.length,
|
|
552
|
+
mode: opts?.mode ?? 'quick',
|
|
553
|
+
language: opts?.language,
|
|
554
|
+
});
|
|
555
|
+
}
|