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,2210 +1,2210 @@
|
|
|
1
|
-
// i18n 双语字典
|
|
2
|
-
const I18N = {
|
|
3
|
-
zh: {
|
|
4
|
-
'app.title_login': '飞虹 Code 控制台',
|
|
5
|
-
'app.login_hint': '请输入手机号码直接登录',
|
|
6
|
-
'app.brand': '飞虹 Code',
|
|
7
|
-
'login.phone_label': '手机号码',
|
|
8
|
-
'login.btn_login': '手机号登录',
|
|
9
|
-
'nav.chat': '💬 对话',
|
|
10
|
-
'nav.automations': '⚡ 自动化',
|
|
11
|
-
'nav.templates': '📚 模板库',
|
|
12
|
-
'nav.market': '🧩 插件',
|
|
13
|
-
'nav.office': '📎 办公',
|
|
14
|
-
'nav.memory': '🧠 记忆',
|
|
15
|
-
'nav.release': '🚀 发布',
|
|
16
|
-
'sidebar.nav': '导航',
|
|
17
|
-
'sidebar.chat': '对话任务',
|
|
18
|
-
'sidebar.automations': '自动化',
|
|
19
|
-
'sidebar.templates': '模板库',
|
|
20
|
-
'sidebar.market': '插件市场',
|
|
21
|
-
'sidebar.office': '办公助理',
|
|
22
|
-
'sidebar.memory': '记忆系统',
|
|
23
|
-
'sidebar.user': '用户',
|
|
24
|
-
'sidebar.settings': '设置',
|
|
25
|
-
'sidebar.task_list': '任务列表',
|
|
26
|
-
'btn.new_task': '+ 新建任务',
|
|
27
|
-
'empty.no_tasks': '暂无任务',
|
|
28
|
-
'empty.no_skills': '没有找到技能',
|
|
29
|
-
'empty.no_builtin_templates': '无内置模板',
|
|
30
|
-
'empty.no_models': '还没有大模型配置,请在下方添加。',
|
|
31
|
-
'empty.preparing_welcome': '正在为您准备引导任务,请稍候…',
|
|
32
|
-
'task.not_selected': '未选择任务',
|
|
33
|
-
'task.select_hint': '从左侧任务列表选择,或在下方输入指令发起新任务',
|
|
34
|
-
'task.refresh': '刷新当前任务',
|
|
35
|
-
'chat.welcome': '点击「+ 新建任务」开启一个新任务,或直接在下方向当前任务发送消息——所有对话都会归属同一个任务运行。',
|
|
36
|
-
'chat.menu': '+ 菜单',
|
|
37
|
-
'chat.screenshot': '📷 截图',
|
|
38
|
-
'chat.upload_file': '📄 上传文件',
|
|
39
|
-
'chat.upload_image': '🖼 上传图片',
|
|
40
|
-
'chat.direct_mode': '🖥 电脑操作',
|
|
41
|
-
'chat.voice': '🎤 语音输入',
|
|
42
|
-
'chat.agent_type': '🤖 选择智能体类型',
|
|
43
|
-
'chat.agent_default': '🤖 通用助手',
|
|
44
|
-
'chat.listening': '● 正在听…',
|
|
45
|
-
'chat.send': '▶ 发送',
|
|
46
|
-
'common.loading': '加载中…',
|
|
47
|
-
'perm.default': '默认权限',
|
|
48
|
-
'call.hint': '通话',
|
|
49
|
-
'call.voice': '📞 语音',
|
|
50
|
-
'call.video': '🎥 视频',
|
|
51
|
-
'call.hangup': '⛔ 挂断',
|
|
52
|
-
'call.voice_call': '语音通话',
|
|
53
|
-
'call.video_call': '视频通话',
|
|
54
|
-
'call.voice_hint': '语音通话中,说话内容实时转文字',
|
|
55
|
-
'call.video_hint': '视频通话中,可截取画面发送',
|
|
56
|
-
'call.ended': '通话已结束,识别的文字已保留在输入框',
|
|
57
|
-
'computer.tab': '🖥 电脑操作',
|
|
58
|
-
'computer.capture': '📷 截取屏幕',
|
|
59
|
-
'computer.mouse_pos': '坐标',
|
|
60
|
-
'computer.click_left': '左键点击',
|
|
61
|
-
'computer.click_right': '右键点击',
|
|
62
|
-
'computer.double_click': '双击',
|
|
63
|
-
'computer.type_hint': '输入文字后回车发送',
|
|
64
|
-
'computer.screenshot_added': '摄像头画面已添加到输入框,可直接发送',
|
|
65
|
-
'computer.screenshot_not_ready': '视频还没准备好,请稍等',
|
|
66
|
-
'msg.copy': '📋 复制',
|
|
67
|
-
'msg.create_doc': '📄 建文档',
|
|
68
|
-
'msg.copied': '已复制到剪贴板',
|
|
69
|
-
'msg.doc_downloaded': '文档已下载',
|
|
70
|
-
'msg.no_content': '没有可复制的内容',
|
|
71
|
-
'crop.tip': '按住鼠标拖拽选择截图区域 · 松开确认 · ESC 取消',
|
|
72
|
-
'page.automations.sub': '把常用目标存成指令,一键发起任务',
|
|
73
|
-
'btn.new_automation': '+ 新建指令',
|
|
74
|
-
'empty.no_automations': '还没有快捷指令',
|
|
75
|
-
'page.templates.sub': '点击模板即可填充到输入框',
|
|
76
|
-
'btn.save_template': '+ 保存当前为模板',
|
|
77
|
-
'tpl.builtin': '内置模板',
|
|
78
|
-
'tpl.my': '我的模板',
|
|
79
|
-
'empty.no_templates': '暂无自定义模板',
|
|
80
|
-
'page.market.sub': '聚合 ClawHub 与 Agent-Foundry 技能生态',
|
|
81
|
-
'market.search_placeholder': '搜索…',
|
|
82
|
-
'market.source_all': '全部来源',
|
|
83
|
-
'btn.refresh': '刷新',
|
|
84
|
-
'market.installed': '已安装',
|
|
85
|
-
'empty.no_installed': '暂无已安装技能',
|
|
86
|
-
'page.office.sub': '文档处理快捷入口,点击即生成提示词',
|
|
87
|
-
'page.memory.sub': '短期记忆每日自动整理,沉淀长期知识',
|
|
88
|
-
'btn.refresh_mem': '🔄 刷新',
|
|
89
|
-
'btn.summarize': '✨ 立即总结',
|
|
90
|
-
'mem.stat_short_days': '短期记忆天数',
|
|
91
|
-
'mem.stat_long_items': '长期记忆条目',
|
|
92
|
-
'mem.stat_last_summary': '上次总结',
|
|
93
|
-
'mem.stat_next_summary': '下次自动总结',
|
|
94
|
-
'mem.tab_short': '📅 短期记忆',
|
|
95
|
-
'mem.tab_long': '💾 长期记忆',
|
|
96
|
-
'mem.tab_history': '📜 总结历史',
|
|
97
|
-
'btn.view': '查看',
|
|
98
|
-
'mem.select_date': '选择日期查看短期记忆',
|
|
99
|
-
'empty.no_history': '暂无总结记录',
|
|
100
|
-
'footbar.title': '对话任务',
|
|
101
|
-
'footbar.desc': '提交自然语言需求,云端静默执行并回传结果',
|
|
102
|
-
'footbar.model': '大模型',
|
|
103
|
-
'footbar.model_title': '切换当前使用的大模型',
|
|
104
|
-
'footbar.model_settings': '大模型设置',
|
|
105
|
-
'footbar.model_default': '默认(系统路由)',
|
|
106
|
-
'right.detail': '📦 任务详情',
|
|
107
|
-
'right.preview': '👁 产物预览',
|
|
108
|
-
'right.detail_hint': '从顶部选择一个任务查看详情。',
|
|
109
|
-
'right.preview_title': '预览',
|
|
110
|
-
'right.preview_hint': '点击任务或对话中的文件/代码进行预览。',
|
|
111
|
-
'modal.auto_title': '新建快捷指令',
|
|
112
|
-
'modal.auto_hint': '保存后可在「自动化」页一键发起任务。',
|
|
113
|
-
'modal.auto_name': '指令名称',
|
|
114
|
-
'modal.auto_goal': '目标(提交给模型的需求)',
|
|
115
|
-
'modal.perm_title': '🔒 权限处理窗口',
|
|
116
|
-
'modal.perm_hint': '配置工作期间 AI 可读范围及可操作权限。',
|
|
117
|
-
'modal.perm_read_scope': '可读范围',
|
|
118
|
-
'modal.perm_workspace': '仅当前工作区',
|
|
119
|
-
'modal.perm_specified': '指定目录',
|
|
120
|
-
'modal.perm_all': '全部文件',
|
|
121
|
-
'modal.perm_actions': '操作权限',
|
|
122
|
-
'modal.perm_read': '读取文件',
|
|
123
|
-
'modal.perm_write': '写入文件',
|
|
124
|
-
'modal.perm_shell': '执行命令',
|
|
125
|
-
'modal.perm_network': '访问网络',
|
|
126
|
-
'modal.perm_browser': '浏览器控制',
|
|
127
|
-
'modal.agent_title': '🤖 选择智能体类型',
|
|
128
|
-
'modal.agent_hint': '不同智能体会自动调整提示词与执行策略。',
|
|
129
|
-
'modal.folder_title': '📂 选择工作区文件夹',
|
|
130
|
-
'btn.select_folder': '选择此文件夹',
|
|
131
|
-
'modal.model_title': '🧠 大模型设置',
|
|
132
|
-
'modal.model_hint': '配置自定义大模型,可保存多个并在对话中随时切换。勾选「默认」的项将作为对话默认模型。',
|
|
133
|
-
'modal.model_name': '模型名称 *',
|
|
134
|
-
'modal.model_api': 'API 地址',
|
|
135
|
-
'modal.model_key': '密钥(API Key)',
|
|
136
|
-
'modal.model_reasoning': '推理过程运行内容(中间环节)',
|
|
137
|
-
'btn.save_config': '保存配置',
|
|
138
|
-
'btn.clear_form': '清空表单',
|
|
139
|
-
'btn.close': '关闭',
|
|
140
|
-
'modal.new_task_title': '+ 新建对话任务',
|
|
141
|
-
'modal.new_task_hint': '创建一个全新任务,中部对话栏的所有消息都将归属该任务运行(支持多轮连续对话)。',
|
|
142
|
-
'modal.new_task_label': '任务描述',
|
|
143
|
-
'modal.new_task_agent': '智能体类型',
|
|
144
|
-
'modal.new_task_workspace': '工作区',
|
|
145
|
-
'btn.create_task': '创建任务',
|
|
146
|
-
'modal.settings_title': '⚙️ 设置',
|
|
147
|
-
'settings.general': '通用',
|
|
148
|
-
'settings.layout': '栏目配置',
|
|
149
|
-
'settings.perm': '权限处理',
|
|
150
|
-
'settings.perm_desc': '配置工作期间可读范围与操作权限',
|
|
151
|
-
'settings.theme': '外观',
|
|
152
|
-
'settings.theme_desc': '切换浅色 / 深色主题',
|
|
153
|
-
'settings.session': '💬 会话管理',
|
|
154
|
-
'menu.version': '体验版',
|
|
155
|
-
'menu.current': '当前',
|
|
156
|
-
'menu.buddy': 'Buddy 加油站',
|
|
157
|
-
'menu.invite': '去邀约',
|
|
158
|
-
'menu.invite_right': '最高得会员',
|
|
159
|
-
'menu.points': '积分余额',
|
|
160
|
-
'menu.growth': '成长计划',
|
|
161
|
-
'menu.growth_right': '连登有奖',
|
|
162
|
-
'menu.help': '帮助与反馈',
|
|
163
|
-
'menu.update': '检查更新',
|
|
164
|
-
'menu.logout': '退出登录',
|
|
165
|
-
'btn.cancel': '取消',
|
|
166
|
-
'btn.save': '保存',
|
|
167
|
-
'sidebar.release': '产品发布',
|
|
168
|
-
'modal.tpl_title_label': '模板标题',
|
|
169
|
-
'modal.tpl_title_ph': '如:SQL 优化',
|
|
170
|
-
'modal.tpl_category': '分类',
|
|
171
|
-
'modal.tpl_category_ph': '自定义',
|
|
172
|
-
'modal.tpl_goal': '目标内容',
|
|
173
|
-
'settings.session_auto_new': '发送消息前自动新建对话',
|
|
174
|
-
'settings.session_auto_open': '页面打开时自动新建对话',
|
|
175
|
-
'settings.session_clear': '新建对话时清空历史上下文',
|
|
176
|
-
'modal.auto_name_ph': '如:每日构建检查',
|
|
177
|
-
'modal.auto_goal_ph': '运行 npm test 并汇总失败用例…',
|
|
178
|
-
'modal.model_name_ph': '如 GPT-4o / 通义千问 / 自建模型',
|
|
179
|
-
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
180
|
-
'modal.model_key_ph': 'sk-...',
|
|
181
|
-
'modal.model_reasoning_ph': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
182
|
-
'modal.perm_path_ph': '输入允许的目录路径',
|
|
183
|
-
// ---- select 的 title 提示(键名固定为 title.<元素id>)----
|
|
184
|
-
'title.modelSelect': '切换当前使用的大模型',
|
|
185
|
-
// ---- 输入框占位符(键名固定为 ph.<元素id>)----
|
|
186
|
-
'ph.goalInput': '输入指令,消息将归属当前任务持续对话;点上方「+ 新建任务」可开启新任务(创建 → 推理 → 工具验证 → 闭环全程追踪)',
|
|
187
|
-
'ph.marketSearch': '搜索…',
|
|
188
|
-
'ph.autoName': '如:每日构建检查',
|
|
189
|
-
'ph.autoGoal': '运行 npm test 并汇总失败用例…',
|
|
190
|
-
'ph.tplTitle': '如:SQL 优化',
|
|
191
|
-
'ph.tplCategory': '自定义',
|
|
192
|
-
'ph.tplGoal': '粘贴要保存的需求文本…',
|
|
193
|
-
'ph.permReadPath': '输入允许的目录路径',
|
|
194
|
-
'ph.modelName': '如 GPT-4o / 通义千问 / 自建模型',
|
|
195
|
-
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
196
|
-
'ph.modelApiKey': 'sk-...',
|
|
197
|
-
'ph.modelReasoning': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
198
|
-
'ph.ntGoal': '例如:请读取当前工作区 package.json,告诉我其中的 name 与 version 字段',
|
|
199
|
-
'ph.imName': '模型名称 *',
|
|
200
|
-
'ph.imBase': 'API 地址',
|
|
201
|
-
},
|
|
202
|
-
en: {
|
|
203
|
-
'app.title_login': 'Feihong Code Console',
|
|
204
|
-
'app.login_hint': 'Enter phone number to login directly',
|
|
205
|
-
'app.brand': 'Feihong Code',
|
|
206
|
-
'login.phone_label': 'Phone Number',
|
|
207
|
-
'login.btn_login': 'Login',
|
|
208
|
-
'nav.chat': '💬 Chat',
|
|
209
|
-
'nav.automations': '⚡ Automations',
|
|
210
|
-
'nav.templates': '📚 Templates',
|
|
211
|
-
'nav.market': '🧩 Market',
|
|
212
|
-
'nav.office': '📎 Office',
|
|
213
|
-
'nav.release': '🚀 Release',
|
|
214
|
-
'nav.memory': '🧠 Memory',
|
|
215
|
-
'sidebar.nav': 'Nav',
|
|
216
|
-
'sidebar.chat': 'Chat',
|
|
217
|
-
'sidebar.automations': 'Automations',
|
|
218
|
-
'sidebar.templates': 'Templates',
|
|
219
|
-
'sidebar.market': 'Market',
|
|
220
|
-
'sidebar.office': 'Office',
|
|
221
|
-
'sidebar.release': 'Release',
|
|
222
|
-
'sidebar.memory': 'Memory',
|
|
223
|
-
'sidebar.user': 'User',
|
|
224
|
-
'sidebar.settings': 'Settings',
|
|
225
|
-
'sidebar.task_list': 'Tasks',
|
|
226
|
-
'btn.new_task': '+ New Task',
|
|
227
|
-
'empty.no_tasks': 'No tasks',
|
|
228
|
-
'empty.no_skills': 'No skills found',
|
|
229
|
-
'empty.no_builtin_templates': 'No built-in templates',
|
|
230
|
-
'empty.no_models': 'No model configured yet, add one below.',
|
|
231
|
-
'empty.preparing_welcome': 'Preparing welcome tasks for you, please wait…',
|
|
232
|
-
'task.not_selected': 'No task selected',
|
|
233
|
-
'task.select_hint': 'Select from task list or enter commands below',
|
|
234
|
-
'task.refresh': 'Refresh',
|
|
235
|
-
'chat.welcome': 'Click + New Task to start a new task, or send messages to continue the current task.',
|
|
236
|
-
'chat.menu': '+ Menu',
|
|
237
|
-
'chat.screenshot': '📷 Screenshot',
|
|
238
|
-
'chat.upload_file': '📄 Upload File',
|
|
239
|
-
'chat.upload_image': '🖼 Upload Image',
|
|
240
|
-
'chat.direct_mode': '🖥 Computer Control',
|
|
241
|
-
'chat.voice': '🎤 Voice Input',
|
|
242
|
-
'chat.agent_type': '🤖 Agent Type',
|
|
243
|
-
'chat.agent_default': '🤖 Assistant',
|
|
244
|
-
'chat.listening': '● Listening…',
|
|
245
|
-
'chat.send': '▶ Send',
|
|
246
|
-
'common.loading': 'Loading…',
|
|
247
|
-
'perm.default': 'Default Permissions',
|
|
248
|
-
'call.hint': 'Call',
|
|
249
|
-
'call.voice': '📞 Voice',
|
|
250
|
-
'call.video': '🎥 Video',
|
|
251
|
-
'call.hangup': '⛔ Hang Up',
|
|
252
|
-
'call.voice_call': 'Voice Call',
|
|
253
|
-
'call.video_call': 'Video Call',
|
|
254
|
-
'call.voice_hint': 'Voice call in progress, speech transcribed in real-time',
|
|
255
|
-
'call.video_hint': 'Video call in progress, capture frames to send',
|
|
256
|
-
'call.ended': 'Call ended, transcribed text kept in input box',
|
|
257
|
-
'computer.tab': '🖥 Computer Control',
|
|
258
|
-
'computer.capture': '📷 Capture Screen',
|
|
259
|
-
'computer.mouse_pos': 'Pos',
|
|
260
|
-
'computer.click_left': 'Left Click',
|
|
261
|
-
'computer.click_right': 'Right Click',
|
|
262
|
-
'computer.double_click': 'Double Click',
|
|
263
|
-
'computer.type_hint': 'Type text and press Enter to send',
|
|
264
|
-
'computer.screenshot_added': 'Camera frame added to input box, ready to send',
|
|
265
|
-
'computer.screenshot_not_ready': 'Video not ready yet, please wait',
|
|
266
|
-
'msg.copy': '📋 Copy',
|
|
267
|
-
'msg.create_doc': '📄 New Doc',
|
|
268
|
-
'msg.copied': 'Copied to clipboard',
|
|
269
|
-
'msg.doc_downloaded': 'Document downloaded',
|
|
270
|
-
'msg.no_content': 'No content to copy',
|
|
271
|
-
'crop.tip': 'Drag to select a screenshot area · Release to confirm · ESC to cancel',
|
|
272
|
-
'page.automations.sub': 'Save common goals as shortcuts, launch tasks with one click',
|
|
273
|
-
'btn.new_automation': '+ New Command',
|
|
274
|
-
'empty.no_automations': 'No shortcuts yet',
|
|
275
|
-
'page.templates.sub': 'Click a template to fill the input box',
|
|
276
|
-
'btn.save_template': '+ Save as Template',
|
|
277
|
-
'tpl.builtin': 'Built-in Templates',
|
|
278
|
-
'tpl.my': 'My Templates',
|
|
279
|
-
'empty.no_templates': 'No custom templates',
|
|
280
|
-
'page.market.sub': 'Aggregating ClawHub & Agent-Foundry skill ecosystems',
|
|
281
|
-
'market.search_placeholder': 'Search…',
|
|
282
|
-
'market.source_all': 'All Sources',
|
|
283
|
-
'btn.refresh': 'Refresh',
|
|
284
|
-
'market.installed': 'Installed',
|
|
285
|
-
'empty.no_installed': 'No installed skills',
|
|
286
|
-
'page.office.sub': 'Document processing shortcuts, generate prompts instantly',
|
|
287
|
-
'page.memory.sub': 'Daily auto-summary of short-term memory, accumulate long-term knowledge',
|
|
288
|
-
'btn.refresh_mem': '🔄 Refresh',
|
|
289
|
-
'btn.summarize': '✨ Summarize Now',
|
|
290
|
-
'mem.stat_short_days': 'Short-term Memory Days',
|
|
291
|
-
'mem.stat_long_items': 'Long-term Memory Items',
|
|
292
|
-
'mem.stat_last_summary': 'Last Summary',
|
|
293
|
-
'mem.stat_next_summary': 'Next Summary',
|
|
294
|
-
'mem.tab_short': '📅 Short-term',
|
|
295
|
-
'mem.tab_long': '💾 Long-term',
|
|
296
|
-
'mem.tab_history': '📜 History',
|
|
297
|
-
'btn.view': 'View',
|
|
298
|
-
'mem.select_date': 'Select date to view',
|
|
299
|
-
'empty.no_history': 'No summary records',
|
|
300
|
-
'footbar.title': 'Chat Task',
|
|
301
|
-
'footbar.desc': 'Submit natural language requirements, cloud silent execution and return results',
|
|
302
|
-
'footbar.model': 'Model',
|
|
303
|
-
'footbar.model_title': 'Switch model',
|
|
304
|
-
'footbar.model_settings': 'Model Settings',
|
|
305
|
-
'footbar.model_default': 'Default (System Route)',
|
|
306
|
-
'right.detail': '📦 Task Detail',
|
|
307
|
-
'right.preview': '👁 Preview',
|
|
308
|
-
'right.detail_hint': 'Select a task from the top to view details.',
|
|
309
|
-
'right.preview_title': 'Preview',
|
|
310
|
-
'right.preview_hint': 'Click files/codes in task or chat to preview.',
|
|
311
|
-
'modal.auto_title': 'New Automation',
|
|
312
|
-
'modal.auto_hint': 'Save for one-click task launch on Automations page.',
|
|
313
|
-
'modal.auto_name': 'Command Name',
|
|
314
|
-
'modal.auto_goal': 'Goal (Requirements for model)',
|
|
315
|
-
'modal.perm_title': '🔒 Permission Window',
|
|
316
|
-
'modal.perm_hint': 'Configure AI readable scope and operable permissions.',
|
|
317
|
-
'modal.perm_read_scope': 'Read Scope',
|
|
318
|
-
'modal.perm_workspace': 'Current Workspace Only',
|
|
319
|
-
'modal.perm_specified': 'Specified Directory',
|
|
320
|
-
'modal.perm_all': 'All Files',
|
|
321
|
-
'modal.perm_actions': 'Operation Permissions',
|
|
322
|
-
'modal.perm_read': 'Read Files',
|
|
323
|
-
'modal.perm_write': 'Write Files',
|
|
324
|
-
'modal.perm_shell': 'Execute Commands',
|
|
325
|
-
'modal.perm_network': 'Access Network',
|
|
326
|
-
'modal.perm_browser': 'Browser Control',
|
|
327
|
-
'modal.agent_title': '🤖 Agent Type',
|
|
328
|
-
'modal.agent_hint': 'Different agents auto-adjust prompts and execution strategies.',
|
|
329
|
-
'modal.folder_title': '📂 Select Workspace Folder',
|
|
330
|
-
'btn.select_folder': 'Select This Folder',
|
|
331
|
-
'modal.model_title': '🧠 Model Settings',
|
|
332
|
-
'modal.model_hint': 'Configure custom models, save multiple and switch anytime. Default model will be used by default.',
|
|
333
|
-
'modal.model_name': 'Model Name *',
|
|
334
|
-
'modal.model_api': 'API URL',
|
|
335
|
-
'modal.model_key': 'API Key',
|
|
336
|
-
'modal.model_reasoning': 'Reasoning Content (Optional)',
|
|
337
|
-
'btn.save_config': 'Save Config',
|
|
338
|
-
'btn.clear_form': 'Clear Form',
|
|
339
|
-
'btn.close': 'Close',
|
|
340
|
-
'modal.new_task_title': '+ New Chat Task',
|
|
341
|
-
'modal.new_task_hint': 'Create a brand new task. All messages in the chat will belong to this task (supports multi-turn conversation).',
|
|
342
|
-
'modal.new_task_label': 'Task Description',
|
|
343
|
-
'modal.new_task_agent': 'Agent Type',
|
|
344
|
-
'modal.new_task_workspace': 'Workspace',
|
|
345
|
-
'btn.create_task': 'Create Task',
|
|
346
|
-
'modal.settings_title': '⚙️ Settings',
|
|
347
|
-
'settings.general': 'General',
|
|
348
|
-
'settings.layout': 'Layout Config',
|
|
349
|
-
'settings.perm': 'Permissions',
|
|
350
|
-
'settings.perm_desc': 'Configure readable scope and operation permissions',
|
|
351
|
-
'settings.theme': 'Appearance',
|
|
352
|
-
'settings.theme_desc': 'Switch light/dark theme',
|
|
353
|
-
'settings.session': '💬 Session Management',
|
|
354
|
-
'menu.version': 'Trial Version',
|
|
355
|
-
'menu.current': 'Current',
|
|
356
|
-
'menu.buddy': 'Buddy Station',
|
|
357
|
-
'menu.invite': 'Invite',
|
|
358
|
-
'menu.invite_right': 'Get Premium',
|
|
359
|
-
'menu.points': 'Points Balance',
|
|
360
|
-
'menu.growth': 'Growth Plan',
|
|
361
|
-
'menu.growth_right': 'Streak Bonus',
|
|
362
|
-
'menu.help': 'Help & Feedback',
|
|
363
|
-
'menu.update': 'Check Update',
|
|
364
|
-
'menu.logout': 'Logout',
|
|
365
|
-
'btn.cancel': 'Cancel',
|
|
366
|
-
'btn.save': 'Save',
|
|
367
|
-
'sidebar.release': 'Release',
|
|
368
|
-
'modal.tpl_title_label': 'Template Title',
|
|
369
|
-
'modal.tpl_title_ph': 'e.g. SQL optimization',
|
|
370
|
-
'modal.tpl_category': 'Category',
|
|
371
|
-
'modal.tpl_category_ph': 'Custom',
|
|
372
|
-
'modal.tpl_goal': 'Goal Content',
|
|
373
|
-
'settings.session_auto_new': 'Auto new conversation before sending',
|
|
374
|
-
'settings.session_auto_open': 'Auto new conversation on page open',
|
|
375
|
-
'settings.session_clear': 'Clear history on new conversation',
|
|
376
|
-
'modal.auto_name_ph': 'e.g. Daily build check',
|
|
377
|
-
'modal.auto_goal_ph': 'Run npm test and summarize failures…',
|
|
378
|
-
'modal.model_name_ph': 'e.g. GPT-4o / Qwen / custom model',
|
|
379
|
-
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
380
|
-
'modal.model_key_ph': 'sk-...',
|
|
381
|
-
'modal.model_reasoning_ph': 'Configure what this model runs in intermediate reasoning steps, e.g. break down requirements first, then propose a solution, then output code.',
|
|
382
|
-
'modal.perm_path_ph': 'Enter allowed directory path',
|
|
383
|
-
// ---- select title hints (key = title.<element id>) ----
|
|
384
|
-
'title.modelSelect': 'Switch the active LLM',
|
|
385
|
-
// ---- input placeholders (key = ph.<element id>) ----
|
|
386
|
-
'ph.goalInput': 'Type an instruction. Messages stay in the current task; click "+ New Task" above to start a new one (create → reason → verify → close the loop).',
|
|
387
|
-
'ph.marketSearch': 'Search…',
|
|
388
|
-
'ph.autoName': 'e.g. Daily build check',
|
|
389
|
-
'ph.autoGoal': 'Run npm test and summarize failing cases…',
|
|
390
|
-
'ph.tplTitle': 'e.g. SQL Optimization',
|
|
391
|
-
'ph.tplCategory': 'Custom',
|
|
392
|
-
'ph.tplGoal': 'Paste the requirement text to save…',
|
|
393
|
-
'ph.permReadPath': 'Enter an allowed directory path',
|
|
394
|
-
'ph.modelName': 'e.g. GPT-4o / Qwen / Self-hosted',
|
|
395
|
-
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
396
|
-
'ph.modelApiKey': 'sk-...',
|
|
397
|
-
'ph.modelReasoning': 'Define what this model does during intermediate reasoning, e.g. break down requirements first, then propose a plan, then output code.',
|
|
398
|
-
'ph.ntGoal': 'e.g. Read package.json in the current workspace and tell me its name and version fields',
|
|
399
|
-
'ph.imName': 'Model name *',
|
|
400
|
-
'ph.imBase': 'API endpoint',
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
let currentLang = localStorage.getItem('fhcode.lang') || 'zh';
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* 安全地把某个元素的 i18n 文案落地。
|
|
409
|
-
* 关键约束(曾导致「模型无法切换」「侧边栏图标消失」的线上问题):
|
|
410
|
-
* - <select> 绝对不能写 textContent,否则会把所有 <option> 一次性清空;
|
|
411
|
-
* - <input>/<textarea> 只改 placeholder,且必须命中专用键,避免误翻;
|
|
412
|
-
* - 含子元素的容器(图标 + 标签)只改 title / 直接文本节点,绝不整体覆盖。
|
|
413
|
-
*/
|
|
414
|
-
|
|
415
|
-
function switchLang(lang) {
|
|
416
|
-
currentLang = lang;
|
|
417
|
-
localStorage.setItem('fhcode.lang', lang);
|
|
418
|
-
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
419
|
-
try { applyI18nToEl(el); } catch (e) { console.warn('[i18n] 应用失败', el, e); }
|
|
420
|
-
});
|
|
421
|
-
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
422
|
-
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
|
423
|
-
});
|
|
424
|
-
document.documentElement.lang = lang;
|
|
425
|
-
// 语言切换后重建由 JS 动态渲染的下拉/列表,保证选项文案与选中值不丢失
|
|
426
|
-
try { if (typeof renderModelSelect === 'function') renderModelSelect(); } catch {}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
430
|
-
btn.addEventListener('click', () => switchLang(btn.getAttribute('data-lang')));
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// 初始化语言
|
|
434
|
-
switchLang(currentLang);
|
|
435
|
-
|
|
436
|
-
const AGENT_TYPES = {
|
|
437
|
-
general: { label: '通用助手', icon: '🤖' },
|
|
438
|
-
'fix-code': { label: '修复代码', icon: '🐛' },
|
|
439
|
-
'write-code': { label: '编写代码', icon: '✍️' },
|
|
440
|
-
'exec-command': { label: '执行命令', icon: '⚡' },
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
const state = {
|
|
444
|
-
token: '',
|
|
445
|
-
phone: '',
|
|
446
|
-
tasks: [],
|
|
447
|
-
currentTaskId: null,
|
|
448
|
-
submittedTaskIds: new Set(),
|
|
449
|
-
completedTaskIds: new Set(),
|
|
450
|
-
pinnedTasks: new Set(), // 置顶任务ID集合
|
|
451
|
-
market: [],
|
|
452
|
-
installed: new Set(),
|
|
453
|
-
agentType: 'general',
|
|
454
|
-
directMode: false,
|
|
455
|
-
permissions: {
|
|
456
|
-
readScope: 'workspace',
|
|
457
|
-
readPath: '',
|
|
458
|
-
allowRead: true,
|
|
459
|
-
allowWrite: false,
|
|
460
|
-
allowShell: false,
|
|
461
|
-
allowNetwork: true,
|
|
462
|
-
allowBrowser: false,
|
|
463
|
-
},
|
|
464
|
-
workspaceDir: '',
|
|
465
|
-
rightTab: 'detail',
|
|
466
|
-
models: [],
|
|
467
|
-
defaultModelId: null,
|
|
468
|
-
modelId: '',
|
|
469
|
-
// 待发送附件暂存区(截图/上传文件/上传图片,点发送时才上传并发给 AI)
|
|
470
|
-
stagedFiles: [],
|
|
471
|
-
// 会话管理配置
|
|
472
|
-
sessionConfig: {
|
|
473
|
-
autoResetOnNew: false, // 打开时自动新建对话
|
|
474
|
-
autoResetOnOpen: false, // 发送消息时自动重置
|
|
475
|
-
clearHistoryOnNew: true, // 新建时清空历史
|
|
476
|
-
},
|
|
477
|
-
procOpen: false, // 「执行过程」折叠面板是否展开(轮询刷新时保留)
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
(function initSession() {
|
|
481
|
-
// 提前检测登录状态,给 html 加 class,配合 CSS 避免登录页闪烁(感觉像打开两次)
|
|
482
|
-
try {
|
|
483
|
-
const urlToken = new URLSearchParams(location.search).get('token');
|
|
484
|
-
if (urlToken) localStorage.setItem('fhcode.token', urlToken);
|
|
485
|
-
const t = localStorage.getItem('fhcode.token');
|
|
486
|
-
const p = localStorage.getItem('fhcode.phone');
|
|
487
|
-
if (t && p) {
|
|
488
|
-
document.documentElement.classList.add('logged-in');
|
|
489
|
-
}
|
|
490
|
-
} catch(e) {}
|
|
491
|
-
const urlToken = new URLSearchParams(location.search).get('token');
|
|
492
|
-
if (urlToken) localStorage.setItem('fhcode.token', urlToken);
|
|
493
|
-
const savedModel = localStorage.getItem('fhcode.model');
|
|
494
|
-
if (savedModel !== null) state.modelId = savedModel;
|
|
495
|
-
state.token = localStorage.getItem('fhcode.token') || '';
|
|
496
|
-
state.phone = localStorage.getItem('fhcode.phone') || '';
|
|
497
|
-
try {
|
|
498
|
-
const p = JSON.parse(localStorage.getItem('fhcode.permissions') || '{}');
|
|
499
|
-
if (p) Object.assign(state.permissions, p);
|
|
500
|
-
} catch {}
|
|
501
|
-
// 恢复主题偏好(脚本位于 body 尾部,尽早生效避免闪烁)
|
|
502
|
-
if (localStorage.getItem('fhcode.theme') === 'dark') {
|
|
503
|
-
document.documentElement.classList.add('dark');
|
|
504
|
-
}
|
|
505
|
-
// 加载会话配置
|
|
506
|
-
try {
|
|
507
|
-
const sc = JSON.parse(localStorage.getItem('fhcode.sessionConfig') || '{}');
|
|
508
|
-
if (sc.autoResetOnNew != null) state.sessionConfig.autoResetOnNew = sc.autoResetOnNew;
|
|
509
|
-
if (sc.autoResetOnOpen != null) state.sessionConfig.autoResetOnOpen = sc.autoResetOnOpen;
|
|
510
|
-
if (sc.clearHistoryOnNew != null) state.sessionConfig.clearHistoryOnNew = sc.clearHistoryOnNew;
|
|
511
|
-
} catch {}
|
|
512
|
-
// autoResetOnOpen: 页面打开时自动新建对话(不自动选中旧任务,显示空白输入区)
|
|
513
|
-
if (state.sessionConfig.autoResetOnOpen && state.token) {
|
|
514
|
-
state.currentTaskId = null;
|
|
515
|
-
const box = document.getElementById('messages');
|
|
516
|
-
if (box) box.innerHTML = '<div class="msg sys">已按设置自动新建对话,输入指令即可发起新任务。</div>';
|
|
517
|
-
}
|
|
518
|
-
})();
|
|
519
|
-
|
|
520
|
-
// 消息气泡通用操作:复制 / 编辑 / 分享 / 创建文档
|
|
521
|
-
function bubbleActions(e, content) {
|
|
522
|
-
e.stopPropagation();
|
|
523
|
-
const menu = document.getElementById('bubbleMenu');
|
|
524
|
-
if (menu) menu.remove();
|
|
525
|
-
const m = document.createElement('div');
|
|
526
|
-
m.id = 'bubbleMenu';
|
|
527
|
-
m.style.cssText = 'position:fixed;z-index:10001;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.18);padding:4px 0;min-width:140px;font-size:13px;';
|
|
528
|
-
const items = [
|
|
529
|
-
{ label: '📋 复制', action: 'copy' },
|
|
530
|
-
{ label: '✏️ 编辑', action: 'edit' },
|
|
531
|
-
{ label: '🔗 分享链接', action: 'share' },
|
|
532
|
-
{ label: '📄 创建文档', action: 'doc' },
|
|
533
|
-
];
|
|
534
|
-
items.forEach((it) => {
|
|
535
|
-
const el = document.createElement('div');
|
|
536
|
-
el.className = 'dropdown-item';
|
|
537
|
-
el.textContent = it.label;
|
|
538
|
-
el.style.cssText = 'padding:7px 16px;cursor:pointer;';
|
|
539
|
-
el.addEventListener('mouseenter', () => { el.style.background = 'var(--brand-soft)'; });
|
|
540
|
-
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
541
|
-
el.addEventListener('click', () => {
|
|
542
|
-
m.remove();
|
|
543
|
-
handleBubbleAction(it.action, content);
|
|
544
|
-
});
|
|
545
|
-
m.appendChild(el);
|
|
546
|
-
});
|
|
547
|
-
document.body.appendChild(m);
|
|
548
|
-
const rect = e.target.getBoundingClientRect();
|
|
549
|
-
m.style.right = (window.innerWidth - rect.right + 4) + 'px';
|
|
550
|
-
m.style.top = rect.top + 'px';
|
|
551
|
-
}
|
|
552
|
-
async function handleBubbleAction(action, content) {
|
|
553
|
-
if (action === 'copy') {
|
|
554
|
-
try { await navigator.clipboard.writeText(content); toast('已复制到剪贴板'); }
|
|
555
|
-
catch { document.execCommand('copy'); toast('已复制'); }
|
|
556
|
-
} else if (action === 'edit') {
|
|
557
|
-
const edited = prompt('编辑内容:', content);
|
|
558
|
-
if (edited != null && edited.trim()) {
|
|
559
|
-
// 就近更新对应的消息气泡正文(最终回复块 / 对话气泡)
|
|
560
|
-
const box = document.getElementById('messages');
|
|
561
|
-
if (box) {
|
|
562
|
-
const candidates = box.querySelectorAll('.step.final > div:last-child, .msg.assistant, .msg.user');
|
|
563
|
-
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
564
|
-
if ((candidates[i].textContent || '').trim() === (content || '').trim()) {
|
|
565
|
-
candidates[i].textContent = edited;
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
toast('已更新消息内容(本地预览)');
|
|
571
|
-
}
|
|
572
|
-
} else if (action === 'share') {
|
|
573
|
-
const payload = btoa(unescape(encodeURIComponent(content.slice(0, 2000))));
|
|
574
|
-
const url = location.origin + location.pathname + '#share/' + payload;
|
|
575
|
-
try { await navigator.clipboard.writeText(url); toast('分享链接已复制'); }
|
|
576
|
-
catch { prompt('复制以下链接分享给他人:', url); }
|
|
577
|
-
} else if (action === 'doc') {
|
|
578
|
-
const title = prompt('文档标题(默认: fhcode-回复):', 'fhcode-回复-' + Date.now());
|
|
579
|
-
if (!title) return;
|
|
580
|
-
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
|
581
|
-
const a = document.createElement('a');
|
|
582
|
-
a.href = URL.createObjectURL(blob);
|
|
583
|
-
a.download = title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_\-]/g, '_') + '.md';
|
|
584
|
-
a.click();
|
|
585
|
-
URL.revokeObjectURL(a.href);
|
|
586
|
-
toast('文档已下载');
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// 消息操作按钮事件委托(复制、创建文档)
|
|
591
|
-
document.getElementById('messages')?.addEventListener('click', (e) => {
|
|
592
|
-
const btn = e.target.closest('.msg-action-btn');
|
|
593
|
-
if (!btn) return;
|
|
594
|
-
e.stopPropagation();
|
|
595
|
-
const action = btn.getAttribute('data-action');
|
|
596
|
-
const msgEl = btn.closest('.msg');
|
|
597
|
-
if (!msgEl) return;
|
|
598
|
-
// 获取消息文本内容(排除操作按钮)
|
|
599
|
-
const actionsEl = msgEl.querySelector('.msg-actions');
|
|
600
|
-
let content = '';
|
|
601
|
-
if (actionsEl) {
|
|
602
|
-
// 临时移除操作按钮,获取文本,再放回去
|
|
603
|
-
const parent = actionsEl.parentNode;
|
|
604
|
-
const nextSibling = actionsEl.nextSibling;
|
|
605
|
-
parent.removeChild(actionsEl);
|
|
606
|
-
content = msgEl.textContent || '';
|
|
607
|
-
if (nextSibling) parent.insertBefore(actionsEl, nextSibling);
|
|
608
|
-
else parent.appendChild(actionsEl);
|
|
609
|
-
} else {
|
|
610
|
-
content = msgEl.textContent || '';
|
|
611
|
-
}
|
|
612
|
-
content = content.trim();
|
|
613
|
-
if (!content) {
|
|
614
|
-
toast('没有可复制的内容');
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (action === 'copy') {
|
|
618
|
-
handleBubbleAction('copy', content);
|
|
619
|
-
} else if (action === 'create-doc') {
|
|
620
|
-
handleBubbleAction('doc', content);
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
/* ========== 登录 ========== */
|
|
625
|
-
document.getElementById('loginBtn').addEventListener('click', doLogin);
|
|
626
|
-
document.getElementById('loginPhone').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
|
|
627
|
-
|
|
628
|
-
async function afterLogin() {
|
|
629
|
-
updateUserBar();
|
|
630
|
-
// 检查是否是首次登录,如果是则显示欢迎引导
|
|
631
|
-
const isFirstLogin = await checkFirstLogin();
|
|
632
|
-
if (isFirstLogin) {
|
|
633
|
-
// 从 localStorage 恢复 welcomeTasks(登录时已创建)
|
|
634
|
-
const savedWelcomeTasks = localStorage.getItem('fhcode.welcomeTasks');
|
|
635
|
-
if (savedWelcomeTasks) {
|
|
636
|
-
try {
|
|
637
|
-
const tasks = JSON.parse(savedWelcomeTasks);
|
|
638
|
-
if (tasks.length > 0) {
|
|
639
|
-
showWelcomeGuide(tasks);
|
|
640
|
-
return; // 引导流程已启动,跳过常规加载
|
|
641
|
-
}
|
|
642
|
-
} catch {}
|
|
643
|
-
}
|
|
644
|
-
showWelcomeGuide([]);
|
|
645
|
-
}
|
|
646
|
-
await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats(), loadNodes(), loadSources()]);
|
|
647
|
-
startRefresh();
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async function checkFirstLogin() {
|
|
651
|
-
if (!state.token) return false;
|
|
652
|
-
try {
|
|
653
|
-
const d = await api('/api/auth/me');
|
|
654
|
-
if (d.isFirstLogin === true) {
|
|
655
|
-
// 保存首次登录标记,供后续使用
|
|
656
|
-
localStorage.setItem('fhcode.firstLogin', 'true');
|
|
657
|
-
}
|
|
658
|
-
return d.isFirstLogin === true;
|
|
659
|
-
} catch {
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
async function runWelcomeTask(taskId) {
|
|
667
|
-
try {
|
|
668
|
-
// 加载任务详情
|
|
669
|
-
const d = await api('/api/tasks/' + taskId);
|
|
670
|
-
state.currentTaskId = d.task.id;
|
|
671
|
-
|
|
672
|
-
// 关闭引导弹窗
|
|
673
|
-
const modal = document.getElementById('welcomeGuideModal');
|
|
674
|
-
if (modal) modal.classList.remove('show');
|
|
675
|
-
|
|
676
|
-
// 显示任务列表
|
|
677
|
-
const taskListSection = document.getElementById('taskListSection');
|
|
678
|
-
if (taskListSection) taskListSection.style.display = 'block';
|
|
679
|
-
|
|
680
|
-
// 加载并刷新任务
|
|
681
|
-
await loadTasks();
|
|
682
|
-
selectTask(taskId);
|
|
683
|
-
toast('已启动引导任务:' + (d.task.goal.slice(0, 20) + '…'));
|
|
684
|
-
} catch (e) {
|
|
685
|
-
toast('启动任务失败:' + e.message);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 运行所有引导任务
|
|
690
|
-
async function runAllWelcomeTasks() {
|
|
691
|
-
const tasks = JSON.parse(localStorage.getItem('fhcode.welcomeTasks') || '[]');
|
|
692
|
-
if (tasks.length === 0) {
|
|
693
|
-
toast('暂无引导任务');
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
// 关闭弹窗
|
|
697
|
-
const modal = document.getElementById('welcomeGuideModal');
|
|
698
|
-
if (modal) modal.classList.remove('show');
|
|
699
|
-
// 显示任务列表
|
|
700
|
-
const taskListSection = document.getElementById('taskListSection');
|
|
701
|
-
if (taskListSection) taskListSection.style.display = 'block';
|
|
702
|
-
// 运行第一个任务
|
|
703
|
-
await runWelcomeTask(tasks[0].taskId);
|
|
704
|
-
toast('已开始执行引导任务,共 ' + tasks.length + ' 个');
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// 绑定"全部运行"按钮
|
|
708
|
-
document.getElementById('welcomeRunAllBtn')?.addEventListener('click', runAllWelcomeTasks);
|
|
709
|
-
|
|
710
|
-
// 在登录后保存 welcomeTasks 到 localStorage
|
|
711
|
-
async function saveWelcomeTasks(tasks) {
|
|
712
|
-
if (tasks && tasks.length > 0) {
|
|
713
|
-
localStorage.setItem('fhcode.welcomeTasks', JSON.stringify(tasks));
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
/* ========== 导航切换 ========== */
|
|
719
|
-
|
|
720
|
-
document.querySelectorAll('.topbar-nav-item').forEach((el) => {
|
|
721
|
-
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
722
|
-
});
|
|
723
|
-
document.querySelectorAll('.sidebar-icon[data-nav]').forEach((el) => {
|
|
724
|
-
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
/* ========== 用户菜单 ========== */
|
|
728
|
-
const userMenuMask = document.getElementById('userMenuMask');
|
|
729
|
-
document.getElementById('topbarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
730
|
-
document.getElementById('sidebarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
731
|
-
userMenuMask.addEventListener('click', (e) => { if (e.target === userMenuMask) userMenuMask.classList.remove('show'); });
|
|
732
|
-
document.querySelectorAll('#userMenu .sheet-item').forEach((el) => {
|
|
733
|
-
el.addEventListener('click', () => {
|
|
734
|
-
const action = el.getAttribute('data-action');
|
|
735
|
-
userMenuMask.classList.remove('show');
|
|
736
|
-
switch (action) {
|
|
737
|
-
case 'settings': openModal('settingsModal'); break;
|
|
738
|
-
case 'models': openModels(); break;
|
|
739
|
-
case 'theme': toggleTheme(); break;
|
|
740
|
-
case 'help': openBrowser('https://github.com/wch887292/feihong-code/issues'); break;
|
|
741
|
-
case 'buddy': openBrowser('https://www.klai.top'); break;
|
|
742
|
-
case 'invite': toast('邀请链接已复制(演示)'); break;
|
|
743
|
-
case 'points': toast('积分余额:0(演示)'); break;
|
|
744
|
-
case 'growth': toast('连续登录 1 天(演示)'); break;
|
|
745
|
-
case 'version': toast('当前为体验版'); break;
|
|
746
|
-
case 'update': checkUpdate(); break;
|
|
747
|
-
case 'logout':
|
|
748
|
-
localStorage.removeItem('fhcode.token');
|
|
749
|
-
localStorage.removeItem('fhcode.phone');
|
|
750
|
-
location.reload();
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
document.getElementById('sidebarSettings').addEventListener('click', () => {
|
|
757
|
-
userMenuMask.classList.remove('show');
|
|
758
|
-
openModal('settingsModal');
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
function openBrowser(url) {
|
|
762
|
-
api('/api/open/browser', 'POST', { url }).then(() => toast('已在系统浏览器打开')).catch((e) => toast('打开失败:' + e.message));
|
|
763
|
-
}
|
|
764
|
-
async function checkUpdate() {
|
|
765
|
-
try {
|
|
766
|
-
const d = await api('/api/health');
|
|
767
|
-
toast('当前版本 ' + d.version + ',已是最新(演示)');
|
|
768
|
-
} catch (e) { toast('检查失败:' + e.message); }
|
|
769
|
-
}
|
|
770
|
-
function toggleTheme() {
|
|
771
|
-
const dark = document.documentElement.classList.toggle('dark');
|
|
772
|
-
localStorage.setItem('fhcode.theme', dark ? 'dark' : 'light');
|
|
773
|
-
toast(dark ? '已切换为深色主题' : '已切换为浅色主题');
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/* ========== 任务列表与详情 ========== */
|
|
777
|
-
async function selectTask(id) {
|
|
778
|
-
state.currentTaskId = id;
|
|
779
|
-
renderSidebarTaskList();
|
|
780
|
-
renderTaskDetail(id);
|
|
781
|
-
await refreshCurrentThread();
|
|
782
|
-
switchRightTab('detail');
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// 切换置顶状态
|
|
786
|
-
function togglePinTask(taskId) {
|
|
787
|
-
if (state.pinnedTasks.has(taskId)) {
|
|
788
|
-
state.pinnedTasks.delete(taskId);
|
|
789
|
-
toast('已取消置顶');
|
|
790
|
-
} else {
|
|
791
|
-
state.pinnedTasks.add(taskId);
|
|
792
|
-
toast('已置顶');
|
|
793
|
-
}
|
|
794
|
-
renderSidebarTaskList();
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// 删除任务
|
|
798
|
-
async function deleteTask(taskId) {
|
|
799
|
-
if (!confirm('确定要删除这个任务吗?')) return;
|
|
800
|
-
try {
|
|
801
|
-
await api('/api/tasks/' + taskId, 'DELETE');
|
|
802
|
-
toast('任务已删除');
|
|
803
|
-
state.tasks = state.tasks.filter((t) => t.id !== taskId);
|
|
804
|
-
// 若删除的是当前任务,清空选中并回退到最新任务或空白态
|
|
805
|
-
if (state.currentTaskId === taskId) {
|
|
806
|
-
state.currentTaskId = null;
|
|
807
|
-
if (state.tasks.length) {
|
|
808
|
-
state.currentTaskId = state.tasks[0].id;
|
|
809
|
-
renderTaskDetail(state.currentTaskId);
|
|
810
|
-
await refreshCurrentThread();
|
|
811
|
-
} else {
|
|
812
|
-
renderTaskThread(null);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
renderSidebarTaskList();
|
|
816
|
-
} catch (e) {
|
|
817
|
-
toast('删除失败:' + e.message);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
async function handleDetailOp(task, op, path) {
|
|
821
|
-
if (op === 'folder') {
|
|
822
|
-
try { await api('/api/open/folder', 'POST', { path: path || task.workspaceDir }); toast('已打开资源管理器'); }
|
|
823
|
-
catch (e) { toast('打开失败:' + e.message); }
|
|
824
|
-
} else if (op === 'browser') {
|
|
825
|
-
const url = extractUrl(task.result?.finalAnswer) || 'https://www.klai.top';
|
|
826
|
-
openBrowser(url);
|
|
827
|
-
} else if (op === 'artifacts') {
|
|
828
|
-
await showArtifacts(path || task.workspaceDir);
|
|
829
|
-
} else if (op === 'preview' || op === 'code') {
|
|
830
|
-
const text = task.result?.finalAnswer || '';
|
|
831
|
-
showPreview('任务产物', linkifyArtifacts(text), 'text');
|
|
832
|
-
switchRightTab('preview');
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
/* ========== 任务思维链路渲染 ========== */
|
|
836
|
-
/** 切换思考过程折叠/展开 */
|
|
837
|
-
/** 切换工具调用参数详情显示/隐藏 */
|
|
838
|
-
|
|
839
|
-
/** 群组渲染助手消息为 Cursor 风格的思考过程 + 回复 */
|
|
840
|
-
|
|
841
|
-
// 拉取当前选中任务的完整详情(含思维链路步骤)并渲染
|
|
842
|
-
async function refreshCurrentThread() {
|
|
843
|
-
if (!state.currentTaskId) { renderTaskThread(null); return; }
|
|
844
|
-
try {
|
|
845
|
-
const d = await api('/api/tasks/' + state.currentTaskId);
|
|
846
|
-
const t = d.task;
|
|
847
|
-
const idx = state.tasks.findIndex((x) => x.id === t.id);
|
|
848
|
-
if (idx >= 0) state.tasks[idx] = t; else state.tasks.push(t);
|
|
849
|
-
renderTaskThread(t);
|
|
850
|
-
} catch (e) { console.warn('刷新当前任务线程失败', e); renderTaskThread(null); }
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
// 将单个任务渲染为对话框:对话流(用户↔助手干净气泡)+ 内部执行过程(默认折叠)
|
|
855
|
-
// 单条执行步骤渲染(思维链路、工具调用、验证结果、自愈/压缩等)
|
|
856
|
-
/* 轻量 Markdown 渲染(用于模型推理内容) */
|
|
857
|
-
/* 工具参数格式化为 key: value 列表 */
|
|
858
|
-
|
|
859
|
-
/* ========== 右侧 Tab ========== */
|
|
860
|
-
document.querySelectorAll('.right-tab').forEach((tab) => {
|
|
861
|
-
tab.addEventListener('click', () => switchRightTab(tab.getAttribute('data-tab')));
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
/* ========== 对话与发送 ========== */
|
|
865
|
-
async function sendTask() {
|
|
866
|
-
const input = document.getElementById('goalInput');
|
|
867
|
-
let goal = input.value.trim();
|
|
868
|
-
// 允许只发送文件(无文本)
|
|
869
|
-
if (!goal && !state.stagedFiles.length) return;
|
|
870
|
-
if (!state.token) { toast('请先登录'); return; }
|
|
871
|
-
if (state.directMode) goal = goal ? ('[电脑操作] ' + goal) : '[电脑操作](仅附件)';
|
|
872
|
-
const btn = document.getElementById('sendBtn');
|
|
873
|
-
btn.disabled = true;
|
|
874
|
-
try {
|
|
875
|
-
// 上传暂存的附件(仅在点发送时才上传)
|
|
876
|
-
const attachments = await uploadStagedFiles();
|
|
877
|
-
|
|
878
|
-
// autoResetOnNew: 发送消息前自动新建对话,旧任务保留在历史
|
|
879
|
-
if (state.sessionConfig.autoResetOnNew && state.currentTaskId) {
|
|
880
|
-
toast('自动新建对话,旧任务保留在历史');
|
|
881
|
-
state.currentTaskId = null;
|
|
882
|
-
}
|
|
883
|
-
// 已有当前任务 → 多轮续接:消息归属同一任务生命周期
|
|
884
|
-
if (state.currentTaskId) {
|
|
885
|
-
const cur = state.tasks.find((t) => t.id === state.currentTaskId);
|
|
886
|
-
if (cur && (cur.status === 'queued' || cur.status === 'running')) {
|
|
887
|
-
toast('当前任务执行中,请等待完成后再继续对话');
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
const d = await api('/api/tasks/' + state.currentTaskId + '/messages', 'POST', {
|
|
891
|
-
message: goal || '(仅附件)',
|
|
892
|
-
attachments,
|
|
893
|
-
});
|
|
894
|
-
input.value = '';
|
|
895
|
-
clearStagedFiles();
|
|
896
|
-
renderSidebarTaskList();
|
|
897
|
-
renderTaskDetail(d.task.id);
|
|
898
|
-
renderTaskThread(d.task);
|
|
899
|
-
await loadTasks();
|
|
900
|
-
toast(attachments.length ? `已发送(含 ${attachments.length} 个附件),继续当前任务对话` : '已发送,继续当前任务对话');
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
const d = await api('/api/tasks', 'POST', {
|
|
904
|
-
goal: goal || '(仅附件)',
|
|
905
|
-
agentType: state.agentType,
|
|
906
|
-
permissions: state.permissions,
|
|
907
|
-
workspaceDir: state.workspaceDir || undefined,
|
|
908
|
-
modelId: state.modelId || undefined,
|
|
909
|
-
attachments: attachments.length ? attachments : undefined,
|
|
910
|
-
});
|
|
911
|
-
input.value = '';
|
|
912
|
-
clearStagedFiles();
|
|
913
|
-
// 新任务即成为当前任务,中部围绕其单一生命周期可视化追踪
|
|
914
|
-
state.currentTaskId = d.task.id;
|
|
915
|
-
const taskListSection = document.getElementById('taskListSection');
|
|
916
|
-
if (taskListSection) taskListSection.style.display = 'block';
|
|
917
|
-
renderSidebarTaskList();
|
|
918
|
-
renderTaskDetail(d.task.id);
|
|
919
|
-
renderTaskThread({ id: d.task.id, goal: goal || '(仅附件)', status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal || '(仅附件)' }] });
|
|
920
|
-
await loadTasks();
|
|
921
|
-
toast(attachments.length ? `任务已提交(含 ${attachments.length} 个附件),开始追踪思维链路` : '任务已提交,开始追踪思维链路');
|
|
922
|
-
} catch (e) { toast('提交失败:' + e.message); }
|
|
923
|
-
finally { btn.disabled = false; }
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/* ========== 输入区·附件暂存(点发送时统一上传) ========== */
|
|
927
|
-
function stageFile(file) {
|
|
928
|
-
const id = 'sf_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
|
929
|
-
state.stagedFiles.push({ id, name: file.name, mime: file.mime, dataUrl: file.dataUrl, size: estimateDataUrlSize(file.dataUrl) });
|
|
930
|
-
renderStagedFiles();
|
|
931
|
-
}
|
|
932
|
-
function removeStagedFile(id) {
|
|
933
|
-
state.stagedFiles = state.stagedFiles.filter((f) => f.id !== id);
|
|
934
|
-
renderStagedFiles();
|
|
935
|
-
}
|
|
936
|
-
function clearStagedFiles() {
|
|
937
|
-
state.stagedFiles = [];
|
|
938
|
-
renderStagedFiles();
|
|
939
|
-
}
|
|
940
|
-
async function uploadStagedFiles() {
|
|
941
|
-
if (!state.stagedFiles.length) return [];
|
|
942
|
-
const paths = [];
|
|
943
|
-
for (const f of state.stagedFiles) {
|
|
944
|
-
try {
|
|
945
|
-
const base64 = (f.dataUrl || '').split(',')[1] || '';
|
|
946
|
-
const d = await api('/api/upload', 'POST', { name: f.name, mime: f.mime, dataBase64: base64 });
|
|
947
|
-
paths.push(d.path);
|
|
948
|
-
} catch (e) { toast('上传失败:' + f.name + '(' + e.message + ')'); }
|
|
949
|
-
}
|
|
950
|
-
return paths;
|
|
951
|
-
}
|
|
952
|
-
function appendMsg(role, text, extras = {}) {
|
|
953
|
-
const box = document.getElementById('messages');
|
|
954
|
-
const el = document.createElement('div');
|
|
955
|
-
el.className = 'msg ' + role;
|
|
956
|
-
if (extras.html) el.innerHTML = extras.html;
|
|
957
|
-
else if (extras.file) {
|
|
958
|
-
el.innerHTML = '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
|
|
959
|
-
} else {
|
|
960
|
-
el.innerHTML = linkifyArtifacts(text);
|
|
961
|
-
}
|
|
962
|
-
box.appendChild(el);
|
|
963
|
-
box.scrollTop = box.scrollHeight;
|
|
964
|
-
bindArtifacts(el);
|
|
965
|
-
}
|
|
966
|
-
document.getElementById('sendBtn').addEventListener('click', sendTask);
|
|
967
|
-
document.getElementById('goalInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendTask(); });
|
|
968
|
-
// 粘贴图片:用户用 Win+Shift+S 截图后可直接粘贴到输入框
|
|
969
|
-
document.getElementById('goalInput').addEventListener('paste', (e) => {
|
|
970
|
-
const items = e.clipboardData?.items;
|
|
971
|
-
if (!items) return;
|
|
972
|
-
for (const item of items) {
|
|
973
|
-
if (item.type.startsWith('image/')) {
|
|
974
|
-
e.preventDefault();
|
|
975
|
-
const file = item.getAsFile();
|
|
976
|
-
if (!file) continue;
|
|
977
|
-
const reader = new FileReader();
|
|
978
|
-
reader.onload = () => {
|
|
979
|
-
stageFile({ name: 'paste-' + Date.now() + '.png', mime: file.type || 'image/png', dataUrl: String(reader.result) });
|
|
980
|
-
toast('图片已粘贴到输入框,可直接输入文字发送');
|
|
981
|
-
};
|
|
982
|
-
reader.readAsDataURL(file);
|
|
983
|
-
break;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
});
|
|
987
|
-
document.getElementById('tchRefresh').addEventListener('click', () => { refreshCurrentThread(); toast('已刷新当前任务'); });
|
|
988
|
-
document.getElementById('tchStop').addEventListener('click', async () => {
|
|
989
|
-
if (!state.currentTaskId) return;
|
|
990
|
-
try {
|
|
991
|
-
await api('/api/tasks/' + state.currentTaskId + '/stop', 'POST');
|
|
992
|
-
toast('已发送停止指令,任务即将终止');
|
|
993
|
-
await refreshCurrentThread();
|
|
994
|
-
} catch (e) { toast('停止失败:' + e.message); }
|
|
995
|
-
});
|
|
996
|
-
document.getElementById('tchNewTask').addEventListener('click', () => { try { openNewTaskModal(); } catch { toast('新建任务面板不可用'); } });
|
|
997
|
-
document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
|
|
998
|
-
document.getElementById('voiceBtn')?.addEventListener('click', startVoice);
|
|
999
|
-
|
|
1000
|
-
/* ========== 输入框右键菜单(复制/剪切/粘贴/全选) ========== */
|
|
1001
|
-
const goalInput = document.getElementById('goalInput');
|
|
1002
|
-
goalInput.addEventListener('contextmenu', (e) => {
|
|
1003
|
-
e.preventDefault();
|
|
1004
|
-
// 移除旧菜单
|
|
1005
|
-
const old = document.getElementById('textCtxMenu');
|
|
1006
|
-
if (old) old.remove();
|
|
1007
|
-
// 记录右键时的选中区间(菜单点击会令文本框失焦,故需先记住)
|
|
1008
|
-
const sel = { start: goalInput.selectionStart, end: goalInput.selectionEnd };
|
|
1009
|
-
const hasSel = sel.end > sel.start;
|
|
1010
|
-
// 创建右键菜单
|
|
1011
|
-
const menu = document.createElement('div');
|
|
1012
|
-
menu.id = 'textCtxMenu';
|
|
1013
|
-
menu.style.cssText = 'position:fixed;z-index:10000;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:4px 0;min-width:160px;';
|
|
1014
|
-
const items = [
|
|
1015
|
-
{ label: '📋 复制', action: 'copy', disabled: !hasSel },
|
|
1016
|
-
{ label: '✂️ 剪切', action: 'cut', disabled: !hasSel },
|
|
1017
|
-
{ label: '📌 粘贴', action: 'paste' },
|
|
1018
|
-
{ label: '⬛ 全选', action: 'selectall' },
|
|
1019
|
-
];
|
|
1020
|
-
// 恢复文本框聚焦与选中,供 execCommand 兜底使用
|
|
1021
|
-
const restoreSel = () => {
|
|
1022
|
-
goalInput.focus();
|
|
1023
|
-
goalInput.setSelectionRange(sel.start, sel.end);
|
|
1024
|
-
};
|
|
1025
|
-
items.forEach((it) => {
|
|
1026
|
-
const el = document.createElement('div');
|
|
1027
|
-
el.className = 'dropdown-item';
|
|
1028
|
-
el.textContent = it.label;
|
|
1029
|
-
el.style.cssText = 'padding:6px 16px;cursor:' + (it.disabled ? 'default' : 'pointer') + ';font-size:13px;opacity:' + (it.disabled ? 0.4 : 1) + ';';
|
|
1030
|
-
el.addEventListener('mouseenter', () => { if (!it.disabled) el.style.background = 'var(--brand-soft)'; });
|
|
1031
|
-
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
1032
|
-
el.addEventListener('click', async () => {
|
|
1033
|
-
menu.remove();
|
|
1034
|
-
if (it.disabled) return;
|
|
1035
|
-
if (it.action === 'copy') {
|
|
1036
|
-
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
1037
|
-
try { await navigator.clipboard.writeText(selText); toast('已复制'); }
|
|
1038
|
-
catch { restoreSel(); document.execCommand('copy'); toast('已复制'); }
|
|
1039
|
-
} else if (it.action === 'cut') {
|
|
1040
|
-
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
1041
|
-
try { await navigator.clipboard.writeText(selText); } catch {}
|
|
1042
|
-
goalInput.setRangeText('', sel.start, sel.end, 'preserve');
|
|
1043
|
-
goalInput.focus();
|
|
1044
|
-
goalInput.setSelectionRange(sel.start, sel.start);
|
|
1045
|
-
if (selText) toast('已剪切');
|
|
1046
|
-
} else if (it.action === 'paste') {
|
|
1047
|
-
let text = '';
|
|
1048
|
-
try { text = await navigator.clipboard.readText().catch(() => ''); } catch {}
|
|
1049
|
-
if (text) {
|
|
1050
|
-
goalInput.setRangeText(text, sel.start, sel.end, 'end');
|
|
1051
|
-
goalInput.focus();
|
|
1052
|
-
} else {
|
|
1053
|
-
// 读取失败则回退到系统粘贴(依赖当前选中)
|
|
1054
|
-
restoreSel();
|
|
1055
|
-
document.execCommand('paste');
|
|
1056
|
-
}
|
|
1057
|
-
} else if (it.action === 'selectall') {
|
|
1058
|
-
goalInput.focus();
|
|
1059
|
-
goalInput.select();
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
menu.appendChild(el);
|
|
1063
|
-
});
|
|
1064
|
-
document.body.appendChild(menu);
|
|
1065
|
-
menu.style.left = e.clientX + 'px';
|
|
1066
|
-
menu.style.top = e.clientY + 'px';
|
|
1067
|
-
});
|
|
1068
|
-
// 点击其他地方或失焦关闭右键菜单
|
|
1069
|
-
document.addEventListener('click', (e) => {
|
|
1070
|
-
const menu = document.getElementById('textCtxMenu');
|
|
1071
|
-
if (menu && !menu.contains(e.target)) menu.remove();
|
|
1072
|
-
});
|
|
1073
|
-
document.addEventListener('blur', () => {
|
|
1074
|
-
const menu = document.getElementById('textCtxMenu');
|
|
1075
|
-
if (menu) menu.remove();
|
|
1076
|
-
});
|
|
1077
|
-
// 支持在输入框内直接粘贴图片(与「上传图片/截图」共用同一暂存区,发送时一起上传)
|
|
1078
|
-
goalInput.addEventListener('paste', (e) => {
|
|
1079
|
-
if (!e.clipboardData) return;
|
|
1080
|
-
const items = e.clipboardData.items;
|
|
1081
|
-
const images = [];
|
|
1082
|
-
for (let i = 0; i < items.length; i++) {
|
|
1083
|
-
const item = items[i];
|
|
1084
|
-
if (item.kind === 'file' && item.type && item.type.startsWith('image/')) {
|
|
1085
|
-
const f = item.getAsFile();
|
|
1086
|
-
if (f) images.push(f);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
if (!images.length) return; // 非图片粘贴(文本等)交给浏览器默认行为
|
|
1090
|
-
e.preventDefault();
|
|
1091
|
-
images.forEach((file, idx) => {
|
|
1092
|
-
const reader = new FileReader();
|
|
1093
|
-
reader.onload = () => {
|
|
1094
|
-
const name = file.name || ('paste-image-' + (idx + 1) + '.png');
|
|
1095
|
-
stageFile({ name, mime: file.type || 'image/png', dataUrl: String(reader.result) });
|
|
1096
|
-
toast('已暂存图片:' + name + '(点发送时一起发给 AI)');
|
|
1097
|
-
};
|
|
1098
|
-
reader.readAsDataURL(file);
|
|
1099
|
-
});
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
/* ========== 新建对话任务 ========== */
|
|
1103
|
-
async function openNewTaskModal() {
|
|
1104
|
-
// 智能体类型下拉
|
|
1105
|
-
const agentSel = document.getElementById('ntAgentType');
|
|
1106
|
-
agentSel.innerHTML = Object.entries(AGENT_TYPES)
|
|
1107
|
-
.map(([k, v]) => '<option value="' + k + '"' + (state.agentType === k ? ' selected' : '') + '>' + v.icon + ' ' + v.label + '</option>')
|
|
1108
|
-
.join('');
|
|
1109
|
-
// 工作区下拉:默认(沿用当前底部所选)+ 磁盘根目录
|
|
1110
|
-
const wsSel = document.getElementById('ntWorkspaceDir');
|
|
1111
|
-
const cur = state.workspaceDir || '';
|
|
1112
|
-
wsSel.innerHTML = '<option value="">默认(' + (cur || '服务器工作区') + ')</option>';
|
|
1113
|
-
try {
|
|
1114
|
-
const d = await api('/api/drives');
|
|
1115
|
-
(d.drives || []).forEach((dr) => {
|
|
1116
|
-
if (dr !== cur) wsSel.innerHTML += '<option value="' + escapeHtml(dr) + '">' + escapeHtml(dr) + '</option>';
|
|
1117
|
-
});
|
|
1118
|
-
} catch {}
|
|
1119
|
-
document.getElementById('ntGoal').value = '';
|
|
1120
|
-
openModal('newTaskModal');
|
|
1121
|
-
setTimeout(() => document.getElementById('ntGoal').focus(), 60);
|
|
1122
|
-
}
|
|
1123
|
-
async function createNewTask() {
|
|
1124
|
-
const goal = document.getElementById('ntGoal').value.trim();
|
|
1125
|
-
if (!goal) { toast('请填写任务描述'); return; }
|
|
1126
|
-
const btn = document.getElementById('ntCreateBtn');
|
|
1127
|
-
btn.disabled = true;
|
|
1128
|
-
try {
|
|
1129
|
-
const wsVal = document.getElementById('ntWorkspaceDir').value;
|
|
1130
|
-
const payload = {
|
|
1131
|
-
goal,
|
|
1132
|
-
agentType: document.getElementById('ntAgentType').value || state.agentType,
|
|
1133
|
-
permissions: state.permissions,
|
|
1134
|
-
modelId: state.modelId || undefined,
|
|
1135
|
-
};
|
|
1136
|
-
// 工作区:仅在明确选择了目录时覆盖
|
|
1137
|
-
const d = await api('/api/tasks', 'POST', wsVal ? Object.assign(payload, { workspaceDir: wsVal }) : payload);
|
|
1138
|
-
document.getElementById('goalInput').value = '';
|
|
1139
|
-
state.currentTaskId = d.task.id;
|
|
1140
|
-
const taskListSection = document.getElementById('taskListSection');
|
|
1141
|
-
if (taskListSection) taskListSection.style.display = 'block';
|
|
1142
|
-
renderSidebarTaskList();
|
|
1143
|
-
renderTaskDetail(d.task.id);
|
|
1144
|
-
renderTaskThread({ id: d.task.id, goal, status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal }] });
|
|
1145
|
-
switchView('chat');
|
|
1146
|
-
closeModal('newTaskModal');
|
|
1147
|
-
await loadTasks();
|
|
1148
|
-
toast('新任务已创建,所有对话将归属该任务');
|
|
1149
|
-
} catch (e) { toast('创建失败:' + e.message); }
|
|
1150
|
-
finally { btn.disabled = false; }
|
|
1151
|
-
}
|
|
1152
|
-
document.getElementById('newTaskBtn')?.addEventListener('click', () => { switchView('chat'); startNewChat(); });
|
|
1153
|
-
document.getElementById('tchNewTask')?.addEventListener('click', startNewChat);
|
|
1154
|
-
document.getElementById('ntCreateBtn')?.addEventListener('click', createNewTask);
|
|
1155
|
-
document.getElementById('ntGoal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) createNewTask(); });
|
|
1156
|
-
|
|
1157
|
-
/* ========== 产物链接化 ========== */
|
|
1158
|
-
|
|
1159
|
-
/* ========== 中间菜单 ========== */
|
|
1160
|
-
const convMenuWrap = document.getElementById('convMenuWrap');
|
|
1161
|
-
const convMenu = document.getElementById('convMenu');
|
|
1162
|
-
document.getElementById('convMenuBtn').addEventListener('click', (e) => {
|
|
1163
|
-
e.stopPropagation();
|
|
1164
|
-
convMenu.classList.toggle('show');
|
|
1165
|
-
});
|
|
1166
|
-
document.addEventListener('click', (e) => { if (!convMenuWrap.contains(e.target)) convMenu.classList.remove('show'); });
|
|
1167
|
-
convMenu.querySelectorAll('.dropdown-item').forEach((el) => {
|
|
1168
|
-
el.addEventListener('click', () => {
|
|
1169
|
-
convMenu.classList.remove('show');
|
|
1170
|
-
const action = el.getAttribute('data-action');
|
|
1171
|
-
if (action === 'screenshot') takeScreenshot();
|
|
1172
|
-
else if (action === 'upload-file') document.getElementById('fileInput').click();
|
|
1173
|
-
else if (action === 'upload-image') document.getElementById('imageInput').click();
|
|
1174
|
-
else if (action === 'direct-mode') toggleDirectMode();
|
|
1175
|
-
else if (action === 'voice') startVoice();
|
|
1176
|
-
else if (action === 'agent-type') openAgentSelector();
|
|
1177
|
-
});
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
// 截图锁,防止重复触发
|
|
1182
|
-
let screenshotRunning = false;
|
|
1183
|
-
|
|
1184
|
-
async function takeScreenshot() {
|
|
1185
|
-
if (screenshotRunning) {
|
|
1186
|
-
toast('正在截图中,请稍候...');
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
screenshotRunning = true;
|
|
1190
|
-
try {
|
|
1191
|
-
const overlay = document.getElementById('cropOverlay');
|
|
1192
|
-
const selection = document.getElementById('cropSelection');
|
|
1193
|
-
const video = document.getElementById('cropVideo');
|
|
1194
|
-
|
|
1195
|
-
let srcImage = null; // 用于裁剪的源图像(Image 对象)
|
|
1196
|
-
let stream = null; // 浏览器模式下的视频流
|
|
1197
|
-
|
|
1198
|
-
// Electron 环境:用主进程截图,更稳定
|
|
1199
|
-
if (window.isElectron && window.electronAPI && window.electronAPI.screenshot) {
|
|
1200
|
-
toast('正在截取屏幕...');
|
|
1201
|
-
const result = await window.electronAPI.screenshot.capture();
|
|
1202
|
-
if (!result || !result.success) {
|
|
1203
|
-
toast('截图失败:' + (result?.error || '未知错误'));
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
// 加载截图为 Image 对象
|
|
1207
|
-
srcImage = new Image();
|
|
1208
|
-
srcImage.src = result.dataUrl;
|
|
1209
|
-
await new Promise((resolve, reject) => {
|
|
1210
|
-
srcImage.onload = resolve;
|
|
1211
|
-
srcImage.onerror = () => reject(new Error('截图加载失败'));
|
|
1212
|
-
});
|
|
1213
|
-
// 把截图显示到 video 元素的位置(用一个 img 覆盖)
|
|
1214
|
-
video.style.display = 'none';
|
|
1215
|
-
let bgImg = document.getElementById('cropBgImg');
|
|
1216
|
-
if (!bgImg) {
|
|
1217
|
-
bgImg = document.createElement('img');
|
|
1218
|
-
bgImg.id = 'cropBgImg';
|
|
1219
|
-
bgImg.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;object-fit:cover;z-index:9998;';
|
|
1220
|
-
overlay.appendChild(bgImg);
|
|
1221
|
-
}
|
|
1222
|
-
bgImg.src = result.dataUrl;
|
|
1223
|
-
bgImg.style.display = 'block';
|
|
1224
|
-
} else {
|
|
1225
|
-
// 浏览器模式:用 getDisplayMedia
|
|
1226
|
-
stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
1227
|
-
video.srcObject = stream;
|
|
1228
|
-
await video.play();
|
|
1229
|
-
video.style.display = 'block';
|
|
1230
|
-
const bgImg = document.getElementById('cropBgImg');
|
|
1231
|
-
if (bgImg) bgImg.style.display = 'none';
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
overlay.style.display = 'block';
|
|
1235
|
-
selection.style.display = 'none';
|
|
1236
|
-
let startX = 0, startY = 0, drawing = false;
|
|
1237
|
-
|
|
1238
|
-
const onDown = (e) => {
|
|
1239
|
-
drawing = true;
|
|
1240
|
-
startX = e.clientX; startY = e.clientY;
|
|
1241
|
-
selection.style.display = 'block';
|
|
1242
|
-
selection.style.left = startX + 'px';
|
|
1243
|
-
selection.style.top = startY + 'px';
|
|
1244
|
-
selection.style.width = '0px';
|
|
1245
|
-
selection.style.height = '0px';
|
|
1246
|
-
};
|
|
1247
|
-
const onMove = (e) => {
|
|
1248
|
-
if (!drawing) return;
|
|
1249
|
-
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1250
|
-
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1251
|
-
selection.style.left = x + 'px';
|
|
1252
|
-
selection.style.top = y + 'px';
|
|
1253
|
-
selection.style.width = w + 'px';
|
|
1254
|
-
selection.style.height = h + 'px';
|
|
1255
|
-
};
|
|
1256
|
-
const finish = async (e) => {
|
|
1257
|
-
if (!drawing) return;
|
|
1258
|
-
drawing = false;
|
|
1259
|
-
window.removeEventListener('mousedown', onDown);
|
|
1260
|
-
window.removeEventListener('mousemove', onMove);
|
|
1261
|
-
window.removeEventListener('mouseup', finish);
|
|
1262
|
-
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1263
|
-
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1264
|
-
overlay.style.display = 'none';
|
|
1265
|
-
if (w < 8 || h < 8) {
|
|
1266
|
-
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
try {
|
|
1270
|
-
let srcW, srcH, source;
|
|
1271
|
-
if (srcImage) {
|
|
1272
|
-
// Electron 模式:从 Image 裁剪
|
|
1273
|
-
srcW = srcImage.naturalWidth;
|
|
1274
|
-
srcH = srcImage.naturalHeight;
|
|
1275
|
-
source = srcImage;
|
|
1276
|
-
} else {
|
|
1277
|
-
// 浏览器模式:从 video 裁剪
|
|
1278
|
-
srcW = video.videoWidth;
|
|
1279
|
-
srcH = video.videoHeight;
|
|
1280
|
-
source = video;
|
|
1281
|
-
}
|
|
1282
|
-
const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
|
|
1283
|
-
const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
|
|
1284
|
-
const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
|
|
1285
|
-
const canvas = document.createElement('canvas');
|
|
1286
|
-
canvas.width = sw; canvas.height = sh;
|
|
1287
|
-
canvas.getContext('2d').drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1288
|
-
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1289
|
-
const dataUrl = canvas.toDataURL('image/png');
|
|
1290
|
-
stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl });
|
|
1291
|
-
toast('截图已添加到输入框,可直接输入文字发送');
|
|
1292
|
-
} catch (err) { toast('截图失败:' + err.message); }
|
|
1293
|
-
screenshotRunning = false;
|
|
1294
|
-
};
|
|
1295
|
-
const onKey = (e) => {
|
|
1296
|
-
if (e.key === 'Escape') {
|
|
1297
|
-
overlay.style.display = 'none';
|
|
1298
|
-
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1299
|
-
window.removeEventListener('mousedown', onDown);
|
|
1300
|
-
window.removeEventListener('mousemove', onMove);
|
|
1301
|
-
window.removeEventListener('mouseup', finish);
|
|
1302
|
-
window.removeEventListener('keydown', onKey);
|
|
1303
|
-
screenshotRunning = false;
|
|
1304
|
-
}
|
|
1305
|
-
};
|
|
1306
|
-
window.addEventListener('mousedown', onDown);
|
|
1307
|
-
window.addEventListener('mousemove', onMove);
|
|
1308
|
-
window.addEventListener('mouseup', finish);
|
|
1309
|
-
window.addEventListener('keydown', onKey);
|
|
1310
|
-
} catch (e) {
|
|
1311
|
-
if (e.name === 'NotAllowedError') {
|
|
1312
|
-
toast('已取消截图。也可以用 Win+Shift+S 截图后直接粘贴到输入框');
|
|
1313
|
-
} else {
|
|
1314
|
-
toast('截图失败:' + e.message);
|
|
1315
|
-
}
|
|
1316
|
-
screenshotRunning = false;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function handleUpload(input, mimePrefix) {
|
|
1321
|
-
const file = input.files[0];
|
|
1322
|
-
if (!file) return;
|
|
1323
|
-
const reader = new FileReader();
|
|
1324
|
-
reader.onload = () => {
|
|
1325
|
-
stageFile({ name: file.name, mime: file.type || mimePrefix, dataUrl: String(reader.result) });
|
|
1326
|
-
toast('已暂存:' + file.name + '(点发送时一起发给 AI)');
|
|
1327
|
-
};
|
|
1328
|
-
reader.readAsDataURL(file);
|
|
1329
|
-
input.value = '';
|
|
1330
|
-
}
|
|
1331
|
-
document.getElementById('fileInput').addEventListener('change', function() { handleUpload(this, 'application/octet-stream'); });
|
|
1332
|
-
document.getElementById('imageInput').addEventListener('change', function() { handleUpload(this, 'image/png'); });
|
|
1333
|
-
|
|
1334
|
-
/* ========== 大模型配置(三重加密:密钥 RSA 加密传输,服务端 AES 加密落盘) ========== */
|
|
1335
|
-
// 第二重(通信层):用服务器 RSA 公钥加密敏感文本(如模型 API Key)
|
|
1336
|
-
// 持久化会话配置到 localStorage
|
|
1337
|
-
// 切换会话模式(打开/发送时自动重置)
|
|
1338
|
-
document.querySelectorAll('.session-toggle').forEach((el) => {
|
|
1339
|
-
el.addEventListener('change', () => {
|
|
1340
|
-
const key = el.dataset.key;
|
|
1341
|
-
state.sessionConfig[key] = el.checked;
|
|
1342
|
-
persistSessionConfig();
|
|
1343
|
-
toast('会话配置已保存');
|
|
1344
|
-
});
|
|
1345
|
-
});
|
|
1346
|
-
document.getElementById('modelSaveBtn').addEventListener('click', saveModel);
|
|
1347
|
-
document.getElementById('modelResetBtn').addEventListener('click', resetModelForm);
|
|
1348
|
-
document.getElementById('modelSettingsShortcut').addEventListener('click', openModels);
|
|
1349
|
-
document.getElementById('modelSelect').addEventListener('change', (e) => {
|
|
1350
|
-
state.modelId = e.target.value || '';
|
|
1351
|
-
localStorage.setItem('fhcode.model', state.modelId);
|
|
1352
|
-
const m = state.models.find((x) => x.id === state.modelId);
|
|
1353
|
-
toast(m ? ('已切换:' + m.name) : '已切换:系统默认路由');
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
/* ========== 设置面板 ========== */
|
|
1357
|
-
document.querySelectorAll('#settingsModal .settings-cat').forEach((c) => {
|
|
1358
|
-
c.addEventListener('click', () => {
|
|
1359
|
-
const cat = c.getAttribute('data-cat');
|
|
1360
|
-
document.querySelectorAll('#settingsModal .settings-cat').forEach((x) => x.classList.toggle('active', x === c));
|
|
1361
|
-
document.querySelectorAll('#settingsModal .settings-group').forEach((g) => {
|
|
1362
|
-
g.style.display = g.getAttribute('data-group') === cat ? '' : 'none';
|
|
1363
|
-
});
|
|
1364
|
-
});
|
|
1365
|
-
});
|
|
1366
|
-
document.querySelectorAll('#settingsModal .settings-item').forEach((it) => {
|
|
1367
|
-
it.addEventListener('click', () => {
|
|
1368
|
-
const act = it.getAttribute('data-action');
|
|
1369
|
-
if (act === 'perm') { closeModal('settingsModal'); openPermissions(); }
|
|
1370
|
-
else if (act === 'theme') { closeModal('settingsModal'); toggleTheme(); }
|
|
1371
|
-
else if (act === 'model-custom') { toggleModelManage(); }
|
|
1372
|
-
});
|
|
1373
|
-
});
|
|
1374
|
-
async function saveInlineModel() {
|
|
1375
|
-
const name = document.getElementById('imName').value.trim();
|
|
1376
|
-
const apiBase = document.getElementById('imBase').value.trim();
|
|
1377
|
-
const apiKey = document.getElementById('imKey').value;
|
|
1378
|
-
const reasoning = document.getElementById('imReasoning').value;
|
|
1379
|
-
const msg = document.getElementById('imMsg');
|
|
1380
|
-
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
1381
|
-
const editing = document.getElementById('imSave').dataset.editing;
|
|
1382
|
-
try {
|
|
1383
|
-
const body = { name, apiBase, reasoning };
|
|
1384
|
-
// 三重加密·通信层:密钥 RSA 加密传输
|
|
1385
|
-
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
1386
|
-
if (editing) body.id = editing;
|
|
1387
|
-
const d = await api('/api/models', 'POST', body);
|
|
1388
|
-
msg.textContent = '已保存:' + d.model.name;
|
|
1389
|
-
msg.classList.remove('err');
|
|
1390
|
-
await loadModels();
|
|
1391
|
-
renderModelListInline();
|
|
1392
|
-
resetInlineForm();
|
|
1393
|
-
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
1394
|
-
}
|
|
1395
|
-
document.getElementById('imSave').addEventListener('click', saveInlineModel);
|
|
1396
|
-
document.getElementById('imReset').addEventListener('click', resetInlineForm);
|
|
1397
|
-
document.getElementById('modelManageClose')?.addEventListener('click', () => { document.getElementById('modelManage').style.display = 'none'; });
|
|
1398
|
-
|
|
1399
|
-
/* ========== 通话控件(语音/视频通话 + 实时语音识别) ========== */
|
|
1400
|
-
let callStream = null;
|
|
1401
|
-
let callKind = null;
|
|
1402
|
-
|
|
1403
|
-
async function startCall(kind) {
|
|
1404
|
-
const status = document.getElementById('callStatus');
|
|
1405
|
-
try {
|
|
1406
|
-
const constraints = kind === 'video'
|
|
1407
|
-
? { audio: true, video: { width: 640, height: 480 } }
|
|
1408
|
-
: { audio: true };
|
|
1409
|
-
callStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
1410
|
-
callKind = kind;
|
|
1411
|
-
|
|
1412
|
-
// 视频通话:显示摄像头画面
|
|
1413
|
-
if (kind === 'video') {
|
|
1414
|
-
const videoWindow = document.getElementById('videoCallWindow');
|
|
1415
|
-
const localVideo = document.getElementById('localVideo');
|
|
1416
|
-
localVideo.srcObject = callStream;
|
|
1417
|
-
videoWindow.style.display = 'block';
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '(语音实时转文字)';
|
|
1421
|
-
document.getElementById('hangupCallBtn').style.display = 'inline-block';
|
|
1422
|
-
document.getElementById('voiceCallBtn').style.display = 'none';
|
|
1423
|
-
document.getElementById('videoCallBtn').style.display = 'none';
|
|
1424
|
-
|
|
1425
|
-
// 开启语音识别(复用语音输入模块)
|
|
1426
|
-
if (!voiceState.isListening) {
|
|
1427
|
-
voiceState.finalText = '';
|
|
1428
|
-
voiceState.interimText = '';
|
|
1429
|
-
voiceState.originalText = document.getElementById('goalInput').value;
|
|
1430
|
-
voiceState.manuallyStopped = false;
|
|
1431
|
-
voiceState.restartCount = 0;
|
|
1432
|
-
updateVoiceUI(true);
|
|
1433
|
-
startRecognition();
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话,说话内容会实时转成文字');
|
|
1437
|
-
} catch (e) {
|
|
1438
|
-
status.textContent = '';
|
|
1439
|
-
if (e.name === 'NotAllowedError') {
|
|
1440
|
-
toast('请允许浏览器使用麦克风和摄像头权限');
|
|
1441
|
-
} else if (e.name === 'NotFoundError') {
|
|
1442
|
-
toast('未找到麦克风或摄像头设备');
|
|
1443
|
-
} else {
|
|
1444
|
-
toast('无法发起通话:' + e.message);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function hangupCall() {
|
|
1450
|
-
// 先保存识别的文字到输入框(确保内容不会丢失)
|
|
1451
|
-
if (voiceState.finalText) {
|
|
1452
|
-
updateVoiceInput();
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// 停止媒体流
|
|
1456
|
-
if (callStream) {
|
|
1457
|
-
callStream.getTracks().forEach((t) => t.stop());
|
|
1458
|
-
callStream = null;
|
|
1459
|
-
}
|
|
1460
|
-
callKind = null;
|
|
1461
|
-
|
|
1462
|
-
// 隐藏视频窗口
|
|
1463
|
-
const videoWindow = document.getElementById('videoCallWindow');
|
|
1464
|
-
const localVideo = document.getElementById('localVideo');
|
|
1465
|
-
if (videoWindow) videoWindow.style.display = 'none';
|
|
1466
|
-
if (localVideo) localVideo.srcObject = null;
|
|
1467
|
-
|
|
1468
|
-
// 停止语音识别
|
|
1469
|
-
if (voiceState.isListening) {
|
|
1470
|
-
stopVoiceInput();
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
document.getElementById('callStatus').textContent = '';
|
|
1474
|
-
document.getElementById('hangupCallBtn').style.display = 'none';
|
|
1475
|
-
document.getElementById('voiceCallBtn').style.display = 'inline-block';
|
|
1476
|
-
document.getElementById('videoCallBtn').style.display = 'inline-block';
|
|
1477
|
-
toast('通话已结束,识别的文字已保留在输入框');
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
document.getElementById('voiceCallBtn').addEventListener('click', () => startCall('voice'));
|
|
1481
|
-
document.getElementById('videoCallBtn').addEventListener('click', () => startCall('video'));
|
|
1482
|
-
document.getElementById('hangupCallBtn').addEventListener('click', hangupCall);
|
|
1483
|
-
document.getElementById('videoCallClose')?.addEventListener('click', hangupCall);
|
|
1484
|
-
|
|
1485
|
-
// 视频通话截图:把当前摄像头画面截取下来放到输入框
|
|
1486
|
-
document.getElementById('videoSnapshotBtn')?.addEventListener('click', () => {
|
|
1487
|
-
const video = document.getElementById('localVideo');
|
|
1488
|
-
if (!video || !video.videoWidth) {
|
|
1489
|
-
toast('视频还没准备好,请稍等');
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
try {
|
|
1493
|
-
const canvas = document.createElement('canvas');
|
|
1494
|
-
canvas.width = video.videoWidth;
|
|
1495
|
-
canvas.height = video.videoHeight;
|
|
1496
|
-
canvas.getContext('2d').drawImage(video, 0, 0);
|
|
1497
|
-
const dataUrl = canvas.toDataURL('image/png');
|
|
1498
|
-
stageFile({
|
|
1499
|
-
name: 'camera-' + Date.now() + '.png',
|
|
1500
|
-
mime: 'image/png',
|
|
1501
|
-
dataUrl: dataUrl,
|
|
1502
|
-
});
|
|
1503
|
-
toast('摄像头画面已添加到输入框,可直接发送');
|
|
1504
|
-
} catch (e) {
|
|
1505
|
-
toast('截图失败:' + e.message);
|
|
1506
|
-
}
|
|
1507
|
-
});
|
|
1508
|
-
|
|
1509
|
-
/* ========== 语音输入模块 ========== */
|
|
1510
|
-
const voiceState = {
|
|
1511
|
-
recognition: null,
|
|
1512
|
-
finalText: '',
|
|
1513
|
-
interimText: '',
|
|
1514
|
-
originalText: '',
|
|
1515
|
-
isListening: false,
|
|
1516
|
-
manuallyStopped: false,
|
|
1517
|
-
restartTimer: null,
|
|
1518
|
-
restartCount: 0,
|
|
1519
|
-
maxRestarts: 10000, // 基本无限制,长时间通话也不会停
|
|
1520
|
-
};
|
|
1521
|
-
|
|
1522
|
-
// 优化语音识别文本:去掉多余空格、自动加标点、清理重复
|
|
1523
|
-
function optimizeVoiceText(text) {
|
|
1524
|
-
if (!text) return '';
|
|
1525
|
-
let result = text.trim();
|
|
1526
|
-
// 去掉多余空格
|
|
1527
|
-
result = result.replace(/\s+/g, ' ');
|
|
1528
|
-
// 去掉首尾空格
|
|
1529
|
-
result = result.trim();
|
|
1530
|
-
// 中文标点优化:如果末尾是中文且没有标点,自动加句号
|
|
1531
|
-
if (result && /[\u4e00-\u9fa5]$/.test(result) && !/[。!?,、;:""''()]$/.test(result)) {
|
|
1532
|
-
result += '。';
|
|
1533
|
-
}
|
|
1534
|
-
return result;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
// 更新输入框显示
|
|
1538
|
-
function updateVoiceInput() {
|
|
1539
|
-
const input = document.getElementById('goalInput');
|
|
1540
|
-
const finalOptimized = optimizeVoiceText(voiceState.finalText);
|
|
1541
|
-
const displayText = voiceState.originalText
|
|
1542
|
-
? (voiceState.originalText + ' ' + finalOptimized + voiceState.interimText)
|
|
1543
|
-
: (finalOptimized + voiceState.interimText);
|
|
1544
|
-
input.value = displayText;
|
|
1545
|
-
input.scrollTop = input.scrollHeight;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// 更新UI状态
|
|
1549
|
-
function updateVoiceUI(listening) {
|
|
1550
|
-
const badge = document.getElementById('voiceBadge');
|
|
1551
|
-
const voiceBtn = document.getElementById('voiceBtn');
|
|
1552
|
-
if (listening) {
|
|
1553
|
-
badge.style.display = 'inline-flex';
|
|
1554
|
-
badge.textContent = '● 正在听…(再次点击停止)';
|
|
1555
|
-
if (voiceBtn) {
|
|
1556
|
-
voiceBtn.style.background = 'var(--brand)';
|
|
1557
|
-
voiceBtn.style.color = '#fff';
|
|
1558
|
-
voiceBtn.textContent = '● 听…';
|
|
1559
|
-
}
|
|
1560
|
-
} else {
|
|
1561
|
-
badge.style.display = 'none';
|
|
1562
|
-
if (voiceBtn) {
|
|
1563
|
-
voiceBtn.style.background = '';
|
|
1564
|
-
voiceBtn.style.color = '';
|
|
1565
|
-
voiceBtn.textContent = '🎤 语音';
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
// 创建新的语音识别对象
|
|
1571
|
-
function createRecognition() {
|
|
1572
|
-
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1573
|
-
if (!SpeechRecognition) return null;
|
|
1574
|
-
|
|
1575
|
-
const rec = new SpeechRecognition();
|
|
1576
|
-
rec.lang = 'zh-CN';
|
|
1577
|
-
rec.continuous = true;
|
|
1578
|
-
rec.interimResults = true;
|
|
1579
|
-
rec.maxAlternatives = 1;
|
|
1580
|
-
|
|
1581
|
-
// 识别结果处理
|
|
1582
|
-
rec.onresult = (e) => {
|
|
1583
|
-
let interim = '';
|
|
1584
|
-
let finalDelta = '';
|
|
1585
|
-
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
1586
|
-
const transcript = e.results[i][0].transcript;
|
|
1587
|
-
if (e.results[i].isFinal) {
|
|
1588
|
-
finalDelta += transcript;
|
|
1589
|
-
} else {
|
|
1590
|
-
interim += transcript;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (finalDelta) {
|
|
1594
|
-
voiceState.finalText += finalDelta;
|
|
1595
|
-
voiceState.restartCount = 0; // 有新结果,重置重启计数
|
|
1596
|
-
}
|
|
1597
|
-
voiceState.interimText = interim;
|
|
1598
|
-
updateVoiceInput();
|
|
1599
|
-
};
|
|
1600
|
-
|
|
1601
|
-
// 错误处理
|
|
1602
|
-
rec.onerror = (e) => {
|
|
1603
|
-
console.warn('[语音识别] 错误:', e.error);
|
|
1604
|
-
if (e.error === 'not-allowed' || e.error === 'service-not-allowed') {
|
|
1605
|
-
toast('请允许浏览器使用麦克风权限(点击地址栏左侧锁图标)');
|
|
1606
|
-
voiceState.manuallyStopped = true;
|
|
1607
|
-
stopVoiceInput();
|
|
1608
|
-
} else if (e.error === 'network') {
|
|
1609
|
-
toast('语音识别网络错误!Chrome用的是Google服务,国内建议用Edge浏览器,或检查网络/代理');
|
|
1610
|
-
voiceState.manuallyStopped = true;
|
|
1611
|
-
stopVoiceInput();
|
|
1612
|
-
} else if (e.error === 'audio-capture') {
|
|
1613
|
-
toast('未检测到麦克风设备,请检查麦克风连接和系统设置');
|
|
1614
|
-
voiceState.manuallyStopped = true;
|
|
1615
|
-
stopVoiceInput();
|
|
1616
|
-
} else if (e.error === 'no-speech') {
|
|
1617
|
-
// 没检测到语音,继续等待,不停止
|
|
1618
|
-
console.log('[语音识别] 未检测到语音,继续等待...');
|
|
1619
|
-
toast('没听到声音,请检查麦克风是否开启,离麦克风近一点说话');
|
|
1620
|
-
} else if (e.error === 'aborted') {
|
|
1621
|
-
// 手动停止,不处理
|
|
1622
|
-
} else {
|
|
1623
|
-
console.warn('[语音识别] 其他错误:', e.error);
|
|
1624
|
-
toast('语音识别出错: ' + e.error);
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
// 识别结束处理
|
|
1629
|
-
rec.onend = () => {
|
|
1630
|
-
voiceState.recognition = null;
|
|
1631
|
-
voiceState.interimText = '';
|
|
1632
|
-
|
|
1633
|
-
// 如果不是手动停止,自动重启(基本无限制)
|
|
1634
|
-
if (!voiceState.manuallyStopped && voiceState.restartCount < voiceState.maxRestarts) {
|
|
1635
|
-
voiceState.restartCount++;
|
|
1636
|
-
// 只在调试时打印,避免控制台太多日志
|
|
1637
|
-
if (voiceState.restartCount % 50 === 0) {
|
|
1638
|
-
console.log(`[语音识别] 自动重启 (${voiceState.restartCount})`);
|
|
1639
|
-
}
|
|
1640
|
-
voiceState.restartTimer = setTimeout(() => {
|
|
1641
|
-
if (!voiceState.manuallyStopped) {
|
|
1642
|
-
startRecognition();
|
|
1643
|
-
}
|
|
1644
|
-
}, 50); // 50ms快速重启,减少停顿感
|
|
1645
|
-
} else if (voiceState.manuallyStopped) {
|
|
1646
|
-
// 手动停止,最终处理
|
|
1647
|
-
updateVoiceUI(false);
|
|
1648
|
-
if (voiceState.finalText) {
|
|
1649
|
-
updateVoiceInput();
|
|
1650
|
-
toast('语音输入完成,文字已在输入框中,可直接发送');
|
|
1651
|
-
}
|
|
1652
|
-
} else {
|
|
1653
|
-
// 超过最大重启次数(基本不会发生)
|
|
1654
|
-
updateVoiceUI(false);
|
|
1655
|
-
toast('语音输入已自动停止,可再次点击开启');
|
|
1656
|
-
}
|
|
1657
|
-
};
|
|
1658
|
-
|
|
1659
|
-
return rec;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// 启动识别
|
|
1663
|
-
function startRecognition() {
|
|
1664
|
-
const rec = createRecognition();
|
|
1665
|
-
if (!rec) {
|
|
1666
|
-
toast('当前浏览器不支持语音输入,请使用 Chrome 或 Edge 浏览器');
|
|
1667
|
-
return false;
|
|
1668
|
-
}
|
|
1669
|
-
try {
|
|
1670
|
-
rec.start();
|
|
1671
|
-
voiceState.recognition = rec;
|
|
1672
|
-
voiceState.isListening = true;
|
|
1673
|
-
return true;
|
|
1674
|
-
} catch (e) {
|
|
1675
|
-
console.error('[语音识别] 启动失败:', e);
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// 停止语音输入
|
|
1681
|
-
function stopVoiceInput() {
|
|
1682
|
-
voiceState.manuallyStopped = true;
|
|
1683
|
-
voiceState.isListening = false;
|
|
1684
|
-
if (voiceState.restartTimer) {
|
|
1685
|
-
clearTimeout(voiceState.restartTimer);
|
|
1686
|
-
voiceState.restartTimer = null;
|
|
1687
|
-
}
|
|
1688
|
-
if (voiceState.recognition) {
|
|
1689
|
-
try { voiceState.recognition.stop(); } catch(e) {}
|
|
1690
|
-
voiceState.recognition = null;
|
|
1691
|
-
}
|
|
1692
|
-
updateVoiceUI(false);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// 开始/停止语音输入(入口函数)
|
|
1696
|
-
function startVoice() {
|
|
1697
|
-
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1698
|
-
if (!SpeechRecognition) {
|
|
1699
|
-
toast('当前环境不支持语音输入,请使用 Chrome 或 Edge 浏览器打开网页版');
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// 如果正在识别,就停止
|
|
1704
|
-
if (voiceState.isListening) {
|
|
1705
|
-
stopVoiceInput();
|
|
1706
|
-
toast('已停止语音输入');
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Electron 桌面版特殊提示
|
|
1711
|
-
if (window.isElectron) {
|
|
1712
|
-
toast('桌面版语音输入可能不稳定,建议用网页版(Edge浏览器)');
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// 开始识别
|
|
1716
|
-
voiceState.finalText = '';
|
|
1717
|
-
voiceState.interimText = '';
|
|
1718
|
-
voiceState.originalText = document.getElementById('goalInput').value;
|
|
1719
|
-
voiceState.manuallyStopped = false;
|
|
1720
|
-
voiceState.restartCount = 0;
|
|
1721
|
-
|
|
1722
|
-
updateVoiceUI(true);
|
|
1723
|
-
toast('语音输入已开启,请说话,再次点击停止');
|
|
1724
|
-
|
|
1725
|
-
if (!startRecognition()) {
|
|
1726
|
-
updateVoiceUI(false);
|
|
1727
|
-
toast('启动语音输入失败,请刷新页面重试');
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
/* ========== 智能体选择 ========== */
|
|
1732
|
-
document.getElementById('skillPill').addEventListener('click', openAgentSelector);
|
|
1733
|
-
|
|
1734
|
-
/* ========== 权限窗口 ========== */
|
|
1735
|
-
document.getElementById('permSaveBtn').addEventListener('click', () => {
|
|
1736
|
-
const scope = document.querySelector('input[name="readScope"]:checked')?.value || 'workspace';
|
|
1737
|
-
state.permissions = {
|
|
1738
|
-
readScope: scope,
|
|
1739
|
-
readPath: document.getElementById('permReadPath').value.trim(),
|
|
1740
|
-
allowRead: document.getElementById('permRead').checked,
|
|
1741
|
-
allowWrite: document.getElementById('permWrite').checked,
|
|
1742
|
-
allowShell: document.getElementById('permShell').checked,
|
|
1743
|
-
allowNetwork: document.getElementById('permNetwork').checked,
|
|
1744
|
-
allowBrowser: document.getElementById('permBrowser').checked,
|
|
1745
|
-
};
|
|
1746
|
-
localStorage.setItem('fhcode.permissions', JSON.stringify(state.permissions));
|
|
1747
|
-
closeModal('permModal');
|
|
1748
|
-
toast('权限配置已保存');
|
|
1749
|
-
});
|
|
1750
|
-
|
|
1751
|
-
/* ========== 工作区 ========== */
|
|
1752
|
-
// 工作区跳转功能已移除(旧布局左侧工作区树)
|
|
1753
|
-
|
|
1754
|
-
document.getElementById('workspacePermBadge')?.addEventListener('click', () => openPermissions());
|
|
1755
|
-
document.getElementById('workspacePickBtn')?.addEventListener('click', () => {
|
|
1756
|
-
openFolderPicker();
|
|
1757
|
-
});
|
|
1758
|
-
|
|
1759
|
-
let fpCurrent = '';
|
|
1760
|
-
let fpSelected = '';
|
|
1761
|
-
|
|
1762
|
-
async function openFolderPicker() {
|
|
1763
|
-
fpCurrent = '';
|
|
1764
|
-
fpSelected = '';
|
|
1765
|
-
document.getElementById('fpDrivesView').style.display = '';
|
|
1766
|
-
document.getElementById('fpFoldersView').style.display = 'none';
|
|
1767
|
-
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1768
|
-
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1769
|
-
document.getElementById('fpConfirm').disabled = true;
|
|
1770
|
-
document.getElementById('fpRenameFolder').disabled = true;
|
|
1771
|
-
document.getElementById('fpTitle').textContent = '📂 选择驱动器';
|
|
1772
|
-
openModal('folderPickerModal');
|
|
1773
|
-
await loadDrives();
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
async function loadDrives() {
|
|
1777
|
-
try {
|
|
1778
|
-
const d = await api('/api/drives');
|
|
1779
|
-
const drives = d.drives || [];
|
|
1780
|
-
const grid = document.getElementById('fpDrivesGrid');
|
|
1781
|
-
grid.innerHTML = drives.map(drv => {
|
|
1782
|
-
const label = drv.replace('\\', '');
|
|
1783
|
-
return '<div class="fp-drive-card" data-drive="' + escapeHtml(drv) + '" style="padding:16px;text-align:center;border:1px solid var(--line);border-radius:8px;cursor:pointer;transition:all .15s;">'
|
|
1784
|
-
+ '<div style="font-size:28px;margin-bottom:6px;">💽</div>'
|
|
1785
|
-
+ '<div style="font-weight:600;font-size:14px;">' + escapeHtml(label) + '</div>'
|
|
1786
|
-
+ '<div style="font-size:11px;color:var(--ink-2);margin-top:2px;">本地磁盘</div>'
|
|
1787
|
-
+ '</div>';
|
|
1788
|
-
}).join('');
|
|
1789
|
-
grid.querySelectorAll('.fp-drive-card').forEach(el => {
|
|
1790
|
-
el.addEventListener('click', () => selectDrive(el.getAttribute('data-drive')));
|
|
1791
|
-
el.addEventListener('mouseenter', () => { el.style.borderColor = 'var(--brand)'; el.style.background = 'var(--brand-soft)'; });
|
|
1792
|
-
el.addEventListener('mouseleave', () => { el.style.borderColor = 'var(--line)'; el.style.background = ''; });
|
|
1793
|
-
});
|
|
1794
|
-
} catch (e) {
|
|
1795
|
-
toast('加载驱动器失败:' + e.message);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
async function selectDrive(drive) {
|
|
1800
|
-
fpCurrent = drive;
|
|
1801
|
-
fpSelected = '';
|
|
1802
|
-
document.getElementById('fpDrivesView').style.display = 'none';
|
|
1803
|
-
document.getElementById('fpFoldersView').style.display = '';
|
|
1804
|
-
document.getElementById('fpTitle').textContent = '📂 选择文件夹';
|
|
1805
|
-
document.getElementById('fpConfirm').disabled = true;
|
|
1806
|
-
document.getElementById('fpRenameFolder').disabled = true;
|
|
1807
|
-
await loadFolderPicker(drive);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function backToDrives() {
|
|
1811
|
-
fpCurrent = '';
|
|
1812
|
-
fpSelected = '';
|
|
1813
|
-
document.getElementById('fpDrivesView').style.display = '';
|
|
1814
|
-
document.getElementById('fpFoldersView').style.display = 'none';
|
|
1815
|
-
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1816
|
-
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1817
|
-
document.getElementById('fpTitle').textContent = '📂 选择驱动器';
|
|
1818
|
-
document.getElementById('fpConfirm').disabled = true;
|
|
1819
|
-
document.getElementById('fpRenameFolder').disabled = true;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
async function upDir() {
|
|
1823
|
-
if (!fpCurrent) return;
|
|
1824
|
-
// 驱动器根目录不能再往上
|
|
1825
|
-
if (/^[A-Za-z]:\\$/.test(fpCurrent)) {
|
|
1826
|
-
backToDrives();
|
|
1827
|
-
return;
|
|
1828
|
-
}
|
|
1829
|
-
const parent = getDirectoryName(fpCurrent);
|
|
1830
|
-
if (parent && parent !== fpCurrent) {
|
|
1831
|
-
fpCurrent = parent;
|
|
1832
|
-
fpSelected = '';
|
|
1833
|
-
document.getElementById('fpConfirm').disabled = true;
|
|
1834
|
-
document.getElementById('fpRenameFolder').disabled = true;
|
|
1835
|
-
await loadFolderPicker(fpCurrent);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
async function loadFolderPicker(dir) {
|
|
1840
|
-
try {
|
|
1841
|
-
const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
|
|
1842
|
-
fpCurrent = d.cwd;
|
|
1843
|
-
const pathEl = document.getElementById('fpPath');
|
|
1844
|
-
pathEl.textContent = d.cwd;
|
|
1845
|
-
pathEl.title = d.cwd;
|
|
1846
|
-
const tree = document.getElementById('fpTree');
|
|
1847
|
-
// 只显示文件夹,不显示文件
|
|
1848
|
-
const dirs = (d.entries || []).filter(e => e.type === 'dir');
|
|
1849
|
-
if (dirs.length === 0) {
|
|
1850
|
-
tree.innerHTML = '<div style="padding:20px;text-align:center;color:var(--ink-2);font-size:13px;">此目录下没有文件夹</div>';
|
|
1851
|
-
} else {
|
|
1852
|
-
tree.innerHTML = dirs.map(e => {
|
|
1853
|
-
const selected = fpSelected === e.path ? 'background:var(--brand-soft);border-color:var(--brand);' : '';
|
|
1854
|
-
return '<div class="file-item fp-folder-item" data-path="' + escapeHtml(e.path) + '" data-name="' + escapeHtml(e.name) + '" style="' + selected + 'padding:8px 10px;border:1px solid transparent;border-radius:6px;cursor:pointer;display:flex;align-items:center;gap:8px;">'
|
|
1855
|
-
+ '<span style="font-size:16px;">📁</span>'
|
|
1856
|
-
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(e.name) + '</span>'
|
|
1857
|
-
+ '</div>';
|
|
1858
|
-
}).join('');
|
|
1859
|
-
}
|
|
1860
|
-
tree.querySelectorAll('.fp-folder-item').forEach(el => {
|
|
1861
|
-
el.addEventListener('click', (e) => {
|
|
1862
|
-
e.stopPropagation();
|
|
1863
|
-
const path = el.getAttribute('data-path');
|
|
1864
|
-
const name = el.getAttribute('data-name');
|
|
1865
|
-
// 单击选中
|
|
1866
|
-
fpSelected = path;
|
|
1867
|
-
document.getElementById('fpConfirm').disabled = false;
|
|
1868
|
-
document.getElementById('fpRenameFolder').disabled = false;
|
|
1869
|
-
document.getElementById('fpRenameName').value = name;
|
|
1870
|
-
// 高亮选中项
|
|
1871
|
-
tree.querySelectorAll('.fp-folder-item').forEach(item => {
|
|
1872
|
-
item.style.background = '';
|
|
1873
|
-
item.style.borderColor = 'transparent';
|
|
1874
|
-
});
|
|
1875
|
-
el.style.background = 'var(--brand-soft)';
|
|
1876
|
-
el.style.borderColor = 'var(--brand)';
|
|
1877
|
-
});
|
|
1878
|
-
el.addEventListener('dblclick', () => {
|
|
1879
|
-
const path = el.getAttribute('data-path');
|
|
1880
|
-
fpSelected = '';
|
|
1881
|
-
document.getElementById('fpConfirm').disabled = true;
|
|
1882
|
-
document.getElementById('fpRenameFolder').disabled = true;
|
|
1883
|
-
loadFolderPicker(path);
|
|
1884
|
-
});
|
|
1885
|
-
});
|
|
1886
|
-
} catch (e) {
|
|
1887
|
-
toast('读取目录失败:' + e.message);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
document.getElementById('fpConfirm')?.addEventListener('click', async () => {
|
|
1892
|
-
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1893
|
-
try {
|
|
1894
|
-
const result = await api('/api/workspace', 'POST', { cwd: fpSelected });
|
|
1895
|
-
state.workspaceDir = result.cwd;
|
|
1896
|
-
localStorage.setItem('fhcode.workspaceDir', result.cwd);
|
|
1897
|
-
renderWorkspaceBar();
|
|
1898
|
-
closeModal('folderPickerModal');
|
|
1899
|
-
toast('已切换工作区:' + result.cwd);
|
|
1900
|
-
} catch (e) {
|
|
1901
|
-
toast('设置失败:' + e.message);
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
// 返回驱动器选择
|
|
1906
|
-
document.getElementById('fpBackToDrives')?.addEventListener('click', backToDrives);
|
|
1907
|
-
// 返回上级目录
|
|
1908
|
-
document.getElementById('fpUpDir')?.addEventListener('click', upDir);
|
|
1909
|
-
|
|
1910
|
-
// 新建文件夹
|
|
1911
|
-
document.getElementById('fpNewFolder')?.addEventListener('click', () => {
|
|
1912
|
-
document.getElementById('fpMkdirBar').style.display = 'flex';
|
|
1913
|
-
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1914
|
-
document.getElementById('fpMkdirName').value = '';
|
|
1915
|
-
setTimeout(() => document.getElementById('fpMkdirName').focus(), 50);
|
|
1916
|
-
});
|
|
1917
|
-
document.getElementById('fpMkdirCancel')?.addEventListener('click', () => {
|
|
1918
|
-
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1919
|
-
});
|
|
1920
|
-
document.getElementById('fpMkdirConfirm')?.addEventListener('click', async () => {
|
|
1921
|
-
const name = document.getElementById('fpMkdirName').value.trim();
|
|
1922
|
-
if (!name) { toast('请输入文件夹名称'); return; }
|
|
1923
|
-
try {
|
|
1924
|
-
const result = await api('/api/workspace/mkdir', 'POST', { parent: fpCurrent, name });
|
|
1925
|
-
toast('文件夹已创建:' + name);
|
|
1926
|
-
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1927
|
-
fpSelected = result.path;
|
|
1928
|
-
await loadFolderPicker(fpCurrent);
|
|
1929
|
-
document.getElementById('fpConfirm').disabled = false;
|
|
1930
|
-
document.getElementById('fpRenameFolder').disabled = false;
|
|
1931
|
-
} catch (e) {
|
|
1932
|
-
toast('创建失败:' + e.message);
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
document.getElementById('fpMkdirName')?.addEventListener('keydown', (e) => {
|
|
1936
|
-
if (e.key === 'Enter') document.getElementById('fpMkdirConfirm').click();
|
|
1937
|
-
if (e.key === 'Escape') document.getElementById('fpMkdirCancel').click();
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
|
-
// 重命名文件夹
|
|
1941
|
-
document.getElementById('fpRenameFolder')?.addEventListener('click', () => {
|
|
1942
|
-
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1943
|
-
document.getElementById('fpRenameBar').style.display = 'flex';
|
|
1944
|
-
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1945
|
-
const name = fpSelected.split(/[\\/]/).filter(Boolean).pop() || '';
|
|
1946
|
-
document.getElementById('fpRenameName').value = name;
|
|
1947
|
-
setTimeout(() => document.getElementById('fpRenameName').focus(), 50);
|
|
1948
|
-
});
|
|
1949
|
-
document.getElementById('fpRenameCancel')?.addEventListener('click', () => {
|
|
1950
|
-
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1951
|
-
});
|
|
1952
|
-
document.getElementById('fpRenameConfirm')?.addEventListener('click', async () => {
|
|
1953
|
-
const newName = document.getElementById('fpRenameName').value.trim();
|
|
1954
|
-
if (!newName) { toast('请输入新名称'); return; }
|
|
1955
|
-
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1956
|
-
try {
|
|
1957
|
-
const result = await api('/api/workspace/rename', 'POST', { path: fpSelected, newName });
|
|
1958
|
-
toast('文件夹已重命名为:' + newName);
|
|
1959
|
-
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1960
|
-
fpSelected = result.path;
|
|
1961
|
-
await loadFolderPicker(fpCurrent);
|
|
1962
|
-
document.getElementById('fpConfirm').disabled = false;
|
|
1963
|
-
document.getElementById('fpRenameFolder').disabled = false;
|
|
1964
|
-
} catch (e) {
|
|
1965
|
-
toast('重命名失败:' + e.message);
|
|
1966
|
-
}
|
|
1967
|
-
});
|
|
1968
|
-
document.getElementById('fpRenameName')?.addEventListener('keydown', (e) => {
|
|
1969
|
-
if (e.key === 'Enter') document.getElementById('fpRenameConfirm').click();
|
|
1970
|
-
if (e.key === 'Escape') document.getElementById('fpRenameCancel').click();
|
|
1971
|
-
});
|
|
1972
|
-
|
|
1973
|
-
/* ========== 自动化、模板、市场、办公助理 ========== */
|
|
1974
|
-
document.getElementById('newAutoBtn')?.addEventListener('click', () => openModal('autoModal'));
|
|
1975
|
-
document.getElementById('autoSaveBtn')?.addEventListener('click', async () => {
|
|
1976
|
-
const name = document.getElementById('autoName').value.trim();
|
|
1977
|
-
const goal = document.getElementById('autoGoal').value.trim();
|
|
1978
|
-
if (!name || !goal) { toast('请填写名称与目标'); return; }
|
|
1979
|
-
try { await api('/api/automations', 'POST', { name, goal }); closeModal('autoModal');
|
|
1980
|
-
const n = document.getElementById('autoName');
|
|
1981
|
-
const g = document.getElementById('autoGoal');
|
|
1982
|
-
if (n) n.value = ''; if (g) g.value = '';
|
|
1983
|
-
await loadAutomations(); toast('已保存'); }
|
|
1984
|
-
catch (e) { toast('保存失败:' + e.message); }
|
|
1985
|
-
});
|
|
1986
|
-
|
|
1987
|
-
document.getElementById('newTplBtn')?.addEventListener('click', () => {
|
|
1988
|
-
const goal = document.getElementById('goalInput')?.value?.trim() || '';
|
|
1989
|
-
const inp = document.getElementById('tplGoal');
|
|
1990
|
-
if (inp) inp.value = goal;
|
|
1991
|
-
openModal('tplModal');
|
|
1992
|
-
});
|
|
1993
|
-
document.getElementById('tplSaveBtn')?.addEventListener('click', async () => {
|
|
1994
|
-
const title = document.getElementById('tplTitle').value.trim();
|
|
1995
|
-
const goal = document.getElementById('tplGoal').value.trim();
|
|
1996
|
-
const category = document.getElementById('tplCategory').value.trim();
|
|
1997
|
-
if (!title || !goal) { toast('请填写标题与内容'); return; }
|
|
1998
|
-
try { await api('/api/templates', 'POST', { title, goal, category }); closeModal('tplModal');
|
|
1999
|
-
const t = document.getElementById('tplTitle');
|
|
2000
|
-
const g = document.getElementById('tplGoal');
|
|
2001
|
-
const c = document.getElementById('tplCategory');
|
|
2002
|
-
if (t) t.value = ''; if (g) g.value = ''; if (c) c.value = '';
|
|
2003
|
-
await loadTemplates(); toast('已保存模板'); }
|
|
2004
|
-
catch (e) { toast('保存失败:' + e.message); }
|
|
2005
|
-
});
|
|
2006
|
-
|
|
2007
|
-
document.getElementById('marketRefresh')?.addEventListener('click', loadMarket);
|
|
2008
|
-
let marketTimer;
|
|
2009
|
-
document.getElementById('marketSearch')?.addEventListener('input', () => { clearTimeout(marketTimer); marketTimer = setTimeout(loadMarket, 400); });
|
|
2010
|
-
document.getElementById('marketSource')?.addEventListener('change', loadMarket);
|
|
2011
|
-
|
|
2012
|
-
/* ========== 节点管理 & 自定义来源 ========== */
|
|
2013
|
-
document.getElementById('addNodeBtn')?.addEventListener('click', () => openNodeModal());
|
|
2014
|
-
document.getElementById('addSourceBtn')?.addEventListener('click', () => openSourceModal());
|
|
2015
|
-
document.getElementById('nodeSaveBtn')?.addEventListener('click', async () => {
|
|
2016
|
-
const id = document.getElementById('nodeEditId').value || '';
|
|
2017
|
-
const name = document.getElementById('nodeName').value.trim();
|
|
2018
|
-
const type = document.getElementById('nodeType').value;
|
|
2019
|
-
const url = document.getElementById('nodeUrl').value.trim();
|
|
2020
|
-
const apiKey = document.getElementById('nodeApiKey').value;
|
|
2021
|
-
const capabilities = [];
|
|
2022
|
-
if (document.getElementById('nodeCapTemplates').checked) capabilities.push('templates');
|
|
2023
|
-
if (document.getElementById('nodeCapSkills').checked) capabilities.push('skills');
|
|
2024
|
-
if (document.getElementById('nodeCapOffice').checked) capabilities.push('office');
|
|
2025
|
-
if (!name || !url) { toast('请填写名称和地址'); return; }
|
|
2026
|
-
if (!capabilities.length) { toast('请至少选择一种能力'); return; }
|
|
2027
|
-
const data = { name, type, url, capabilities };
|
|
2028
|
-
if (apiKey) data.apiKey = apiKey;
|
|
2029
|
-
if (id) data.id = id;
|
|
2030
|
-
await saveNode(data);
|
|
2031
|
-
closeModal('nodeModal');
|
|
2032
|
-
});
|
|
2033
|
-
document.getElementById('sourceSaveBtn')?.addEventListener('click', async () => {
|
|
2034
|
-
const id = document.getElementById('sourceEditId').value || '';
|
|
2035
|
-
const name = document.getElementById('sourceName').value.trim();
|
|
2036
|
-
const type = document.getElementById('sourceType').value;
|
|
2037
|
-
const url = document.getElementById('sourceUrl').value.trim();
|
|
2038
|
-
if (!name || !url) { toast('请填写名称和地址'); return; }
|
|
2039
|
-
const data = { name, type, url };
|
|
2040
|
-
if (id) data.id = id;
|
|
2041
|
-
await saveSource(data);
|
|
2042
|
-
closeModal('sourceModal');
|
|
2043
|
-
// 刷新相关数据
|
|
2044
|
-
if (type === 'templates') await loadTemplates();
|
|
2045
|
-
else if (type === 'skills') await loadMarket();
|
|
2046
|
-
else if (type === 'office') await loadOffice();
|
|
2047
|
-
});
|
|
2048
|
-
|
|
2049
|
-
function openNodeModal(node) {
|
|
2050
|
-
document.getElementById('nodeModalTitle').textContent = node ? '编辑节点' : '添加节点';
|
|
2051
|
-
document.getElementById('nodeEditId').value = node?.id || '';
|
|
2052
|
-
document.getElementById('nodeName').value = node?.name || '';
|
|
2053
|
-
document.getElementById('nodeType').value = node?.type || 'http';
|
|
2054
|
-
document.getElementById('nodeUrl').value = node?.url || '';
|
|
2055
|
-
document.getElementById('nodeApiKey').value = '';
|
|
2056
|
-
document.getElementById('nodeCapTemplates').checked = node?.capabilities?.includes('templates') ?? true;
|
|
2057
|
-
document.getElementById('nodeCapSkills').checked = node?.capabilities?.includes('skills') ?? true;
|
|
2058
|
-
document.getElementById('nodeCapOffice').checked = node?.capabilities?.includes('office') ?? true;
|
|
2059
|
-
openModal('nodeModal');
|
|
2060
|
-
}
|
|
2061
|
-
function editNode(id) {
|
|
2062
|
-
const node = (window._nodesCache || []).find((n) => n.id === id);
|
|
2063
|
-
openNodeModal(node || { id });
|
|
2064
|
-
}
|
|
2065
|
-
function openSourceModal(source) {
|
|
2066
|
-
document.getElementById('sourceModalTitle').textContent = source ? '编辑自定义来源' : '添加自定义来源';
|
|
2067
|
-
document.getElementById('sourceEditId').value = source?.id || '';
|
|
2068
|
-
document.getElementById('sourceName').value = source?.name || '';
|
|
2069
|
-
document.getElementById('sourceType').value = source?.type || 'templates';
|
|
2070
|
-
document.getElementById('sourceUrl').value = source?.url || '';
|
|
2071
|
-
openModal('sourceModal');
|
|
2072
|
-
}
|
|
2073
|
-
function editSource(id) {
|
|
2074
|
-
const source = (window._sourcesCache || []).find((s) => s.id === id);
|
|
2075
|
-
openSourceModal(source || { id });
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', () => closeModal(el.getAttribute('data-close'))));
|
|
2080
|
-
document.querySelectorAll('.mask').forEach((m) => m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('show'); }));
|
|
2081
|
-
// 文件夹选择器事件已在 loadFolderPicker / loadDrives 内直接绑定,此处保留空壳避免旧引用报错
|
|
2082
|
-
function startRefresh() {
|
|
2083
|
-
setInterval(async () => {
|
|
2084
|
-
await loadTasks();
|
|
2085
|
-
// 如果当前有正在运行的任务,自动刷新对话流,让用户实时看到思考过程
|
|
2086
|
-
if (state.currentTaskId) {
|
|
2087
|
-
const cur = state.tasks.find((t) => t.id === state.currentTaskId);
|
|
2088
|
-
if (cur && (cur.status === 'running' || cur.status === 'queued')) {
|
|
2089
|
-
await refreshCurrentThread();
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
}, 1000); // 1秒轮询,确保思考过程实时显示
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
/* ========== 电脑操作(鼠标/键盘/截图) ========== */
|
|
2096
|
-
let selectedMousePos = { x: 0, y: 0 };
|
|
2097
|
-
|
|
2098
|
-
// 截取屏幕
|
|
2099
|
-
async function captureScreen() {
|
|
2100
|
-
try {
|
|
2101
|
-
const d = await api('/api/computer/screenshot', 'POST', {});
|
|
2102
|
-
if (d.ok && d.image) {
|
|
2103
|
-
const preview = document.getElementById('screenPreview');
|
|
2104
|
-
preview.innerHTML = `<img id="screenImg" src="${d.image}" alt="屏幕截图" />`;
|
|
2105
|
-
const img = document.getElementById('screenImg');
|
|
2106
|
-
// 点击图片获取坐标
|
|
2107
|
-
img.addEventListener('click', (e) => {
|
|
2108
|
-
const rect = img.getBoundingClientRect();
|
|
2109
|
-
const scaleX = (d.width || 1920) / rect.width;
|
|
2110
|
-
const scaleY = (d.height || 1080) / rect.height;
|
|
2111
|
-
selectedMousePos.x = Math.round((e.clientX - rect.left) * scaleX);
|
|
2112
|
-
selectedMousePos.y = Math.round((e.clientY - rect.top) * scaleY);
|
|
2113
|
-
document.getElementById('mousePos').textContent = `坐标: ${selectedMousePos.x}, ${selectedMousePos.y}`;
|
|
2114
|
-
toast(`已选择坐标: ${selectedMousePos.x}, ${selectedMousePos.y},点击操作按钮执行`);
|
|
2115
|
-
});
|
|
2116
|
-
toast('屏幕截图已更新,点击图片选择坐标');
|
|
2117
|
-
} else {
|
|
2118
|
-
toast('截图失败: ' + (d.error || '未知错误'));
|
|
2119
|
-
}
|
|
2120
|
-
} catch (e) {
|
|
2121
|
-
toast('截图失败: ' + e.message);
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
// 鼠标点击
|
|
2126
|
-
async function mouseClick(button, doubleClick = false) {
|
|
2127
|
-
try {
|
|
2128
|
-
const d = await api('/api/computer/mouse/click', 'POST', {
|
|
2129
|
-
button,
|
|
2130
|
-
x: selectedMousePos.x,
|
|
2131
|
-
y: selectedMousePos.y,
|
|
2132
|
-
double: doubleClick,
|
|
2133
|
-
});
|
|
2134
|
-
if (d.ok) {
|
|
2135
|
-
toast(`${doubleClick ? '双击' : (button === 'right' ? '右键点击' : '左键点击')} (${selectedMousePos.x}, ${selectedMousePos.y})`);
|
|
2136
|
-
} else {
|
|
2137
|
-
toast('操作失败: ' + (d.error || '未知错误'));
|
|
2138
|
-
}
|
|
2139
|
-
} catch (e) {
|
|
2140
|
-
toast('操作失败: ' + e.message);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
// 输入文字
|
|
2145
|
-
async function typeText(text) {
|
|
2146
|
-
try {
|
|
2147
|
-
const d = await api('/api/computer/keyboard/type', 'POST', { text });
|
|
2148
|
-
if (d.ok) {
|
|
2149
|
-
toast('已输入文字');
|
|
2150
|
-
} else {
|
|
2151
|
-
toast('输入失败: ' + (d.error || '未知错误'));
|
|
2152
|
-
}
|
|
2153
|
-
} catch (e) {
|
|
2154
|
-
toast('输入失败: ' + e.message);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
// 按键
|
|
2159
|
-
async function pressKey(key) {
|
|
2160
|
-
try {
|
|
2161
|
-
const d = await api('/api/computer/keyboard/press', 'POST', { key });
|
|
2162
|
-
if (d.ok) {
|
|
2163
|
-
toast('已按键: ' + key);
|
|
2164
|
-
} else {
|
|
2165
|
-
toast('按键失败: ' + (d.error || '未知错误'));
|
|
2166
|
-
}
|
|
2167
|
-
} catch (e) {
|
|
2168
|
-
toast('按键失败: ' + e.message);
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// 绑定电脑操作事件
|
|
2173
|
-
document.getElementById('screenCaptureBtn')?.addEventListener('click', captureScreen);
|
|
2174
|
-
document.getElementById('clickLeftBtn')?.addEventListener('click', () => mouseClick('left'));
|
|
2175
|
-
document.getElementById('clickRightBtn')?.addEventListener('click', () => mouseClick('right'));
|
|
2176
|
-
document.getElementById('doubleClickBtn')?.addEventListener('click', () => mouseClick('left', true));
|
|
2177
|
-
document.getElementById('typeTextInput')?.addEventListener('keydown', (e) => {
|
|
2178
|
-
if (e.key === 'Enter') {
|
|
2179
|
-
const text = e.target.value.trim();
|
|
2180
|
-
if (text) {
|
|
2181
|
-
typeText(text);
|
|
2182
|
-
e.target.value = '';
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
});
|
|
2186
|
-
document.querySelectorAll('.computer-actions [data-key]').forEach((btn) => {
|
|
2187
|
-
btn.addEventListener('click', () => pressKey(btn.getAttribute('data-key') || ''));
|
|
2188
|
-
});
|
|
2189
|
-
|
|
2190
|
-
/* ========== 记忆系统 ========== */
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
// 当切换到记忆页面时初始化
|
|
2198
|
-
const origSwitchView = switchView;
|
|
2199
|
-
window.switchView = function(nav) {
|
|
2200
|
-
origSwitchView(nav);
|
|
2201
|
-
if (nav === 'memory') {
|
|
2202
|
-
setTimeout(initMemoryUI, 100);
|
|
2203
|
-
}
|
|
2204
|
-
};
|
|
2205
|
-
|
|
2206
|
-
if (state.token && state.phone) {
|
|
2207
|
-
document.getElementById('loginOverlay').style.display = 'none';
|
|
2208
|
-
document.getElementById('appLayout').classList.add('show');
|
|
2209
|
-
afterLogin();
|
|
2210
|
-
}
|
|
1
|
+
// i18n 双语字典
|
|
2
|
+
const I18N = {
|
|
3
|
+
zh: {
|
|
4
|
+
'app.title_login': '飞虹 Code 控制台',
|
|
5
|
+
'app.login_hint': '请输入手机号码直接登录',
|
|
6
|
+
'app.brand': '飞虹 Code',
|
|
7
|
+
'login.phone_label': '手机号码',
|
|
8
|
+
'login.btn_login': '手机号登录',
|
|
9
|
+
'nav.chat': '💬 对话',
|
|
10
|
+
'nav.automations': '⚡ 自动化',
|
|
11
|
+
'nav.templates': '📚 模板库',
|
|
12
|
+
'nav.market': '🧩 插件',
|
|
13
|
+
'nav.office': '📎 办公',
|
|
14
|
+
'nav.memory': '🧠 记忆',
|
|
15
|
+
'nav.release': '🚀 发布',
|
|
16
|
+
'sidebar.nav': '导航',
|
|
17
|
+
'sidebar.chat': '对话任务',
|
|
18
|
+
'sidebar.automations': '自动化',
|
|
19
|
+
'sidebar.templates': '模板库',
|
|
20
|
+
'sidebar.market': '插件市场',
|
|
21
|
+
'sidebar.office': '办公助理',
|
|
22
|
+
'sidebar.memory': '记忆系统',
|
|
23
|
+
'sidebar.user': '用户',
|
|
24
|
+
'sidebar.settings': '设置',
|
|
25
|
+
'sidebar.task_list': '任务列表',
|
|
26
|
+
'btn.new_task': '+ 新建任务',
|
|
27
|
+
'empty.no_tasks': '暂无任务',
|
|
28
|
+
'empty.no_skills': '没有找到技能',
|
|
29
|
+
'empty.no_builtin_templates': '无内置模板',
|
|
30
|
+
'empty.no_models': '还没有大模型配置,请在下方添加。',
|
|
31
|
+
'empty.preparing_welcome': '正在为您准备引导任务,请稍候…',
|
|
32
|
+
'task.not_selected': '未选择任务',
|
|
33
|
+
'task.select_hint': '从左侧任务列表选择,或在下方输入指令发起新任务',
|
|
34
|
+
'task.refresh': '刷新当前任务',
|
|
35
|
+
'chat.welcome': '点击「+ 新建任务」开启一个新任务,或直接在下方向当前任务发送消息——所有对话都会归属同一个任务运行。',
|
|
36
|
+
'chat.menu': '+ 菜单',
|
|
37
|
+
'chat.screenshot': '📷 截图',
|
|
38
|
+
'chat.upload_file': '📄 上传文件',
|
|
39
|
+
'chat.upload_image': '🖼 上传图片',
|
|
40
|
+
'chat.direct_mode': '🖥 电脑操作',
|
|
41
|
+
'chat.voice': '🎤 语音输入',
|
|
42
|
+
'chat.agent_type': '🤖 选择智能体类型',
|
|
43
|
+
'chat.agent_default': '🤖 通用助手',
|
|
44
|
+
'chat.listening': '● 正在听…',
|
|
45
|
+
'chat.send': '▶ 发送',
|
|
46
|
+
'common.loading': '加载中…',
|
|
47
|
+
'perm.default': '默认权限',
|
|
48
|
+
'call.hint': '通话',
|
|
49
|
+
'call.voice': '📞 语音',
|
|
50
|
+
'call.video': '🎥 视频',
|
|
51
|
+
'call.hangup': '⛔ 挂断',
|
|
52
|
+
'call.voice_call': '语音通话',
|
|
53
|
+
'call.video_call': '视频通话',
|
|
54
|
+
'call.voice_hint': '语音通话中,说话内容实时转文字',
|
|
55
|
+
'call.video_hint': '视频通话中,可截取画面发送',
|
|
56
|
+
'call.ended': '通话已结束,识别的文字已保留在输入框',
|
|
57
|
+
'computer.tab': '🖥 电脑操作',
|
|
58
|
+
'computer.capture': '📷 截取屏幕',
|
|
59
|
+
'computer.mouse_pos': '坐标',
|
|
60
|
+
'computer.click_left': '左键点击',
|
|
61
|
+
'computer.click_right': '右键点击',
|
|
62
|
+
'computer.double_click': '双击',
|
|
63
|
+
'computer.type_hint': '输入文字后回车发送',
|
|
64
|
+
'computer.screenshot_added': '摄像头画面已添加到输入框,可直接发送',
|
|
65
|
+
'computer.screenshot_not_ready': '视频还没准备好,请稍等',
|
|
66
|
+
'msg.copy': '📋 复制',
|
|
67
|
+
'msg.create_doc': '📄 建文档',
|
|
68
|
+
'msg.copied': '已复制到剪贴板',
|
|
69
|
+
'msg.doc_downloaded': '文档已下载',
|
|
70
|
+
'msg.no_content': '没有可复制的内容',
|
|
71
|
+
'crop.tip': '按住鼠标拖拽选择截图区域 · 松开确认 · ESC 取消',
|
|
72
|
+
'page.automations.sub': '把常用目标存成指令,一键发起任务',
|
|
73
|
+
'btn.new_automation': '+ 新建指令',
|
|
74
|
+
'empty.no_automations': '还没有快捷指令',
|
|
75
|
+
'page.templates.sub': '点击模板即可填充到输入框',
|
|
76
|
+
'btn.save_template': '+ 保存当前为模板',
|
|
77
|
+
'tpl.builtin': '内置模板',
|
|
78
|
+
'tpl.my': '我的模板',
|
|
79
|
+
'empty.no_templates': '暂无自定义模板',
|
|
80
|
+
'page.market.sub': '聚合 ClawHub 与 Agent-Foundry 技能生态',
|
|
81
|
+
'market.search_placeholder': '搜索…',
|
|
82
|
+
'market.source_all': '全部来源',
|
|
83
|
+
'btn.refresh': '刷新',
|
|
84
|
+
'market.installed': '已安装',
|
|
85
|
+
'empty.no_installed': '暂无已安装技能',
|
|
86
|
+
'page.office.sub': '文档处理快捷入口,点击即生成提示词',
|
|
87
|
+
'page.memory.sub': '短期记忆每日自动整理,沉淀长期知识',
|
|
88
|
+
'btn.refresh_mem': '🔄 刷新',
|
|
89
|
+
'btn.summarize': '✨ 立即总结',
|
|
90
|
+
'mem.stat_short_days': '短期记忆天数',
|
|
91
|
+
'mem.stat_long_items': '长期记忆条目',
|
|
92
|
+
'mem.stat_last_summary': '上次总结',
|
|
93
|
+
'mem.stat_next_summary': '下次自动总结',
|
|
94
|
+
'mem.tab_short': '📅 短期记忆',
|
|
95
|
+
'mem.tab_long': '💾 长期记忆',
|
|
96
|
+
'mem.tab_history': '📜 总结历史',
|
|
97
|
+
'btn.view': '查看',
|
|
98
|
+
'mem.select_date': '选择日期查看短期记忆',
|
|
99
|
+
'empty.no_history': '暂无总结记录',
|
|
100
|
+
'footbar.title': '对话任务',
|
|
101
|
+
'footbar.desc': '提交自然语言需求,云端静默执行并回传结果',
|
|
102
|
+
'footbar.model': '大模型',
|
|
103
|
+
'footbar.model_title': '切换当前使用的大模型',
|
|
104
|
+
'footbar.model_settings': '大模型设置',
|
|
105
|
+
'footbar.model_default': '默认(系统路由)',
|
|
106
|
+
'right.detail': '📦 任务详情',
|
|
107
|
+
'right.preview': '👁 产物预览',
|
|
108
|
+
'right.detail_hint': '从顶部选择一个任务查看详情。',
|
|
109
|
+
'right.preview_title': '预览',
|
|
110
|
+
'right.preview_hint': '点击任务或对话中的文件/代码进行预览。',
|
|
111
|
+
'modal.auto_title': '新建快捷指令',
|
|
112
|
+
'modal.auto_hint': '保存后可在「自动化」页一键发起任务。',
|
|
113
|
+
'modal.auto_name': '指令名称',
|
|
114
|
+
'modal.auto_goal': '目标(提交给模型的需求)',
|
|
115
|
+
'modal.perm_title': '🔒 权限处理窗口',
|
|
116
|
+
'modal.perm_hint': '配置工作期间 AI 可读范围及可操作权限。',
|
|
117
|
+
'modal.perm_read_scope': '可读范围',
|
|
118
|
+
'modal.perm_workspace': '仅当前工作区',
|
|
119
|
+
'modal.perm_specified': '指定目录',
|
|
120
|
+
'modal.perm_all': '全部文件',
|
|
121
|
+
'modal.perm_actions': '操作权限',
|
|
122
|
+
'modal.perm_read': '读取文件',
|
|
123
|
+
'modal.perm_write': '写入文件',
|
|
124
|
+
'modal.perm_shell': '执行命令',
|
|
125
|
+
'modal.perm_network': '访问网络',
|
|
126
|
+
'modal.perm_browser': '浏览器控制',
|
|
127
|
+
'modal.agent_title': '🤖 选择智能体类型',
|
|
128
|
+
'modal.agent_hint': '不同智能体会自动调整提示词与执行策略。',
|
|
129
|
+
'modal.folder_title': '📂 选择工作区文件夹',
|
|
130
|
+
'btn.select_folder': '选择此文件夹',
|
|
131
|
+
'modal.model_title': '🧠 大模型设置',
|
|
132
|
+
'modal.model_hint': '配置自定义大模型,可保存多个并在对话中随时切换。勾选「默认」的项将作为对话默认模型。',
|
|
133
|
+
'modal.model_name': '模型名称 *',
|
|
134
|
+
'modal.model_api': 'API 地址',
|
|
135
|
+
'modal.model_key': '密钥(API Key)',
|
|
136
|
+
'modal.model_reasoning': '推理过程运行内容(中间环节)',
|
|
137
|
+
'btn.save_config': '保存配置',
|
|
138
|
+
'btn.clear_form': '清空表单',
|
|
139
|
+
'btn.close': '关闭',
|
|
140
|
+
'modal.new_task_title': '+ 新建对话任务',
|
|
141
|
+
'modal.new_task_hint': '创建一个全新任务,中部对话栏的所有消息都将归属该任务运行(支持多轮连续对话)。',
|
|
142
|
+
'modal.new_task_label': '任务描述',
|
|
143
|
+
'modal.new_task_agent': '智能体类型',
|
|
144
|
+
'modal.new_task_workspace': '工作区',
|
|
145
|
+
'btn.create_task': '创建任务',
|
|
146
|
+
'modal.settings_title': '⚙️ 设置',
|
|
147
|
+
'settings.general': '通用',
|
|
148
|
+
'settings.layout': '栏目配置',
|
|
149
|
+
'settings.perm': '权限处理',
|
|
150
|
+
'settings.perm_desc': '配置工作期间可读范围与操作权限',
|
|
151
|
+
'settings.theme': '外观',
|
|
152
|
+
'settings.theme_desc': '切换浅色 / 深色主题',
|
|
153
|
+
'settings.session': '💬 会话管理',
|
|
154
|
+
'menu.version': '体验版',
|
|
155
|
+
'menu.current': '当前',
|
|
156
|
+
'menu.buddy': 'Buddy 加油站',
|
|
157
|
+
'menu.invite': '去邀约',
|
|
158
|
+
'menu.invite_right': '最高得会员',
|
|
159
|
+
'menu.points': '积分余额',
|
|
160
|
+
'menu.growth': '成长计划',
|
|
161
|
+
'menu.growth_right': '连登有奖',
|
|
162
|
+
'menu.help': '帮助与反馈',
|
|
163
|
+
'menu.update': '检查更新',
|
|
164
|
+
'menu.logout': '退出登录',
|
|
165
|
+
'btn.cancel': '取消',
|
|
166
|
+
'btn.save': '保存',
|
|
167
|
+
'sidebar.release': '产品发布',
|
|
168
|
+
'modal.tpl_title_label': '模板标题',
|
|
169
|
+
'modal.tpl_title_ph': '如:SQL 优化',
|
|
170
|
+
'modal.tpl_category': '分类',
|
|
171
|
+
'modal.tpl_category_ph': '自定义',
|
|
172
|
+
'modal.tpl_goal': '目标内容',
|
|
173
|
+
'settings.session_auto_new': '发送消息前自动新建对话',
|
|
174
|
+
'settings.session_auto_open': '页面打开时自动新建对话',
|
|
175
|
+
'settings.session_clear': '新建对话时清空历史上下文',
|
|
176
|
+
'modal.auto_name_ph': '如:每日构建检查',
|
|
177
|
+
'modal.auto_goal_ph': '运行 npm test 并汇总失败用例…',
|
|
178
|
+
'modal.model_name_ph': '如 GPT-4o / 通义千问 / 自建模型',
|
|
179
|
+
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
180
|
+
'modal.model_key_ph': 'sk-...',
|
|
181
|
+
'modal.model_reasoning_ph': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
182
|
+
'modal.perm_path_ph': '输入允许的目录路径',
|
|
183
|
+
// ---- select 的 title 提示(键名固定为 title.<元素id>)----
|
|
184
|
+
'title.modelSelect': '切换当前使用的大模型',
|
|
185
|
+
// ---- 输入框占位符(键名固定为 ph.<元素id>)----
|
|
186
|
+
'ph.goalInput': '输入指令,消息将归属当前任务持续对话;点上方「+ 新建任务」可开启新任务(创建 → 推理 → 工具验证 → 闭环全程追踪)',
|
|
187
|
+
'ph.marketSearch': '搜索…',
|
|
188
|
+
'ph.autoName': '如:每日构建检查',
|
|
189
|
+
'ph.autoGoal': '运行 npm test 并汇总失败用例…',
|
|
190
|
+
'ph.tplTitle': '如:SQL 优化',
|
|
191
|
+
'ph.tplCategory': '自定义',
|
|
192
|
+
'ph.tplGoal': '粘贴要保存的需求文本…',
|
|
193
|
+
'ph.permReadPath': '输入允许的目录路径',
|
|
194
|
+
'ph.modelName': '如 GPT-4o / 通义千问 / 自建模型',
|
|
195
|
+
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
196
|
+
'ph.modelApiKey': 'sk-...',
|
|
197
|
+
'ph.modelReasoning': '配置该模型在中间推理环节的运行内容,例如:先列出需求拆解,再给出实现方案,最后输出代码。',
|
|
198
|
+
'ph.ntGoal': '例如:请读取当前工作区 package.json,告诉我其中的 name 与 version 字段',
|
|
199
|
+
'ph.imName': '模型名称 *',
|
|
200
|
+
'ph.imBase': 'API 地址',
|
|
201
|
+
},
|
|
202
|
+
en: {
|
|
203
|
+
'app.title_login': 'Feihong Code Console',
|
|
204
|
+
'app.login_hint': 'Enter phone number to login directly',
|
|
205
|
+
'app.brand': 'Feihong Code',
|
|
206
|
+
'login.phone_label': 'Phone Number',
|
|
207
|
+
'login.btn_login': 'Login',
|
|
208
|
+
'nav.chat': '💬 Chat',
|
|
209
|
+
'nav.automations': '⚡ Automations',
|
|
210
|
+
'nav.templates': '📚 Templates',
|
|
211
|
+
'nav.market': '🧩 Market',
|
|
212
|
+
'nav.office': '📎 Office',
|
|
213
|
+
'nav.release': '🚀 Release',
|
|
214
|
+
'nav.memory': '🧠 Memory',
|
|
215
|
+
'sidebar.nav': 'Nav',
|
|
216
|
+
'sidebar.chat': 'Chat',
|
|
217
|
+
'sidebar.automations': 'Automations',
|
|
218
|
+
'sidebar.templates': 'Templates',
|
|
219
|
+
'sidebar.market': 'Market',
|
|
220
|
+
'sidebar.office': 'Office',
|
|
221
|
+
'sidebar.release': 'Release',
|
|
222
|
+
'sidebar.memory': 'Memory',
|
|
223
|
+
'sidebar.user': 'User',
|
|
224
|
+
'sidebar.settings': 'Settings',
|
|
225
|
+
'sidebar.task_list': 'Tasks',
|
|
226
|
+
'btn.new_task': '+ New Task',
|
|
227
|
+
'empty.no_tasks': 'No tasks',
|
|
228
|
+
'empty.no_skills': 'No skills found',
|
|
229
|
+
'empty.no_builtin_templates': 'No built-in templates',
|
|
230
|
+
'empty.no_models': 'No model configured yet, add one below.',
|
|
231
|
+
'empty.preparing_welcome': 'Preparing welcome tasks for you, please wait…',
|
|
232
|
+
'task.not_selected': 'No task selected',
|
|
233
|
+
'task.select_hint': 'Select from task list or enter commands below',
|
|
234
|
+
'task.refresh': 'Refresh',
|
|
235
|
+
'chat.welcome': 'Click + New Task to start a new task, or send messages to continue the current task.',
|
|
236
|
+
'chat.menu': '+ Menu',
|
|
237
|
+
'chat.screenshot': '📷 Screenshot',
|
|
238
|
+
'chat.upload_file': '📄 Upload File',
|
|
239
|
+
'chat.upload_image': '🖼 Upload Image',
|
|
240
|
+
'chat.direct_mode': '🖥 Computer Control',
|
|
241
|
+
'chat.voice': '🎤 Voice Input',
|
|
242
|
+
'chat.agent_type': '🤖 Agent Type',
|
|
243
|
+
'chat.agent_default': '🤖 Assistant',
|
|
244
|
+
'chat.listening': '● Listening…',
|
|
245
|
+
'chat.send': '▶ Send',
|
|
246
|
+
'common.loading': 'Loading…',
|
|
247
|
+
'perm.default': 'Default Permissions',
|
|
248
|
+
'call.hint': 'Call',
|
|
249
|
+
'call.voice': '📞 Voice',
|
|
250
|
+
'call.video': '🎥 Video',
|
|
251
|
+
'call.hangup': '⛔ Hang Up',
|
|
252
|
+
'call.voice_call': 'Voice Call',
|
|
253
|
+
'call.video_call': 'Video Call',
|
|
254
|
+
'call.voice_hint': 'Voice call in progress, speech transcribed in real-time',
|
|
255
|
+
'call.video_hint': 'Video call in progress, capture frames to send',
|
|
256
|
+
'call.ended': 'Call ended, transcribed text kept in input box',
|
|
257
|
+
'computer.tab': '🖥 Computer Control',
|
|
258
|
+
'computer.capture': '📷 Capture Screen',
|
|
259
|
+
'computer.mouse_pos': 'Pos',
|
|
260
|
+
'computer.click_left': 'Left Click',
|
|
261
|
+
'computer.click_right': 'Right Click',
|
|
262
|
+
'computer.double_click': 'Double Click',
|
|
263
|
+
'computer.type_hint': 'Type text and press Enter to send',
|
|
264
|
+
'computer.screenshot_added': 'Camera frame added to input box, ready to send',
|
|
265
|
+
'computer.screenshot_not_ready': 'Video not ready yet, please wait',
|
|
266
|
+
'msg.copy': '📋 Copy',
|
|
267
|
+
'msg.create_doc': '📄 New Doc',
|
|
268
|
+
'msg.copied': 'Copied to clipboard',
|
|
269
|
+
'msg.doc_downloaded': 'Document downloaded',
|
|
270
|
+
'msg.no_content': 'No content to copy',
|
|
271
|
+
'crop.tip': 'Drag to select a screenshot area · Release to confirm · ESC to cancel',
|
|
272
|
+
'page.automations.sub': 'Save common goals as shortcuts, launch tasks with one click',
|
|
273
|
+
'btn.new_automation': '+ New Command',
|
|
274
|
+
'empty.no_automations': 'No shortcuts yet',
|
|
275
|
+
'page.templates.sub': 'Click a template to fill the input box',
|
|
276
|
+
'btn.save_template': '+ Save as Template',
|
|
277
|
+
'tpl.builtin': 'Built-in Templates',
|
|
278
|
+
'tpl.my': 'My Templates',
|
|
279
|
+
'empty.no_templates': 'No custom templates',
|
|
280
|
+
'page.market.sub': 'Aggregating ClawHub & Agent-Foundry skill ecosystems',
|
|
281
|
+
'market.search_placeholder': 'Search…',
|
|
282
|
+
'market.source_all': 'All Sources',
|
|
283
|
+
'btn.refresh': 'Refresh',
|
|
284
|
+
'market.installed': 'Installed',
|
|
285
|
+
'empty.no_installed': 'No installed skills',
|
|
286
|
+
'page.office.sub': 'Document processing shortcuts, generate prompts instantly',
|
|
287
|
+
'page.memory.sub': 'Daily auto-summary of short-term memory, accumulate long-term knowledge',
|
|
288
|
+
'btn.refresh_mem': '🔄 Refresh',
|
|
289
|
+
'btn.summarize': '✨ Summarize Now',
|
|
290
|
+
'mem.stat_short_days': 'Short-term Memory Days',
|
|
291
|
+
'mem.stat_long_items': 'Long-term Memory Items',
|
|
292
|
+
'mem.stat_last_summary': 'Last Summary',
|
|
293
|
+
'mem.stat_next_summary': 'Next Summary',
|
|
294
|
+
'mem.tab_short': '📅 Short-term',
|
|
295
|
+
'mem.tab_long': '💾 Long-term',
|
|
296
|
+
'mem.tab_history': '📜 History',
|
|
297
|
+
'btn.view': 'View',
|
|
298
|
+
'mem.select_date': 'Select date to view',
|
|
299
|
+
'empty.no_history': 'No summary records',
|
|
300
|
+
'footbar.title': 'Chat Task',
|
|
301
|
+
'footbar.desc': 'Submit natural language requirements, cloud silent execution and return results',
|
|
302
|
+
'footbar.model': 'Model',
|
|
303
|
+
'footbar.model_title': 'Switch model',
|
|
304
|
+
'footbar.model_settings': 'Model Settings',
|
|
305
|
+
'footbar.model_default': 'Default (System Route)',
|
|
306
|
+
'right.detail': '📦 Task Detail',
|
|
307
|
+
'right.preview': '👁 Preview',
|
|
308
|
+
'right.detail_hint': 'Select a task from the top to view details.',
|
|
309
|
+
'right.preview_title': 'Preview',
|
|
310
|
+
'right.preview_hint': 'Click files/codes in task or chat to preview.',
|
|
311
|
+
'modal.auto_title': 'New Automation',
|
|
312
|
+
'modal.auto_hint': 'Save for one-click task launch on Automations page.',
|
|
313
|
+
'modal.auto_name': 'Command Name',
|
|
314
|
+
'modal.auto_goal': 'Goal (Requirements for model)',
|
|
315
|
+
'modal.perm_title': '🔒 Permission Window',
|
|
316
|
+
'modal.perm_hint': 'Configure AI readable scope and operable permissions.',
|
|
317
|
+
'modal.perm_read_scope': 'Read Scope',
|
|
318
|
+
'modal.perm_workspace': 'Current Workspace Only',
|
|
319
|
+
'modal.perm_specified': 'Specified Directory',
|
|
320
|
+
'modal.perm_all': 'All Files',
|
|
321
|
+
'modal.perm_actions': 'Operation Permissions',
|
|
322
|
+
'modal.perm_read': 'Read Files',
|
|
323
|
+
'modal.perm_write': 'Write Files',
|
|
324
|
+
'modal.perm_shell': 'Execute Commands',
|
|
325
|
+
'modal.perm_network': 'Access Network',
|
|
326
|
+
'modal.perm_browser': 'Browser Control',
|
|
327
|
+
'modal.agent_title': '🤖 Agent Type',
|
|
328
|
+
'modal.agent_hint': 'Different agents auto-adjust prompts and execution strategies.',
|
|
329
|
+
'modal.folder_title': '📂 Select Workspace Folder',
|
|
330
|
+
'btn.select_folder': 'Select This Folder',
|
|
331
|
+
'modal.model_title': '🧠 Model Settings',
|
|
332
|
+
'modal.model_hint': 'Configure custom models, save multiple and switch anytime. Default model will be used by default.',
|
|
333
|
+
'modal.model_name': 'Model Name *',
|
|
334
|
+
'modal.model_api': 'API URL',
|
|
335
|
+
'modal.model_key': 'API Key',
|
|
336
|
+
'modal.model_reasoning': 'Reasoning Content (Optional)',
|
|
337
|
+
'btn.save_config': 'Save Config',
|
|
338
|
+
'btn.clear_form': 'Clear Form',
|
|
339
|
+
'btn.close': 'Close',
|
|
340
|
+
'modal.new_task_title': '+ New Chat Task',
|
|
341
|
+
'modal.new_task_hint': 'Create a brand new task. All messages in the chat will belong to this task (supports multi-turn conversation).',
|
|
342
|
+
'modal.new_task_label': 'Task Description',
|
|
343
|
+
'modal.new_task_agent': 'Agent Type',
|
|
344
|
+
'modal.new_task_workspace': 'Workspace',
|
|
345
|
+
'btn.create_task': 'Create Task',
|
|
346
|
+
'modal.settings_title': '⚙️ Settings',
|
|
347
|
+
'settings.general': 'General',
|
|
348
|
+
'settings.layout': 'Layout Config',
|
|
349
|
+
'settings.perm': 'Permissions',
|
|
350
|
+
'settings.perm_desc': 'Configure readable scope and operation permissions',
|
|
351
|
+
'settings.theme': 'Appearance',
|
|
352
|
+
'settings.theme_desc': 'Switch light/dark theme',
|
|
353
|
+
'settings.session': '💬 Session Management',
|
|
354
|
+
'menu.version': 'Trial Version',
|
|
355
|
+
'menu.current': 'Current',
|
|
356
|
+
'menu.buddy': 'Buddy Station',
|
|
357
|
+
'menu.invite': 'Invite',
|
|
358
|
+
'menu.invite_right': 'Get Premium',
|
|
359
|
+
'menu.points': 'Points Balance',
|
|
360
|
+
'menu.growth': 'Growth Plan',
|
|
361
|
+
'menu.growth_right': 'Streak Bonus',
|
|
362
|
+
'menu.help': 'Help & Feedback',
|
|
363
|
+
'menu.update': 'Check Update',
|
|
364
|
+
'menu.logout': 'Logout',
|
|
365
|
+
'btn.cancel': 'Cancel',
|
|
366
|
+
'btn.save': 'Save',
|
|
367
|
+
'sidebar.release': 'Release',
|
|
368
|
+
'modal.tpl_title_label': 'Template Title',
|
|
369
|
+
'modal.tpl_title_ph': 'e.g. SQL optimization',
|
|
370
|
+
'modal.tpl_category': 'Category',
|
|
371
|
+
'modal.tpl_category_ph': 'Custom',
|
|
372
|
+
'modal.tpl_goal': 'Goal Content',
|
|
373
|
+
'settings.session_auto_new': 'Auto new conversation before sending',
|
|
374
|
+
'settings.session_auto_open': 'Auto new conversation on page open',
|
|
375
|
+
'settings.session_clear': 'Clear history on new conversation',
|
|
376
|
+
'modal.auto_name_ph': 'e.g. Daily build check',
|
|
377
|
+
'modal.auto_goal_ph': 'Run npm test and summarize failures…',
|
|
378
|
+
'modal.model_name_ph': 'e.g. GPT-4o / Qwen / custom model',
|
|
379
|
+
'modal.model_api_ph': 'https://api.openai.com/v1',
|
|
380
|
+
'modal.model_key_ph': 'sk-...',
|
|
381
|
+
'modal.model_reasoning_ph': 'Configure what this model runs in intermediate reasoning steps, e.g. break down requirements first, then propose a solution, then output code.',
|
|
382
|
+
'modal.perm_path_ph': 'Enter allowed directory path',
|
|
383
|
+
// ---- select title hints (key = title.<element id>) ----
|
|
384
|
+
'title.modelSelect': 'Switch the active LLM',
|
|
385
|
+
// ---- input placeholders (key = ph.<element id>) ----
|
|
386
|
+
'ph.goalInput': 'Type an instruction. Messages stay in the current task; click "+ New Task" above to start a new one (create → reason → verify → close the loop).',
|
|
387
|
+
'ph.marketSearch': 'Search…',
|
|
388
|
+
'ph.autoName': 'e.g. Daily build check',
|
|
389
|
+
'ph.autoGoal': 'Run npm test and summarize failing cases…',
|
|
390
|
+
'ph.tplTitle': 'e.g. SQL Optimization',
|
|
391
|
+
'ph.tplCategory': 'Custom',
|
|
392
|
+
'ph.tplGoal': 'Paste the requirement text to save…',
|
|
393
|
+
'ph.permReadPath': 'Enter an allowed directory path',
|
|
394
|
+
'ph.modelName': 'e.g. GPT-4o / Qwen / Self-hosted',
|
|
395
|
+
'ph.modelApiBase': 'https://api.openai.com/v1',
|
|
396
|
+
'ph.modelApiKey': 'sk-...',
|
|
397
|
+
'ph.modelReasoning': 'Define what this model does during intermediate reasoning, e.g. break down requirements first, then propose a plan, then output code.',
|
|
398
|
+
'ph.ntGoal': 'e.g. Read package.json in the current workspace and tell me its name and version fields',
|
|
399
|
+
'ph.imName': 'Model name *',
|
|
400
|
+
'ph.imBase': 'API endpoint',
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
let currentLang = localStorage.getItem('fhcode.lang') || 'zh';
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 安全地把某个元素的 i18n 文案落地。
|
|
409
|
+
* 关键约束(曾导致「模型无法切换」「侧边栏图标消失」的线上问题):
|
|
410
|
+
* - <select> 绝对不能写 textContent,否则会把所有 <option> 一次性清空;
|
|
411
|
+
* - <input>/<textarea> 只改 placeholder,且必须命中专用键,避免误翻;
|
|
412
|
+
* - 含子元素的容器(图标 + 标签)只改 title / 直接文本节点,绝不整体覆盖。
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
function switchLang(lang) {
|
|
416
|
+
currentLang = lang;
|
|
417
|
+
localStorage.setItem('fhcode.lang', lang);
|
|
418
|
+
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
419
|
+
try { applyI18nToEl(el); } catch (e) { console.warn('[i18n] 应用失败', el, e); }
|
|
420
|
+
});
|
|
421
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
422
|
+
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
|
423
|
+
});
|
|
424
|
+
document.documentElement.lang = lang;
|
|
425
|
+
// 语言切换后重建由 JS 动态渲染的下拉/列表,保证选项文案与选中值不丢失
|
|
426
|
+
try { if (typeof renderModelSelect === 'function') renderModelSelect(); } catch {}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
430
|
+
btn.addEventListener('click', () => switchLang(btn.getAttribute('data-lang')));
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// 初始化语言
|
|
434
|
+
switchLang(currentLang);
|
|
435
|
+
|
|
436
|
+
const AGENT_TYPES = {
|
|
437
|
+
general: { label: '通用助手', icon: '🤖' },
|
|
438
|
+
'fix-code': { label: '修复代码', icon: '🐛' },
|
|
439
|
+
'write-code': { label: '编写代码', icon: '✍️' },
|
|
440
|
+
'exec-command': { label: '执行命令', icon: '⚡' },
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const state = {
|
|
444
|
+
token: '',
|
|
445
|
+
phone: '',
|
|
446
|
+
tasks: [],
|
|
447
|
+
currentTaskId: null,
|
|
448
|
+
submittedTaskIds: new Set(),
|
|
449
|
+
completedTaskIds: new Set(),
|
|
450
|
+
pinnedTasks: new Set(), // 置顶任务ID集合
|
|
451
|
+
market: [],
|
|
452
|
+
installed: new Set(),
|
|
453
|
+
agentType: 'general',
|
|
454
|
+
directMode: false,
|
|
455
|
+
permissions: {
|
|
456
|
+
readScope: 'workspace',
|
|
457
|
+
readPath: '',
|
|
458
|
+
allowRead: true,
|
|
459
|
+
allowWrite: false,
|
|
460
|
+
allowShell: false,
|
|
461
|
+
allowNetwork: true,
|
|
462
|
+
allowBrowser: false,
|
|
463
|
+
},
|
|
464
|
+
workspaceDir: '',
|
|
465
|
+
rightTab: 'detail',
|
|
466
|
+
models: [],
|
|
467
|
+
defaultModelId: null,
|
|
468
|
+
modelId: '',
|
|
469
|
+
// 待发送附件暂存区(截图/上传文件/上传图片,点发送时才上传并发给 AI)
|
|
470
|
+
stagedFiles: [],
|
|
471
|
+
// 会话管理配置
|
|
472
|
+
sessionConfig: {
|
|
473
|
+
autoResetOnNew: false, // 打开时自动新建对话
|
|
474
|
+
autoResetOnOpen: false, // 发送消息时自动重置
|
|
475
|
+
clearHistoryOnNew: true, // 新建时清空历史
|
|
476
|
+
},
|
|
477
|
+
procOpen: false, // 「执行过程」折叠面板是否展开(轮询刷新时保留)
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
(function initSession() {
|
|
481
|
+
// 提前检测登录状态,给 html 加 class,配合 CSS 避免登录页闪烁(感觉像打开两次)
|
|
482
|
+
try {
|
|
483
|
+
const urlToken = new URLSearchParams(location.search).get('token');
|
|
484
|
+
if (urlToken) localStorage.setItem('fhcode.token', urlToken);
|
|
485
|
+
const t = localStorage.getItem('fhcode.token');
|
|
486
|
+
const p = localStorage.getItem('fhcode.phone');
|
|
487
|
+
if (t && p) {
|
|
488
|
+
document.documentElement.classList.add('logged-in');
|
|
489
|
+
}
|
|
490
|
+
} catch(e) {}
|
|
491
|
+
const urlToken = new URLSearchParams(location.search).get('token');
|
|
492
|
+
if (urlToken) localStorage.setItem('fhcode.token', urlToken);
|
|
493
|
+
const savedModel = localStorage.getItem('fhcode.model');
|
|
494
|
+
if (savedModel !== null) state.modelId = savedModel;
|
|
495
|
+
state.token = localStorage.getItem('fhcode.token') || '';
|
|
496
|
+
state.phone = localStorage.getItem('fhcode.phone') || '';
|
|
497
|
+
try {
|
|
498
|
+
const p = JSON.parse(localStorage.getItem('fhcode.permissions') || '{}');
|
|
499
|
+
if (p) Object.assign(state.permissions, p);
|
|
500
|
+
} catch {}
|
|
501
|
+
// 恢复主题偏好(脚本位于 body 尾部,尽早生效避免闪烁)
|
|
502
|
+
if (localStorage.getItem('fhcode.theme') === 'dark') {
|
|
503
|
+
document.documentElement.classList.add('dark');
|
|
504
|
+
}
|
|
505
|
+
// 加载会话配置
|
|
506
|
+
try {
|
|
507
|
+
const sc = JSON.parse(localStorage.getItem('fhcode.sessionConfig') || '{}');
|
|
508
|
+
if (sc.autoResetOnNew != null) state.sessionConfig.autoResetOnNew = sc.autoResetOnNew;
|
|
509
|
+
if (sc.autoResetOnOpen != null) state.sessionConfig.autoResetOnOpen = sc.autoResetOnOpen;
|
|
510
|
+
if (sc.clearHistoryOnNew != null) state.sessionConfig.clearHistoryOnNew = sc.clearHistoryOnNew;
|
|
511
|
+
} catch {}
|
|
512
|
+
// autoResetOnOpen: 页面打开时自动新建对话(不自动选中旧任务,显示空白输入区)
|
|
513
|
+
if (state.sessionConfig.autoResetOnOpen && state.token) {
|
|
514
|
+
state.currentTaskId = null;
|
|
515
|
+
const box = document.getElementById('messages');
|
|
516
|
+
if (box) box.innerHTML = '<div class="msg sys">已按设置自动新建对话,输入指令即可发起新任务。</div>';
|
|
517
|
+
}
|
|
518
|
+
})();
|
|
519
|
+
|
|
520
|
+
// 消息气泡通用操作:复制 / 编辑 / 分享 / 创建文档
|
|
521
|
+
function bubbleActions(e, content) {
|
|
522
|
+
e.stopPropagation();
|
|
523
|
+
const menu = document.getElementById('bubbleMenu');
|
|
524
|
+
if (menu) menu.remove();
|
|
525
|
+
const m = document.createElement('div');
|
|
526
|
+
m.id = 'bubbleMenu';
|
|
527
|
+
m.style.cssText = 'position:fixed;z-index:10001;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.18);padding:4px 0;min-width:140px;font-size:13px;';
|
|
528
|
+
const items = [
|
|
529
|
+
{ label: '📋 复制', action: 'copy' },
|
|
530
|
+
{ label: '✏️ 编辑', action: 'edit' },
|
|
531
|
+
{ label: '🔗 分享链接', action: 'share' },
|
|
532
|
+
{ label: '📄 创建文档', action: 'doc' },
|
|
533
|
+
];
|
|
534
|
+
items.forEach((it) => {
|
|
535
|
+
const el = document.createElement('div');
|
|
536
|
+
el.className = 'dropdown-item';
|
|
537
|
+
el.textContent = it.label;
|
|
538
|
+
el.style.cssText = 'padding:7px 16px;cursor:pointer;';
|
|
539
|
+
el.addEventListener('mouseenter', () => { el.style.background = 'var(--brand-soft)'; });
|
|
540
|
+
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
541
|
+
el.addEventListener('click', () => {
|
|
542
|
+
m.remove();
|
|
543
|
+
handleBubbleAction(it.action, content);
|
|
544
|
+
});
|
|
545
|
+
m.appendChild(el);
|
|
546
|
+
});
|
|
547
|
+
document.body.appendChild(m);
|
|
548
|
+
const rect = e.target.getBoundingClientRect();
|
|
549
|
+
m.style.right = (window.innerWidth - rect.right + 4) + 'px';
|
|
550
|
+
m.style.top = rect.top + 'px';
|
|
551
|
+
}
|
|
552
|
+
async function handleBubbleAction(action, content) {
|
|
553
|
+
if (action === 'copy') {
|
|
554
|
+
try { await navigator.clipboard.writeText(content); toast('已复制到剪贴板'); }
|
|
555
|
+
catch { document.execCommand('copy'); toast('已复制'); }
|
|
556
|
+
} else if (action === 'edit') {
|
|
557
|
+
const edited = prompt('编辑内容:', content);
|
|
558
|
+
if (edited != null && edited.trim()) {
|
|
559
|
+
// 就近更新对应的消息气泡正文(最终回复块 / 对话气泡)
|
|
560
|
+
const box = document.getElementById('messages');
|
|
561
|
+
if (box) {
|
|
562
|
+
const candidates = box.querySelectorAll('.step.final > div:last-child, .msg.assistant, .msg.user');
|
|
563
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
564
|
+
if ((candidates[i].textContent || '').trim() === (content || '').trim()) {
|
|
565
|
+
candidates[i].textContent = edited;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
toast('已更新消息内容(本地预览)');
|
|
571
|
+
}
|
|
572
|
+
} else if (action === 'share') {
|
|
573
|
+
const payload = btoa(unescape(encodeURIComponent(content.slice(0, 2000))));
|
|
574
|
+
const url = location.origin + location.pathname + '#share/' + payload;
|
|
575
|
+
try { await navigator.clipboard.writeText(url); toast('分享链接已复制'); }
|
|
576
|
+
catch { prompt('复制以下链接分享给他人:', url); }
|
|
577
|
+
} else if (action === 'doc') {
|
|
578
|
+
const title = prompt('文档标题(默认: fhcode-回复):', 'fhcode-回复-' + Date.now());
|
|
579
|
+
if (!title) return;
|
|
580
|
+
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
|
581
|
+
const a = document.createElement('a');
|
|
582
|
+
a.href = URL.createObjectURL(blob);
|
|
583
|
+
a.download = title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_\-]/g, '_') + '.md';
|
|
584
|
+
a.click();
|
|
585
|
+
URL.revokeObjectURL(a.href);
|
|
586
|
+
toast('文档已下载');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 消息操作按钮事件委托(复制、创建文档)
|
|
591
|
+
document.getElementById('messages')?.addEventListener('click', (e) => {
|
|
592
|
+
const btn = e.target.closest('.msg-action-btn');
|
|
593
|
+
if (!btn) return;
|
|
594
|
+
e.stopPropagation();
|
|
595
|
+
const action = btn.getAttribute('data-action');
|
|
596
|
+
const msgEl = btn.closest('.msg');
|
|
597
|
+
if (!msgEl) return;
|
|
598
|
+
// 获取消息文本内容(排除操作按钮)
|
|
599
|
+
const actionsEl = msgEl.querySelector('.msg-actions');
|
|
600
|
+
let content = '';
|
|
601
|
+
if (actionsEl) {
|
|
602
|
+
// 临时移除操作按钮,获取文本,再放回去
|
|
603
|
+
const parent = actionsEl.parentNode;
|
|
604
|
+
const nextSibling = actionsEl.nextSibling;
|
|
605
|
+
parent.removeChild(actionsEl);
|
|
606
|
+
content = msgEl.textContent || '';
|
|
607
|
+
if (nextSibling) parent.insertBefore(actionsEl, nextSibling);
|
|
608
|
+
else parent.appendChild(actionsEl);
|
|
609
|
+
} else {
|
|
610
|
+
content = msgEl.textContent || '';
|
|
611
|
+
}
|
|
612
|
+
content = content.trim();
|
|
613
|
+
if (!content) {
|
|
614
|
+
toast('没有可复制的内容');
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (action === 'copy') {
|
|
618
|
+
handleBubbleAction('copy', content);
|
|
619
|
+
} else if (action === 'create-doc') {
|
|
620
|
+
handleBubbleAction('doc', content);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
/* ========== 登录 ========== */
|
|
625
|
+
document.getElementById('loginBtn').addEventListener('click', doLogin);
|
|
626
|
+
document.getElementById('loginPhone').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
|
|
627
|
+
|
|
628
|
+
async function afterLogin() {
|
|
629
|
+
updateUserBar();
|
|
630
|
+
// 检查是否是首次登录,如果是则显示欢迎引导
|
|
631
|
+
const isFirstLogin = await checkFirstLogin();
|
|
632
|
+
if (isFirstLogin) {
|
|
633
|
+
// 从 localStorage 恢复 welcomeTasks(登录时已创建)
|
|
634
|
+
const savedWelcomeTasks = localStorage.getItem('fhcode.welcomeTasks');
|
|
635
|
+
if (savedWelcomeTasks) {
|
|
636
|
+
try {
|
|
637
|
+
const tasks = JSON.parse(savedWelcomeTasks);
|
|
638
|
+
if (tasks.length > 0) {
|
|
639
|
+
showWelcomeGuide(tasks);
|
|
640
|
+
return; // 引导流程已启动,跳过常规加载
|
|
641
|
+
}
|
|
642
|
+
} catch {}
|
|
643
|
+
}
|
|
644
|
+
showWelcomeGuide([]);
|
|
645
|
+
}
|
|
646
|
+
await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats(), loadNodes(), loadSources()]);
|
|
647
|
+
startRefresh();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function checkFirstLogin() {
|
|
651
|
+
if (!state.token) return false;
|
|
652
|
+
try {
|
|
653
|
+
const d = await api('/api/auth/me');
|
|
654
|
+
if (d.isFirstLogin === true) {
|
|
655
|
+
// 保存首次登录标记,供后续使用
|
|
656
|
+
localStorage.setItem('fhcode.firstLogin', 'true');
|
|
657
|
+
}
|
|
658
|
+
return d.isFirstLogin === true;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
async function runWelcomeTask(taskId) {
|
|
667
|
+
try {
|
|
668
|
+
// 加载任务详情
|
|
669
|
+
const d = await api('/api/tasks/' + taskId);
|
|
670
|
+
state.currentTaskId = d.task.id;
|
|
671
|
+
|
|
672
|
+
// 关闭引导弹窗
|
|
673
|
+
const modal = document.getElementById('welcomeGuideModal');
|
|
674
|
+
if (modal) modal.classList.remove('show');
|
|
675
|
+
|
|
676
|
+
// 显示任务列表
|
|
677
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
678
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
679
|
+
|
|
680
|
+
// 加载并刷新任务
|
|
681
|
+
await loadTasks();
|
|
682
|
+
selectTask(taskId);
|
|
683
|
+
toast('已启动引导任务:' + (d.task.goal.slice(0, 20) + '…'));
|
|
684
|
+
} catch (e) {
|
|
685
|
+
toast('启动任务失败:' + e.message);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 运行所有引导任务
|
|
690
|
+
async function runAllWelcomeTasks() {
|
|
691
|
+
const tasks = JSON.parse(localStorage.getItem('fhcode.welcomeTasks') || '[]');
|
|
692
|
+
if (tasks.length === 0) {
|
|
693
|
+
toast('暂无引导任务');
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// 关闭弹窗
|
|
697
|
+
const modal = document.getElementById('welcomeGuideModal');
|
|
698
|
+
if (modal) modal.classList.remove('show');
|
|
699
|
+
// 显示任务列表
|
|
700
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
701
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
702
|
+
// 运行第一个任务
|
|
703
|
+
await runWelcomeTask(tasks[0].taskId);
|
|
704
|
+
toast('已开始执行引导任务,共 ' + tasks.length + ' 个');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 绑定"全部运行"按钮
|
|
708
|
+
document.getElementById('welcomeRunAllBtn')?.addEventListener('click', runAllWelcomeTasks);
|
|
709
|
+
|
|
710
|
+
// 在登录后保存 welcomeTasks 到 localStorage
|
|
711
|
+
async function saveWelcomeTasks(tasks) {
|
|
712
|
+
if (tasks && tasks.length > 0) {
|
|
713
|
+
localStorage.setItem('fhcode.welcomeTasks', JSON.stringify(tasks));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
/* ========== 导航切换 ========== */
|
|
719
|
+
|
|
720
|
+
document.querySelectorAll('.topbar-nav-item').forEach((el) => {
|
|
721
|
+
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
722
|
+
});
|
|
723
|
+
document.querySelectorAll('.sidebar-icon[data-nav]').forEach((el) => {
|
|
724
|
+
el.addEventListener('click', () => switchView(el.getAttribute('data-nav')));
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
/* ========== 用户菜单 ========== */
|
|
728
|
+
const userMenuMask = document.getElementById('userMenuMask');
|
|
729
|
+
document.getElementById('topbarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
730
|
+
document.getElementById('sidebarUser').addEventListener('click', () => userMenuMask.classList.add('show'));
|
|
731
|
+
userMenuMask.addEventListener('click', (e) => { if (e.target === userMenuMask) userMenuMask.classList.remove('show'); });
|
|
732
|
+
document.querySelectorAll('#userMenu .sheet-item').forEach((el) => {
|
|
733
|
+
el.addEventListener('click', () => {
|
|
734
|
+
const action = el.getAttribute('data-action');
|
|
735
|
+
userMenuMask.classList.remove('show');
|
|
736
|
+
switch (action) {
|
|
737
|
+
case 'settings': openModal('settingsModal'); break;
|
|
738
|
+
case 'models': openModels(); break;
|
|
739
|
+
case 'theme': toggleTheme(); break;
|
|
740
|
+
case 'help': openBrowser('https://github.com/wch887292/feihong-code/issues'); break;
|
|
741
|
+
case 'buddy': openBrowser('https://www.klai.top'); break;
|
|
742
|
+
case 'invite': toast('邀请链接已复制(演示)'); break;
|
|
743
|
+
case 'points': toast('积分余额:0(演示)'); break;
|
|
744
|
+
case 'growth': toast('连续登录 1 天(演示)'); break;
|
|
745
|
+
case 'version': toast('当前为体验版'); break;
|
|
746
|
+
case 'update': checkUpdate(); break;
|
|
747
|
+
case 'logout':
|
|
748
|
+
localStorage.removeItem('fhcode.token');
|
|
749
|
+
localStorage.removeItem('fhcode.phone');
|
|
750
|
+
location.reload();
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
document.getElementById('sidebarSettings').addEventListener('click', () => {
|
|
757
|
+
userMenuMask.classList.remove('show');
|
|
758
|
+
openModal('settingsModal');
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
function openBrowser(url) {
|
|
762
|
+
api('/api/open/browser', 'POST', { url }).then(() => toast('已在系统浏览器打开')).catch((e) => toast('打开失败:' + e.message));
|
|
763
|
+
}
|
|
764
|
+
async function checkUpdate() {
|
|
765
|
+
try {
|
|
766
|
+
const d = await api('/api/health');
|
|
767
|
+
toast('当前版本 ' + d.version + ',已是最新(演示)');
|
|
768
|
+
} catch (e) { toast('检查失败:' + e.message); }
|
|
769
|
+
}
|
|
770
|
+
function toggleTheme() {
|
|
771
|
+
const dark = document.documentElement.classList.toggle('dark');
|
|
772
|
+
localStorage.setItem('fhcode.theme', dark ? 'dark' : 'light');
|
|
773
|
+
toast(dark ? '已切换为深色主题' : '已切换为浅色主题');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* ========== 任务列表与详情 ========== */
|
|
777
|
+
async function selectTask(id) {
|
|
778
|
+
state.currentTaskId = id;
|
|
779
|
+
renderSidebarTaskList();
|
|
780
|
+
renderTaskDetail(id);
|
|
781
|
+
await refreshCurrentThread();
|
|
782
|
+
switchRightTab('detail');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 切换置顶状态
|
|
786
|
+
function togglePinTask(taskId) {
|
|
787
|
+
if (state.pinnedTasks.has(taskId)) {
|
|
788
|
+
state.pinnedTasks.delete(taskId);
|
|
789
|
+
toast('已取消置顶');
|
|
790
|
+
} else {
|
|
791
|
+
state.pinnedTasks.add(taskId);
|
|
792
|
+
toast('已置顶');
|
|
793
|
+
}
|
|
794
|
+
renderSidebarTaskList();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// 删除任务
|
|
798
|
+
async function deleteTask(taskId) {
|
|
799
|
+
if (!confirm('确定要删除这个任务吗?')) return;
|
|
800
|
+
try {
|
|
801
|
+
await api('/api/tasks/' + taskId, 'DELETE');
|
|
802
|
+
toast('任务已删除');
|
|
803
|
+
state.tasks = state.tasks.filter((t) => t.id !== taskId);
|
|
804
|
+
// 若删除的是当前任务,清空选中并回退到最新任务或空白态
|
|
805
|
+
if (state.currentTaskId === taskId) {
|
|
806
|
+
state.currentTaskId = null;
|
|
807
|
+
if (state.tasks.length) {
|
|
808
|
+
state.currentTaskId = state.tasks[0].id;
|
|
809
|
+
renderTaskDetail(state.currentTaskId);
|
|
810
|
+
await refreshCurrentThread();
|
|
811
|
+
} else {
|
|
812
|
+
renderTaskThread(null);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
renderSidebarTaskList();
|
|
816
|
+
} catch (e) {
|
|
817
|
+
toast('删除失败:' + e.message);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function handleDetailOp(task, op, path) {
|
|
821
|
+
if (op === 'folder') {
|
|
822
|
+
try { await api('/api/open/folder', 'POST', { path: path || task.workspaceDir }); toast('已打开资源管理器'); }
|
|
823
|
+
catch (e) { toast('打开失败:' + e.message); }
|
|
824
|
+
} else if (op === 'browser') {
|
|
825
|
+
const url = extractUrl(task.result?.finalAnswer) || 'https://www.klai.top';
|
|
826
|
+
openBrowser(url);
|
|
827
|
+
} else if (op === 'artifacts') {
|
|
828
|
+
await showArtifacts(path || task.workspaceDir);
|
|
829
|
+
} else if (op === 'preview' || op === 'code') {
|
|
830
|
+
const text = task.result?.finalAnswer || '';
|
|
831
|
+
showPreview('任务产物', linkifyArtifacts(text), 'text');
|
|
832
|
+
switchRightTab('preview');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/* ========== 任务思维链路渲染 ========== */
|
|
836
|
+
/** 切换思考过程折叠/展开 */
|
|
837
|
+
/** 切换工具调用参数详情显示/隐藏 */
|
|
838
|
+
|
|
839
|
+
/** 群组渲染助手消息为 Cursor 风格的思考过程 + 回复 */
|
|
840
|
+
|
|
841
|
+
// 拉取当前选中任务的完整详情(含思维链路步骤)并渲染
|
|
842
|
+
async function refreshCurrentThread() {
|
|
843
|
+
if (!state.currentTaskId) { renderTaskThread(null); return; }
|
|
844
|
+
try {
|
|
845
|
+
const d = await api('/api/tasks/' + state.currentTaskId);
|
|
846
|
+
const t = d.task;
|
|
847
|
+
const idx = state.tasks.findIndex((x) => x.id === t.id);
|
|
848
|
+
if (idx >= 0) state.tasks[idx] = t; else state.tasks.push(t);
|
|
849
|
+
renderTaskThread(t);
|
|
850
|
+
} catch (e) { console.warn('刷新当前任务线程失败', e); renderTaskThread(null); }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
// 将单个任务渲染为对话框:对话流(用户↔助手干净气泡)+ 内部执行过程(默认折叠)
|
|
855
|
+
// 单条执行步骤渲染(思维链路、工具调用、验证结果、自愈/压缩等)
|
|
856
|
+
/* 轻量 Markdown 渲染(用于模型推理内容) */
|
|
857
|
+
/* 工具参数格式化为 key: value 列表 */
|
|
858
|
+
|
|
859
|
+
/* ========== 右侧 Tab ========== */
|
|
860
|
+
document.querySelectorAll('.right-tab').forEach((tab) => {
|
|
861
|
+
tab.addEventListener('click', () => switchRightTab(tab.getAttribute('data-tab')));
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
/* ========== 对话与发送 ========== */
|
|
865
|
+
async function sendTask() {
|
|
866
|
+
const input = document.getElementById('goalInput');
|
|
867
|
+
let goal = input.value.trim();
|
|
868
|
+
// 允许只发送文件(无文本)
|
|
869
|
+
if (!goal && !state.stagedFiles.length) return;
|
|
870
|
+
if (!state.token) { toast('请先登录'); return; }
|
|
871
|
+
if (state.directMode) goal = goal ? ('[电脑操作] ' + goal) : '[电脑操作](仅附件)';
|
|
872
|
+
const btn = document.getElementById('sendBtn');
|
|
873
|
+
btn.disabled = true;
|
|
874
|
+
try {
|
|
875
|
+
// 上传暂存的附件(仅在点发送时才上传)
|
|
876
|
+
const attachments = await uploadStagedFiles();
|
|
877
|
+
|
|
878
|
+
// autoResetOnNew: 发送消息前自动新建对话,旧任务保留在历史
|
|
879
|
+
if (state.sessionConfig.autoResetOnNew && state.currentTaskId) {
|
|
880
|
+
toast('自动新建对话,旧任务保留在历史');
|
|
881
|
+
state.currentTaskId = null;
|
|
882
|
+
}
|
|
883
|
+
// 已有当前任务 → 多轮续接:消息归属同一任务生命周期
|
|
884
|
+
if (state.currentTaskId) {
|
|
885
|
+
const cur = state.tasks.find((t) => t.id === state.currentTaskId);
|
|
886
|
+
if (cur && (cur.status === 'queued' || cur.status === 'running')) {
|
|
887
|
+
toast('当前任务执行中,请等待完成后再继续对话');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const d = await api('/api/tasks/' + state.currentTaskId + '/messages', 'POST', {
|
|
891
|
+
message: goal || '(仅附件)',
|
|
892
|
+
attachments,
|
|
893
|
+
});
|
|
894
|
+
input.value = '';
|
|
895
|
+
clearStagedFiles();
|
|
896
|
+
renderSidebarTaskList();
|
|
897
|
+
renderTaskDetail(d.task.id);
|
|
898
|
+
renderTaskThread(d.task);
|
|
899
|
+
await loadTasks();
|
|
900
|
+
toast(attachments.length ? `已发送(含 ${attachments.length} 个附件),继续当前任务对话` : '已发送,继续当前任务对话');
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const d = await api('/api/tasks', 'POST', {
|
|
904
|
+
goal: goal || '(仅附件)',
|
|
905
|
+
agentType: state.agentType,
|
|
906
|
+
permissions: state.permissions,
|
|
907
|
+
workspaceDir: state.workspaceDir || undefined,
|
|
908
|
+
modelId: state.modelId || undefined,
|
|
909
|
+
attachments: attachments.length ? attachments : undefined,
|
|
910
|
+
});
|
|
911
|
+
input.value = '';
|
|
912
|
+
clearStagedFiles();
|
|
913
|
+
// 新任务即成为当前任务,中部围绕其单一生命周期可视化追踪
|
|
914
|
+
state.currentTaskId = d.task.id;
|
|
915
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
916
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
917
|
+
renderSidebarTaskList();
|
|
918
|
+
renderTaskDetail(d.task.id);
|
|
919
|
+
renderTaskThread({ id: d.task.id, goal: goal || '(仅附件)', status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal || '(仅附件)' }] });
|
|
920
|
+
await loadTasks();
|
|
921
|
+
toast(attachments.length ? `任务已提交(含 ${attachments.length} 个附件),开始追踪思维链路` : '任务已提交,开始追踪思维链路');
|
|
922
|
+
} catch (e) { toast('提交失败:' + e.message); }
|
|
923
|
+
finally { btn.disabled = false; }
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* ========== 输入区·附件暂存(点发送时统一上传) ========== */
|
|
927
|
+
function stageFile(file) {
|
|
928
|
+
const id = 'sf_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
|
929
|
+
state.stagedFiles.push({ id, name: file.name, mime: file.mime, dataUrl: file.dataUrl, size: estimateDataUrlSize(file.dataUrl) });
|
|
930
|
+
renderStagedFiles();
|
|
931
|
+
}
|
|
932
|
+
function removeStagedFile(id) {
|
|
933
|
+
state.stagedFiles = state.stagedFiles.filter((f) => f.id !== id);
|
|
934
|
+
renderStagedFiles();
|
|
935
|
+
}
|
|
936
|
+
function clearStagedFiles() {
|
|
937
|
+
state.stagedFiles = [];
|
|
938
|
+
renderStagedFiles();
|
|
939
|
+
}
|
|
940
|
+
async function uploadStagedFiles() {
|
|
941
|
+
if (!state.stagedFiles.length) return [];
|
|
942
|
+
const paths = [];
|
|
943
|
+
for (const f of state.stagedFiles) {
|
|
944
|
+
try {
|
|
945
|
+
const base64 = (f.dataUrl || '').split(',')[1] || '';
|
|
946
|
+
const d = await api('/api/upload', 'POST', { name: f.name, mime: f.mime, dataBase64: base64 });
|
|
947
|
+
paths.push(d.path);
|
|
948
|
+
} catch (e) { toast('上传失败:' + f.name + '(' + e.message + ')'); }
|
|
949
|
+
}
|
|
950
|
+
return paths;
|
|
951
|
+
}
|
|
952
|
+
function appendMsg(role, text, extras = {}) {
|
|
953
|
+
const box = document.getElementById('messages');
|
|
954
|
+
const el = document.createElement('div');
|
|
955
|
+
el.className = 'msg ' + role;
|
|
956
|
+
if (extras.html) el.innerHTML = extras.html;
|
|
957
|
+
else if (extras.file) {
|
|
958
|
+
el.innerHTML = '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
|
|
959
|
+
} else {
|
|
960
|
+
el.innerHTML = linkifyArtifacts(text);
|
|
961
|
+
}
|
|
962
|
+
box.appendChild(el);
|
|
963
|
+
box.scrollTop = box.scrollHeight;
|
|
964
|
+
bindArtifacts(el);
|
|
965
|
+
}
|
|
966
|
+
document.getElementById('sendBtn').addEventListener('click', sendTask);
|
|
967
|
+
document.getElementById('goalInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendTask(); });
|
|
968
|
+
// 粘贴图片:用户用 Win+Shift+S 截图后可直接粘贴到输入框
|
|
969
|
+
document.getElementById('goalInput').addEventListener('paste', (e) => {
|
|
970
|
+
const items = e.clipboardData?.items;
|
|
971
|
+
if (!items) return;
|
|
972
|
+
for (const item of items) {
|
|
973
|
+
if (item.type.startsWith('image/')) {
|
|
974
|
+
e.preventDefault();
|
|
975
|
+
const file = item.getAsFile();
|
|
976
|
+
if (!file) continue;
|
|
977
|
+
const reader = new FileReader();
|
|
978
|
+
reader.onload = () => {
|
|
979
|
+
stageFile({ name: 'paste-' + Date.now() + '.png', mime: file.type || 'image/png', dataUrl: String(reader.result) });
|
|
980
|
+
toast('图片已粘贴到输入框,可直接输入文字发送');
|
|
981
|
+
};
|
|
982
|
+
reader.readAsDataURL(file);
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
document.getElementById('tchRefresh').addEventListener('click', () => { refreshCurrentThread(); toast('已刷新当前任务'); });
|
|
988
|
+
document.getElementById('tchStop').addEventListener('click', async () => {
|
|
989
|
+
if (!state.currentTaskId) return;
|
|
990
|
+
try {
|
|
991
|
+
await api('/api/tasks/' + state.currentTaskId + '/stop', 'POST');
|
|
992
|
+
toast('已发送停止指令,任务即将终止');
|
|
993
|
+
await refreshCurrentThread();
|
|
994
|
+
} catch (e) { toast('停止失败:' + e.message); }
|
|
995
|
+
});
|
|
996
|
+
document.getElementById('tchNewTask').addEventListener('click', () => { try { openNewTaskModal(); } catch { toast('新建任务面板不可用'); } });
|
|
997
|
+
document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
|
|
998
|
+
document.getElementById('voiceBtn')?.addEventListener('click', startVoice);
|
|
999
|
+
|
|
1000
|
+
/* ========== 输入框右键菜单(复制/剪切/粘贴/全选) ========== */
|
|
1001
|
+
const goalInput = document.getElementById('goalInput');
|
|
1002
|
+
goalInput.addEventListener('contextmenu', (e) => {
|
|
1003
|
+
e.preventDefault();
|
|
1004
|
+
// 移除旧菜单
|
|
1005
|
+
const old = document.getElementById('textCtxMenu');
|
|
1006
|
+
if (old) old.remove();
|
|
1007
|
+
// 记录右键时的选中区间(菜单点击会令文本框失焦,故需先记住)
|
|
1008
|
+
const sel = { start: goalInput.selectionStart, end: goalInput.selectionEnd };
|
|
1009
|
+
const hasSel = sel.end > sel.start;
|
|
1010
|
+
// 创建右键菜单
|
|
1011
|
+
const menu = document.createElement('div');
|
|
1012
|
+
menu.id = 'textCtxMenu';
|
|
1013
|
+
menu.style.cssText = 'position:fixed;z-index:10000;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);padding:4px 0;min-width:160px;';
|
|
1014
|
+
const items = [
|
|
1015
|
+
{ label: '📋 复制', action: 'copy', disabled: !hasSel },
|
|
1016
|
+
{ label: '✂️ 剪切', action: 'cut', disabled: !hasSel },
|
|
1017
|
+
{ label: '📌 粘贴', action: 'paste' },
|
|
1018
|
+
{ label: '⬛ 全选', action: 'selectall' },
|
|
1019
|
+
];
|
|
1020
|
+
// 恢复文本框聚焦与选中,供 execCommand 兜底使用
|
|
1021
|
+
const restoreSel = () => {
|
|
1022
|
+
goalInput.focus();
|
|
1023
|
+
goalInput.setSelectionRange(sel.start, sel.end);
|
|
1024
|
+
};
|
|
1025
|
+
items.forEach((it) => {
|
|
1026
|
+
const el = document.createElement('div');
|
|
1027
|
+
el.className = 'dropdown-item';
|
|
1028
|
+
el.textContent = it.label;
|
|
1029
|
+
el.style.cssText = 'padding:6px 16px;cursor:' + (it.disabled ? 'default' : 'pointer') + ';font-size:13px;opacity:' + (it.disabled ? 0.4 : 1) + ';';
|
|
1030
|
+
el.addEventListener('mouseenter', () => { if (!it.disabled) el.style.background = 'var(--brand-soft)'; });
|
|
1031
|
+
el.addEventListener('mouseleave', () => { el.style.background = ''; });
|
|
1032
|
+
el.addEventListener('click', async () => {
|
|
1033
|
+
menu.remove();
|
|
1034
|
+
if (it.disabled) return;
|
|
1035
|
+
if (it.action === 'copy') {
|
|
1036
|
+
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
1037
|
+
try { await navigator.clipboard.writeText(selText); toast('已复制'); }
|
|
1038
|
+
catch { restoreSel(); document.execCommand('copy'); toast('已复制'); }
|
|
1039
|
+
} else if (it.action === 'cut') {
|
|
1040
|
+
const selText = goalInput.value.slice(sel.start, sel.end);
|
|
1041
|
+
try { await navigator.clipboard.writeText(selText); } catch {}
|
|
1042
|
+
goalInput.setRangeText('', sel.start, sel.end, 'preserve');
|
|
1043
|
+
goalInput.focus();
|
|
1044
|
+
goalInput.setSelectionRange(sel.start, sel.start);
|
|
1045
|
+
if (selText) toast('已剪切');
|
|
1046
|
+
} else if (it.action === 'paste') {
|
|
1047
|
+
let text = '';
|
|
1048
|
+
try { text = await navigator.clipboard.readText().catch(() => ''); } catch {}
|
|
1049
|
+
if (text) {
|
|
1050
|
+
goalInput.setRangeText(text, sel.start, sel.end, 'end');
|
|
1051
|
+
goalInput.focus();
|
|
1052
|
+
} else {
|
|
1053
|
+
// 读取失败则回退到系统粘贴(依赖当前选中)
|
|
1054
|
+
restoreSel();
|
|
1055
|
+
document.execCommand('paste');
|
|
1056
|
+
}
|
|
1057
|
+
} else if (it.action === 'selectall') {
|
|
1058
|
+
goalInput.focus();
|
|
1059
|
+
goalInput.select();
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
menu.appendChild(el);
|
|
1063
|
+
});
|
|
1064
|
+
document.body.appendChild(menu);
|
|
1065
|
+
menu.style.left = e.clientX + 'px';
|
|
1066
|
+
menu.style.top = e.clientY + 'px';
|
|
1067
|
+
});
|
|
1068
|
+
// 点击其他地方或失焦关闭右键菜单
|
|
1069
|
+
document.addEventListener('click', (e) => {
|
|
1070
|
+
const menu = document.getElementById('textCtxMenu');
|
|
1071
|
+
if (menu && !menu.contains(e.target)) menu.remove();
|
|
1072
|
+
});
|
|
1073
|
+
document.addEventListener('blur', () => {
|
|
1074
|
+
const menu = document.getElementById('textCtxMenu');
|
|
1075
|
+
if (menu) menu.remove();
|
|
1076
|
+
});
|
|
1077
|
+
// 支持在输入框内直接粘贴图片(与「上传图片/截图」共用同一暂存区,发送时一起上传)
|
|
1078
|
+
goalInput.addEventListener('paste', (e) => {
|
|
1079
|
+
if (!e.clipboardData) return;
|
|
1080
|
+
const items = e.clipboardData.items;
|
|
1081
|
+
const images = [];
|
|
1082
|
+
for (let i = 0; i < items.length; i++) {
|
|
1083
|
+
const item = items[i];
|
|
1084
|
+
if (item.kind === 'file' && item.type && item.type.startsWith('image/')) {
|
|
1085
|
+
const f = item.getAsFile();
|
|
1086
|
+
if (f) images.push(f);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!images.length) return; // 非图片粘贴(文本等)交给浏览器默认行为
|
|
1090
|
+
e.preventDefault();
|
|
1091
|
+
images.forEach((file, idx) => {
|
|
1092
|
+
const reader = new FileReader();
|
|
1093
|
+
reader.onload = () => {
|
|
1094
|
+
const name = file.name || ('paste-image-' + (idx + 1) + '.png');
|
|
1095
|
+
stageFile({ name, mime: file.type || 'image/png', dataUrl: String(reader.result) });
|
|
1096
|
+
toast('已暂存图片:' + name + '(点发送时一起发给 AI)');
|
|
1097
|
+
};
|
|
1098
|
+
reader.readAsDataURL(file);
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
/* ========== 新建对话任务 ========== */
|
|
1103
|
+
async function openNewTaskModal() {
|
|
1104
|
+
// 智能体类型下拉
|
|
1105
|
+
const agentSel = document.getElementById('ntAgentType');
|
|
1106
|
+
agentSel.innerHTML = Object.entries(AGENT_TYPES)
|
|
1107
|
+
.map(([k, v]) => '<option value="' + k + '"' + (state.agentType === k ? ' selected' : '') + '>' + v.icon + ' ' + v.label + '</option>')
|
|
1108
|
+
.join('');
|
|
1109
|
+
// 工作区下拉:默认(沿用当前底部所选)+ 磁盘根目录
|
|
1110
|
+
const wsSel = document.getElementById('ntWorkspaceDir');
|
|
1111
|
+
const cur = state.workspaceDir || '';
|
|
1112
|
+
wsSel.innerHTML = '<option value="">默认(' + (cur || '服务器工作区') + ')</option>';
|
|
1113
|
+
try {
|
|
1114
|
+
const d = await api('/api/drives');
|
|
1115
|
+
(d.drives || []).forEach((dr) => {
|
|
1116
|
+
if (dr !== cur) wsSel.innerHTML += '<option value="' + escapeHtml(dr) + '">' + escapeHtml(dr) + '</option>';
|
|
1117
|
+
});
|
|
1118
|
+
} catch {}
|
|
1119
|
+
document.getElementById('ntGoal').value = '';
|
|
1120
|
+
openModal('newTaskModal');
|
|
1121
|
+
setTimeout(() => document.getElementById('ntGoal').focus(), 60);
|
|
1122
|
+
}
|
|
1123
|
+
async function createNewTask() {
|
|
1124
|
+
const goal = document.getElementById('ntGoal').value.trim();
|
|
1125
|
+
if (!goal) { toast('请填写任务描述'); return; }
|
|
1126
|
+
const btn = document.getElementById('ntCreateBtn');
|
|
1127
|
+
btn.disabled = true;
|
|
1128
|
+
try {
|
|
1129
|
+
const wsVal = document.getElementById('ntWorkspaceDir').value;
|
|
1130
|
+
const payload = {
|
|
1131
|
+
goal,
|
|
1132
|
+
agentType: document.getElementById('ntAgentType').value || state.agentType,
|
|
1133
|
+
permissions: state.permissions,
|
|
1134
|
+
modelId: state.modelId || undefined,
|
|
1135
|
+
};
|
|
1136
|
+
// 工作区:仅在明确选择了目录时覆盖
|
|
1137
|
+
const d = await api('/api/tasks', 'POST', wsVal ? Object.assign(payload, { workspaceDir: wsVal }) : payload);
|
|
1138
|
+
document.getElementById('goalInput').value = '';
|
|
1139
|
+
state.currentTaskId = d.task.id;
|
|
1140
|
+
const taskListSection = document.getElementById('taskListSection');
|
|
1141
|
+
if (taskListSection) taskListSection.style.display = 'block';
|
|
1142
|
+
renderSidebarTaskList();
|
|
1143
|
+
renderTaskDetail(d.task.id);
|
|
1144
|
+
renderTaskThread({ id: d.task.id, goal, status: 'queued', createdAt: new Date().toISOString(), steps: [], conversation: [{ role: 'user', content: goal }] });
|
|
1145
|
+
switchView('chat');
|
|
1146
|
+
closeModal('newTaskModal');
|
|
1147
|
+
await loadTasks();
|
|
1148
|
+
toast('新任务已创建,所有对话将归属该任务');
|
|
1149
|
+
} catch (e) { toast('创建失败:' + e.message); }
|
|
1150
|
+
finally { btn.disabled = false; }
|
|
1151
|
+
}
|
|
1152
|
+
document.getElementById('newTaskBtn')?.addEventListener('click', () => { switchView('chat'); startNewChat(); });
|
|
1153
|
+
document.getElementById('tchNewTask')?.addEventListener('click', startNewChat);
|
|
1154
|
+
document.getElementById('ntCreateBtn')?.addEventListener('click', createNewTask);
|
|
1155
|
+
document.getElementById('ntGoal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) createNewTask(); });
|
|
1156
|
+
|
|
1157
|
+
/* ========== 产物链接化 ========== */
|
|
1158
|
+
|
|
1159
|
+
/* ========== 中间菜单 ========== */
|
|
1160
|
+
const convMenuWrap = document.getElementById('convMenuWrap');
|
|
1161
|
+
const convMenu = document.getElementById('convMenu');
|
|
1162
|
+
document.getElementById('convMenuBtn').addEventListener('click', (e) => {
|
|
1163
|
+
e.stopPropagation();
|
|
1164
|
+
convMenu.classList.toggle('show');
|
|
1165
|
+
});
|
|
1166
|
+
document.addEventListener('click', (e) => { if (!convMenuWrap.contains(e.target)) convMenu.classList.remove('show'); });
|
|
1167
|
+
convMenu.querySelectorAll('.dropdown-item').forEach((el) => {
|
|
1168
|
+
el.addEventListener('click', () => {
|
|
1169
|
+
convMenu.classList.remove('show');
|
|
1170
|
+
const action = el.getAttribute('data-action');
|
|
1171
|
+
if (action === 'screenshot') takeScreenshot();
|
|
1172
|
+
else if (action === 'upload-file') document.getElementById('fileInput').click();
|
|
1173
|
+
else if (action === 'upload-image') document.getElementById('imageInput').click();
|
|
1174
|
+
else if (action === 'direct-mode') toggleDirectMode();
|
|
1175
|
+
else if (action === 'voice') startVoice();
|
|
1176
|
+
else if (action === 'agent-type') openAgentSelector();
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
// 截图锁,防止重复触发
|
|
1182
|
+
let screenshotRunning = false;
|
|
1183
|
+
|
|
1184
|
+
async function takeScreenshot() {
|
|
1185
|
+
if (screenshotRunning) {
|
|
1186
|
+
toast('正在截图中,请稍候...');
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
screenshotRunning = true;
|
|
1190
|
+
try {
|
|
1191
|
+
const overlay = document.getElementById('cropOverlay');
|
|
1192
|
+
const selection = document.getElementById('cropSelection');
|
|
1193
|
+
const video = document.getElementById('cropVideo');
|
|
1194
|
+
|
|
1195
|
+
let srcImage = null; // 用于裁剪的源图像(Image 对象)
|
|
1196
|
+
let stream = null; // 浏览器模式下的视频流
|
|
1197
|
+
|
|
1198
|
+
// Electron 环境:用主进程截图,更稳定
|
|
1199
|
+
if (window.isElectron && window.electronAPI && window.electronAPI.screenshot) {
|
|
1200
|
+
toast('正在截取屏幕...');
|
|
1201
|
+
const result = await window.electronAPI.screenshot.capture();
|
|
1202
|
+
if (!result || !result.success) {
|
|
1203
|
+
toast('截图失败:' + (result?.error || '未知错误'));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
// 加载截图为 Image 对象
|
|
1207
|
+
srcImage = new Image();
|
|
1208
|
+
srcImage.src = result.dataUrl;
|
|
1209
|
+
await new Promise((resolve, reject) => {
|
|
1210
|
+
srcImage.onload = resolve;
|
|
1211
|
+
srcImage.onerror = () => reject(new Error('截图加载失败'));
|
|
1212
|
+
});
|
|
1213
|
+
// 把截图显示到 video 元素的位置(用一个 img 覆盖)
|
|
1214
|
+
video.style.display = 'none';
|
|
1215
|
+
let bgImg = document.getElementById('cropBgImg');
|
|
1216
|
+
if (!bgImg) {
|
|
1217
|
+
bgImg = document.createElement('img');
|
|
1218
|
+
bgImg.id = 'cropBgImg';
|
|
1219
|
+
bgImg.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;object-fit:cover;z-index:9998;';
|
|
1220
|
+
overlay.appendChild(bgImg);
|
|
1221
|
+
}
|
|
1222
|
+
bgImg.src = result.dataUrl;
|
|
1223
|
+
bgImg.style.display = 'block';
|
|
1224
|
+
} else {
|
|
1225
|
+
// 浏览器模式:用 getDisplayMedia
|
|
1226
|
+
stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
1227
|
+
video.srcObject = stream;
|
|
1228
|
+
await video.play();
|
|
1229
|
+
video.style.display = 'block';
|
|
1230
|
+
const bgImg = document.getElementById('cropBgImg');
|
|
1231
|
+
if (bgImg) bgImg.style.display = 'none';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
overlay.style.display = 'block';
|
|
1235
|
+
selection.style.display = 'none';
|
|
1236
|
+
let startX = 0, startY = 0, drawing = false;
|
|
1237
|
+
|
|
1238
|
+
const onDown = (e) => {
|
|
1239
|
+
drawing = true;
|
|
1240
|
+
startX = e.clientX; startY = e.clientY;
|
|
1241
|
+
selection.style.display = 'block';
|
|
1242
|
+
selection.style.left = startX + 'px';
|
|
1243
|
+
selection.style.top = startY + 'px';
|
|
1244
|
+
selection.style.width = '0px';
|
|
1245
|
+
selection.style.height = '0px';
|
|
1246
|
+
};
|
|
1247
|
+
const onMove = (e) => {
|
|
1248
|
+
if (!drawing) return;
|
|
1249
|
+
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1250
|
+
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1251
|
+
selection.style.left = x + 'px';
|
|
1252
|
+
selection.style.top = y + 'px';
|
|
1253
|
+
selection.style.width = w + 'px';
|
|
1254
|
+
selection.style.height = h + 'px';
|
|
1255
|
+
};
|
|
1256
|
+
const finish = async (e) => {
|
|
1257
|
+
if (!drawing) return;
|
|
1258
|
+
drawing = false;
|
|
1259
|
+
window.removeEventListener('mousedown', onDown);
|
|
1260
|
+
window.removeEventListener('mousemove', onMove);
|
|
1261
|
+
window.removeEventListener('mouseup', finish);
|
|
1262
|
+
const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
|
|
1263
|
+
const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
|
|
1264
|
+
overlay.style.display = 'none';
|
|
1265
|
+
if (w < 8 || h < 8) {
|
|
1266
|
+
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
let srcW, srcH, source;
|
|
1271
|
+
if (srcImage) {
|
|
1272
|
+
// Electron 模式:从 Image 裁剪
|
|
1273
|
+
srcW = srcImage.naturalWidth;
|
|
1274
|
+
srcH = srcImage.naturalHeight;
|
|
1275
|
+
source = srcImage;
|
|
1276
|
+
} else {
|
|
1277
|
+
// 浏览器模式:从 video 裁剪
|
|
1278
|
+
srcW = video.videoWidth;
|
|
1279
|
+
srcH = video.videoHeight;
|
|
1280
|
+
source = video;
|
|
1281
|
+
}
|
|
1282
|
+
const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
|
|
1283
|
+
const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
|
|
1284
|
+
const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
|
|
1285
|
+
const canvas = document.createElement('canvas');
|
|
1286
|
+
canvas.width = sw; canvas.height = sh;
|
|
1287
|
+
canvas.getContext('2d').drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1288
|
+
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1289
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
1290
|
+
stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl });
|
|
1291
|
+
toast('截图已添加到输入框,可直接输入文字发送');
|
|
1292
|
+
} catch (err) { toast('截图失败:' + err.message); }
|
|
1293
|
+
screenshotRunning = false;
|
|
1294
|
+
};
|
|
1295
|
+
const onKey = (e) => {
|
|
1296
|
+
if (e.key === 'Escape') {
|
|
1297
|
+
overlay.style.display = 'none';
|
|
1298
|
+
if (stream) stream.getTracks().forEach((t) => t.stop());
|
|
1299
|
+
window.removeEventListener('mousedown', onDown);
|
|
1300
|
+
window.removeEventListener('mousemove', onMove);
|
|
1301
|
+
window.removeEventListener('mouseup', finish);
|
|
1302
|
+
window.removeEventListener('keydown', onKey);
|
|
1303
|
+
screenshotRunning = false;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
window.addEventListener('mousedown', onDown);
|
|
1307
|
+
window.addEventListener('mousemove', onMove);
|
|
1308
|
+
window.addEventListener('mouseup', finish);
|
|
1309
|
+
window.addEventListener('keydown', onKey);
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
if (e.name === 'NotAllowedError') {
|
|
1312
|
+
toast('已取消截图。也可以用 Win+Shift+S 截图后直接粘贴到输入框');
|
|
1313
|
+
} else {
|
|
1314
|
+
toast('截图失败:' + e.message);
|
|
1315
|
+
}
|
|
1316
|
+
screenshotRunning = false;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function handleUpload(input, mimePrefix) {
|
|
1321
|
+
const file = input.files[0];
|
|
1322
|
+
if (!file) return;
|
|
1323
|
+
const reader = new FileReader();
|
|
1324
|
+
reader.onload = () => {
|
|
1325
|
+
stageFile({ name: file.name, mime: file.type || mimePrefix, dataUrl: String(reader.result) });
|
|
1326
|
+
toast('已暂存:' + file.name + '(点发送时一起发给 AI)');
|
|
1327
|
+
};
|
|
1328
|
+
reader.readAsDataURL(file);
|
|
1329
|
+
input.value = '';
|
|
1330
|
+
}
|
|
1331
|
+
document.getElementById('fileInput').addEventListener('change', function() { handleUpload(this, 'application/octet-stream'); });
|
|
1332
|
+
document.getElementById('imageInput').addEventListener('change', function() { handleUpload(this, 'image/png'); });
|
|
1333
|
+
|
|
1334
|
+
/* ========== 大模型配置(三重加密:密钥 RSA 加密传输,服务端 AES 加密落盘) ========== */
|
|
1335
|
+
// 第二重(通信层):用服务器 RSA 公钥加密敏感文本(如模型 API Key)
|
|
1336
|
+
// 持久化会话配置到 localStorage
|
|
1337
|
+
// 切换会话模式(打开/发送时自动重置)
|
|
1338
|
+
document.querySelectorAll('.session-toggle').forEach((el) => {
|
|
1339
|
+
el.addEventListener('change', () => {
|
|
1340
|
+
const key = el.dataset.key;
|
|
1341
|
+
state.sessionConfig[key] = el.checked;
|
|
1342
|
+
persistSessionConfig();
|
|
1343
|
+
toast('会话配置已保存');
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
document.getElementById('modelSaveBtn').addEventListener('click', saveModel);
|
|
1347
|
+
document.getElementById('modelResetBtn').addEventListener('click', resetModelForm);
|
|
1348
|
+
document.getElementById('modelSettingsShortcut').addEventListener('click', openModels);
|
|
1349
|
+
document.getElementById('modelSelect').addEventListener('change', (e) => {
|
|
1350
|
+
state.modelId = e.target.value || '';
|
|
1351
|
+
localStorage.setItem('fhcode.model', state.modelId);
|
|
1352
|
+
const m = state.models.find((x) => x.id === state.modelId);
|
|
1353
|
+
toast(m ? ('已切换:' + m.name) : '已切换:系统默认路由');
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
/* ========== 设置面板 ========== */
|
|
1357
|
+
document.querySelectorAll('#settingsModal .settings-cat').forEach((c) => {
|
|
1358
|
+
c.addEventListener('click', () => {
|
|
1359
|
+
const cat = c.getAttribute('data-cat');
|
|
1360
|
+
document.querySelectorAll('#settingsModal .settings-cat').forEach((x) => x.classList.toggle('active', x === c));
|
|
1361
|
+
document.querySelectorAll('#settingsModal .settings-group').forEach((g) => {
|
|
1362
|
+
g.style.display = g.getAttribute('data-group') === cat ? '' : 'none';
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
document.querySelectorAll('#settingsModal .settings-item').forEach((it) => {
|
|
1367
|
+
it.addEventListener('click', () => {
|
|
1368
|
+
const act = it.getAttribute('data-action');
|
|
1369
|
+
if (act === 'perm') { closeModal('settingsModal'); openPermissions(); }
|
|
1370
|
+
else if (act === 'theme') { closeModal('settingsModal'); toggleTheme(); }
|
|
1371
|
+
else if (act === 'model-custom') { toggleModelManage(); }
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
async function saveInlineModel() {
|
|
1375
|
+
const name = document.getElementById('imName').value.trim();
|
|
1376
|
+
const apiBase = document.getElementById('imBase').value.trim();
|
|
1377
|
+
const apiKey = document.getElementById('imKey').value;
|
|
1378
|
+
const reasoning = document.getElementById('imReasoning').value;
|
|
1379
|
+
const msg = document.getElementById('imMsg');
|
|
1380
|
+
if (!name) { msg.textContent = '请填写模型名称'; msg.classList.add('err'); return; }
|
|
1381
|
+
const editing = document.getElementById('imSave').dataset.editing;
|
|
1382
|
+
try {
|
|
1383
|
+
const body = { name, apiBase, reasoning };
|
|
1384
|
+
// 三重加密·通信层:密钥 RSA 加密传输
|
|
1385
|
+
if (apiKey) body.apiKeyEnc = await rsaEncryptText(apiKey);
|
|
1386
|
+
if (editing) body.id = editing;
|
|
1387
|
+
const d = await api('/api/models', 'POST', body);
|
|
1388
|
+
msg.textContent = '已保存:' + d.model.name;
|
|
1389
|
+
msg.classList.remove('err');
|
|
1390
|
+
await loadModels();
|
|
1391
|
+
renderModelListInline();
|
|
1392
|
+
resetInlineForm();
|
|
1393
|
+
} catch (e) { msg.textContent = '保存失败:' + e.message; msg.classList.add('err'); }
|
|
1394
|
+
}
|
|
1395
|
+
document.getElementById('imSave').addEventListener('click', saveInlineModel);
|
|
1396
|
+
document.getElementById('imReset').addEventListener('click', resetInlineForm);
|
|
1397
|
+
document.getElementById('modelManageClose')?.addEventListener('click', () => { document.getElementById('modelManage').style.display = 'none'; });
|
|
1398
|
+
|
|
1399
|
+
/* ========== 通话控件(语音/视频通话 + 实时语音识别) ========== */
|
|
1400
|
+
let callStream = null;
|
|
1401
|
+
let callKind = null;
|
|
1402
|
+
|
|
1403
|
+
async function startCall(kind) {
|
|
1404
|
+
const status = document.getElementById('callStatus');
|
|
1405
|
+
try {
|
|
1406
|
+
const constraints = kind === 'video'
|
|
1407
|
+
? { audio: true, video: { width: 640, height: 480 } }
|
|
1408
|
+
: { audio: true };
|
|
1409
|
+
callStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
1410
|
+
callKind = kind;
|
|
1411
|
+
|
|
1412
|
+
// 视频通话:显示摄像头画面
|
|
1413
|
+
if (kind === 'video') {
|
|
1414
|
+
const videoWindow = document.getElementById('videoCallWindow');
|
|
1415
|
+
const localVideo = document.getElementById('localVideo');
|
|
1416
|
+
localVideo.srcObject = callStream;
|
|
1417
|
+
videoWindow.style.display = 'block';
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '(语音实时转文字)';
|
|
1421
|
+
document.getElementById('hangupCallBtn').style.display = 'inline-block';
|
|
1422
|
+
document.getElementById('voiceCallBtn').style.display = 'none';
|
|
1423
|
+
document.getElementById('videoCallBtn').style.display = 'none';
|
|
1424
|
+
|
|
1425
|
+
// 开启语音识别(复用语音输入模块)
|
|
1426
|
+
if (!voiceState.isListening) {
|
|
1427
|
+
voiceState.finalText = '';
|
|
1428
|
+
voiceState.interimText = '';
|
|
1429
|
+
voiceState.originalText = document.getElementById('goalInput').value;
|
|
1430
|
+
voiceState.manuallyStopped = false;
|
|
1431
|
+
voiceState.restartCount = 0;
|
|
1432
|
+
updateVoiceUI(true);
|
|
1433
|
+
startRecognition();
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话,说话内容会实时转成文字');
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
status.textContent = '';
|
|
1439
|
+
if (e.name === 'NotAllowedError') {
|
|
1440
|
+
toast('请允许浏览器使用麦克风和摄像头权限');
|
|
1441
|
+
} else if (e.name === 'NotFoundError') {
|
|
1442
|
+
toast('未找到麦克风或摄像头设备');
|
|
1443
|
+
} else {
|
|
1444
|
+
toast('无法发起通话:' + e.message);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function hangupCall() {
|
|
1450
|
+
// 先保存识别的文字到输入框(确保内容不会丢失)
|
|
1451
|
+
if (voiceState.finalText) {
|
|
1452
|
+
updateVoiceInput();
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// 停止媒体流
|
|
1456
|
+
if (callStream) {
|
|
1457
|
+
callStream.getTracks().forEach((t) => t.stop());
|
|
1458
|
+
callStream = null;
|
|
1459
|
+
}
|
|
1460
|
+
callKind = null;
|
|
1461
|
+
|
|
1462
|
+
// 隐藏视频窗口
|
|
1463
|
+
const videoWindow = document.getElementById('videoCallWindow');
|
|
1464
|
+
const localVideo = document.getElementById('localVideo');
|
|
1465
|
+
if (videoWindow) videoWindow.style.display = 'none';
|
|
1466
|
+
if (localVideo) localVideo.srcObject = null;
|
|
1467
|
+
|
|
1468
|
+
// 停止语音识别
|
|
1469
|
+
if (voiceState.isListening) {
|
|
1470
|
+
stopVoiceInput();
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
document.getElementById('callStatus').textContent = '';
|
|
1474
|
+
document.getElementById('hangupCallBtn').style.display = 'none';
|
|
1475
|
+
document.getElementById('voiceCallBtn').style.display = 'inline-block';
|
|
1476
|
+
document.getElementById('videoCallBtn').style.display = 'inline-block';
|
|
1477
|
+
toast('通话已结束,识别的文字已保留在输入框');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
document.getElementById('voiceCallBtn').addEventListener('click', () => startCall('voice'));
|
|
1481
|
+
document.getElementById('videoCallBtn').addEventListener('click', () => startCall('video'));
|
|
1482
|
+
document.getElementById('hangupCallBtn').addEventListener('click', hangupCall);
|
|
1483
|
+
document.getElementById('videoCallClose')?.addEventListener('click', hangupCall);
|
|
1484
|
+
|
|
1485
|
+
// 视频通话截图:把当前摄像头画面截取下来放到输入框
|
|
1486
|
+
document.getElementById('videoSnapshotBtn')?.addEventListener('click', () => {
|
|
1487
|
+
const video = document.getElementById('localVideo');
|
|
1488
|
+
if (!video || !video.videoWidth) {
|
|
1489
|
+
toast('视频还没准备好,请稍等');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const canvas = document.createElement('canvas');
|
|
1494
|
+
canvas.width = video.videoWidth;
|
|
1495
|
+
canvas.height = video.videoHeight;
|
|
1496
|
+
canvas.getContext('2d').drawImage(video, 0, 0);
|
|
1497
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
1498
|
+
stageFile({
|
|
1499
|
+
name: 'camera-' + Date.now() + '.png',
|
|
1500
|
+
mime: 'image/png',
|
|
1501
|
+
dataUrl: dataUrl,
|
|
1502
|
+
});
|
|
1503
|
+
toast('摄像头画面已添加到输入框,可直接发送');
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
toast('截图失败:' + e.message);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
/* ========== 语音输入模块 ========== */
|
|
1510
|
+
const voiceState = {
|
|
1511
|
+
recognition: null,
|
|
1512
|
+
finalText: '',
|
|
1513
|
+
interimText: '',
|
|
1514
|
+
originalText: '',
|
|
1515
|
+
isListening: false,
|
|
1516
|
+
manuallyStopped: false,
|
|
1517
|
+
restartTimer: null,
|
|
1518
|
+
restartCount: 0,
|
|
1519
|
+
maxRestarts: 10000, // 基本无限制,长时间通话也不会停
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// 优化语音识别文本:去掉多余空格、自动加标点、清理重复
|
|
1523
|
+
function optimizeVoiceText(text) {
|
|
1524
|
+
if (!text) return '';
|
|
1525
|
+
let result = text.trim();
|
|
1526
|
+
// 去掉多余空格
|
|
1527
|
+
result = result.replace(/\s+/g, ' ');
|
|
1528
|
+
// 去掉首尾空格
|
|
1529
|
+
result = result.trim();
|
|
1530
|
+
// 中文标点优化:如果末尾是中文且没有标点,自动加句号
|
|
1531
|
+
if (result && /[\u4e00-\u9fa5]$/.test(result) && !/[。!?,、;:""''()]$/.test(result)) {
|
|
1532
|
+
result += '。';
|
|
1533
|
+
}
|
|
1534
|
+
return result;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// 更新输入框显示
|
|
1538
|
+
function updateVoiceInput() {
|
|
1539
|
+
const input = document.getElementById('goalInput');
|
|
1540
|
+
const finalOptimized = optimizeVoiceText(voiceState.finalText);
|
|
1541
|
+
const displayText = voiceState.originalText
|
|
1542
|
+
? (voiceState.originalText + ' ' + finalOptimized + voiceState.interimText)
|
|
1543
|
+
: (finalOptimized + voiceState.interimText);
|
|
1544
|
+
input.value = displayText;
|
|
1545
|
+
input.scrollTop = input.scrollHeight;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// 更新UI状态
|
|
1549
|
+
function updateVoiceUI(listening) {
|
|
1550
|
+
const badge = document.getElementById('voiceBadge');
|
|
1551
|
+
const voiceBtn = document.getElementById('voiceBtn');
|
|
1552
|
+
if (listening) {
|
|
1553
|
+
badge.style.display = 'inline-flex';
|
|
1554
|
+
badge.textContent = '● 正在听…(再次点击停止)';
|
|
1555
|
+
if (voiceBtn) {
|
|
1556
|
+
voiceBtn.style.background = 'var(--brand)';
|
|
1557
|
+
voiceBtn.style.color = '#fff';
|
|
1558
|
+
voiceBtn.textContent = '● 听…';
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
badge.style.display = 'none';
|
|
1562
|
+
if (voiceBtn) {
|
|
1563
|
+
voiceBtn.style.background = '';
|
|
1564
|
+
voiceBtn.style.color = '';
|
|
1565
|
+
voiceBtn.textContent = '🎤 语音';
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// 创建新的语音识别对象
|
|
1571
|
+
function createRecognition() {
|
|
1572
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1573
|
+
if (!SpeechRecognition) return null;
|
|
1574
|
+
|
|
1575
|
+
const rec = new SpeechRecognition();
|
|
1576
|
+
rec.lang = 'zh-CN';
|
|
1577
|
+
rec.continuous = true;
|
|
1578
|
+
rec.interimResults = true;
|
|
1579
|
+
rec.maxAlternatives = 1;
|
|
1580
|
+
|
|
1581
|
+
// 识别结果处理
|
|
1582
|
+
rec.onresult = (e) => {
|
|
1583
|
+
let interim = '';
|
|
1584
|
+
let finalDelta = '';
|
|
1585
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
1586
|
+
const transcript = e.results[i][0].transcript;
|
|
1587
|
+
if (e.results[i].isFinal) {
|
|
1588
|
+
finalDelta += transcript;
|
|
1589
|
+
} else {
|
|
1590
|
+
interim += transcript;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (finalDelta) {
|
|
1594
|
+
voiceState.finalText += finalDelta;
|
|
1595
|
+
voiceState.restartCount = 0; // 有新结果,重置重启计数
|
|
1596
|
+
}
|
|
1597
|
+
voiceState.interimText = interim;
|
|
1598
|
+
updateVoiceInput();
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
// 错误处理
|
|
1602
|
+
rec.onerror = (e) => {
|
|
1603
|
+
console.warn('[语音识别] 错误:', e.error);
|
|
1604
|
+
if (e.error === 'not-allowed' || e.error === 'service-not-allowed') {
|
|
1605
|
+
toast('请允许浏览器使用麦克风权限(点击地址栏左侧锁图标)');
|
|
1606
|
+
voiceState.manuallyStopped = true;
|
|
1607
|
+
stopVoiceInput();
|
|
1608
|
+
} else if (e.error === 'network') {
|
|
1609
|
+
toast('语音识别网络错误!Chrome用的是Google服务,国内建议用Edge浏览器,或检查网络/代理');
|
|
1610
|
+
voiceState.manuallyStopped = true;
|
|
1611
|
+
stopVoiceInput();
|
|
1612
|
+
} else if (e.error === 'audio-capture') {
|
|
1613
|
+
toast('未检测到麦克风设备,请检查麦克风连接和系统设置');
|
|
1614
|
+
voiceState.manuallyStopped = true;
|
|
1615
|
+
stopVoiceInput();
|
|
1616
|
+
} else if (e.error === 'no-speech') {
|
|
1617
|
+
// 没检测到语音,继续等待,不停止
|
|
1618
|
+
console.log('[语音识别] 未检测到语音,继续等待...');
|
|
1619
|
+
toast('没听到声音,请检查麦克风是否开启,离麦克风近一点说话');
|
|
1620
|
+
} else if (e.error === 'aborted') {
|
|
1621
|
+
// 手动停止,不处理
|
|
1622
|
+
} else {
|
|
1623
|
+
console.warn('[语音识别] 其他错误:', e.error);
|
|
1624
|
+
toast('语音识别出错: ' + e.error);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// 识别结束处理
|
|
1629
|
+
rec.onend = () => {
|
|
1630
|
+
voiceState.recognition = null;
|
|
1631
|
+
voiceState.interimText = '';
|
|
1632
|
+
|
|
1633
|
+
// 如果不是手动停止,自动重启(基本无限制)
|
|
1634
|
+
if (!voiceState.manuallyStopped && voiceState.restartCount < voiceState.maxRestarts) {
|
|
1635
|
+
voiceState.restartCount++;
|
|
1636
|
+
// 只在调试时打印,避免控制台太多日志
|
|
1637
|
+
if (voiceState.restartCount % 50 === 0) {
|
|
1638
|
+
console.log(`[语音识别] 自动重启 (${voiceState.restartCount})`);
|
|
1639
|
+
}
|
|
1640
|
+
voiceState.restartTimer = setTimeout(() => {
|
|
1641
|
+
if (!voiceState.manuallyStopped) {
|
|
1642
|
+
startRecognition();
|
|
1643
|
+
}
|
|
1644
|
+
}, 50); // 50ms快速重启,减少停顿感
|
|
1645
|
+
} else if (voiceState.manuallyStopped) {
|
|
1646
|
+
// 手动停止,最终处理
|
|
1647
|
+
updateVoiceUI(false);
|
|
1648
|
+
if (voiceState.finalText) {
|
|
1649
|
+
updateVoiceInput();
|
|
1650
|
+
toast('语音输入完成,文字已在输入框中,可直接发送');
|
|
1651
|
+
}
|
|
1652
|
+
} else {
|
|
1653
|
+
// 超过最大重启次数(基本不会发生)
|
|
1654
|
+
updateVoiceUI(false);
|
|
1655
|
+
toast('语音输入已自动停止,可再次点击开启');
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
return rec;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// 启动识别
|
|
1663
|
+
function startRecognition() {
|
|
1664
|
+
const rec = createRecognition();
|
|
1665
|
+
if (!rec) {
|
|
1666
|
+
toast('当前浏览器不支持语音输入,请使用 Chrome 或 Edge 浏览器');
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
try {
|
|
1670
|
+
rec.start();
|
|
1671
|
+
voiceState.recognition = rec;
|
|
1672
|
+
voiceState.isListening = true;
|
|
1673
|
+
return true;
|
|
1674
|
+
} catch (e) {
|
|
1675
|
+
console.error('[语音识别] 启动失败:', e);
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// 停止语音输入
|
|
1681
|
+
function stopVoiceInput() {
|
|
1682
|
+
voiceState.manuallyStopped = true;
|
|
1683
|
+
voiceState.isListening = false;
|
|
1684
|
+
if (voiceState.restartTimer) {
|
|
1685
|
+
clearTimeout(voiceState.restartTimer);
|
|
1686
|
+
voiceState.restartTimer = null;
|
|
1687
|
+
}
|
|
1688
|
+
if (voiceState.recognition) {
|
|
1689
|
+
try { voiceState.recognition.stop(); } catch(e) {}
|
|
1690
|
+
voiceState.recognition = null;
|
|
1691
|
+
}
|
|
1692
|
+
updateVoiceUI(false);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 开始/停止语音输入(入口函数)
|
|
1696
|
+
function startVoice() {
|
|
1697
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1698
|
+
if (!SpeechRecognition) {
|
|
1699
|
+
toast('当前环境不支持语音输入,请使用 Chrome 或 Edge 浏览器打开网页版');
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// 如果正在识别,就停止
|
|
1704
|
+
if (voiceState.isListening) {
|
|
1705
|
+
stopVoiceInput();
|
|
1706
|
+
toast('已停止语音输入');
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Electron 桌面版特殊提示
|
|
1711
|
+
if (window.isElectron) {
|
|
1712
|
+
toast('桌面版语音输入可能不稳定,建议用网页版(Edge浏览器)');
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// 开始识别
|
|
1716
|
+
voiceState.finalText = '';
|
|
1717
|
+
voiceState.interimText = '';
|
|
1718
|
+
voiceState.originalText = document.getElementById('goalInput').value;
|
|
1719
|
+
voiceState.manuallyStopped = false;
|
|
1720
|
+
voiceState.restartCount = 0;
|
|
1721
|
+
|
|
1722
|
+
updateVoiceUI(true);
|
|
1723
|
+
toast('语音输入已开启,请说话,再次点击停止');
|
|
1724
|
+
|
|
1725
|
+
if (!startRecognition()) {
|
|
1726
|
+
updateVoiceUI(false);
|
|
1727
|
+
toast('启动语音输入失败,请刷新页面重试');
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/* ========== 智能体选择 ========== */
|
|
1732
|
+
document.getElementById('skillPill').addEventListener('click', openAgentSelector);
|
|
1733
|
+
|
|
1734
|
+
/* ========== 权限窗口 ========== */
|
|
1735
|
+
document.getElementById('permSaveBtn').addEventListener('click', () => {
|
|
1736
|
+
const scope = document.querySelector('input[name="readScope"]:checked')?.value || 'workspace';
|
|
1737
|
+
state.permissions = {
|
|
1738
|
+
readScope: scope,
|
|
1739
|
+
readPath: document.getElementById('permReadPath').value.trim(),
|
|
1740
|
+
allowRead: document.getElementById('permRead').checked,
|
|
1741
|
+
allowWrite: document.getElementById('permWrite').checked,
|
|
1742
|
+
allowShell: document.getElementById('permShell').checked,
|
|
1743
|
+
allowNetwork: document.getElementById('permNetwork').checked,
|
|
1744
|
+
allowBrowser: document.getElementById('permBrowser').checked,
|
|
1745
|
+
};
|
|
1746
|
+
localStorage.setItem('fhcode.permissions', JSON.stringify(state.permissions));
|
|
1747
|
+
closeModal('permModal');
|
|
1748
|
+
toast('权限配置已保存');
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
/* ========== 工作区 ========== */
|
|
1752
|
+
// 工作区跳转功能已移除(旧布局左侧工作区树)
|
|
1753
|
+
|
|
1754
|
+
document.getElementById('workspacePermBadge')?.addEventListener('click', () => openPermissions());
|
|
1755
|
+
document.getElementById('workspacePickBtn')?.addEventListener('click', () => {
|
|
1756
|
+
openFolderPicker();
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
let fpCurrent = '';
|
|
1760
|
+
let fpSelected = '';
|
|
1761
|
+
|
|
1762
|
+
async function openFolderPicker() {
|
|
1763
|
+
fpCurrent = '';
|
|
1764
|
+
fpSelected = '';
|
|
1765
|
+
document.getElementById('fpDrivesView').style.display = '';
|
|
1766
|
+
document.getElementById('fpFoldersView').style.display = 'none';
|
|
1767
|
+
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1768
|
+
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1769
|
+
document.getElementById('fpConfirm').disabled = true;
|
|
1770
|
+
document.getElementById('fpRenameFolder').disabled = true;
|
|
1771
|
+
document.getElementById('fpTitle').textContent = '📂 选择驱动器';
|
|
1772
|
+
openModal('folderPickerModal');
|
|
1773
|
+
await loadDrives();
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
async function loadDrives() {
|
|
1777
|
+
try {
|
|
1778
|
+
const d = await api('/api/drives');
|
|
1779
|
+
const drives = d.drives || [];
|
|
1780
|
+
const grid = document.getElementById('fpDrivesGrid');
|
|
1781
|
+
grid.innerHTML = drives.map(drv => {
|
|
1782
|
+
const label = drv.replace('\\', '');
|
|
1783
|
+
return '<div class="fp-drive-card" data-drive="' + escapeHtml(drv) + '" style="padding:16px;text-align:center;border:1px solid var(--line);border-radius:8px;cursor:pointer;transition:all .15s;">'
|
|
1784
|
+
+ '<div style="font-size:28px;margin-bottom:6px;">💽</div>'
|
|
1785
|
+
+ '<div style="font-weight:600;font-size:14px;">' + escapeHtml(label) + '</div>'
|
|
1786
|
+
+ '<div style="font-size:11px;color:var(--ink-2);margin-top:2px;">本地磁盘</div>'
|
|
1787
|
+
+ '</div>';
|
|
1788
|
+
}).join('');
|
|
1789
|
+
grid.querySelectorAll('.fp-drive-card').forEach(el => {
|
|
1790
|
+
el.addEventListener('click', () => selectDrive(el.getAttribute('data-drive')));
|
|
1791
|
+
el.addEventListener('mouseenter', () => { el.style.borderColor = 'var(--brand)'; el.style.background = 'var(--brand-soft)'; });
|
|
1792
|
+
el.addEventListener('mouseleave', () => { el.style.borderColor = 'var(--line)'; el.style.background = ''; });
|
|
1793
|
+
});
|
|
1794
|
+
} catch (e) {
|
|
1795
|
+
toast('加载驱动器失败:' + e.message);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async function selectDrive(drive) {
|
|
1800
|
+
fpCurrent = drive;
|
|
1801
|
+
fpSelected = '';
|
|
1802
|
+
document.getElementById('fpDrivesView').style.display = 'none';
|
|
1803
|
+
document.getElementById('fpFoldersView').style.display = '';
|
|
1804
|
+
document.getElementById('fpTitle').textContent = '📂 选择文件夹';
|
|
1805
|
+
document.getElementById('fpConfirm').disabled = true;
|
|
1806
|
+
document.getElementById('fpRenameFolder').disabled = true;
|
|
1807
|
+
await loadFolderPicker(drive);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function backToDrives() {
|
|
1811
|
+
fpCurrent = '';
|
|
1812
|
+
fpSelected = '';
|
|
1813
|
+
document.getElementById('fpDrivesView').style.display = '';
|
|
1814
|
+
document.getElementById('fpFoldersView').style.display = 'none';
|
|
1815
|
+
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1816
|
+
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1817
|
+
document.getElementById('fpTitle').textContent = '📂 选择驱动器';
|
|
1818
|
+
document.getElementById('fpConfirm').disabled = true;
|
|
1819
|
+
document.getElementById('fpRenameFolder').disabled = true;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
async function upDir() {
|
|
1823
|
+
if (!fpCurrent) return;
|
|
1824
|
+
// 驱动器根目录不能再往上
|
|
1825
|
+
if (/^[A-Za-z]:\\$/.test(fpCurrent)) {
|
|
1826
|
+
backToDrives();
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const parent = getDirectoryName(fpCurrent);
|
|
1830
|
+
if (parent && parent !== fpCurrent) {
|
|
1831
|
+
fpCurrent = parent;
|
|
1832
|
+
fpSelected = '';
|
|
1833
|
+
document.getElementById('fpConfirm').disabled = true;
|
|
1834
|
+
document.getElementById('fpRenameFolder').disabled = true;
|
|
1835
|
+
await loadFolderPicker(fpCurrent);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
async function loadFolderPicker(dir) {
|
|
1840
|
+
try {
|
|
1841
|
+
const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
|
|
1842
|
+
fpCurrent = d.cwd;
|
|
1843
|
+
const pathEl = document.getElementById('fpPath');
|
|
1844
|
+
pathEl.textContent = d.cwd;
|
|
1845
|
+
pathEl.title = d.cwd;
|
|
1846
|
+
const tree = document.getElementById('fpTree');
|
|
1847
|
+
// 只显示文件夹,不显示文件
|
|
1848
|
+
const dirs = (d.entries || []).filter(e => e.type === 'dir');
|
|
1849
|
+
if (dirs.length === 0) {
|
|
1850
|
+
tree.innerHTML = '<div style="padding:20px;text-align:center;color:var(--ink-2);font-size:13px;">此目录下没有文件夹</div>';
|
|
1851
|
+
} else {
|
|
1852
|
+
tree.innerHTML = dirs.map(e => {
|
|
1853
|
+
const selected = fpSelected === e.path ? 'background:var(--brand-soft);border-color:var(--brand);' : '';
|
|
1854
|
+
return '<div class="file-item fp-folder-item" data-path="' + escapeHtml(e.path) + '" data-name="' + escapeHtml(e.name) + '" style="' + selected + 'padding:8px 10px;border:1px solid transparent;border-radius:6px;cursor:pointer;display:flex;align-items:center;gap:8px;">'
|
|
1855
|
+
+ '<span style="font-size:16px;">📁</span>'
|
|
1856
|
+
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(e.name) + '</span>'
|
|
1857
|
+
+ '</div>';
|
|
1858
|
+
}).join('');
|
|
1859
|
+
}
|
|
1860
|
+
tree.querySelectorAll('.fp-folder-item').forEach(el => {
|
|
1861
|
+
el.addEventListener('click', (e) => {
|
|
1862
|
+
e.stopPropagation();
|
|
1863
|
+
const path = el.getAttribute('data-path');
|
|
1864
|
+
const name = el.getAttribute('data-name');
|
|
1865
|
+
// 单击选中
|
|
1866
|
+
fpSelected = path;
|
|
1867
|
+
document.getElementById('fpConfirm').disabled = false;
|
|
1868
|
+
document.getElementById('fpRenameFolder').disabled = false;
|
|
1869
|
+
document.getElementById('fpRenameName').value = name;
|
|
1870
|
+
// 高亮选中项
|
|
1871
|
+
tree.querySelectorAll('.fp-folder-item').forEach(item => {
|
|
1872
|
+
item.style.background = '';
|
|
1873
|
+
item.style.borderColor = 'transparent';
|
|
1874
|
+
});
|
|
1875
|
+
el.style.background = 'var(--brand-soft)';
|
|
1876
|
+
el.style.borderColor = 'var(--brand)';
|
|
1877
|
+
});
|
|
1878
|
+
el.addEventListener('dblclick', () => {
|
|
1879
|
+
const path = el.getAttribute('data-path');
|
|
1880
|
+
fpSelected = '';
|
|
1881
|
+
document.getElementById('fpConfirm').disabled = true;
|
|
1882
|
+
document.getElementById('fpRenameFolder').disabled = true;
|
|
1883
|
+
loadFolderPicker(path);
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
toast('读取目录失败:' + e.message);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
document.getElementById('fpConfirm')?.addEventListener('click', async () => {
|
|
1892
|
+
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1893
|
+
try {
|
|
1894
|
+
const result = await api('/api/workspace', 'POST', { cwd: fpSelected });
|
|
1895
|
+
state.workspaceDir = result.cwd;
|
|
1896
|
+
localStorage.setItem('fhcode.workspaceDir', result.cwd);
|
|
1897
|
+
renderWorkspaceBar();
|
|
1898
|
+
closeModal('folderPickerModal');
|
|
1899
|
+
toast('已切换工作区:' + result.cwd);
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
toast('设置失败:' + e.message);
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
// 返回驱动器选择
|
|
1906
|
+
document.getElementById('fpBackToDrives')?.addEventListener('click', backToDrives);
|
|
1907
|
+
// 返回上级目录
|
|
1908
|
+
document.getElementById('fpUpDir')?.addEventListener('click', upDir);
|
|
1909
|
+
|
|
1910
|
+
// 新建文件夹
|
|
1911
|
+
document.getElementById('fpNewFolder')?.addEventListener('click', () => {
|
|
1912
|
+
document.getElementById('fpMkdirBar').style.display = 'flex';
|
|
1913
|
+
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1914
|
+
document.getElementById('fpMkdirName').value = '';
|
|
1915
|
+
setTimeout(() => document.getElementById('fpMkdirName').focus(), 50);
|
|
1916
|
+
});
|
|
1917
|
+
document.getElementById('fpMkdirCancel')?.addEventListener('click', () => {
|
|
1918
|
+
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1919
|
+
});
|
|
1920
|
+
document.getElementById('fpMkdirConfirm')?.addEventListener('click', async () => {
|
|
1921
|
+
const name = document.getElementById('fpMkdirName').value.trim();
|
|
1922
|
+
if (!name) { toast('请输入文件夹名称'); return; }
|
|
1923
|
+
try {
|
|
1924
|
+
const result = await api('/api/workspace/mkdir', 'POST', { parent: fpCurrent, name });
|
|
1925
|
+
toast('文件夹已创建:' + name);
|
|
1926
|
+
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1927
|
+
fpSelected = result.path;
|
|
1928
|
+
await loadFolderPicker(fpCurrent);
|
|
1929
|
+
document.getElementById('fpConfirm').disabled = false;
|
|
1930
|
+
document.getElementById('fpRenameFolder').disabled = false;
|
|
1931
|
+
} catch (e) {
|
|
1932
|
+
toast('创建失败:' + e.message);
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
document.getElementById('fpMkdirName')?.addEventListener('keydown', (e) => {
|
|
1936
|
+
if (e.key === 'Enter') document.getElementById('fpMkdirConfirm').click();
|
|
1937
|
+
if (e.key === 'Escape') document.getElementById('fpMkdirCancel').click();
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
// 重命名文件夹
|
|
1941
|
+
document.getElementById('fpRenameFolder')?.addEventListener('click', () => {
|
|
1942
|
+
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1943
|
+
document.getElementById('fpRenameBar').style.display = 'flex';
|
|
1944
|
+
document.getElementById('fpMkdirBar').style.display = 'none';
|
|
1945
|
+
const name = fpSelected.split(/[\\/]/).filter(Boolean).pop() || '';
|
|
1946
|
+
document.getElementById('fpRenameName').value = name;
|
|
1947
|
+
setTimeout(() => document.getElementById('fpRenameName').focus(), 50);
|
|
1948
|
+
});
|
|
1949
|
+
document.getElementById('fpRenameCancel')?.addEventListener('click', () => {
|
|
1950
|
+
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1951
|
+
});
|
|
1952
|
+
document.getElementById('fpRenameConfirm')?.addEventListener('click', async () => {
|
|
1953
|
+
const newName = document.getElementById('fpRenameName').value.trim();
|
|
1954
|
+
if (!newName) { toast('请输入新名称'); return; }
|
|
1955
|
+
if (!fpSelected) { toast('请先选择一个文件夹'); return; }
|
|
1956
|
+
try {
|
|
1957
|
+
const result = await api('/api/workspace/rename', 'POST', { path: fpSelected, newName });
|
|
1958
|
+
toast('文件夹已重命名为:' + newName);
|
|
1959
|
+
document.getElementById('fpRenameBar').style.display = 'none';
|
|
1960
|
+
fpSelected = result.path;
|
|
1961
|
+
await loadFolderPicker(fpCurrent);
|
|
1962
|
+
document.getElementById('fpConfirm').disabled = false;
|
|
1963
|
+
document.getElementById('fpRenameFolder').disabled = false;
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
toast('重命名失败:' + e.message);
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
document.getElementById('fpRenameName')?.addEventListener('keydown', (e) => {
|
|
1969
|
+
if (e.key === 'Enter') document.getElementById('fpRenameConfirm').click();
|
|
1970
|
+
if (e.key === 'Escape') document.getElementById('fpRenameCancel').click();
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
/* ========== 自动化、模板、市场、办公助理 ========== */
|
|
1974
|
+
document.getElementById('newAutoBtn')?.addEventListener('click', () => openModal('autoModal'));
|
|
1975
|
+
document.getElementById('autoSaveBtn')?.addEventListener('click', async () => {
|
|
1976
|
+
const name = document.getElementById('autoName').value.trim();
|
|
1977
|
+
const goal = document.getElementById('autoGoal').value.trim();
|
|
1978
|
+
if (!name || !goal) { toast('请填写名称与目标'); return; }
|
|
1979
|
+
try { await api('/api/automations', 'POST', { name, goal }); closeModal('autoModal');
|
|
1980
|
+
const n = document.getElementById('autoName');
|
|
1981
|
+
const g = document.getElementById('autoGoal');
|
|
1982
|
+
if (n) n.value = ''; if (g) g.value = '';
|
|
1983
|
+
await loadAutomations(); toast('已保存'); }
|
|
1984
|
+
catch (e) { toast('保存失败:' + e.message); }
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
document.getElementById('newTplBtn')?.addEventListener('click', () => {
|
|
1988
|
+
const goal = document.getElementById('goalInput')?.value?.trim() || '';
|
|
1989
|
+
const inp = document.getElementById('tplGoal');
|
|
1990
|
+
if (inp) inp.value = goal;
|
|
1991
|
+
openModal('tplModal');
|
|
1992
|
+
});
|
|
1993
|
+
document.getElementById('tplSaveBtn')?.addEventListener('click', async () => {
|
|
1994
|
+
const title = document.getElementById('tplTitle').value.trim();
|
|
1995
|
+
const goal = document.getElementById('tplGoal').value.trim();
|
|
1996
|
+
const category = document.getElementById('tplCategory').value.trim();
|
|
1997
|
+
if (!title || !goal) { toast('请填写标题与内容'); return; }
|
|
1998
|
+
try { await api('/api/templates', 'POST', { title, goal, category }); closeModal('tplModal');
|
|
1999
|
+
const t = document.getElementById('tplTitle');
|
|
2000
|
+
const g = document.getElementById('tplGoal');
|
|
2001
|
+
const c = document.getElementById('tplCategory');
|
|
2002
|
+
if (t) t.value = ''; if (g) g.value = ''; if (c) c.value = '';
|
|
2003
|
+
await loadTemplates(); toast('已保存模板'); }
|
|
2004
|
+
catch (e) { toast('保存失败:' + e.message); }
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
document.getElementById('marketRefresh')?.addEventListener('click', loadMarket);
|
|
2008
|
+
let marketTimer;
|
|
2009
|
+
document.getElementById('marketSearch')?.addEventListener('input', () => { clearTimeout(marketTimer); marketTimer = setTimeout(loadMarket, 400); });
|
|
2010
|
+
document.getElementById('marketSource')?.addEventListener('change', loadMarket);
|
|
2011
|
+
|
|
2012
|
+
/* ========== 节点管理 & 自定义来源 ========== */
|
|
2013
|
+
document.getElementById('addNodeBtn')?.addEventListener('click', () => openNodeModal());
|
|
2014
|
+
document.getElementById('addSourceBtn')?.addEventListener('click', () => openSourceModal());
|
|
2015
|
+
document.getElementById('nodeSaveBtn')?.addEventListener('click', async () => {
|
|
2016
|
+
const id = document.getElementById('nodeEditId').value || '';
|
|
2017
|
+
const name = document.getElementById('nodeName').value.trim();
|
|
2018
|
+
const type = document.getElementById('nodeType').value;
|
|
2019
|
+
const url = document.getElementById('nodeUrl').value.trim();
|
|
2020
|
+
const apiKey = document.getElementById('nodeApiKey').value;
|
|
2021
|
+
const capabilities = [];
|
|
2022
|
+
if (document.getElementById('nodeCapTemplates').checked) capabilities.push('templates');
|
|
2023
|
+
if (document.getElementById('nodeCapSkills').checked) capabilities.push('skills');
|
|
2024
|
+
if (document.getElementById('nodeCapOffice').checked) capabilities.push('office');
|
|
2025
|
+
if (!name || !url) { toast('请填写名称和地址'); return; }
|
|
2026
|
+
if (!capabilities.length) { toast('请至少选择一种能力'); return; }
|
|
2027
|
+
const data = { name, type, url, capabilities };
|
|
2028
|
+
if (apiKey) data.apiKey = apiKey;
|
|
2029
|
+
if (id) data.id = id;
|
|
2030
|
+
await saveNode(data);
|
|
2031
|
+
closeModal('nodeModal');
|
|
2032
|
+
});
|
|
2033
|
+
document.getElementById('sourceSaveBtn')?.addEventListener('click', async () => {
|
|
2034
|
+
const id = document.getElementById('sourceEditId').value || '';
|
|
2035
|
+
const name = document.getElementById('sourceName').value.trim();
|
|
2036
|
+
const type = document.getElementById('sourceType').value;
|
|
2037
|
+
const url = document.getElementById('sourceUrl').value.trim();
|
|
2038
|
+
if (!name || !url) { toast('请填写名称和地址'); return; }
|
|
2039
|
+
const data = { name, type, url };
|
|
2040
|
+
if (id) data.id = id;
|
|
2041
|
+
await saveSource(data);
|
|
2042
|
+
closeModal('sourceModal');
|
|
2043
|
+
// 刷新相关数据
|
|
2044
|
+
if (type === 'templates') await loadTemplates();
|
|
2045
|
+
else if (type === 'skills') await loadMarket();
|
|
2046
|
+
else if (type === 'office') await loadOffice();
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
function openNodeModal(node) {
|
|
2050
|
+
document.getElementById('nodeModalTitle').textContent = node ? '编辑节点' : '添加节点';
|
|
2051
|
+
document.getElementById('nodeEditId').value = node?.id || '';
|
|
2052
|
+
document.getElementById('nodeName').value = node?.name || '';
|
|
2053
|
+
document.getElementById('nodeType').value = node?.type || 'http';
|
|
2054
|
+
document.getElementById('nodeUrl').value = node?.url || '';
|
|
2055
|
+
document.getElementById('nodeApiKey').value = '';
|
|
2056
|
+
document.getElementById('nodeCapTemplates').checked = node?.capabilities?.includes('templates') ?? true;
|
|
2057
|
+
document.getElementById('nodeCapSkills').checked = node?.capabilities?.includes('skills') ?? true;
|
|
2058
|
+
document.getElementById('nodeCapOffice').checked = node?.capabilities?.includes('office') ?? true;
|
|
2059
|
+
openModal('nodeModal');
|
|
2060
|
+
}
|
|
2061
|
+
function editNode(id) {
|
|
2062
|
+
const node = (window._nodesCache || []).find((n) => n.id === id);
|
|
2063
|
+
openNodeModal(node || { id });
|
|
2064
|
+
}
|
|
2065
|
+
function openSourceModal(source) {
|
|
2066
|
+
document.getElementById('sourceModalTitle').textContent = source ? '编辑自定义来源' : '添加自定义来源';
|
|
2067
|
+
document.getElementById('sourceEditId').value = source?.id || '';
|
|
2068
|
+
document.getElementById('sourceName').value = source?.name || '';
|
|
2069
|
+
document.getElementById('sourceType').value = source?.type || 'templates';
|
|
2070
|
+
document.getElementById('sourceUrl').value = source?.url || '';
|
|
2071
|
+
openModal('sourceModal');
|
|
2072
|
+
}
|
|
2073
|
+
function editSource(id) {
|
|
2074
|
+
const source = (window._sourcesCache || []).find((s) => s.id === id);
|
|
2075
|
+
openSourceModal(source || { id });
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', () => closeModal(el.getAttribute('data-close'))));
|
|
2080
|
+
document.querySelectorAll('.mask').forEach((m) => m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('show'); }));
|
|
2081
|
+
// 文件夹选择器事件已在 loadFolderPicker / loadDrives 内直接绑定,此处保留空壳避免旧引用报错
|
|
2082
|
+
function startRefresh() {
|
|
2083
|
+
setInterval(async () => {
|
|
2084
|
+
await loadTasks();
|
|
2085
|
+
// 如果当前有正在运行的任务,自动刷新对话流,让用户实时看到思考过程
|
|
2086
|
+
if (state.currentTaskId) {
|
|
2087
|
+
const cur = state.tasks.find((t) => t.id === state.currentTaskId);
|
|
2088
|
+
if (cur && (cur.status === 'running' || cur.status === 'queued')) {
|
|
2089
|
+
await refreshCurrentThread();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}, 1000); // 1秒轮询,确保思考过程实时显示
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/* ========== 电脑操作(鼠标/键盘/截图) ========== */
|
|
2096
|
+
let selectedMousePos = { x: 0, y: 0 };
|
|
2097
|
+
|
|
2098
|
+
// 截取屏幕
|
|
2099
|
+
async function captureScreen() {
|
|
2100
|
+
try {
|
|
2101
|
+
const d = await api('/api/computer/screenshot', 'POST', {});
|
|
2102
|
+
if (d.ok && d.image) {
|
|
2103
|
+
const preview = document.getElementById('screenPreview');
|
|
2104
|
+
preview.innerHTML = `<img id="screenImg" src="${d.image}" alt="屏幕截图" />`;
|
|
2105
|
+
const img = document.getElementById('screenImg');
|
|
2106
|
+
// 点击图片获取坐标
|
|
2107
|
+
img.addEventListener('click', (e) => {
|
|
2108
|
+
const rect = img.getBoundingClientRect();
|
|
2109
|
+
const scaleX = (d.width || 1920) / rect.width;
|
|
2110
|
+
const scaleY = (d.height || 1080) / rect.height;
|
|
2111
|
+
selectedMousePos.x = Math.round((e.clientX - rect.left) * scaleX);
|
|
2112
|
+
selectedMousePos.y = Math.round((e.clientY - rect.top) * scaleY);
|
|
2113
|
+
document.getElementById('mousePos').textContent = `坐标: ${selectedMousePos.x}, ${selectedMousePos.y}`;
|
|
2114
|
+
toast(`已选择坐标: ${selectedMousePos.x}, ${selectedMousePos.y},点击操作按钮执行`);
|
|
2115
|
+
});
|
|
2116
|
+
toast('屏幕截图已更新,点击图片选择坐标');
|
|
2117
|
+
} else {
|
|
2118
|
+
toast('截图失败: ' + (d.error || '未知错误'));
|
|
2119
|
+
}
|
|
2120
|
+
} catch (e) {
|
|
2121
|
+
toast('截图失败: ' + e.message);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// 鼠标点击
|
|
2126
|
+
async function mouseClick(button, doubleClick = false) {
|
|
2127
|
+
try {
|
|
2128
|
+
const d = await api('/api/computer/mouse/click', 'POST', {
|
|
2129
|
+
button,
|
|
2130
|
+
x: selectedMousePos.x,
|
|
2131
|
+
y: selectedMousePos.y,
|
|
2132
|
+
double: doubleClick,
|
|
2133
|
+
});
|
|
2134
|
+
if (d.ok) {
|
|
2135
|
+
toast(`${doubleClick ? '双击' : (button === 'right' ? '右键点击' : '左键点击')} (${selectedMousePos.x}, ${selectedMousePos.y})`);
|
|
2136
|
+
} else {
|
|
2137
|
+
toast('操作失败: ' + (d.error || '未知错误'));
|
|
2138
|
+
}
|
|
2139
|
+
} catch (e) {
|
|
2140
|
+
toast('操作失败: ' + e.message);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// 输入文字
|
|
2145
|
+
async function typeText(text) {
|
|
2146
|
+
try {
|
|
2147
|
+
const d = await api('/api/computer/keyboard/type', 'POST', { text });
|
|
2148
|
+
if (d.ok) {
|
|
2149
|
+
toast('已输入文字');
|
|
2150
|
+
} else {
|
|
2151
|
+
toast('输入失败: ' + (d.error || '未知错误'));
|
|
2152
|
+
}
|
|
2153
|
+
} catch (e) {
|
|
2154
|
+
toast('输入失败: ' + e.message);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// 按键
|
|
2159
|
+
async function pressKey(key) {
|
|
2160
|
+
try {
|
|
2161
|
+
const d = await api('/api/computer/keyboard/press', 'POST', { key });
|
|
2162
|
+
if (d.ok) {
|
|
2163
|
+
toast('已按键: ' + key);
|
|
2164
|
+
} else {
|
|
2165
|
+
toast('按键失败: ' + (d.error || '未知错误'));
|
|
2166
|
+
}
|
|
2167
|
+
} catch (e) {
|
|
2168
|
+
toast('按键失败: ' + e.message);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// 绑定电脑操作事件
|
|
2173
|
+
document.getElementById('screenCaptureBtn')?.addEventListener('click', captureScreen);
|
|
2174
|
+
document.getElementById('clickLeftBtn')?.addEventListener('click', () => mouseClick('left'));
|
|
2175
|
+
document.getElementById('clickRightBtn')?.addEventListener('click', () => mouseClick('right'));
|
|
2176
|
+
document.getElementById('doubleClickBtn')?.addEventListener('click', () => mouseClick('left', true));
|
|
2177
|
+
document.getElementById('typeTextInput')?.addEventListener('keydown', (e) => {
|
|
2178
|
+
if (e.key === 'Enter') {
|
|
2179
|
+
const text = e.target.value.trim();
|
|
2180
|
+
if (text) {
|
|
2181
|
+
typeText(text);
|
|
2182
|
+
e.target.value = '';
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
document.querySelectorAll('.computer-actions [data-key]').forEach((btn) => {
|
|
2187
|
+
btn.addEventListener('click', () => pressKey(btn.getAttribute('data-key') || ''));
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
/* ========== 记忆系统 ========== */
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
|
|
2197
|
+
// 当切换到记忆页面时初始化
|
|
2198
|
+
const origSwitchView = switchView;
|
|
2199
|
+
window.switchView = function(nav) {
|
|
2200
|
+
origSwitchView(nav);
|
|
2201
|
+
if (nav === 'memory') {
|
|
2202
|
+
setTimeout(initMemoryUI, 100);
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
if (state.token && state.phone) {
|
|
2207
|
+
document.getElementById('loginOverlay').style.display = 'none';
|
|
2208
|
+
document.getElementById('appLayout').classList.add('show');
|
|
2209
|
+
afterLogin();
|
|
2210
|
+
}
|