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.
Files changed (249) hide show
  1. package/.github/workflows/ci.yml +180 -140
  2. package/.github/workflows/release.yml +200 -173
  3. package/CHANGELOG.md +94 -115
  4. package/README.md +609 -586
  5. package/dist/agent/completion-engine.js +76 -45
  6. package/dist/agent/completion-engine.js.map +1 -1
  7. package/dist/agent/completion-postprocess.js +109 -0
  8. package/dist/agent/completion-postprocess.js.map +1 -0
  9. package/dist/agent/context-budget.js +121 -0
  10. package/dist/agent/context-budget.js.map +1 -0
  11. package/dist/agent/context-compactor.js +47 -0
  12. package/dist/agent/context-compactor.js.map +1 -1
  13. package/dist/agent/custom-agent.js +52 -52
  14. package/dist/agent/design-to-code.js +40 -40
  15. package/dist/agent/lint.js +121 -0
  16. package/dist/agent/lint.js.map +1 -0
  17. package/dist/agent/multi-agent.js +44 -44
  18. package/dist/agent/orchestrator.js +20 -20
  19. package/dist/agent/prompts.js +54 -54
  20. package/dist/agent/self-correction.js +24 -24
  21. package/dist/agent/self-heal.js +27 -27
  22. package/dist/cli/commands.js +9 -0
  23. package/dist/cli/commands.js.map +1 -1
  24. package/dist/cli/index.js +9 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/multi-pane-tui.js +217 -0
  27. package/dist/cli/multi-pane-tui.js.map +1 -0
  28. package/dist/cli/run.js +129 -40
  29. package/dist/cli/run.js.map +1 -1
  30. package/dist/cli/tui-run.js +79 -0
  31. package/dist/cli/tui-run.js.map +1 -0
  32. package/dist/cli/tui.js.map +1 -1
  33. package/dist/cli/version.js +1 -1
  34. package/dist/lsp/lsp-client.js +183 -0
  35. package/dist/lsp/lsp-client.js.map +1 -0
  36. package/dist/lsp/lsp-service.js +159 -0
  37. package/dist/lsp/lsp-service.js.map +1 -0
  38. package/dist/plugins/manager.js +50 -2
  39. package/dist/plugins/manager.js.map +1 -1
  40. package/dist/shared/i18n.js +108 -100
  41. package/dist/shared/i18n.js.map +1 -1
  42. package/dist/tools/generator/code-generator.js +40 -40
  43. package/dist/tools/generator/test-generator.js +9 -9
  44. package/dist/tools/shell/exec.js +23 -1
  45. package/dist/tools/shell/exec.js.map +1 -1
  46. package/dist/web/extra-apis.js +440 -0
  47. package/dist/web/extra-apis.js.map +1 -0
  48. package/dist/web/public/css/style.css +1715 -1715
  49. package/dist/web/public/index.html +1242 -1200
  50. package/dist/web/public/js/api.js +555 -496
  51. package/dist/web/public/js/app.js +2210 -2210
  52. package/dist/web/public/js/mock-api.js +234 -234
  53. package/dist/web/public/js/monaco-editor.js +363 -203
  54. package/dist/web/public/js/ui.js +2251 -1981
  55. package/dist/web/public/js/utils.js +214 -214
  56. package/dist/web/public/manifest.json +59 -59
  57. package/dist/web/public/service-worker.js +173 -173
  58. package/dist/web/server.js +97 -45
  59. package/dist/web/server.js.map +1 -1
  60. package/docs/BENCHMARK_REPORT_en.md +386 -329
  61. package/docs/BENCHMARK_REPORT_zh.md +386 -329
  62. package/docs/DPA.md +63 -0
  63. 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
  64. package/docs/SECURITY_WHITEPAPER.md +74 -0
  65. package/docs/SWE_BENCH_REPORT.md +55 -0
  66. package/docs/TECHNICAL_SPEC_en.md +292 -292
  67. package/docs/TECHNICAL_SPEC_zh.md +292 -292
  68. package/docs/UPDATE_NOTES_en.md +159 -159
  69. package/docs/UPDATE_NOTES_zh.md +159 -159
  70. package/docs/UPGRADE_7.2.0.md +309 -0
  71. package/docs/UPGRADE_GUIDE_7_5.md +86 -0
  72. package/docs/geo-feihong-code.html +391 -0
  73. 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
  74. 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
  75. 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
  76. 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
  77. package/docs//345/256/211/345/205/250/347/231/275/347/232/256/344/271/246_v7.2.0.md +82 -0
  78. package/electron/main.js +404 -602
  79. package/package.json +175 -172
  80. package/templates/skills/api-design/SKILL.md +27 -0
  81. package/templates/skills/code-review/SKILL.md +35 -0
  82. package/templates/skills/dependency-upgrade/SKILL.md +29 -0
  83. package/templates/skills/doc-gen/SKILL.md +29 -0
  84. package/templates/skills/git-flow/SKILL.md +26 -0
  85. package/templates/skills/onboarding/SKILL.md +27 -0
  86. package/templates/skills/performance/SKILL.md +29 -0
  87. package/templates/skills/refactor/SKILL.md +28 -0
  88. package/templates/skills/security-audit/SKILL.md +28 -0
  89. package/templates/skills/test-gen/SKILL.md +30 -0
  90. package/tool-schema.json +197 -197
  91. package/dist/hello.js +0 -14
  92. package/dist/hello.js.map +0 -1
  93. package/dist/self-evolve/hook.ts +0 -80
  94. package/dist/self-evolve/manager.d.ts +0 -8
  95. package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +0 -1
  96. package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +0 -1
  97. package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +0 -97
  98. package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +0 -30
  99. package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +0 -30
  100. package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +0 -474
  101. package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +0 -62
  102. package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +0 -67722
  103. package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +0 -1
  104. package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +0 -1
  105. package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +0 -1
  106. package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +0 -2
  107. package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +0 -1
  108. package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +0 -1
  109. package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +0 -1
  110. package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +0 -1
  111. package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +0 -1
  112. package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +0 -1
  113. package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +0 -3
  114. package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +0 -1
  115. package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +0 -1
  116. package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +0 -1
  117. package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +0 -1
  118. package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +0 -1
  119. package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +0 -1
  120. package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +0 -1
  121. package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +0 -1
  122. package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +0 -1
  123. package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +0 -831
  124. package/dist/web/public/vendor/monaco/vs/editor.js +0 -1
  125. package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +0 -35
  126. package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +0 -1
  127. package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +0 -1
  128. package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +0 -3
  129. package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +0 -1
  130. package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +0 -1
  131. package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +0 -1
  132. package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +0 -1
  133. package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +0 -1
  134. package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +0 -1
  135. package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +0 -1
  136. package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +0 -1
  137. package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +0 -1
  138. package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +0 -5
  139. package/dist/web/public/vendor/monaco/vs/index.js +0 -1
  140. package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +0 -1
  141. package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +0 -1
  142. package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +0 -1
  143. package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +0 -1
  144. package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +0 -1
  145. package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +0 -1
  146. package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +0 -1
  147. package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +0 -1
  148. package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +0 -66
  149. package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +0 -1
  150. package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +0 -443
  151. package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +0 -1
  152. package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +0 -17
  153. package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +0 -1
  154. package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +0 -1
  155. package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +0 -67693
  156. package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +0 -2
  157. package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +0 -1
  158. package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +0 -1
  159. package/dist/web/public/vendor/monaco/vs/loader.js +0 -1368
  160. package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +0 -2
  161. package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +0 -1
  162. package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +0 -1
  163. package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +0 -15
  164. package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +0 -3
  165. package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +0 -1
  166. package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +0 -1
  167. package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +0 -1
  168. package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +0 -1
  169. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +0 -1
  170. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +0 -1
  171. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +0 -1
  172. package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +0 -1
  173. package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +0 -1
  174. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +0 -1
  175. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +0 -9
  176. package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +0 -1
  177. package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +0 -9
  178. package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +0 -1
  179. package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +0 -9
  180. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +0 -1
  181. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +0 -9
  182. package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +0 -1
  183. package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +0 -9
  184. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +0 -1
  185. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +0 -9
  186. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +0 -1
  187. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +0 -9
  188. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +0 -1
  189. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +0 -9
  190. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +0 -1
  191. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +0 -9
  192. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +0 -1
  193. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +0 -9
  194. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +0 -1
  195. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +0 -9
  196. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +0 -1
  197. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +0 -9
  198. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +0 -1
  199. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +0 -9
  200. package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +0 -1
  201. package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +0 -1
  202. package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +0 -1
  203. package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +0 -1
  204. package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +0 -1
  205. package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +0 -1
  206. package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +0 -1
  207. package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +0 -1
  208. package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +0 -1
  209. package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +0 -1
  210. package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +0 -1
  211. package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +0 -2
  212. package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +0 -1
  213. package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +0 -1
  214. package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +0 -1
  215. package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +0 -1
  216. package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +0 -1
  217. package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +0 -1
  218. package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +0 -1
  219. package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +0 -1
  220. package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +0 -1
  221. package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +0 -1
  222. package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +0 -1
  223. package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +0 -1
  224. package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +0 -1
  225. package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +0 -3
  226. package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +0 -1
  227. package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +0 -1
  228. package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +0 -1
  229. package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +0 -1
  230. package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +0 -1
  231. package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +0 -1
  232. package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +0 -1
  233. package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +0 -1
  234. package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +0 -1
  235. package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +0 -61
  236. package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +0 -1
  237. package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +0 -11
  238. package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +0 -1
  239. package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +0 -1
  240. package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +0 -1
  241. package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +0 -1
  242. package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +0 -298
  243. package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +0 -1
  244. package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +0 -1
  245. package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +0 -1
  246. package/dist/web/web-config.js +0 -143
  247. package/dist/web/web-config.js.map +0 -1
  248. package/docs/DEPLOYMENT.md +0 -382
  249. package/docs/MODEL_FINETUNING.md +0 -405
@@ -1,1981 +1,2251 @@
1
- /**
2
- * UI 渲染模块:DOM 渲染与交互,依赖 utils 和 api
3
- */
4
-
5
- function initMemoryUI() {
6
- // 设置默认日期为今天
7
- const today = new Date().toISOString().split('T')[0];
8
- document.getElementById('memDateInput').value = today;
9
- loadMemoryContent(today);
10
-
11
- // 事件只绑定一次(多次进出记忆页会累积监听器,导致一次点击多次请求)
12
- if (!window._memUIInited) {
13
- window._memUIInited = true;
14
-
15
- // Tab 切换
16
- document.querySelectorAll('.mem-tab').forEach(tab => {
17
- tab.addEventListener('click', () => {
18
- document.querySelectorAll('.mem-tab').forEach(t => t.classList.remove('active'));
19
- document.querySelectorAll('.mem-panel').forEach(p => p.classList.toggle('active', false));
20
- tab.classList.add('active');
21
- const panelId = tab.getAttribute('data-memtab') + 'Panel';
22
- document.getElementById(panelId)?.classList.add('active');
23
- });
24
- });
25
-
26
- // 查看按钮
27
- document.getElementById('memLoadDay')?.addEventListener('click', () => {
28
- const dateStr = document.getElementById('memDateInput').value;
29
- if (dateStr) loadMemoryContent(dateStr);
30
- });
31
-
32
- // 刷新按钮
33
- document.getElementById('memRefreshBtn')?.addEventListener('click', () => {
34
- loadMemoryStats();
35
- const dateStr = document.getElementById('memDateInput').value;
36
- if (dateStr) loadMemoryContent(dateStr);
37
- loadLongTermMemory();
38
- });
39
-
40
- // 立即总结按钮
41
- document.getElementById('memSummarizeBtn')?.addEventListener('click', async () => {
42
- const btn = document.getElementById('memSummarizeBtn');
43
- btn.disabled = true;
44
- btn.textContent = '⏳ 总结中...';
45
- try {
46
- const dateStr = document.getElementById('memDateInput').value || today;
47
- const d = await api('/api/memory/summarize', 'POST', { date: dateStr });
48
- toast('✅ ' + (d.message || '总结完成'));
49
- loadMemoryStats();
50
- loadSummaryHistory();
51
- loadLongTermMemory();
52
- } catch (e) {
53
- toast('总结失败:' + e.message);
54
- } finally {
55
- btn.disabled = false;
56
- btn.textContent = '✨ 立即总结';
57
- }
58
- });
59
-
60
- // 短期记忆:添加记录按钮(展开文本框)
61
- document.getElementById('memAddShort')?.addEventListener('click', () => {
62
- const editor = document.getElementById('memShortEditor');
63
- const input = document.getElementById('memShortInput');
64
- if (editor) {
65
- editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
66
- if (editor.style.display !== 'none' && input) {
67
- input.value = '';
68
- input.focus();
69
- }
70
- }
71
- });
72
- // 短期记忆:取消
73
- document.getElementById('memShortCancel')?.addEventListener('click', () => {
74
- const editor = document.getElementById('memShortEditor');
75
- if (editor) editor.style.display = 'none';
76
- });
77
- // 短期记忆:保存
78
- document.getElementById('memShortSave')?.addEventListener('click', async () => {
79
- const input = document.getElementById('memShortInput');
80
- const content = input ? input.value.trim() : '';
81
- if (!content) { toast('请输入记录内容'); return; }
82
- try {
83
- await api('/api/memory/short', 'POST', {
84
- title: content.slice(0, 30) + (content.length > 30 ? '...' : ''),
85
- type: 'note',
86
- content: content,
87
- });
88
- toast('已添加到短期记忆');
89
- const editor = document.getElementById('memShortEditor');
90
- if (editor) editor.style.display = 'none';
91
- const dateStr = document.getElementById('memDateInput').value;
92
- if (dateStr) loadMemoryContent(dateStr);
93
- loadMemoryStats();
94
- } catch (e) {
95
- toast('添加失败:' + e.message);
96
- }
97
- });
98
-
99
- // 长期记忆:编辑按钮
100
- document.getElementById('memEditLong')?.addEventListener('click', () => {
101
- const content = document.getElementById('memLongContent');
102
- const editor = document.getElementById('memLongEditor');
103
- const editBtn = document.getElementById('memEditLong');
104
- const saveBtn = document.getElementById('memSaveLong');
105
- const cancelBtn = document.getElementById('memCancelLong');
106
- const appendBtn = document.getElementById('memAppendLong');
107
- const appendEditor = document.getElementById('memLongAppendEditor');
108
- // 把当前内容放到编辑器
109
- if (editor && content) {
110
- editor.value = content.innerText || content.textContent || '';
111
- }
112
- if (content) content.style.display = 'none';
113
- if (editor) editor.style.display = 'block';
114
- if (editBtn) editBtn.style.display = 'none';
115
- if (appendBtn) appendBtn.style.display = 'none';
116
- if (appendEditor) appendEditor.style.display = 'none';
117
- if (saveBtn) saveBtn.style.display = '';
118
- if (cancelBtn) cancelBtn.style.display = '';
119
- });
120
- // 长期记忆:取消编辑
121
- document.getElementById('memCancelLong')?.addEventListener('click', () => {
122
- const content = document.getElementById('memLongContent');
123
- const editor = document.getElementById('memLongEditor');
124
- const editBtn = document.getElementById('memEditLong');
125
- const saveBtn = document.getElementById('memSaveLong');
126
- const cancelBtn = document.getElementById('memCancelLong');
127
- const appendBtn = document.getElementById('memAppendLong');
128
- if (content) content.style.display = '';
129
- if (editor) editor.style.display = 'none';
130
- if (editBtn) editBtn.style.display = '';
131
- if (appendBtn) appendBtn.style.display = '';
132
- if (saveBtn) saveBtn.style.display = 'none';
133
- if (cancelBtn) cancelBtn.style.display = 'none';
134
- });
135
- // 长期记忆:保存编辑
136
- document.getElementById('memSaveLong')?.addEventListener('click', async () => {
137
- const editor = document.getElementById('memLongEditor');
138
- const content = editor ? editor.value : '';
139
- try {
140
- await api('/api/memory/long', 'POST', { content });
141
- toast('长期记忆已保存');
142
- document.getElementById('memCancelLong')?.click();
143
- loadLongTermMemory();
144
- loadMemoryStats();
145
- } catch (e) {
146
- toast('保存失败:' + e.message);
147
- }
148
- });
149
- // 长期记忆:追加按钮(展开文本框)
150
- document.getElementById('memAppendLong')?.addEventListener('click', () => {
151
- const editor = document.getElementById('memLongAppendEditor');
152
- const titleInput = document.getElementById('memAppendTitle');
153
- const contentInput = document.getElementById('memAppendContent');
154
- if (editor) {
155
- editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
156
- if (editor.style.display !== 'none') {
157
- if (titleInput) titleInput.value = '';
158
- if (contentInput) contentInput.value = '';
159
- if (titleInput) titleInput.focus();
160
- }
161
- }
162
- });
163
- // 长期记忆:取消追加
164
- document.getElementById('memAppendCancel')?.addEventListener('click', () => {
165
- const editor = document.getElementById('memLongAppendEditor');
166
- if (editor) editor.style.display = 'none';
167
- });
168
- // 长期记忆:确认追加
169
- document.getElementById('memAppendSave')?.addEventListener('click', async () => {
170
- const titleInput = document.getElementById('memAppendTitle');
171
- const contentInput = document.getElementById('memAppendContent');
172
- const title = titleInput ? titleInput.value.trim() : '';
173
- const content = contentInput ? contentInput.value.trim() : '';
174
- if (!title || !content) { toast('请输入标题和内容'); return; }
175
- try {
176
- await api('/api/memory/long/append', 'POST', { title, category: '自定义', content });
177
- toast('已追加到长期记忆');
178
- const editor = document.getElementById('memLongAppendEditor');
179
- if (editor) editor.style.display = 'none';
180
- loadLongTermMemory();
181
- loadMemoryStats();
182
- } catch (e) {
183
- toast('追加失败:' + e.message);
184
- }
185
- });
186
- }
187
-
188
- // 每次进入都刷新数据(事件不重复绑定,数据实时刷新)
189
- loadMemoryStats();
190
- loadLongTermMemory();
191
- loadSummaryHistory();
192
- }
193
-
194
- function renderMarketGrid() {
195
- const grid = document.getElementById('marketGrid');
196
- if (!state.market.length) { renderEmpty(grid, t('empty.no_skills')); return; }
197
- grid.innerHTML = state.market.map((s) => {
198
- const installed = state.installed.has(s.id);
199
- const btn = installed
200
- ? '<button class="install" disabled style="opacity:.55;cursor:default;">✅ 已安装</button>'
201
- : '<button class="install" data-id="' + encodeURIComponent(s.id) + '" data-name="' + encodeURIComponent(s.name) + '" data-source="' + s.source + '">安装</button>';
202
- return '<div class="tile"><div class="icon">' + (s.source === 'clawhub' ? '🧩' : '🛠') + '</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description).slice(0, 70) + '…<br><span class="muted">' + (s.source === 'clawhub' ? 'ClawHub' : 'Agent-Foundry') + ' · 下载 ' + (s.downloads || 0) + '</span></div><div class="ops">' + btn + '</div></div>';
203
- }).join('');
204
- grid.querySelectorAll('button.install').forEach((b) => b.addEventListener('click', (e) => {
205
- e.stopPropagation();
206
- installSkill(b.getAttribute('data-id'), b.getAttribute('data-name'), b.getAttribute('data-source'));
207
- }));
208
- }
209
-
210
- function bindTplCards(grid) {
211
- grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
212
- e.stopPropagation();
213
- const inp = document.getElementById('goalInput');
214
- if (inp) inp.value = decodeURIComponent(b.getAttribute('data-goal'));
215
- switchView('chat');
216
- if (inp) inp.focus();
217
- toast('已填入输入框,去发送吧');
218
- }));
219
- grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', async (e) => {
220
- e.stopPropagation();
221
- try { await api('/api/templates/' + b.getAttribute('data-id'), 'DELETE'); await loadTemplates(); toast('已删除'); }
222
- catch (err) { toast('删除失败:' + err.message); }
223
- }));
224
- }
225
-
226
- function renderUserTpl(list) {
227
- const grid = document.getElementById('userTplGrid');
228
- if (!list.length) { renderEmpty(grid, t('empty.no_templates')); return; }
229
- grid.innerHTML = list.map((t) => tplCard(t, true)).join('');
230
- bindTplCards(grid);
231
- }
232
-
233
- function renderBuiltin(list) {
234
- const grid = document.getElementById('builtinGrid');
235
- if (!list.length) { renderEmpty(grid, t('empty.no_builtin_templates')); return; }
236
- grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
237
- bindTplCards(grid);
238
- }
239
-
240
- function tplCard(t, deletable) {
241
- return '<div class="tile" data-goal="' + encodeURIComponent(t.goal) + '"><div class="icon">' + (t.icon || '📄') + '</div><div class="title">' + escapeHtml(t.title) + '</div><div class="desc">' + escapeHtml(t.category || '') + ' · ' + escapeHtml(t.goal).slice(0, 40) + '…</div><div class="ops"><button class="use" data-goal="' + encodeURIComponent(t.goal) + '">填入输入框</button>' + (deletable ? '<button class="ghost del" data-id="' + t.id + '">删除</button>' : '') + '</div></div>';
242
- }
243
-
244
- function renderBuiltinAutomations(list) {
245
- const grid = document.getElementById('builtinAutoGrid');
246
- if (!grid) return;
247
- if (!list.length) { renderEmpty(grid, '暂无预置指令'); return; }
248
- grid.innerHTML = list.map((a) => {
249
- const icon = a.icon || '⚡';
250
- const category = a.category || '常用';
251
- const goalPreview = (a.goal || '').slice(0, 50) + ((a.goal || '').length > 50 ? '…' : '');
252
- return '<div class="tile builtin-tile" data-id="' + escapeHtml(a.id) + '">'
253
- + '<div class="icon">' + icon + '</div>'
254
- + '<div class="title">' + escapeHtml(a.name) + '</div>'
255
- + '<div class="desc"><span style="color:var(--brand);font-weight:500;">' + escapeHtml(category) + '</span> · ' + escapeHtml(goalPreview) + '</div>'
256
- + '<div class="ops">'
257
- + '<button class="run builtin-run" data-id="' + escapeHtml(a.id) + '">▶ 运行</button>'
258
- + '<button class="ghost builtin-save" data-id="' + escapeHtml(a.id) + '" data-name="' + escapeHtml(a.name) + '" data-goal="' + encodeURIComponent(a.goal || '') + '">+ 保存为我的</button>'
259
- + '</div></div>';
260
- }).join('');
261
- grid.querySelectorAll('button.builtin-run').forEach((b) => b.addEventListener('click', (e) => {
262
- e.stopPropagation();
263
- runAuto(b.getAttribute('data-id'));
264
- }));
265
- grid.querySelectorAll('button.builtin-save').forEach((b) => b.addEventListener('click', (e) => {
266
- e.stopPropagation();
267
- const name = b.getAttribute('data-name') || '';
268
- const goal = decodeURIComponent(b.getAttribute('data-goal') || '');
269
- saveBuiltinAsMine(name, goal);
270
- }));
271
- }
272
-
273
- async function saveBuiltinAsMine(name, goal) {
274
- try {
275
- await api('/api/automations', 'POST', { name, goal });
276
- await loadAutomations();
277
- toast('已保存到「我的指令」,可在那里编辑');
278
- } catch (e) { toast('保存失败:' + e.message); }
279
- }
280
-
281
- function renderAutoGrid(list) {
282
- const grid = document.getElementById('autoGrid');
283
- if (!list.length) { renderEmpty(grid, t('empty.no_automations')); return; }
284
- grid.innerHTML = list.map((a) => {
285
- const icon = a.icon || '⚡';
286
- const category = a.category ? ' · ' + escapeHtml(a.category) : '';
287
- return '<div class="tile" data-id="' + a.id + '">'
288
- + '<div class="icon">' + icon + '</div>'
289
- + '<div class="title">' + escapeHtml(a.name) + category + '</div>'
290
- + '<div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div>'
291
- + '<div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>';
292
- }).join('');
293
- grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
294
- grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
295
- }
296
-
297
- /* ========== 节点模板 & 自定义来源模板 ========== */
298
- function renderNodeTpl(list) {
299
- const grid = document.getElementById('nodeTplGrid');
300
- const title = document.getElementById('nodeTplTitle');
301
- if (!grid) return;
302
- if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
303
- if (title) title.style.display = '';
304
- grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
305
- bindTplCards(grid);
306
- }
307
- function renderCustomTpl(list) {
308
- const grid = document.getElementById('customTplGrid');
309
- const title = document.getElementById('customTplTitle');
310
- if (!grid) return;
311
- if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
312
- if (title) title.style.display = '';
313
- grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
314
- bindTplCards(grid);
315
- }
316
-
317
- /* ========== 节点管理渲染 ========== */
318
- function renderNodesGrid(list) {
319
- const grid = document.getElementById('nodesGrid');
320
- if (!grid) return;
321
- window._nodesCache = list;
322
- if (!list.length) { grid.innerHTML = '<div class="empty">还没有添加节点,点击上方「添加节点」连接外部服务</div>'; return; }
323
- grid.innerHTML = list.map((n) => {
324
- const statusColor = n.status === 'connected' ? 'var(--ok)' : n.status === 'error' ? 'var(--err)' : 'var(--muted)';
325
- const statusText = n.status === 'connected' ? '已连接' : n.status === 'error' ? '连接失败' : '未连接';
326
- const caps = (n.capabilities || []).map((c) => c === 'templates' ? '模板' : c === 'skills' ? '插件' : '办公').join('、');
327
- return '<div class="tile node-tile" data-id="' + n.id + '">'
328
- + '<div class="icon">' + (n.type === 'http' ? '🌐' : n.type === 'local' ? '📁' : '📦') + '</div>'
329
- + '<div class="title">' + escapeHtml(n.name) + ' <span style="font-size:11px;color:' + statusColor + ';">● ' + statusText + '</span></div>'
330
- + '<div class="desc">' + escapeHtml(n.url).slice(0, 50) + '…<br>能力:' + caps + (n.lastSyncAt ? '<br>上次同步:' + fmtTime(n.lastSyncAt) : '') + (n.lastError ? '<br style="color:var(--err);">错误:' + escapeHtml(n.lastError) : '') + '</div>'
331
- + '<div class="ops" style="flex-wrap:wrap;">'
332
- + '<button class="run node-sync" data-id="' + n.id + '">🔄 同步</button>'
333
- + '<button class="ghost node-test" data-id="' + n.id + '">测试</button>'
334
- + '<button class="ghost node-edit" data-id="' + n.id + '">编辑</button>'
335
- + '<button class="ghost del node-del" data-id="' + n.id + '">删除</button>'
336
- + '</div></div>';
337
- }).join('');
338
- grid.querySelectorAll('button.node-sync').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); syncNode(b.getAttribute('data-id')); }));
339
- grid.querySelectorAll('button.node-test').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); testNode(b.getAttribute('data-id')); }));
340
- grid.querySelectorAll('button.node-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editNode(b.getAttribute('data-id')); }));
341
- grid.querySelectorAll('button.node-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此节点?')) deleteNode(b.getAttribute('data-id')); }));
342
- }
343
-
344
- /* ========== 自定义来源渲染 ========== */
345
- function renderSourcesGrid(list) {
346
- const grid = document.getElementById('sourcesGrid');
347
- if (!grid) return;
348
- window._sourcesCache = list;
349
- if (!list.length) { grid.innerHTML = '<div class="empty">还没有自定义来源,点击上方「自定义来源」添加远程数据源</div>'; return; }
350
- const typeMap = { templates: '模板', skills: '插件', office: '办公' };
351
- grid.innerHTML = list.map((s) => '<div class="tile" data-id="' + s.id + '">'
352
- + '<div class="icon">🔗</div>'
353
- + '<div class="title">' + escapeHtml(s.name) + ' <span style="font-size:11px;color:var(--brand);">' + (typeMap[s.type] || s.type) + '</span></div>'
354
- + '<div class="desc">' + escapeHtml(s.url).slice(0, 60) + '…<br>状态:' + (s.enabled ? '已启用' : '已禁用') + '</div>'
355
- + '<div class="ops">'
356
- + '<button class="ghost source-edit" data-id="' + s.id + '">编辑</button>'
357
- + '<button class="ghost del source-del" data-id="' + s.id + '">删除</button>'
358
- + '</div></div>').join('');
359
- grid.querySelectorAll('button.source-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editSource(b.getAttribute('data-id')); }));
360
- grid.querySelectorAll('button.source-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此来源?')) deleteSource(b.getAttribute('data-id')); }));
361
- }
362
-
363
- function renderWorkspaceBar() {
364
- const el = document.getElementById('workspaceCwdBottom');
365
- if (el) el.textContent = state.workspaceDir || '(未设置)';
366
- }
367
-
368
- function openPermissions() {
369
- const p = state.permissions;
370
- document.querySelector('input[name="readScope"][value="' + p.readScope + '"]').checked = true;
371
- document.getElementById('permReadPath').value = p.readPath || '';
372
- document.getElementById('permRead').checked = p.allowRead;
373
- document.getElementById('permWrite').checked = p.allowWrite;
374
- document.getElementById('permShell').checked = p.allowShell;
375
- document.getElementById('permNetwork').checked = p.allowNetwork;
376
- document.getElementById('permBrowser').checked = p.allowBrowser;
377
- openModal('permModal');
378
- }
379
-
380
- function openAgentSelector() {
381
- const grid = document.getElementById('agentGrid');
382
- grid.innerHTML = Object.entries(AGENT_TYPES).map(([k, v]) =>
383
- '<div class="agent-card ' + (state.agentType === k ? 'selected' : '') + '" data-type="' + k + '"><div class="icon">' + v.icon + '</div><div class="title">' + v.label + '</div></div>'
384
- ).join('');
385
- grid.querySelectorAll('.agent-card').forEach((c) => c.addEventListener('click', () => {
386
- state.agentType = c.getAttribute('data-type');
387
- updateUserBar();
388
- closeModal('agentModal');
389
- toast('已切换:' + AGENT_TYPES[state.agentType].label);
390
- }));
391
- openModal('agentModal');
392
- }
393
-
394
- function fillInlineForm(id) {
395
- const m = state.models.find((x) => x.id === id);
396
- if (!m) return;
397
- document.getElementById('imName').value = m.name;
398
- document.getElementById('imBase').value = m.apiBase || '';
399
- document.getElementById('imKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
400
- document.getElementById('imReasoning').value = m.reasoning || '';
401
- document.getElementById('imMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
402
- document.getElementById('imMsg').classList.remove('err');
403
- document.getElementById('imSave').dataset.editing = id;
404
- }
405
-
406
- function resetInlineForm() {
407
- document.getElementById('imName').value = '';
408
- document.getElementById('imBase').value = '';
409
- document.getElementById('imKey').value = '';
410
- document.getElementById('imReasoning').value = '';
411
- document.getElementById('imMsg').textContent = '';
412
- document.getElementById('imMsg').classList.remove('err');
413
- document.getElementById('imSave').dataset.editing = '';
414
- }
415
-
416
- function renderModelListInline() {
417
- const list = document.getElementById('modelListInline');
418
- if (!state.models.length) {
419
- list.innerHTML =
420
- '<div class="mm-empty">' +
421
- '<div class="mm-empty-icon">🧠</div>' +
422
- '<div>还没有模型配置</div>' +
423
- '<div class="mm-empty-hint">在下方添加第一个模型,保存后即可在对话中使用</div>' +
424
- '</div>';
425
- return;
426
- }
427
- list.innerHTML = state.models.map((m) => {
428
- const url = (m.apiBase || '').trim() || '(未填写 API 地址)';
429
- const meta = m.reasoning ? '已配置推理中间环节' : '未配置推理中间环节';
430
- const tag = m.default ? '<span class="mm-default-tag">默认</span>' : '';
431
- const defaultBtn = m.default ? '' : '<button class="mm-act mm-act-primary" data-act="default">设为默认</button>';
432
- return (
433
- '<div class="mm-card" data-id="' + escapeHtml(m.id) + '">' +
434
- '<div class="mm-card-head">' +
435
- '<span class="mm-card-name">' + escapeHtml(m.name) + '</span>' +
436
- tag +
437
- '</div>' +
438
- '<div class="mm-card-url">' + escapeHtml(url) + '</div>' +
439
- '<div class="mm-card-meta">' + meta + '</div>' +
440
- '<div class="mm-card-actions">' +
441
- defaultBtn +
442
- '<button class="mm-act" data-act="edit">编辑</button>' +
443
- '<button class="mm-act mm-act-danger" data-act="del">删除</button>' +
444
- '</div>' +
445
- '</div>'
446
- );
447
- }).join('');
448
- list.querySelectorAll('.mm-card').forEach((el) => {
449
- const id = el.getAttribute('data-id');
450
- el.querySelectorAll('button').forEach((b) => {
451
- b.addEventListener('click', (e) => {
452
- e.stopPropagation();
453
- const act = b.getAttribute('data-act');
454
- if (act === 'default') setDefaultModel(id);
455
- else if (act === 'edit') fillInlineForm(id);
456
- else if (act === 'del') deleteModel(id);
457
- });
458
- });
459
- });
460
- }
461
-
462
- function toggleModelManage() {
463
- const box = document.getElementById('modelManage');
464
- const open = box.style.display === 'none' || !box.style.display;
465
- box.style.display = open ? '' : 'none';
466
- if (open) { renderModelListInline(); resetInlineForm(); }
467
- }
468
-
469
- function openSettings() {
470
- // 打开设置时同步会话开关状态,避免界面显示与真实配置不一致
471
- document.querySelectorAll('.session-toggle').forEach((el) => {
472
- const key = el.dataset.key;
473
- if (key && key in state.sessionConfig) el.checked = !!state.sessionConfig[key];
474
- });
475
- openModal('settingsModal');
476
- }
477
-
478
- function persistSessionConfig() {
479
- localStorage.setItem('fhcode.sessionConfig', JSON.stringify(state.sessionConfig));
480
- }
481
-
482
- function editModel(id) {
483
- const m = state.models.find((x) => x.id === id);
484
- if (!m) return;
485
- document.getElementById('modelName').value = m.name;
486
- document.getElementById('modelApiBase').value = m.apiBase || '';
487
- document.getElementById('modelApiKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
488
- document.getElementById('modelReasoning').value = m.reasoning || '';
489
- document.getElementById('modelFormMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
490
- document.getElementById('modelFormMsg').classList.remove('err');
491
- document.getElementById('modelSaveBtn').dataset.editing = id;
492
- }
493
-
494
- function resetModelForm() {
495
- document.getElementById('modelName').value = '';
496
- document.getElementById('modelApiBase').value = '';
497
- document.getElementById('modelApiKey').value = '';
498
- document.getElementById('modelReasoning').value = '';
499
- document.getElementById('modelFormMsg').textContent = '';
500
- document.getElementById('modelFormMsg').classList.remove('err');
501
- document.getElementById('modelSaveBtn').dataset.editing = '';
502
- }
503
-
504
- function renderModelList() {
505
- const list = document.getElementById('modelList');
506
- if (!state.models.length) {
507
- renderEmpty(list, t('empty.no_models'));
508
- return;
509
- }
510
- list.innerHTML = state.models.map((m) => (
511
- '<div class="model-item" data-id="' + escapeHtml(m.id) + '">' +
512
- '<div><div class="mi-name">' + escapeHtml(m.name) + '</div>' +
513
- '<div class="mi-base">' + escapeHtml(m.apiBase || '(未填写 API 地址)') + (m.reasoning ? ' · 已配推理内容' : '') + '</div></div>' +
514
- (m.default ? '<span class="mi-default">默认</span>' : '') +
515
- '<div class="mi-actions">' +
516
- (m.default ? '' : '<button data-act="default">设为默认</button>') +
517
- '<button data-act="edit">编辑</button>' +
518
- '<button class="danger" data-act="del">删除</button>' +
519
- '</div>' +
520
- '</div>'
521
- )).join('');
522
- list.querySelectorAll('.model-item').forEach((el) => {
523
- const id = el.getAttribute('data-id');
524
- el.querySelectorAll('button').forEach((b) => {
525
- b.addEventListener('click', (e) => {
526
- e.stopPropagation();
527
- const act = b.getAttribute('data-act');
528
- if (act === 'default') setDefaultModel(id);
529
- else if (act === 'edit') editModel(id);
530
- else if (act === 'del') deleteModel(id);
531
- });
532
- });
533
- });
534
- }
535
-
536
- function openModels() {
537
- renderModelList();
538
- resetModelForm();
539
- openModal('modelModal');
540
- }
541
-
542
- function renderModelSelect() {
543
- const sel = document.getElementById('modelSelect');
544
- const opts = ['<option value="">默认(系统路由)</option>'];
545
- state.models.forEach((m) => {
546
- opts.push('<option value="' + escapeHtml(m.id) + '">' + escapeHtml(m.name) + (m.default ? ' ★' : '') + '</option>');
547
- });
548
- sel.innerHTML = opts.join('');
549
- sel.value = state.modelId || '';
550
- }
551
-
552
- function toggleDirectMode() {
553
- state.directMode = !state.directMode;
554
- document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
555
- const computerTab = document.getElementById('computerTab');
556
- if (computerTab) {
557
- computerTab.style.display = state.directMode ? '' : 'none';
558
- if (state.directMode) {
559
- // 切换到电脑操作标签页
560
- document.querySelectorAll('.right-tab').forEach(t => t.classList.remove('active'));
561
- computerTab.classList.add('active');
562
- document.querySelectorAll('.preview-panel').forEach(p => p.classList.remove('active'));
563
- document.getElementById('computerPanel')?.classList.add('active');
564
- }
565
- }
566
- toast(state.directMode ? '已开启「电脑操作」模式,可以用语言控制电脑' : '已关闭「电脑操作」模式');
567
- }
568
-
569
- function previewImage(path) {
570
- document.getElementById('previewTitle').textContent = '图片: ' + path;
571
- const box = document.getElementById('previewContent');
572
- const img = document.createElement('img');
573
- img.src = 'file://' + path;
574
- img.style.maxWidth = '100%';
575
- img.style.borderRadius = '8px';
576
- img.addEventListener('error', () => {
577
- box.innerHTML = '<div class="muted">无法直接显示本地图片,请用系统相册打开</div>';
578
- });
579
- box.innerHTML = '';
580
- box.appendChild(img);
581
- switchRightTab('preview');
582
- }
583
-
584
- async function previewFile(path) {
585
- try {
586
- const d = await api('/api/files/read', 'POST', { path });
587
- showPreview('文件: ' + path, d.content, 'file');
588
- switchRightTab('preview');
589
- } catch (e) { toast('预览失败:' + e.message); }
590
- }
591
-
592
- function bindArtifacts(container) {
593
- container.querySelectorAll('.artifact').forEach((el) => {
594
- el.addEventListener('click', () => {
595
- const kind = el.getAttribute('data-kind');
596
- const path = el.getAttribute('data-path');
597
- if (kind === 'url') openBrowser(path);
598
- else previewFile(path);
599
- });
600
- });
601
- container.querySelectorAll('.file-chip').forEach((el) => {
602
- el.addEventListener('click', () => {
603
- const path = el.getAttribute('data-path');
604
- const mime = el.getAttribute('data-type') || '';
605
- if (mime.startsWith('image/')) previewImage(path);
606
- else previewFile(path);
607
- });
608
- });
609
- }
610
-
611
- function renderStagedFiles() {
612
- const box = document.getElementById('stagedArea');
613
- if (!box) return;
614
- if (!state.stagedFiles.length) {
615
- box.classList.remove('show');
616
- box.innerHTML = '';
617
- return;
618
- }
619
- box.classList.add('show');
620
- box.innerHTML = state.stagedFiles.map((f) => {
621
- const isImg = (f.mime || '').startsWith('image/');
622
- const icon = isImg ? '🖼' : '📎';
623
- return (
624
- '<div class="staged-chip" data-id="' + escapeHtml(f.id) + '">' +
625
- '<span>' + icon + '</span>' +
626
- '<span class="staged-name">' + escapeHtml(f.name) + '</span>' +
627
- '<span class="staged-size">' + formatSize(f.size) + '</span>' +
628
- '<button class="staged-remove" data-act="remove" title="移除">×</button>' +
629
- '</div>'
630
- );
631
- }).join('') + '<span class="staged-hint">💡 点发送时一起上传</span>';
632
- box.querySelectorAll('.staged-remove').forEach((b) => {
633
- b.addEventListener('click', (e) => {
634
- e.stopPropagation();
635
- const chip = b.closest('.staged-chip');
636
- if (chip) removeStagedFile(chip.getAttribute('data-id'));
637
- });
638
- });
639
- }
640
-
641
- function showPreview(title, content, mode) {
642
- document.getElementById('previewTitle').textContent = title;
643
- const box = document.getElementById('previewContent');
644
- const isCode = mode === 'code' || mode === 'file';
645
- // 从标题提取文件路径(标题格式:"文件: /path/to/file")
646
- const filePath = title.startsWith('文件: ') ? title.slice(4).trim() : '';
647
- if (isCode) {
648
- box.innerHTML =
649
- '<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;align-items:center;">' +
650
- '<button class="ghost-btn" id="previewEditBtn" style="font-size:11px;padding:3px 10px;">✏️ 编辑</button>' +
651
- '<button class="ghost-btn" id="previewStageBtn" style="font-size:11px;padding:3px 10px;display:none;background:#2d5a3d;color:#fff;">📝 暂存变更</button>' +
652
- '<button class="ghost-btn" id="previewCancelBtn" style="font-size:11px;padding:3px 10px;display:none;">取消</button>' +
653
- '<span class="muted" id="previewEditHint" style="font-size:11px;"></span>' +
654
- '<span class="muted" id="monacoStatus" style="font-size:10px;margin-left:auto;"></span>' +
655
- '</div>' +
656
- // P0-1: Monaco 编辑器容器(VSCode 同款内核,语法高亮+智能补全)
657
- '<div id="monacoContainer" style="width:100%;height:500px;border:1px solid var(--border);border-radius:6px;overflow:hidden;"></div>';
658
- const editBtn = document.getElementById('previewEditBtn');
659
- const stageBtn = document.getElementById('previewStageBtn');
660
- const cancelBtn = document.getElementById('previewCancelBtn');
661
- const hint = document.getElementById('previewEditHint');
662
- const monacoStatus = document.getElementById('monacoStatus');
663
- let monacoEditor = null;
664
- let originalContent = content;
665
- let isEditing = false;
666
-
667
- // 初始化 Monaco 只读视图(语法高亮)
668
- async function initMonacoReadOnly() {
669
- if (typeof FHMonaco === 'undefined') {
670
- monacoStatus.textContent = 'Monaco 未加载';
671
- return;
672
- }
673
- try {
674
- monacoStatus.textContent = '加载编辑器…';
675
- monacoEditor = await FHMonaco.createEditor('monacoContainer', content, filePath || 'untitled', {
676
- readOnly: true,
677
- domReadOnly: true,
678
- minimap: { enabled: content.length > 500 },
679
- });
680
- monacoStatus.textContent = '';
681
- } catch (e) {
682
- monacoStatus.textContent = '编辑器加载失败: ' + e.message;
683
- }
684
- }
685
-
686
- // 切换到编辑模式
687
- async function enterEditMode() {
688
- registerMonacoCompletions();
689
- if (!monacoEditor) {
690
- // 如果只读模式还没初始化,先创建可编辑实例
691
- monacoEditor = await FHMonaco.createEditor('monacoContainer', originalContent, filePath || 'untitled', {
692
- readOnly: false,
693
- minimap: { enabled: true },
694
- });
695
- } else {
696
- monacoEditor.updateOptions({ readOnly: false, domReadOnly: false });
697
- }
698
- isEditing = true;
699
- editBtn.style.display = 'none';
700
- stageBtn.style.display = 'inline-block';
701
- cancelBtn.style.display = 'inline-block';
702
- hint.textContent = filePath ? '编辑后点「暂存变更」· Tab 接受内联补全 · Ctrl+Space 选择补全' : '(未关联文件路径)';
703
- monacoStatus.textContent = '编辑模式';
704
- // 聚焦到编辑器末尾
705
- monacoEditor.focus();
706
- const model = monacoEditor.getModel();
707
- const lastLine = model.getLineCount();
708
- monacoEditor.setPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) });
709
- }
710
-
711
- // 退出编辑模式(取消)
712
- function exitEditMode() {
713
- if (monacoEditor) {
714
- monacoEditor.setValue(originalContent);
715
- monacoEditor.updateOptions({ readOnly: true, domReadOnly: true });
716
- }
717
- isEditing = false;
718
- editBtn.style.display = 'inline-block';
719
- stageBtn.style.display = 'none';
720
- cancelBtn.style.display = 'none';
721
- hint.textContent = '';
722
- monacoStatus.textContent = '';
723
- }
724
-
725
- editBtn.addEventListener('click', enterEditMode);
726
- cancelBtn.addEventListener('click', exitEditMode);
727
- stageBtn.addEventListener('click', async () => {
728
- if (!monacoEditor) return;
729
- const newContent = monacoEditor.getValue();
730
- if (newContent === originalContent) { toast('内容未变化'); return; }
731
- const stagePath = filePath || prompt('请输入文件路径(用于暂存变更):');
732
- if (!stagePath) return;
733
- try {
734
- await api('/api/changes/stage', 'POST', { path: stagePath, content: newContent });
735
- toast(' 已暂存到变更面板');
736
- originalContent = newContent;
737
- exitEditMode();
738
- switchRightTab('changes');
739
- } catch (e) { toast('暂存失败:' + e.message); }
740
- });
741
-
742
- // 启动只读视图
743
- initMonacoReadOnly();
744
- } else {
745
- box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
746
- }
747
- }
748
-
749
- /* ========== P0-1: Monaco 补全 Provider 注册(内联 ghost text + 补全弹窗) ========== */
750
- let _monacoCompletionsRegistered = false;
751
- function registerMonacoCompletions() {
752
- if (_monacoCompletionsRegistered || typeof FHMonaco === 'undefined') return;
753
- _monacoCompletionsRegistered = true;
754
-
755
- // 内联补全(ghost text,Tab 接受)—— Monaco 原生支持,无需手动叠加
756
- FHMonaco.registerInlineCompletions(async function ({ filePath, content, offset, token }) {
757
- try {
758
- const d = await api('/api/completion', 'POST', {
759
- filePath: filePath,
760
- fileContent: content,
761
- cursorOffset: offset,
762
- mode: 'quick',
763
- });
764
- const suggs = Array.isArray(d.suggestions) ? d.suggestions : [];
765
- if (suggs.length > 0 && suggs[0].text) {
766
- return { text: suggs[0].text };
767
- }
768
- return null;
769
- } catch (e) {
770
- return null;
771
- }
772
- });
773
-
774
- // 补全项弹窗(Ctrl+Space 触发,↑↓选择,Enter接受)—— Monaco 原生支持
775
- FHMonaco.registerCompletionItems(async function ({ filePath, content, offset, prefix, token }) {
776
- try {
777
- const d = await api('/api/completion', 'POST', {
778
- filePath: filePath,
779
- fileContent: content,
780
- cursorOffset: offset,
781
- mode: 'full',
782
- });
783
- const suggs = Array.isArray(d.suggestions) ? d.suggestions : [];
784
- return suggs.map((s) => ({
785
- label: (s.text || '').slice(0, 50) + ((s.text || '').length > 50 ? '…' : ''),
786
- kind: s.kind === 'function' ? 'Function' : s.kind === 'import' ? 'Module' : s.kind === 'block' ? 'Snippet' : 'Text',
787
- insertText: s.text || '',
788
- detail: (s.kind || 'text') + (s.confidence ? ' · ' + Math.round(s.confidence * 100) + '%' : ''),
789
- }));
790
- } catch (e) {
791
- return [];
792
- }
793
- });
794
- }
795
-
796
- function switchRightTab(name) {
797
- state.rightTab = name;
798
- document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
799
- document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
800
- document.getElementById('previewPanel').classList.toggle('active', name === 'preview');
801
- document.getElementById('computerPanel')?.classList.toggle('active', name === 'computer');
802
- document.getElementById('changesPanel')?.classList.toggle('active', name === 'changes');
803
- document.getElementById('designPanel')?.classList.toggle('active', name === 'design');
804
- document.getElementById('gitPanel')?.classList.toggle('active', name === 'git');
805
- document.getElementById('teamPanel')?.classList.toggle('active', name === 'team');
806
- if (name === 'changes') {
807
- bindChangesButtonsOnce();
808
- loadChanges();
809
- }
810
- if (name === 'design') {
811
- bindDesignButtonsOnce();
812
- }
813
- if (name === 'git') {
814
- bindGitButtonsOnce();
815
- loadGitStatus();
816
- }
817
- if (name === 'team') {
818
- bindTeamButtonsOnce();
819
- loadTeamData();
820
- }
821
- }
822
-
823
- // 变更面板顶部按钮只绑定一次
824
- let _changesButtonsBound = false;
825
- let _conflictFiles = []; // 冲突文件路径列表
826
- function bindChangesButtonsOnce() {
827
- if (_changesButtonsBound) return;
828
- _changesButtonsBound = true;
829
- document.getElementById('changesRefreshBtn')?.addEventListener('click', () => { _conflictFiles = []; loadChanges(); });
830
- document.getElementById('changesCommitBtn')?.addEventListener('click', async () => {
831
- try {
832
- const d = await api('/api/changes/commit', 'POST');
833
- toast(d.success ? '✅ 已提交所有变更' : '⚠️ 部分提交失败:' + (d.error || ''));
834
- _conflictFiles = [];
835
- loadChanges();
836
- } catch (e) { toast('提交失败:' + e.message); }
837
- });
838
- document.getElementById('changesDiscardBtn')?.addEventListener('click', async () => {
839
- if (!confirm('确定丢弃所有暂存变更?')) return;
840
- try {
841
- await api('/api/changes/discard', 'POST');
842
- toast('已丢弃所有变更');
843
- _conflictFiles = [];
844
- loadChanges();
845
- } catch (e) { toast('丢弃失败:' + e.message); }
846
- });
847
- // P3: 检测冲突
848
- document.getElementById('changesDetectBtn')?.addEventListener('click', async () => {
849
- try {
850
- const d = await api('/api/changes/detect-conflicts', 'POST');
851
- _conflictFiles = Array.isArray(d.conflicts) ? d.conflicts.map(c => c.path || c) : [];
852
- if (_conflictFiles.length > 0) {
853
- toast('⚠️ 检测到 ' + _conflictFiles.length + ' 个文件冲突(已高亮)');
854
- } else {
855
- toast('✅ 无文件冲突');
856
- }
857
- loadChanges(); // 重新渲染以高亮冲突文件
858
- } catch (e) { toast('冲突检测失败:' + e.message); }
859
- });
860
- }
861
-
862
- /* ========== P3: 多文件变更面板 ========== */
863
- async function loadChanges() {
864
- try {
865
- const d = await api('/api/changes', 'GET');
866
- renderChanges(d);
867
- } catch (e) {
868
- document.getElementById('changesList').innerHTML = '<div class="muted" style="text-align:center;padding:20px;">加载失败:' + escapeHtml(e.message) + '</div>';
869
- }
870
- }
871
-
872
- function renderChanges(data) {
873
- const changes = Array.isArray(data.changes) ? data.changes : [];
874
- const count = changes.length;
875
- document.getElementById('changesCount').textContent = count;
876
- const list = document.getElementById('changesList');
877
- if (!count) {
878
- list.innerHTML = '<div class="muted" style="text-align:center;padding:30px;font-size:12px;">暂无暂存变更<br/><span style="font-size:11px;">AI 生成的文件修改会先暂存在这里,审批后才写入磁盘</span></div>';
879
- return;
880
- }
881
- list.innerHTML = changes.map((c, idx) => {
882
- const typeLabel = c.type === 'create' ? '🆕 新增' : c.type === 'delete' ? '🗑 删除' : '✏️ 修改';
883
- const typeColor = c.type === 'create' ? '#2d8a4e' : c.type === 'delete' ? '#c0392b' : '#d4a017';
884
- const isConflict = _conflictFiles.some(cf => c.path === cf || c.path.endsWith(cf) || cf.endsWith(c.path));
885
- const conflictBorder = isConflict ? 'border:2px solid #c0392b;' : 'border:1px solid var(--border);';
886
- const conflictBadge = isConflict ? '<span style="color:#c0392b;font-weight:700;margin-left:6px;">⚠️ 冲突</span>' : '';
887
- const hunks = Array.isArray(c.hunks) ? c.hunks : [];
888
- let hunksHtml = '';
889
- if (hunks.length) {
890
- hunksHtml = '<div style="margin-top:6px;">' + hunks.map((h, hi) => {
891
- const lines = (h.lines || []).map(l => {
892
- const cls = l.type === 'add' ? 'diff-add' : l.type === 'del' ? 'diff-del' : 'diff-ctx';
893
- return '<div class="' + cls + '">' + escapeHtml(l.content) + '</div>';
894
- }).join('');
895
- return '<div class="diff-hunk" style="margin-bottom:6px;border:1px solid var(--border);border-radius:4px;overflow:hidden;">'
896
- + '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 6px;background:var(--bg);font-size:11px;">'
897
- + '<span class="muted">@@ ' + escapeHtml(String(h.header || '')) + '</span>'
898
- + '<span style="display:flex;gap:4px;">'
899
- + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="accept-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✓</button>'
900
- + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="reject-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✕</button>'
901
- + '</span></div>'
902
- + '<div style="font-family:monospace;font-size:11px;line-height:1.5;">' + lines + '</div></div>';
903
- }).join('') + '</div>';
904
- }
905
- return '<div class="change-item" style="padding:8px;margin-bottom:8px;' + conflictBorder + 'border-radius:6px;background:var(--card);">'
906
- + '<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;">'
907
- + '<div style="flex:1;min-width:0;">'
908
- + '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="' + escapeHtml(c.path) + '">' + escapeHtml(c.path) + conflictBadge + '</div>'
909
- + '<div style="font-size:11px;margin-top:2px;"><span style="color:' + typeColor + ';">' + typeLabel + '</span>'
910
- + (c.additions != null ? ' · <span style="color:#2d8a4e;">+' + c.additions + '</span>' : '')
911
- + (c.deletions != null ? ' · <span style="color:#c0392b;">-' + c.deletions + '</span>' : '')
912
- + ' · <span class="muted">' + escapeHtml(c.status || 'pending') + '</span></div>'
913
- + '</div>'
914
- + '<div style="display:flex;gap:4px;flex-shrink:0;">'
915
- + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;background:#2d5a3d;color:#fff;" data-act="accept" data-idx="' + idx + '">接受</button>'
916
- + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;" data-act="reject" data-idx="' + idx + '">拒绝</button>'
917
- + '</div></div>'
918
- + hunksHtml
919
- + '</div>';
920
- }).join('');
921
- // 绑定事件
922
- list.querySelectorAll('[data-act]').forEach(btn => {
923
- btn.addEventListener('click', async () => {
924
- const act = btn.getAttribute('data-act');
925
- const idx = parseInt(btn.getAttribute('data-idx'));
926
- const change = changes[idx];
927
- if (!change) return;
928
- try {
929
- if (act === 'accept') {
930
- await api('/api/changes/accept', 'POST', { path: change.path });
931
- toast('已接受:' + change.path);
932
- } else if (act === 'reject') {
933
- await api('/api/changes/reject', 'POST', { path: change.path });
934
- toast('已拒绝:' + change.path);
935
- } else if (act === 'accept-hunk') {
936
- const hi = parseInt(btn.getAttribute('data-hunk'));
937
- await api('/api/changes/hunk/accept', 'POST', { path: change.path, hunkIndex: hi });
938
- toast('已接受 Hunk');
939
- } else if (act === 'reject-hunk') {
940
- const hi = parseInt(btn.getAttribute('data-hunk'));
941
- await api('/api/changes/hunk/reject', 'POST', { path: change.path, hunkIndex: hi });
942
- toast('已拒绝 Hunk');
943
- }
944
- loadChanges();
945
- } catch (e) { toast('操作失败:' + e.message); }
946
- });
947
- });
948
- }
949
-
950
- async function showArtifacts(dir) {
951
- try {
952
- const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
953
- const files = (d.entries || []).filter((e) => e.type !== 'dir');
954
- const box = document.getElementById('taskDetail');
955
- const listHtml = files.length
956
- ? files.map((f) => '<div class="artifact-file" data-path="' + escapeHtml(f.path) + '">📄 ' + escapeHtml(f.name) + '</div>').join('')
957
- : '<div class="muted">该目录暂无文件产物。</div>';
958
- const hint = '<div class="muted" style="margin-bottom:8px;">产物文件(来自:' + escapeHtml(dir) + '),点击在右侧预览:</div>';
959
- const cur = box.innerHTML;
960
- box.innerHTML = cur + '<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">' + hint + '<div class="artifact-list">' + listHtml + '</div>';
961
- box.querySelectorAll('.artifact-file').forEach((el) => el.addEventListener('click', () => {
962
- previewFile(el.getAttribute('data-path'));
963
- toast('已打开:' + el.getAttribute('data-path'));
964
- }));
965
- switchRightTab('detail');
966
- } catch (e) { toast('读取产物失败:' + e.message); }
967
- }
968
-
969
- function renderStepHtml(step) {
970
- const d = step.data || {};
971
- if (step.type === 'model.response') {
972
- const content = (d.content || '').toString();
973
- const toolCalls = Array.isArray(d.toolCalls) ? d.toolCalls : [];
974
- let inner = '<div class="reasoning-head">🧠 模型推理 · ' + escapeHtml(d.model || '') + '</div>';
975
- if (content) inner += '<div style="white-space:pre-wrap;word-break:break-word;">' + renderMarkdown(content) + '</div>';
976
- if (toolCalls.length) inner += '<div class="muted" style="margin-top:6px;">↳ 计划调用工具:' + toolCalls.map((t) => '<code>' + escapeHtml(t) + '</code>').join('、') + '</div>';
977
- return '<div class="step reasoning">' + inner + '</div>';
978
- } else if (step.type === 'tool.call') {
979
- const argsHtml = formatToolArgs(d.args);
980
- return '<div class="step toolcall"><div class="tc-name">🔧 调用 ' + escapeHtml(d.name || '') + '</div>' + argsHtml + '</div>';
981
- } else if (step.type === 'tool.result') {
982
- const ok = !!d.ok;
983
- const out = (d.output || '').toString();
984
- return '<div class="step result ' + (ok ? 'ok' : 'fail') + '"><div class="res-head">' + (ok ? '✅' : '❌') + ' ' + escapeHtml(d.name || '') + ' ' + (ok ? '成功' : '失败') + '</div>' + (out.trim() ? '<div class="res-out">' + escapeHtml(out).slice(0, 500) + '</div>' : '') + '</div>';
985
- } else if (step.type === 'self-heal') {
986
- return '<div class="step note">🩹 进入自愈:检测到 <b>' + escapeHtml(d.category || '') + '</b> 类错误,已注入反思重试(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
987
- } else if (step.type === 'context.compact') {
988
- return '<div class="step note">📦 上下文压缩:从 ' + (d.originalLength != null ? d.originalLength : '?') + ' 条压缩至 ' + (d.compressedLength != null ? d.compressedLength : '?') + ' 条,保留首条目标与近期对话</div>';
989
- }
990
- return '';
991
- }
992
-
993
- // 消息操作按钮(复制、创建文档)
994
- function renderMsgActions() {
995
- return '<div class="msg-actions">'
996
- + '<button class="msg-action-btn" data-action="copy" title="复制整条消息">📋 复制</button>'
997
- + '<button class="msg-action-btn" data-action="create-doc" title="创建文档">📄 建文档</button>'
998
- + '</div>';
999
- }
1000
-
1001
- function renderTaskThread(task) {
1002
- const box = document.getElementById('messages');
1003
- const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
1004
- renderTaskHeader(task);
1005
- if (!task) {
1006
- box.innerHTML = '<div class="msg sys">从左侧「任务列表」选择历史任务查看对话,或在下方输入指令发起新任务。刷新页面会自动清空当前对话视图,历史对话已保存在任务列表中。</div>';
1007
- return;
1008
- }
1009
- const finalAnswer = (task.result && task.result.finalAnswer || '').trim();
1010
- let html = '';
1011
- const conv = Array.isArray(task.conversation) ? task.conversation : [];
1012
- const steps = Array.isArray(task.steps) ? task.steps : [];
1013
-
1014
- // 统一处理:先显示用户消息,再从 steps 提取思考过程(实时更新),最后显示最终回复
1015
- // 这样确保思考过程能实时显示,不会因为 conversation 有内容就跳过 steps
1016
-
1017
- // 1. 显示用户消息
1018
- let userMsgShown = false;
1019
- if (conv.length > 0) {
1020
- for (const m of conv) {
1021
- if (m && m.role === 'user' && m.content) {
1022
- html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(m.content) + '</div>';
1023
- userMsgShown = true;
1024
- break; // 只显示第一条用户消息,后续的在多轮对话中处理
1025
- }
1026
- }
1027
- }
1028
- if (!userMsgShown && task.goal) {
1029
- html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(task.goal) + '</div>';
1030
- }
1031
-
1032
- // 2. 从 steps 提取思考过程(实时更新,这是关键)
1033
- const thinkingTexts = [];
1034
- for (const s of steps) {
1035
- if (s.type === 'model.response' && s.data && s.data.content && s.data.content.trim()) {
1036
- thinkingTexts.push(s.data.content.trim());
1037
- } else if (s.type === 'self-heal') {
1038
- thinkingTexts.push('刚才遇到点小问题,我调整一下思路再试试。');
1039
- }
1040
- }
1041
- if (thinkingTexts.length > 0) {
1042
- html += '<div class="msg assistant">'
1043
- + renderMsgActions()
1044
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(thinkingTexts.join('\n\n')) + '</div>'
1045
- + '</div>';
1046
- }
1047
-
1048
- // 3. 如果 conversation 中有完整的 assistant 文本回复(任务完成后),也显示出来
1049
- if (conv.length > 0) {
1050
- const assistantTexts = [];
1051
- for (const m of conv) {
1052
- if (m && m.role === 'assistant' && m.content && m.content.trim() && !(m.toolCalls && m.toolCalls.length > 0)) {
1053
- assistantTexts.push(m.content.trim());
1054
- }
1055
- }
1056
- // 避免和 steps 重复:只显示 steps 中没有的最终回复
1057
- if (assistantTexts.length > 0) {
1058
- const lastAssistantText = assistantTexts[assistantTexts.length - 1];
1059
- const alreadyInSteps = thinkingTexts.some(t => t === lastAssistantText || lastAssistantText.includes(t) || t.includes(lastAssistantText));
1060
- if (!alreadyInSteps && lastAssistantText !== finalAnswer) {
1061
- html += '<div class="msg assistant">'
1062
- + renderMsgActions()
1063
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(lastAssistantText) + '</div>'
1064
- + '</div>';
1065
- }
1066
- }
1067
- }
1068
-
1069
- // 终态:如果对话流里没有最终回复(旧任务或被中断),再显示 finalAnswer
1070
- const hasFinalInConv = conv.some(m => m.role === 'assistant' && !(m.toolCalls || []).length && (m.content || '').trim());
1071
- if (task.status === 'done' && finalAnswer && !hasFinalInConv) {
1072
- html += '<div class="msg assistant">'
1073
- + renderMsgActions()
1074
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
1075
- + '</div>';
1076
- } else if (task.status === 'failed') {
1077
- if (finalAnswer && !hasFinalInConv) {
1078
- html += '<div class="msg assistant">'
1079
- + renderMsgActions()
1080
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
1081
- + '</div>';
1082
- }
1083
- html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
1084
- } else if (task.status === 'running') {
1085
- // 计算已运行时间,让用户知道等了多久
1086
- let waitTip = '';
1087
- if (task.createdAt) {
1088
- const elapsed = Math.floor((Date.now() - new Date(task.createdAt).getTime()) / 1000);
1089
- if (elapsed > 5) {
1090
- const mins = Math.floor(elapsed / 60);
1091
- const secs = elapsed % 60;
1092
- const timeStr = mins > 0 ? `${mins}分${secs}秒` : `${secs}秒`;
1093
- waitTip = `<span style="color:var(--muted);font-size:12px;margin-left:8px;">已等待 ${timeStr}</span>`;
1094
- }
1095
- // 分级超时警告:让用户明确感知任务可能卡住,而非无限期"思考中"
1096
- if (elapsed >= 300 && elapsed < 900) {
1097
- // 5-15 分钟:橙色温和提醒
1098
- waitTip += '<div style="color:#e65100;font-size:12px;margin-top:8px;line-height:1.6;background:#fff3e0;padding:8px 10px;border-radius:6px;border-left:3px solid #ff9800;">'
1099
- + '⚠️ 已等待较久,模型可能正在处理复杂推理或执行耗时操作。'
1100
- + '<br>若长时间无任何输出,可点击右上角「停止」后重新提交。'
1101
- + '</div>';
1102
- } else if (elapsed >= 900 && elapsed < 1500) {
1103
- // 15-25 分钟:红色明确警告
1104
- waitTip += '<div style="color:#c62828;font-size:12px;margin-top:8px;line-height:1.6;background:#ffebee;padding:8px 10px;border-radius:6px;border-left:3px solid #e53935;">'
1105
- + '🚨 任务可能已卡住(已等待超过15分钟无输出)。'
1106
- + '<br>常见原因:模型 API 无响应、网络中断、或工具调用死锁。'
1107
- + '<br>建议点击下方按钮停止任务,检查模型配置后重新提交。'
1108
- + '<br><button onclick="document.getElementById(\'tchStop\')?.click()" style="margin-top:6px;background:#e53935;color:#fff;border:none;padding:4px 14px;border-radius:5px;cursor:pointer;font-size:12px;">⏹ 停止当前任务</button>'
1109
- + '</div>';
1110
- } else if (elapsed >= 1500) {
1111
- // 25 分钟以上:紧急警告,提示即将自动超时
1112
- const remain = Math.max(0, 30 - Math.floor(elapsed / 60));
1113
- waitTip += '<div style="color:#b71c1c;font-size:12px;margin-top:8px;line-height:1.6;background:#ffcdd2;padding:8px 10px;border-radius:6px;border-left:3px solid #c62828;font-weight:500;">'
1114
- + `⛔ 任务即将到达 30 分钟自动超时限制(约 ${remain} 分钟后自动终止)。`
1115
- + '<br>可立即点击下方按钮手动停止,或等待系统自动终止。'
1116
- + '<br><button onclick="document.getElementById(\'tchStop\')?.click()" style="margin-top:6px;background:#c62828;color:#fff;border:none;padding:4px 14px;border-radius:5px;cursor:pointer;font-size:12px;">⏹ 立即停止任务</button>'
1117
- + '</div>';
1118
- } else if (elapsed > 60) {
1119
- // 1-5 分钟:普通提示
1120
- waitTip += '<div style="color:var(--muted);font-size:12px;margin-top:6px;line-height:1.5;">模型正在深度思考或执行复杂操作,请耐心等待...</div>';
1121
- }
1122
- }
1123
- html += '<div class="thinking-indicator">'
1124
- + '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
1125
- + '<span>思考中...</span>'
1126
- + waitTip
1127
- + '</div>';
1128
- } else if (task.status === 'queued') {
1129
- html += '<div class="msg sys">任务已入队,等待执行…</div>';
1130
- }
1131
- box.innerHTML = html;
1132
- if (nearBottom) box.scrollTop = box.scrollHeight;
1133
- }
1134
-
1135
- function renderTaskHeader(task) {
1136
- const titleEl = document.getElementById('tchTitle');
1137
- const statusEl = document.getElementById('tchStatus');
1138
- const timeEl = document.getElementById('tchTime');
1139
- const subEl = document.getElementById('tchSub');
1140
- if (!task) {
1141
- titleEl.textContent = '未选择任务';
1142
- statusEl.className = 'badge'; statusEl.textContent = '—';
1143
- timeEl.textContent = '';
1144
- subEl.textContent = '从左侧任务列表选择,或在下方输入指令发起新任务';
1145
- return;
1146
- }
1147
- titleEl.textContent = task.goal.length > 60 ? task.goal.slice(0, 60) + '…' : task.goal;
1148
- statusEl.className = 'badge ' + task.status;
1149
- statusEl.textContent = task.status;
1150
- timeEl.textContent = '创建于 ' + fmtTime(task.createdAt);
1151
- subEl.textContent = '对话流 + 内部执行过程追踪 · ' + (task.steps ? task.steps.length : 0) + ' 个步骤';
1152
- // 仅当任务正在运行/排队时显示「停止」按钮(解救卡死、无法继续的任务)
1153
- const stopEl = document.getElementById('tchStop');
1154
- if (stopEl) stopEl.style.display = (task && (task.status === 'running' || task.status === 'queued')) ? '' : 'none';
1155
- }
1156
-
1157
- function renderThinkingProcess(msgs) {
1158
- // 收集所有思考文本,直接展示,不折叠、不显示工具调用
1159
- const textParts = [];
1160
- for (const m of msgs) {
1161
- const text = (m.content || '').trim();
1162
- if (text) textParts.push(text);
1163
- }
1164
- const textContent = textParts.join('\n\n');
1165
- if (!textContent) return '';
1166
- // 用普通 assistant 气泡样式展示思考过程,让用户看到模型在想什么
1167
- return '<div class="msg assistant thinking-msg" style="opacity:0.85;">'
1168
- + renderMsgActions()
1169
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(textContent) + '</div>'
1170
- + '</div>';
1171
- }
1172
-
1173
- function toggleArgs(el) {
1174
- const args = el.parentElement.querySelector('.thinking-args');
1175
- if (!args) return;
1176
- args.classList.toggle('show');
1177
- el.textContent = args.classList.contains('show') ? '收起参数' : '查看参数';
1178
- }
1179
-
1180
- function toggleThinking(header) {
1181
- const body = header.parentElement.querySelector('.thinking-body');
1182
- if (!body) return;
1183
- const arrow = header.querySelector('.thinking-arrow');
1184
- if (body.style.display === 'none' || body.style.display === '') {
1185
- body.style.display = 'block';
1186
- if (arrow) arrow.textContent = '▼';
1187
- } else {
1188
- body.style.display = 'none';
1189
- if (arrow) arrow.textContent = '▶';
1190
- }
1191
- }
1192
-
1193
- function renderTaskDetail(id) {
1194
- const t = state.tasks.find((x) => x.id === id);
1195
- const box = document.getElementById('taskDetail');
1196
- if (!t) { box.innerHTML = '<div class="muted">任务不存在。</div>'; return; }
1197
- const r = t.result;
1198
- let ops = '';
1199
- if (t.workspaceDir) {
1200
- ops = '<div class="detail-ops">' +
1201
- '<button class="secondary" data-op="browser">🌐 打开浏览器</button>' +
1202
- '<button class="secondary" data-op="folder" data-path="' + escapeHtml(t.workspaceDir) + '">📂 打开资源管理器</button>' +
1203
- '<button class="secondary" data-op="artifacts" data-path="' + escapeHtml(t.workspaceDir) + '">📑 查看产物文件</button>' +
1204
- '</div>';
1205
- }
1206
- const rows = [
1207
- ops,
1208
- '<div><b>目标</b><br>' + escapeHtml(t.goal) + '</div>',
1209
- '<div><b>状态</b> ' + statusBadge(t.status) + '</div>',
1210
- t.agentType ? '<div><b>智能体</b> ' + escapeHtml(AGENT_TYPES[t.agentType]?.label || t.agentType) + '</div>' : '',
1211
- r ? '<div><b>迭代</b> ' + r.iterations + ' · <b>成本</b> $' + r.costUsd.toFixed(6) + '</div>' : '',
1212
- r && r.logFile ? '<div><b>日志</b> <code>' + escapeHtml(r.logFile) + '</code></div>' : '',
1213
- r && r.finalAnswer ? '<div><b>结果</b><br><pre style="white-space:pre-wrap;word-break:break-word;">' + escapeHtml(r.finalAnswer) + '</pre></div>' : '',
1214
- t.error ? '<div style="color:var(--err)"><b>错误</b><br>' + escapeHtml(t.error) + '</div>' : '',
1215
- ].filter(Boolean).join('<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">');
1216
- box.innerHTML = rows || '<div class="muted">无详情</div>';
1217
- box.querySelectorAll('[data-op]').forEach((b) => b.addEventListener('click', () => handleDetailOp(t, b.getAttribute('data-op'), b.getAttribute('data-path'))));
1218
- }
1219
-
1220
- function renderSidebarTaskList() {
1221
- const container = document.getElementById('taskListContainer');
1222
- const section = document.getElementById('taskListSection');
1223
- const badge = document.getElementById('taskCountBadge');
1224
- if (!container || !section) return;
1225
-
1226
- // 置顶任务排在前面,其余按时间倒序
1227
- const sortedTasks = [...state.tasks].sort((a, b) => {
1228
- if (state.pinnedTasks.has(a.id) && !state.pinnedTasks.has(b.id)) return -1;
1229
- if (!state.pinnedTasks.has(a.id) && state.pinnedTasks.has(b.id)) return 1;
1230
- return new Date(b.createdAt) - new Date(a.createdAt);
1231
- });
1232
-
1233
- const activeTasks = sortedTasks.filter(t => t.status === 'queued' || t.status === 'running' || t.status === 'done' || t.status === 'failed');
1234
-
1235
- if (activeTasks.length === 0) {
1236
- section.style.display = 'none';
1237
- return;
1238
- }
1239
-
1240
- section.style.display = 'block';
1241
- badge.textContent = activeTasks.length;
1242
-
1243
- let html = '';
1244
- activeTasks.slice(0, 15).forEach(t => {
1245
- const icon = t.status === 'running' ? '⏳' : t.status === 'queued' ? '📥' : t.error ? '❌' : '✅';
1246
- const title = t.goal.slice(0, 18) + (t.goal.length > 18 ? '…' : '');
1247
- const isPinned = state.pinnedTasks.has(t.id);
1248
- const activeClass = state.currentTaskId === t.id ? 'active' : '';
1249
- html += `<div class="task-item ${activeClass}" data-task-id="${t.id}" data-pinned="${isPinned}">
1250
- <span class="task-icon">${icon}</span>
1251
- <span class="task-title">${title}</span>
1252
- ${isPinned ? '<span class="task-pinned">📌</span>' : ''}
1253
- <div class="task-actions">
1254
- <button class="pin-btn" data-id="${t.id}" data-pinned="${isPinned}" title="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '📌' : '📍'}</button>
1255
- ${t.status === 'done' || t.status === 'failed' ? '<button class="delete-btn" data-id="' + t.id + '" title="删除任务">🗑️</button>' : ''}
1256
- </div>
1257
- </div>`;
1258
- });
1259
-
1260
- container.innerHTML = html;
1261
-
1262
- // 绑定点击事件
1263
- container.querySelectorAll('.task-item[data-task-id]').forEach(el => {
1264
- el.addEventListener('click', (e) => {
1265
- // 如果是点击pin按钮,不切换任务
1266
- if (e.target.classList.contains('pin-btn')) return;
1267
- const id = el.getAttribute('data-task-id');
1268
- selectTask(id);
1269
- });
1270
-
1271
- // 右键菜单 - 置顶/取消置顶
1272
- el.addEventListener('contextmenu', (e) => {
1273
- e.preventDefault();
1274
- const id = el.getAttribute('data-task-id');
1275
- togglePinTask(id);
1276
- });
1277
- });
1278
-
1279
- // 绑定pin按钮
1280
- container.querySelectorAll('.pin-btn').forEach(btn => {
1281
- btn.addEventListener('click', (e) => {
1282
- e.stopPropagation();
1283
- const id = btn.getAttribute('data-id');
1284
- togglePinTask(id);
1285
- });
1286
- });
1287
-
1288
- // 绑定删除按钮
1289
- container.querySelectorAll('.delete-btn').forEach(btn => {
1290
- btn.addEventListener('click', (e) => {
1291
- e.stopPropagation();
1292
- const id = btn.getAttribute('data-id');
1293
- deleteTask(id);
1294
- });
1295
- });
1296
- }
1297
-
1298
- function switchView(nav) {
1299
- document.querySelectorAll('.topbar-nav-item').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1300
- document.querySelectorAll('.sidebar-icon').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1301
- document.querySelectorAll('.view, .page-view').forEach((v) => v.classList.toggle('active', v.getAttribute('data-view') === nav));
1302
- // 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
1303
- const isChat = nav === 'chat';
1304
- document.getElementById('chatFootbar').style.display = isChat ? 'flex' : 'none';
1305
- // 切换到对话视图时,如果当前任务已完成或不存在,自动开启新对话
1306
- if (isChat) {
1307
- const cur = state.tasks.find((t) => t.id === state.currentTaskId);
1308
- if (!cur || cur.status === 'done' || cur.status === 'failed') {
1309
- startNewChat();
1310
- }
1311
- }
1312
- }
1313
-
1314
- // 开启新对话:清空当前任务选中,显示空状态,聚焦输入框
1315
- function startNewChat() {
1316
- state.currentTaskId = null;
1317
- renderTaskThread(null);
1318
- renderTaskDetail(null);
1319
- renderSidebarTaskList();
1320
- // 显示任务列表区域
1321
- const taskListSection = document.getElementById('taskListSection');
1322
- if (taskListSection) taskListSection.style.display = 'block';
1323
- // 聚焦输入框
1324
- setTimeout(() => {
1325
- const input = document.getElementById('goalInput');
1326
- if (input) input.focus();
1327
- }, 100);
1328
- }
1329
-
1330
- function updateUserBar() {
1331
- document.getElementById('topbarPhone').textContent = state.phone || '未登录';
1332
- document.getElementById('sheetPhone').textContent = state.phone || '未登录';
1333
- const a = AGENT_TYPES[state.agentType];
1334
- document.getElementById('skillPill').textContent = (a?.icon || '🤖') + ' ' + (a?.label || '通用助手');
1335
- }
1336
-
1337
- function showWelcomeModal(welcomeTasks) {
1338
- const modal = document.getElementById('welcomeGuideModal');
1339
- if (!modal) return;
1340
-
1341
- const listEl = document.getElementById('welcomeTaskList');
1342
- if (listEl) {
1343
- if (welcomeTasks.length > 0) {
1344
- listEl.innerHTML = welcomeTasks.map((t, i) => `
1345
- <div class="welcome-task-item" data-task-id="${t.taskId}" data-goal="${encodeURIComponent(t.goal)}">
1346
- <div class="welcome-task-index">${i + 1}</div>
1347
- <div class="welcome-task-content">
1348
- <div class="welcome-task-title">${escapeHtml(t.title || '任务 ' + (i + 1))}</div>
1349
- <div class="welcome-task-desc">${escapeHtml(t.description || t.goal.slice(0, 50) + '…')}</div>
1350
- </div>
1351
- <button class="welcome-task-run" data-id="${t.taskId}">▶ 运行</button>
1352
- </div>
1353
- `).join('');
1354
-
1355
- // 绑定运行按钮
1356
- listEl.querySelectorAll('.welcome-task-run').forEach(btn => {
1357
- btn.addEventListener('click', (e) => {
1358
- e.stopPropagation();
1359
- const taskId = btn.getAttribute('data-id');
1360
- runWelcomeTask(taskId);
1361
- });
1362
- });
1363
- } else {
1364
- listEl.innerHTML = '<div class="empty-welcome">' + t('empty.preparing_welcome') + '</div>';
1365
- }
1366
- }
1367
-
1368
- modal.classList.add('show');
1369
- toast('🎉 欢迎!系统已为您准备了三个引导任务');
1370
- }
1371
-
1372
- function showWelcomeGuide(welcomeTasks = []) {
1373
- // 显示欢迎引导弹窗
1374
- showWelcomeModal(welcomeTasks);
1375
- }
1376
-
1377
- /* ========== P2-1: 设计稿转代码 ========== */
1378
- let _designButtonsBound = false;
1379
- let _designImageBase64 = '';
1380
- let _designLastCode = '';
1381
-
1382
- function bindDesignButtonsOnce() {
1383
- if (_designButtonsBound) return;
1384
- _designButtonsBound = true;
1385
-
1386
- const dropZone = document.getElementById('designDropZone');
1387
- const fileInput = document.getElementById('designFileInput');
1388
- const removeBtn = document.getElementById('designRemoveImgBtn');
1389
- const generateBtn = document.getElementById('designGenerateBtn');
1390
- const refineBtn = document.getElementById('designRefineBtn');
1391
- const copyBtn = document.getElementById('designCopyBtn');
1392
-
1393
- if (!dropZone || !fileInput) return;
1394
-
1395
- // 点击上传
1396
- dropZone.addEventListener('click', () => fileInput.click());
1397
-
1398
- // 文件选择
1399
- fileInput.addEventListener('change', (e) => {
1400
- const file = e.target.files?.[0];
1401
- if (file) handleDesignFile(file);
1402
- });
1403
-
1404
- // 拖拽上传
1405
- dropZone.addEventListener('dragover', (e) => {
1406
- e.preventDefault();
1407
- dropZone.style.borderColor = 'var(--accent, #2d5a3d)';
1408
- });
1409
- dropZone.addEventListener('dragleave', () => {
1410
- dropZone.style.borderColor = 'var(--border)';
1411
- });
1412
- dropZone.addEventListener('drop', (e) => {
1413
- e.preventDefault();
1414
- dropZone.style.borderColor = 'var(--border)';
1415
- const file = e.dataTransfer?.files?.[0];
1416
- if (file) handleDesignFile(file);
1417
- });
1418
-
1419
- // 移除图片
1420
- removeBtn?.addEventListener('click', () => {
1421
- _designImageBase64 = '';
1422
- document.getElementById('designImagePreview').style.display = 'none';
1423
- document.getElementById('designDropZone').style.display = 'block';
1424
- document.getElementById('designResult').style.display = 'none';
1425
- });
1426
-
1427
- // 生成代码
1428
- generateBtn.addEventListener('click', () => generateDesignCode(false));
1429
-
1430
- // 修正代码
1431
- refineBtn?.addEventListener('click', () => generateDesignCode(true));
1432
-
1433
- // 复制代码
1434
- copyBtn?.addEventListener('click', () => {
1435
- if (_designLastCode) {
1436
- navigator.clipboard.writeText(_designLastCode).then(() => {
1437
- toast('✅ 代码已复制到剪贴板');
1438
- }).catch(() => {
1439
- toast('复制失败,请手动复制');
1440
- });
1441
- }
1442
- });
1443
-
1444
- // 结果 tab 切换
1445
- document.querySelectorAll('.design-result-tab').forEach(tab => {
1446
- tab.addEventListener('click', () => {
1447
- document.querySelectorAll('.design-result-tab').forEach(t => t.classList.remove('active'));
1448
- tab.classList.add('active');
1449
- const target = tab.getAttribute('data-tab');
1450
- document.getElementById('designCodeView').style.display = target === 'code' ? 'block' : 'none';
1451
- document.getElementById('designPreviewView').style.display = target === 'preview' ? 'block' : 'none';
1452
- });
1453
- });
1454
- }
1455
-
1456
- function handleDesignFile(file) {
1457
- if (!file.type.startsWith('image/')) {
1458
- toast('请上传图片文件');
1459
- return;
1460
- }
1461
- if (file.size > 8 * 1024 * 1024) {
1462
- toast('图片大小不能超过 8MB');
1463
- return;
1464
- }
1465
- const reader = new FileReader();
1466
- reader.onload = (e) => {
1467
- _designImageBase64 = e.target.result;
1468
- const img = document.getElementById('designPreviewImg');
1469
- img.src = _designImageBase64;
1470
- document.getElementById('designImagePreview').style.display = 'block';
1471
- document.getElementById('designDropZone').style.display = 'none';
1472
- document.getElementById('designResult').style.display = 'none';
1473
- };
1474
- reader.readAsDataURL(file);
1475
- }
1476
-
1477
- async function generateDesignCode(isRefine) {
1478
- if (!_designImageBase64) {
1479
- toast('请先上传设计稿图片');
1480
- return;
1481
- }
1482
-
1483
- const framework = document.getElementById('designFramework').value;
1484
- const instructions = document.getElementById('designInstructions').value;
1485
- const feedback = isRefine ? document.getElementById('designFeedback').value : '';
1486
-
1487
- if (isRefine && !feedback.trim()) {
1488
- toast('请输入修正反馈');
1489
- return;
1490
- }
1491
-
1492
- const statusEl = document.getElementById('designStatus');
1493
- const statusText = document.getElementById('designStatusText');
1494
- const generateBtn = document.getElementById('designGenerateBtn');
1495
-
1496
- statusEl.style.display = 'block';
1497
- statusText.textContent = isRefine ? '正在修正代码...' : '正在分析设计稿并生成代码...';
1498
- generateBtn.disabled = true;
1499
- generateBtn.style.opacity = '0.6';
1500
-
1501
- try {
1502
- const body = {
1503
- image: _designImageBase64,
1504
- framework,
1505
- };
1506
- if (instructions) body.instructions = instructions;
1507
- if (isRefine) {
1508
- body.previousCode = _designLastCode;
1509
- body.feedback = feedback;
1510
- }
1511
-
1512
- const d = await api('/api/design-to-code', 'POST', body);
1513
-
1514
- if (d.ok && d.code) {
1515
- _designLastCode = d.code;
1516
- document.getElementById('designCode').textContent = d.code;
1517
- document.getElementById('designCodeLang').textContent = d.language || framework;
1518
-
1519
- // 渲染预览 iframe
1520
- if (d.previewHtml) {
1521
- const iframe = document.getElementById('designPreviewFrame');
1522
- iframe.srcdoc = d.previewHtml;
1523
- }
1524
-
1525
- document.getElementById('designResult').style.display = 'block';
1526
- document.getElementById('designFeedback').value = '';
1527
- toast(isRefine ? '✅ 代码已修正' : '✅ 代码生成成功');
1528
- } else {
1529
- toast('生成失败: ' + (d.error || '未知错误'));
1530
- }
1531
- } catch (e) {
1532
- toast('请求失败: ' + (e.message || '网络错误'));
1533
- } finally {
1534
- statusEl.style.display = 'none';
1535
- generateBtn.disabled = false;
1536
- generateBtn.style.opacity = '1';
1537
- }
1538
- }
1539
-
1540
- /* ========== P2-2: Git 集成 ========== */
1541
- let _gitButtonsBound = false;
1542
- let _gitStatus = null;
1543
- let _gitBranches = [];
1544
-
1545
- function bindGitButtonsOnce() {
1546
- if (_gitButtonsBound) return;
1547
- _gitButtonsBound = true;
1548
-
1549
- document.getElementById('gitRefreshBtn')?.addEventListener('click', loadGitStatus);
1550
- document.getElementById('gitPullBtn')?.addEventListener('click', gitPull);
1551
- document.getElementById('gitPushBtn')?.addEventListener('click', gitPush);
1552
- document.getElementById('gitInitBtn')?.addEventListener('click', gitInit);
1553
- document.getElementById('gitCommitBtn')?.addEventListener('click', gitCommit);
1554
- document.getElementById('gitAddAllBtn')?.addEventListener('click', () => gitAddAll('modified'));
1555
- document.getElementById('gitResetAllBtn')?.addEventListener('click', gitResetAll);
1556
- document.getElementById('gitBranchSelect')?.addEventListener('change', (e) => gitCheckout(e.target.value));
1557
- document.getElementById('gitCommitMessage')?.addEventListener('keydown', (e) => {
1558
- if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); gitCommit(); }
1559
- });
1560
- document.getElementById('gitLogToggle')?.addEventListener('click', () => {
1561
- const list = document.getElementById('gitLogList');
1562
- const toggle = document.getElementById('gitLogToggle');
1563
- if (list.style.display === 'none') {
1564
- list.style.display = 'block';
1565
- toggle.textContent = '📜 提交历史 ▲';
1566
- loadGitLog();
1567
- } else {
1568
- list.style.display = 'none';
1569
- toggle.textContent = '📜 提交历史 ▼';
1570
- }
1571
- });
1572
- document.getElementById('gitDiffCloseBtn')?.addEventListener('click', closeGitDiff);
1573
- document.getElementById('gitDiffModal')?.addEventListener('click', (e) => {
1574
- if (e.target.id === 'gitDiffModal') closeGitDiff();
1575
- });
1576
- }
1577
-
1578
- async function loadGitStatus() {
1579
- try {
1580
- const d = await api('/api/git/status', 'GET');
1581
- _gitStatus = d;
1582
- if (!d.isRepo) {
1583
- document.getElementById('gitNoRepo').style.display = 'block';
1584
- document.getElementById('gitContent').style.display = 'none';
1585
- document.getElementById('gitBranchName').textContent = '';
1586
- return;
1587
- }
1588
- document.getElementById('gitNoRepo').style.display = 'none';
1589
- document.getElementById('gitContent').style.display = 'block';
1590
- document.getElementById('gitBranchName').textContent = `(${d.branch}${d.ahead ? ' ↑' + d.ahead : ''}${d.behind ? ' ↓' + d.behind : ''})`;
1591
-
1592
- // 分类文件
1593
- const staged = d.files.filter((f) => f.indexStatus !== '.' && f.indexStatus !== '?');
1594
- const modified = d.files.filter((f) => f.workTreeStatus !== '.' && f.workTreeStatus !== '?' && f.indexStatus !== '?');
1595
- const untracked = d.files.filter((f) => f.indexStatus === '?');
1596
-
1597
- document.getElementById('gitStagedCount').textContent = staged.length;
1598
- document.getElementById('gitModifiedCount').textContent = modified.length;
1599
- document.getElementById('gitUntrackedCount').textContent = untracked.length;
1600
- document.getElementById('gitAddAllBtn').style.display = modified.length ? 'inline-block' : 'none';
1601
- document.getElementById('gitResetAllBtn').style.display = staged.length ? 'inline-block' : 'none';
1602
-
1603
- renderGitFileList('gitStagedList', staged, 'staged');
1604
- renderGitFileList('gitModifiedList', modified, 'modified');
1605
- renderGitFileList('gitUntrackedList', untracked, 'untracked');
1606
-
1607
- // 加载分支列表
1608
- loadGitBranches();
1609
- } catch (e) {
1610
- toast('Git 状态加载失败: ' + (e.message || '网络错误'));
1611
- }
1612
- }
1613
-
1614
- function renderGitFileList(containerId, files, type) {
1615
- const container = document.getElementById(containerId);
1616
- if (!files.length) {
1617
- container.innerHTML = '<div class="muted" style="font-size:10px;padding:4px 8px;">无</div>';
1618
- return;
1619
- }
1620
- container.innerHTML = files.map((f, idx) => {
1621
- const statusColor = f.conflict ? '#c0392b' : type === 'staged' ? '#2d8a4e' : type === 'untracked' ? '#888' : '#c0392b';
1622
- const statusLabel = f.indexStatus !== '.' && f.indexStatus !== '?' ? f.indexStatus : f.workTreeStatus;
1623
- const actions = type === 'staged'
1624
- ? `<button class="git-file-btn" data-act="reset" data-idx="${idx}" style="font-size:10px;padding:1px 5px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:var(--ink);">取消</button>`
1625
- : `<button class="git-file-btn" data-act="add" data-idx="${idx}" style="font-size:10px;padding:1px 5px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:var(--ink);">暂存</button>`;
1626
- return `<div style="display:flex;align-items:center;justify-content:space-between;padding:3px 8px;font-size:11px;border-bottom:1px solid var(--border);">
1627
- <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;" class="git-file-path" data-path="${f.path}" data-staged="${type === 'staged'}">
1628
- <span style="color:${statusColor};font-weight:600;margin-right:4px;">${statusLabel}</span>${escapeHtml(f.path)}
1629
- </span>
1630
- ${actions}
1631
- </div>`;
1632
- }).join('');
1633
-
1634
- // 绑定事件
1635
- container.querySelectorAll('.git-file-btn').forEach((btn) => {
1636
- btn.addEventListener('click', async () => {
1637
- const act = btn.getAttribute('data-act');
1638
- const idx = parseInt(btn.getAttribute('data-idx'));
1639
- const file = files[idx];
1640
- if (!file) return;
1641
- if (act === 'add') {
1642
- await gitAdd([file.path]);
1643
- } else if (act === 'reset') {
1644
- await gitReset([file.path]);
1645
- }
1646
- });
1647
- });
1648
- container.querySelectorAll('.git-file-path').forEach((el) => {
1649
- el.addEventListener('click', () => {
1650
- const path = el.getAttribute('data-path');
1651
- const staged = el.getAttribute('data-staged') === 'true';
1652
- showGitDiff(path, staged);
1653
- });
1654
- });
1655
- }
1656
-
1657
- async function gitAdd(paths) {
1658
- try {
1659
- const d = await api('/api/git/add', 'POST', { paths });
1660
- if (d.ok) { toast(d.message); loadGitStatus(); }
1661
- else { toast('暂存失败: ' + (d.error || d.message)); }
1662
- } catch (e) { toast('暂存失败: ' + (e.message || '网络错误')); }
1663
- }
1664
-
1665
- async function gitAddAll(type) {
1666
- if (!_gitStatus) return;
1667
- const files = _gitStatus.files.filter((f) => f.workTreeStatus !== '.' && f.workTreeStatus !== '?' && f.indexStatus !== '?');
1668
- const paths = files.map((f) => f.path);
1669
- if (paths.length) await gitAdd(paths);
1670
- }
1671
-
1672
- async function gitReset(paths) {
1673
- try {
1674
- const d = await api('/api/git/reset', 'POST', { paths });
1675
- if (d.ok) { toast(d.message); loadGitStatus(); }
1676
- else { toast('取消暂存失败: ' + (d.error || d.message)); }
1677
- } catch (e) { toast('取消暂存失败: ' + (e.message || '网络错误')); }
1678
- }
1679
-
1680
- async function gitResetAll() {
1681
- if (!_gitStatus) return;
1682
- const files = _gitStatus.files.filter((f) => f.indexStatus !== '.' && f.indexStatus !== '?');
1683
- const paths = files.map((f) => f.path);
1684
- if (paths.length) await gitReset(paths);
1685
- }
1686
-
1687
- async function gitCommit() {
1688
- const message = document.getElementById('gitCommitMessage').value.trim();
1689
- if (!message) { toast('请输入提交信息'); return; }
1690
- try {
1691
- const d = await api('/api/git/commit', 'POST', { message });
1692
- if (d.ok) {
1693
- toast('✅ 提交成功: ' + (d.hash || '').slice(0, 7));
1694
- document.getElementById('gitCommitMessage').value = '';
1695
- loadGitStatus();
1696
- } else {
1697
- toast('提交失败: ' + (d.error || d.message));
1698
- }
1699
- } catch (e) { toast('提交失败: ' + (e.message || '网络错误')); }
1700
- }
1701
-
1702
- async function loadGitBranches() {
1703
- try {
1704
- const d = await api('/api/git/branches', 'GET');
1705
- _gitBranches = d.branches || [];
1706
- const select = document.getElementById('gitBranchSelect');
1707
- const localBranches = _gitBranches.filter((b) => !b.remote);
1708
- select.innerHTML = localBranches.map((b) =>
1709
- `<option value="${escapeHtml(b.name)}" ${b.current ? 'selected' : ''}>${b.current ? '★ ' : ''}${escapeHtml(b.name)}${b.ahead ? '' + b.ahead : ''}${b.behind ? ' ↓' + b.behind : ''}</option>`
1710
- ).join('');
1711
- } catch (e) { /* 静默失败 */ }
1712
- }
1713
-
1714
- async function gitCheckout(branch) {
1715
- try {
1716
- const d = await api('/api/git/checkout', 'POST', { branch });
1717
- if (d.ok) { toast(d.message); loadGitStatus(); }
1718
- else { toast('切换失败: ' + (d.error || d.message)); loadGitStatus(); }
1719
- } catch (e) { toast('切换失败: ' + (e.message || '网络错误')); }
1720
- }
1721
-
1722
- async function gitPull() {
1723
- try {
1724
- const d = await api('/api/git/pull', 'POST', {});
1725
- if (d.ok) { toast('✅ ' + d.message); loadGitStatus(); }
1726
- else { toast('拉取失败: ' + (d.error || d.message)); }
1727
- } catch (e) { toast('拉取失败: ' + (e.message || '网络错误')); }
1728
- }
1729
-
1730
- async function gitPush() {
1731
- try {
1732
- const d = await api('/api/git/push', 'POST', {});
1733
- if (d.ok) { toast('✅ ' + d.message); loadGitStatus(); }
1734
- else { toast('推送失败: ' + (d.error || d.message)); }
1735
- } catch (e) { toast('推送失败: ' + (e.message || '网络错误')); }
1736
- }
1737
-
1738
- async function gitInit() {
1739
- try {
1740
- const d = await api('/api/git/init', 'POST', {});
1741
- if (d.ok) { toast('✅ ' + d.message); loadGitStatus(); }
1742
- else { toast('初始化失败: ' + (d.error || d.message)); }
1743
- } catch (e) { toast('初始化失败: ' + (e.message || '网络错误')); }
1744
- }
1745
-
1746
- async function loadGitLog() {
1747
- try {
1748
- const d = await api('/api/git/log?count=20', 'GET');
1749
- const list = document.getElementById('gitLogList');
1750
- if (!d.commits || !d.commits.length) {
1751
- list.innerHTML = '<div class="muted" style="font-size:10px;padding:4px;">无提交历史</div>';
1752
- return;
1753
- }
1754
- list.innerHTML = d.commits.map((c) =>
1755
- `<div style="padding:4px 8px;border-bottom:1px solid var(--border);">
1756
- <div style="font-weight:600;color:var(--ink);">${escapeHtml(c.subject)}</div>
1757
- <div style="font-size:10px;color:var(--muted);margin-top:2px;">
1758
- <span style="color:#2d5a3d;font-family:monospace;">${c.shortHash}</span>
1759
- · ${escapeHtml(c.author)} · ${new Date(c.date).toLocaleString('zh-CN')}
1760
- </div>
1761
- </div>`
1762
- ).join('');
1763
- } catch (e) { /* 静默失败 */ }
1764
- }
1765
-
1766
- async function showGitDiff(path, staged) {
1767
- try {
1768
- const d = await api('/api/git/diff', 'POST', { path, staged });
1769
- const modal = document.getElementById('gitDiffModal');
1770
- const title = document.getElementById('gitDiffTitle');
1771
- const content = document.getElementById('gitDiffContent');
1772
- title.textContent = (staged ? '📥 ' : '📝 ') + path;
1773
- if (d.diffs && d.diffs.length) {
1774
- const diff = d.diffs[0];
1775
- let html = '';
1776
- for (const hunk of diff.hunks) {
1777
- html += `<div style="color:#888;background:var(--card);padding:2px 6px;margin:4px 0;font-size:10px;">${escapeHtml(hunk.header)}</div>`;
1778
- for (const line of hunk.lines) {
1779
- const color = line.type === 'add' ? '#2d8a4e' : line.type === 'remove' ? '#c0392b' : 'var(--ink)';
1780
- const bg = line.type === 'add' ? 'rgba(45,138,78,0.1)' : line.type === 'remove' ? 'rgba(192,57,43,0.1)' : 'transparent';
1781
- html += `<div style="color:${color};background:${bg};padding:0 6px;">${line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '}${escapeHtml(line.content)}</div>`;
1782
- }
1783
- }
1784
- content.innerHTML = html || '<div class="muted">无差异</div>';
1785
- } else {
1786
- content.innerHTML = '<div class="muted">无差异或二进制文件</div>';
1787
- }
1788
- modal.style.display = 'flex';
1789
- } catch (e) { toast('Diff 加载失败: ' + (e.message || '网络错误')); }
1790
- }
1791
-
1792
- function closeGitDiff() {
1793
- document.getElementById('gitDiffModal').style.display = 'none';
1794
- }
1795
-
1796
- /* ========== P2-3: 团队协作 ========== */
1797
- let _teamButtonsBound = false;
1798
- let _teamData = null;
1799
- let _teamTaskFilter = '';
1800
-
1801
- function bindTeamButtonsOnce() {
1802
- if (_teamButtonsBound) return;
1803
- _teamButtonsBound = true;
1804
-
1805
- document.getElementById('teamRefreshBtn')?.addEventListener('click', loadTeamData);
1806
- document.getElementById('teamInviteBtn')?.addEventListener('click', teamInviteMember);
1807
- document.getElementById('teamCreateTaskBtn')?.addEventListener('click', teamCreateTask);
1808
-
1809
- // Tab 切换
1810
- document.querySelectorAll('.team-tab').forEach((tab) => {
1811
- tab.addEventListener('click', () => {
1812
- document.querySelectorAll('.team-tab').forEach((t) => {
1813
- t.classList.remove('active');
1814
- t.style.color = 'var(--muted)';
1815
- t.style.borderBottomColor = 'transparent';
1816
- t.style.fontWeight = 'normal';
1817
- });
1818
- tab.classList.add('active');
1819
- tab.style.color = 'var(--ink)';
1820
- tab.style.borderBottomColor = '#2d5a3d';
1821
- tab.style.fontWeight = '600';
1822
- const target = tab.getAttribute('data-tab');
1823
- document.getElementById('teamMembersTab').style.display = target === 'members' ? 'block' : 'none';
1824
- document.getElementById('teamTasksTab').style.display = target === 'tasks' ? 'block' : 'none';
1825
- });
1826
- });
1827
-
1828
- // 任务筛选
1829
- document.querySelectorAll('.team-task-filter').forEach((btn) => {
1830
- btn.addEventListener('click', () => {
1831
- document.querySelectorAll('.team-task-filter').forEach((b) => b.classList.remove('active'));
1832
- btn.classList.add('active');
1833
- _teamTaskFilter = btn.getAttribute('data-status') || '';
1834
- renderTeamTasks();
1835
- });
1836
- });
1837
- }
1838
-
1839
- async function loadTeamData() {
1840
- try {
1841
- const d = await api('/api/team', 'GET');
1842
- _teamData = d;
1843
- document.getElementById('teamName').textContent = d.team?.config?.name || '团队';
1844
- document.getElementById('teamMemberCount').textContent = d.stats?.totalMembers || 0;
1845
- document.getElementById('teamTaskCount').textContent = d.stats?.totalTasks || 0;
1846
- renderTeamMembers();
1847
- renderTeamTasks();
1848
- } catch (e) {
1849
- toast('团队数据加载失败: ' + (e.message || '网络错误'));
1850
- }
1851
- }
1852
-
1853
- function renderTeamMembers() {
1854
- const container = document.getElementById('teamMembersList');
1855
- const members = _teamData?.team?.members || [];
1856
- if (!members.length) {
1857
- container.innerHTML = '<div class="muted" style="font-size:11px;padding:20px;text-align:center;">暂无成员</div>';
1858
- return;
1859
- }
1860
- const roleColors = { owner: '#c0392b', admin: '#e67e22', developer: '#2d5a3d', viewer: '#7f8c8d' };
1861
- container.innerHTML = members.map((m) =>
1862
- `<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-bottom:1px solid var(--border);font-size:11px;">
1863
- <div style="flex:1;overflow:hidden;">
1864
- <div style="font-weight:600;">${escapeHtml(m.name)} <span style="color:${roleColors[m.role] || '#888'};font-size:10px;">[${m.role}]</span></div>
1865
- <div style="font-size:10px;color:var(--muted);">${escapeHtml(m.email)} · ${m.status}</div>
1866
- </div>
1867
- <div style="display:flex;gap:4px;flex-shrink:0;">
1868
- <select class="team-role-select" data-id="${m.id}" style="font-size:10px;padding:2px 4px;border:1px solid var(--border);border-radius:3px;background:var(--card);color:var(--ink);" ${m.role === 'owner' ? 'disabled' : ''}>
1869
- <option value="admin" ${m.role === 'admin' ? 'selected' : ''}>管理员</option>
1870
- <option value="developer" ${m.role === 'developer' ? 'selected' : ''}>开发者</option>
1871
- <option value="viewer" ${m.role === 'viewer' ? 'selected' : ''}>观察者</option>
1872
- </select>
1873
- <button class="team-remove-btn" data-id="${m.id}" style="font-size:10px;padding:2px 6px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:#c0392b;" ${m.role === 'owner' ? 'disabled style="opacity:0.3;"' : ''}>移除</button>
1874
- </div>
1875
- </div>`
1876
- ).join('');
1877
-
1878
- container.querySelectorAll('.team-role-select').forEach((sel) => {
1879
- sel.addEventListener('change', async () => {
1880
- const id = sel.getAttribute('data-id');
1881
- const role = sel.value;
1882
- try {
1883
- await api(`/api/team/members/${id}/role`, 'PUT', { role });
1884
- toast('角色已更新');
1885
- loadTeamData();
1886
- } catch (e) { toast('更新失败: ' + (e.message || '网络错误')); }
1887
- });
1888
- });
1889
- container.querySelectorAll('.team-remove-btn').forEach((btn) => {
1890
- btn.addEventListener('click', async () => {
1891
- const id = btn.getAttribute('data-id');
1892
- if (!confirm('确定移除该成员?')) return;
1893
- try {
1894
- await api(`/api/team/members/${id}`, 'DELETE');
1895
- toast('成员已移除');
1896
- loadTeamData();
1897
- } catch (e) { toast('移除失败: ' + (e.message || '网络错误')); }
1898
- });
1899
- });
1900
- }
1901
-
1902
- async function teamInviteMember() {
1903
- const name = document.getElementById('teamInviteName').value.trim();
1904
- const email = document.getElementById('teamInviteEmail').value.trim();
1905
- if (!name || !email) { toast('请输入姓名和邮箱'); return; }
1906
- try {
1907
- await api('/api/team/members/invite', 'POST', { name, email, role: 'developer' });
1908
- toast('✅ 邀请已发送');
1909
- document.getElementById('teamInviteName').value = '';
1910
- document.getElementById('teamInviteEmail').value = '';
1911
- loadTeamData();
1912
- } catch (e) { toast('邀请失败: ' + (e.message || '网络错误')); }
1913
- }
1914
-
1915
- function renderTeamTasks() {
1916
- const container = document.getElementById('teamTasksList');
1917
- let tasks = _teamData?.team?.tasks || [];
1918
- if (_teamTaskFilter) tasks = tasks.filter((t) => t.status === _teamTaskFilter);
1919
- if (!tasks.length) {
1920
- container.innerHTML = '<div class="muted" style="font-size:11px;padding:20px;text-align:center;">暂无任务</div>';
1921
- return;
1922
- }
1923
- const statusColors = { todo: '#7f8c8d', 'in-progress': '#3498db', review: '#e67e22', done: '#2d5a3d' };
1924
- const statusLabels = { todo: '待办', 'in-progress': '进行中', review: '评审', done: '完成' };
1925
- container.innerHTML = tasks.map((t) =>
1926
- `<div style="padding:8px;border-bottom:1px solid var(--border);font-size:11px;">
1927
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
1928
- <span style="font-weight:600;flex:1;">${escapeHtml(t.title)}</span>
1929
- <span style="font-size:10px;padding:2px 6px;border-radius:3px;background:${statusColors[t.status]}20;color:${statusColors[t.status]};font-weight:600;">${statusLabels[t.status]}</span>
1930
- </div>
1931
- ${t.description ? `<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">${escapeHtml(t.description.slice(0, 100))}</div>` : ''}
1932
- <div style="display:flex;gap:4px;align-items:center;font-size:10px;color:var(--muted);">
1933
- <span>👤 ${t.createdBy}</span>
1934
- <span>·</span>
1935
- <span>${new Date(t.updatedAt).toLocaleDateString('zh-CN')}</span>
1936
- ${t.comments?.length ? `<span>· 💬 ${t.comments.length}</span>` : ''}
1937
- <select class="team-task-status" data-id="${t.id}" style="margin-left:auto;font-size:10px;padding:2px 4px;border:1px solid var(--border);border-radius:3px;background:var(--card);color:var(--ink);">
1938
- <option value="todo" ${t.status === 'todo' ? 'selected' : ''}>待办</option>
1939
- <option value="in-progress" ${t.status === 'in-progress' ? 'selected' : ''}>进行中</option>
1940
- <option value="review" ${t.status === 'review' ? 'selected' : ''}>评审</option>
1941
- <option value="done" ${t.status === 'done' ? 'selected' : ''}>完成</option>
1942
- </select>
1943
- <button class="team-task-delete" data-id="${t.id}" style="font-size:10px;padding:2px 6px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:#c0392b;">删除</button>
1944
- </div>
1945
- </div>`
1946
- ).join('');
1947
-
1948
- container.querySelectorAll('.team-task-status').forEach((sel) => {
1949
- sel.addEventListener('change', async () => {
1950
- const id = sel.getAttribute('data-id');
1951
- const status = sel.value;
1952
- try {
1953
- await api(`/api/team/tasks/${id}`, 'PUT', { status });
1954
- toast('任务状态已更新');
1955
- loadTeamData();
1956
- } catch (e) { toast('更新失败: ' + (e.message || '网络错误')); }
1957
- });
1958
- });
1959
- container.querySelectorAll('.team-task-delete').forEach((btn) => {
1960
- btn.addEventListener('click', async () => {
1961
- const id = btn.getAttribute('data-id');
1962
- if (!confirm('确定删除该任务?')) return;
1963
- try {
1964
- await api(`/api/team/tasks/${id}`, 'DELETE');
1965
- toast('任务已删除');
1966
- loadTeamData();
1967
- } catch (e) { toast('删除失败: ' + (e.message || '网络错误')); }
1968
- });
1969
- });
1970
- }
1971
-
1972
- async function teamCreateTask() {
1973
- const title = document.getElementById('teamTaskTitle').value.trim();
1974
- if (!title) { toast('请输入任务标题'); return; }
1975
- try {
1976
- await api('/api/team/tasks', 'POST', { title, createdBy: '当前用户', status: 'todo' });
1977
- toast('✅ 任务已创建');
1978
- document.getElementById('teamTaskTitle').value = '';
1979
- loadTeamData();
1980
- } catch (e) { toast('创建失败: ' + (e.message || '网络错误')); }
1981
- }
1
+ /**
2
+ * UI 渲染模块:DOM 渲染与交互,依赖 utils 和 api
3
+ */
4
+
5
+ function initMemoryUI() {
6
+ // 设置默认日期为今天
7
+ const today = new Date().toISOString().split('T')[0];
8
+ document.getElementById('memDateInput').value = today;
9
+ loadMemoryContent(today);
10
+
11
+ // 事件只绑定一次(多次进出记忆页会累积监听器,导致一次点击多次请求)
12
+ if (!window._memUIInited) {
13
+ window._memUIInited = true;
14
+
15
+ // Tab 切换
16
+ document.querySelectorAll('.mem-tab').forEach(tab => {
17
+ tab.addEventListener('click', () => {
18
+ document.querySelectorAll('.mem-tab').forEach(t => t.classList.remove('active'));
19
+ document.querySelectorAll('.mem-panel').forEach(p => p.classList.toggle('active', false));
20
+ tab.classList.add('active');
21
+ const panelId = tab.getAttribute('data-memtab') + 'Panel';
22
+ document.getElementById(panelId)?.classList.add('active');
23
+ });
24
+ });
25
+
26
+ // 查看按钮
27
+ document.getElementById('memLoadDay')?.addEventListener('click', () => {
28
+ const dateStr = document.getElementById('memDateInput').value;
29
+ if (dateStr) loadMemoryContent(dateStr);
30
+ });
31
+
32
+ // 刷新按钮
33
+ document.getElementById('memRefreshBtn')?.addEventListener('click', () => {
34
+ loadMemoryStats();
35
+ const dateStr = document.getElementById('memDateInput').value;
36
+ if (dateStr) loadMemoryContent(dateStr);
37
+ loadLongTermMemory();
38
+ });
39
+
40
+ // 立即总结按钮
41
+ document.getElementById('memSummarizeBtn')?.addEventListener('click', async () => {
42
+ const btn = document.getElementById('memSummarizeBtn');
43
+ btn.disabled = true;
44
+ btn.textContent = '⏳ 总结中...';
45
+ try {
46
+ const dateStr = document.getElementById('memDateInput').value || today;
47
+ const d = await api('/api/memory/summarize', 'POST', { date: dateStr });
48
+ toast('✅ ' + (d.message || '总结完成'));
49
+ loadMemoryStats();
50
+ loadSummaryHistory();
51
+ loadLongTermMemory();
52
+ } catch (e) {
53
+ toast('总结失败:' + e.message);
54
+ } finally {
55
+ btn.disabled = false;
56
+ btn.textContent = '✨ 立即总结';
57
+ }
58
+ });
59
+
60
+ // 短期记忆:添加记录按钮(展开文本框)
61
+ document.getElementById('memAddShort')?.addEventListener('click', () => {
62
+ const editor = document.getElementById('memShortEditor');
63
+ const input = document.getElementById('memShortInput');
64
+ if (editor) {
65
+ editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
66
+ if (editor.style.display !== 'none' && input) {
67
+ input.value = '';
68
+ input.focus();
69
+ }
70
+ }
71
+ });
72
+ // 短期记忆:取消
73
+ document.getElementById('memShortCancel')?.addEventListener('click', () => {
74
+ const editor = document.getElementById('memShortEditor');
75
+ if (editor) editor.style.display = 'none';
76
+ });
77
+ // 短期记忆:保存
78
+ document.getElementById('memShortSave')?.addEventListener('click', async () => {
79
+ const input = document.getElementById('memShortInput');
80
+ const content = input ? input.value.trim() : '';
81
+ if (!content) { toast('请输入记录内容'); return; }
82
+ try {
83
+ await api('/api/memory/short', 'POST', {
84
+ title: content.slice(0, 30) + (content.length > 30 ? '...' : ''),
85
+ type: 'note',
86
+ content: content,
87
+ });
88
+ toast('已添加到短期记忆');
89
+ const editor = document.getElementById('memShortEditor');
90
+ if (editor) editor.style.display = 'none';
91
+ const dateStr = document.getElementById('memDateInput').value;
92
+ if (dateStr) loadMemoryContent(dateStr);
93
+ loadMemoryStats();
94
+ } catch (e) {
95
+ toast('添加失败:' + e.message);
96
+ }
97
+ });
98
+
99
+ // 长期记忆:编辑按钮
100
+ document.getElementById('memEditLong')?.addEventListener('click', () => {
101
+ bindGhostTextOnce(); // P1-1: 编辑时启用 ghost text 补全
102
+ const content = document.getElementById('memLongContent');
103
+ const editor = document.getElementById('memLongEditor');
104
+ const editBtn = document.getElementById('memEditLong');
105
+ const saveBtn = document.getElementById('memSaveLong');
106
+ const cancelBtn = document.getElementById('memCancelLong');
107
+ const appendBtn = document.getElementById('memAppendLong');
108
+ const appendEditor = document.getElementById('memLongAppendEditor');
109
+ // 把当前内容放到编辑器
110
+ if (editor && content) {
111
+ editor.value = content.innerText || content.textContent || '';
112
+ }
113
+ if (content) content.style.display = 'none';
114
+ if (editor) editor.style.display = 'block';
115
+ if (editBtn) editBtn.style.display = 'none';
116
+ if (appendBtn) appendBtn.style.display = 'none';
117
+ if (appendEditor) appendEditor.style.display = 'none';
118
+ if (saveBtn) saveBtn.style.display = '';
119
+ if (cancelBtn) cancelBtn.style.display = '';
120
+ });
121
+ // 长期记忆:取消编辑
122
+ document.getElementById('memCancelLong')?.addEventListener('click', () => {
123
+ const content = document.getElementById('memLongContent');
124
+ const editor = document.getElementById('memLongEditor');
125
+ const editBtn = document.getElementById('memEditLong');
126
+ const saveBtn = document.getElementById('memSaveLong');
127
+ const cancelBtn = document.getElementById('memCancelLong');
128
+ const appendBtn = document.getElementById('memAppendLong');
129
+ if (content) content.style.display = '';
130
+ if (editor) editor.style.display = 'none';
131
+ if (editBtn) editBtn.style.display = '';
132
+ if (appendBtn) appendBtn.style.display = '';
133
+ if (saveBtn) saveBtn.style.display = 'none';
134
+ if (cancelBtn) cancelBtn.style.display = 'none';
135
+ });
136
+ // 长期记忆:保存编辑
137
+ document.getElementById('memSaveLong')?.addEventListener('click', async () => {
138
+ const editor = document.getElementById('memLongEditor');
139
+ const content = editor ? editor.value : '';
140
+ try {
141
+ await api('/api/memory/long', 'POST', { content });
142
+ toast('长期记忆已保存');
143
+ document.getElementById('memCancelLong')?.click();
144
+ loadLongTermMemory();
145
+ loadMemoryStats();
146
+ } catch (e) {
147
+ toast('保存失败:' + e.message);
148
+ }
149
+ });
150
+ // 长期记忆:追加按钮(展开文本框)
151
+ document.getElementById('memAppendLong')?.addEventListener('click', () => {
152
+ const editor = document.getElementById('memLongAppendEditor');
153
+ const titleInput = document.getElementById('memAppendTitle');
154
+ const contentInput = document.getElementById('memAppendContent');
155
+ if (editor) {
156
+ editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
157
+ if (editor.style.display !== 'none') {
158
+ if (titleInput) titleInput.value = '';
159
+ if (contentInput) contentInput.value = '';
160
+ if (titleInput) titleInput.focus();
161
+ }
162
+ }
163
+ });
164
+ // 长期记忆:取消追加
165
+ document.getElementById('memAppendCancel')?.addEventListener('click', () => {
166
+ const editor = document.getElementById('memLongAppendEditor');
167
+ if (editor) editor.style.display = 'none';
168
+ });
169
+ // 长期记忆:确认追加
170
+ document.getElementById('memAppendSave')?.addEventListener('click', async () => {
171
+ const titleInput = document.getElementById('memAppendTitle');
172
+ const contentInput = document.getElementById('memAppendContent');
173
+ const title = titleInput ? titleInput.value.trim() : '';
174
+ const content = contentInput ? contentInput.value.trim() : '';
175
+ if (!title || !content) { toast('请输入标题和内容'); return; }
176
+ try {
177
+ await api('/api/memory/long/append', 'POST', { title, category: '自定义', content });
178
+ toast('已追加到长期记忆');
179
+ const editor = document.getElementById('memLongAppendEditor');
180
+ if (editor) editor.style.display = 'none';
181
+ loadLongTermMemory();
182
+ loadMemoryStats();
183
+ } catch (e) {
184
+ toast('追加失败:' + e.message);
185
+ }
186
+ });
187
+ }
188
+
189
+ // 每次进入都刷新数据(事件不重复绑定,数据实时刷新)
190
+ loadMemoryStats();
191
+ loadLongTermMemory();
192
+ loadSummaryHistory();
193
+ }
194
+
195
+ function renderMarketGrid() {
196
+ const grid = document.getElementById('marketGrid');
197
+ if (!state.market.length) { renderEmpty(grid, t('empty.no_skills')); return; }
198
+ grid.innerHTML = state.market.map((s) => {
199
+ const installed = state.installed.has(s.id);
200
+ const btn = installed
201
+ ? '<button class="install" disabled style="opacity:.55;cursor:default;">✅ 已安装</button>'
202
+ : '<button class="install" data-id="' + encodeURIComponent(s.id) + '" data-name="' + encodeURIComponent(s.name) + '" data-source="' + s.source + '">安装</button>';
203
+ return '<div class="tile"><div class="icon">' + (s.source === 'clawhub' ? '🧩' : '🛠') + '</div><div class="title">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description).slice(0, 70) + '…<br><span class="muted">' + (s.source === 'clawhub' ? 'ClawHub' : 'Agent-Foundry') + ' · 下载 ' + (s.downloads || 0) + '</span></div><div class="ops">' + btn + '</div></div>';
204
+ }).join('');
205
+ grid.querySelectorAll('button.install').forEach((b) => b.addEventListener('click', (e) => {
206
+ e.stopPropagation();
207
+ installSkill(b.getAttribute('data-id'), b.getAttribute('data-name'), b.getAttribute('data-source'));
208
+ }));
209
+ }
210
+
211
+ function bindTplCards(grid) {
212
+ grid.querySelectorAll('button.use').forEach((b) => b.addEventListener('click', (e) => {
213
+ e.stopPropagation();
214
+ const inp = document.getElementById('goalInput');
215
+ if (inp) inp.value = decodeURIComponent(b.getAttribute('data-goal'));
216
+ switchView('chat');
217
+ if (inp) inp.focus();
218
+ toast('已填入输入框,去发送吧');
219
+ }));
220
+ grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', async (e) => {
221
+ e.stopPropagation();
222
+ try { await api('/api/templates/' + b.getAttribute('data-id'), 'DELETE'); await loadTemplates(); toast('已删除'); }
223
+ catch (err) { toast('删除失败:' + err.message); }
224
+ }));
225
+ }
226
+
227
+ function renderUserTpl(list) {
228
+ const grid = document.getElementById('userTplGrid');
229
+ if (!list.length) { renderEmpty(grid, t('empty.no_templates')); return; }
230
+ grid.innerHTML = list.map((t) => tplCard(t, true)).join('');
231
+ bindTplCards(grid);
232
+ }
233
+
234
+ function renderBuiltin(list) {
235
+ const grid = document.getElementById('builtinGrid');
236
+ if (!list.length) { renderEmpty(grid, t('empty.no_builtin_templates')); return; }
237
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
238
+ bindTplCards(grid);
239
+ }
240
+
241
+ function tplCard(t, deletable) {
242
+ return '<div class="tile" data-goal="' + encodeURIComponent(t.goal) + '"><div class="icon">' + (t.icon || '📄') + '</div><div class="title">' + escapeHtml(t.title) + '</div><div class="desc">' + escapeHtml(t.category || '') + ' · ' + escapeHtml(t.goal).slice(0, 40) + '…</div><div class="ops"><button class="use" data-goal="' + encodeURIComponent(t.goal) + '">填入输入框</button>' + (deletable ? '<button class="ghost del" data-id="' + t.id + '">删除</button>' : '') + '</div></div>';
243
+ }
244
+
245
+ function renderBuiltinAutomations(list) {
246
+ const grid = document.getElementById('builtinAutoGrid');
247
+ if (!grid) return;
248
+ if (!list.length) { renderEmpty(grid, '暂无预置指令'); return; }
249
+ grid.innerHTML = list.map((a) => {
250
+ const icon = a.icon || '';
251
+ const category = a.category || '常用';
252
+ const goalPreview = (a.goal || '').slice(0, 50) + ((a.goal || '').length > 50 ? '…' : '');
253
+ return '<div class="tile builtin-tile" data-id="' + escapeHtml(a.id) + '">'
254
+ + '<div class="icon">' + icon + '</div>'
255
+ + '<div class="title">' + escapeHtml(a.name) + '</div>'
256
+ + '<div class="desc"><span style="color:var(--brand);font-weight:500;">' + escapeHtml(category) + '</span> · ' + escapeHtml(goalPreview) + '</div>'
257
+ + '<div class="ops">'
258
+ + '<button class="run builtin-run" data-id="' + escapeHtml(a.id) + '">▶ 运行</button>'
259
+ + '<button class="ghost builtin-save" data-id="' + escapeHtml(a.id) + '" data-name="' + escapeHtml(a.name) + '" data-goal="' + encodeURIComponent(a.goal || '') + '">+ 保存为我的</button>'
260
+ + '</div></div>';
261
+ }).join('');
262
+ grid.querySelectorAll('button.builtin-run').forEach((b) => b.addEventListener('click', (e) => {
263
+ e.stopPropagation();
264
+ runAuto(b.getAttribute('data-id'));
265
+ }));
266
+ grid.querySelectorAll('button.builtin-save').forEach((b) => b.addEventListener('click', (e) => {
267
+ e.stopPropagation();
268
+ const name = b.getAttribute('data-name') || '';
269
+ const goal = decodeURIComponent(b.getAttribute('data-goal') || '');
270
+ saveBuiltinAsMine(name, goal);
271
+ }));
272
+ }
273
+
274
+ async function saveBuiltinAsMine(name, goal) {
275
+ try {
276
+ await api('/api/automations', 'POST', { name, goal });
277
+ await loadAutomations();
278
+ toast('已保存到「我的指令」,可在那里编辑');
279
+ } catch (e) { toast('保存失败:' + e.message); }
280
+ }
281
+
282
+ function renderAutoGrid(list) {
283
+ const grid = document.getElementById('autoGrid');
284
+ if (!list.length) { renderEmpty(grid, t('empty.no_automations')); return; }
285
+ grid.innerHTML = list.map((a) => {
286
+ const icon = a.icon || '';
287
+ const category = a.category ? ' · ' + escapeHtml(a.category) : '';
288
+ return '<div class="tile" data-id="' + a.id + '">'
289
+ + '<div class="icon">' + icon + '</div>'
290
+ + '<div class="title">' + escapeHtml(a.name) + category + '</div>'
291
+ + '<div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div>'
292
+ + '<div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>';
293
+ }).join('');
294
+ grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
295
+ grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
296
+ }
297
+
298
+ /* ========== 节点模板 & 自定义来源模板 ========== */
299
+ function renderNodeTpl(list) {
300
+ const grid = document.getElementById('nodeTplGrid');
301
+ const title = document.getElementById('nodeTplTitle');
302
+ if (!grid) return;
303
+ if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
304
+ if (title) title.style.display = '';
305
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
306
+ bindTplCards(grid);
307
+ }
308
+ function renderCustomTpl(list) {
309
+ const grid = document.getElementById('customTplGrid');
310
+ const title = document.getElementById('customTplTitle');
311
+ if (!grid) return;
312
+ if (!list.length) { if (title) title.style.display = 'none'; grid.innerHTML = ''; return; }
313
+ if (title) title.style.display = '';
314
+ grid.innerHTML = list.map((t) => tplCard(t, false)).join('');
315
+ bindTplCards(grid);
316
+ }
317
+
318
+ /* ========== 节点管理渲染 ========== */
319
+ function renderNodesGrid(list) {
320
+ const grid = document.getElementById('nodesGrid');
321
+ if (!grid) return;
322
+ window._nodesCache = list;
323
+ if (!list.length) { grid.innerHTML = '<div class="empty">还没有添加节点,点击上方「添加节点」连接外部服务</div>'; return; }
324
+ grid.innerHTML = list.map((n) => {
325
+ const statusColor = n.status === 'connected' ? 'var(--ok)' : n.status === 'error' ? 'var(--err)' : 'var(--muted)';
326
+ const statusText = n.status === 'connected' ? '已连接' : n.status === 'error' ? '连接失败' : '未连接';
327
+ const caps = (n.capabilities || []).map((c) => c === 'templates' ? '模板' : c === 'skills' ? '插件' : '办公').join('、');
328
+ return '<div class="tile node-tile" data-id="' + n.id + '">'
329
+ + '<div class="icon">' + (n.type === 'http' ? '🌐' : n.type === 'local' ? '📁' : '📦') + '</div>'
330
+ + '<div class="title">' + escapeHtml(n.name) + ' <span style="font-size:11px;color:' + statusColor + ';">● ' + statusText + '</span></div>'
331
+ + '<div class="desc">' + escapeHtml(n.url).slice(0, 50) + '…<br>能力:' + caps + (n.lastSyncAt ? '<br>上次同步:' + fmtTime(n.lastSyncAt) : '') + (n.lastError ? '<br style="color:var(--err);">错误:' + escapeHtml(n.lastError) : '') + '</div>'
332
+ + '<div class="ops" style="flex-wrap:wrap;">'
333
+ + '<button class="run node-sync" data-id="' + n.id + '">🔄 同步</button>'
334
+ + '<button class="ghost node-test" data-id="' + n.id + '">测试</button>'
335
+ + '<button class="ghost node-edit" data-id="' + n.id + '">编辑</button>'
336
+ + '<button class="ghost del node-del" data-id="' + n.id + '">删除</button>'
337
+ + '</div></div>';
338
+ }).join('');
339
+ grid.querySelectorAll('button.node-sync').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); syncNode(b.getAttribute('data-id')); }));
340
+ grid.querySelectorAll('button.node-test').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); testNode(b.getAttribute('data-id')); }));
341
+ grid.querySelectorAll('button.node-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editNode(b.getAttribute('data-id')); }));
342
+ grid.querySelectorAll('button.node-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此节点?')) deleteNode(b.getAttribute('data-id')); }));
343
+ }
344
+
345
+ /* ========== 自定义来源渲染 ========== */
346
+ function renderSourcesGrid(list) {
347
+ const grid = document.getElementById('sourcesGrid');
348
+ if (!grid) return;
349
+ window._sourcesCache = list;
350
+ if (!list.length) { grid.innerHTML = '<div class="empty">还没有自定义来源,点击上方「自定义来源」添加远程数据源</div>'; return; }
351
+ const typeMap = { templates: '模板', skills: '插件', office: '办公' };
352
+ grid.innerHTML = list.map((s) => '<div class="tile" data-id="' + s.id + '">'
353
+ + '<div class="icon">🔗</div>'
354
+ + '<div class="title">' + escapeHtml(s.name) + ' <span style="font-size:11px;color:var(--brand);">' + (typeMap[s.type] || s.type) + '</span></div>'
355
+ + '<div class="desc">' + escapeHtml(s.url).slice(0, 60) + '…<br>状态:' + (s.enabled ? '已启用' : '已禁用') + '</div>'
356
+ + '<div class="ops">'
357
+ + '<button class="ghost source-edit" data-id="' + s.id + '">编辑</button>'
358
+ + '<button class="ghost del source-del" data-id="' + s.id + '">删除</button>'
359
+ + '</div></div>').join('');
360
+ grid.querySelectorAll('button.source-edit').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); editSource(b.getAttribute('data-id')); }));
361
+ grid.querySelectorAll('button.source-del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定删除此来源?')) deleteSource(b.getAttribute('data-id')); }));
362
+ }
363
+
364
+ function renderWorkspaceBar() {
365
+ const el = document.getElementById('workspaceCwdBottom');
366
+ if (el) el.textContent = state.workspaceDir || '(未设置)';
367
+ }
368
+
369
+ function openPermissions() {
370
+ const p = state.permissions;
371
+ document.querySelector('input[name="readScope"][value="' + p.readScope + '"]').checked = true;
372
+ document.getElementById('permReadPath').value = p.readPath || '';
373
+ document.getElementById('permRead').checked = p.allowRead;
374
+ document.getElementById('permWrite').checked = p.allowWrite;
375
+ document.getElementById('permShell').checked = p.allowShell;
376
+ document.getElementById('permNetwork').checked = p.allowNetwork;
377
+ document.getElementById('permBrowser').checked = p.allowBrowser;
378
+ openModal('permModal');
379
+ }
380
+
381
+ function openAgentSelector() {
382
+ const grid = document.getElementById('agentGrid');
383
+ grid.innerHTML = Object.entries(AGENT_TYPES).map(([k, v]) =>
384
+ '<div class="agent-card ' + (state.agentType === k ? 'selected' : '') + '" data-type="' + k + '"><div class="icon">' + v.icon + '</div><div class="title">' + v.label + '</div></div>'
385
+ ).join('');
386
+ grid.querySelectorAll('.agent-card').forEach((c) => c.addEventListener('click', () => {
387
+ state.agentType = c.getAttribute('data-type');
388
+ updateUserBar();
389
+ closeModal('agentModal');
390
+ toast('已切换:' + AGENT_TYPES[state.agentType].label);
391
+ }));
392
+ openModal('agentModal');
393
+ }
394
+
395
+ function fillInlineForm(id) {
396
+ const m = state.models.find((x) => x.id === id);
397
+ if (!m) return;
398
+ document.getElementById('imName').value = m.name;
399
+ document.getElementById('imBase').value = m.apiBase || '';
400
+ document.getElementById('imKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
401
+ document.getElementById('imReasoning').value = m.reasoning || '';
402
+ document.getElementById('imMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
403
+ document.getElementById('imMsg').classList.remove('err');
404
+ document.getElementById('imSave').dataset.editing = id;
405
+ }
406
+
407
+ function resetInlineForm() {
408
+ document.getElementById('imName').value = '';
409
+ document.getElementById('imBase').value = '';
410
+ document.getElementById('imKey').value = '';
411
+ document.getElementById('imReasoning').value = '';
412
+ document.getElementById('imMsg').textContent = '';
413
+ document.getElementById('imMsg').classList.remove('err');
414
+ document.getElementById('imSave').dataset.editing = '';
415
+ }
416
+
417
+ function renderModelListInline() {
418
+ const list = document.getElementById('modelListInline');
419
+ if (!state.models.length) {
420
+ list.innerHTML =
421
+ '<div class="mm-empty">' +
422
+ '<div class="mm-empty-icon">🧠</div>' +
423
+ '<div>还没有模型配置</div>' +
424
+ '<div class="mm-empty-hint">在下方添加第一个模型,保存后即可在对话中使用</div>' +
425
+ '</div>';
426
+ return;
427
+ }
428
+ list.innerHTML = state.models.map((m) => {
429
+ const url = (m.apiBase || '').trim() || '(未填写 API 地址)';
430
+ const meta = m.reasoning ? '已配置推理中间环节' : '未配置推理中间环节';
431
+ const tag = m.default ? '<span class="mm-default-tag">默认</span>' : '';
432
+ const defaultBtn = m.default ? '' : '<button class="mm-act mm-act-primary" data-act="default">设为默认</button>';
433
+ return (
434
+ '<div class="mm-card" data-id="' + escapeHtml(m.id) + '">' +
435
+ '<div class="mm-card-head">' +
436
+ '<span class="mm-card-name">' + escapeHtml(m.name) + '</span>' +
437
+ tag +
438
+ '</div>' +
439
+ '<div class="mm-card-url">' + escapeHtml(url) + '</div>' +
440
+ '<div class="mm-card-meta">' + meta + '</div>' +
441
+ '<div class="mm-card-actions">' +
442
+ defaultBtn +
443
+ '<button class="mm-act" data-act="edit">编辑</button>' +
444
+ '<button class="mm-act mm-act-danger" data-act="del">删除</button>' +
445
+ '</div>' +
446
+ '</div>'
447
+ );
448
+ }).join('');
449
+ list.querySelectorAll('.mm-card').forEach((el) => {
450
+ const id = el.getAttribute('data-id');
451
+ el.querySelectorAll('button').forEach((b) => {
452
+ b.addEventListener('click', (e) => {
453
+ e.stopPropagation();
454
+ const act = b.getAttribute('data-act');
455
+ if (act === 'default') setDefaultModel(id);
456
+ else if (act === 'edit') fillInlineForm(id);
457
+ else if (act === 'del') deleteModel(id);
458
+ });
459
+ });
460
+ });
461
+ }
462
+
463
+ function toggleModelManage() {
464
+ const box = document.getElementById('modelManage');
465
+ const open = box.style.display === 'none' || !box.style.display;
466
+ box.style.display = open ? '' : 'none';
467
+ if (open) { renderModelListInline(); resetInlineForm(); }
468
+ }
469
+
470
+ function openSettings() {
471
+ // 打开设置时同步会话开关状态,避免界面显示与真实配置不一致
472
+ document.querySelectorAll('.session-toggle').forEach((el) => {
473
+ const key = el.dataset.key;
474
+ if (key && key in state.sessionConfig) el.checked = !!state.sessionConfig[key];
475
+ });
476
+ openModal('settingsModal');
477
+ }
478
+
479
+ function persistSessionConfig() {
480
+ localStorage.setItem('fhcode.sessionConfig', JSON.stringify(state.sessionConfig));
481
+ }
482
+
483
+ function editModel(id) {
484
+ const m = state.models.find((x) => x.id === id);
485
+ if (!m) return;
486
+ document.getElementById('modelName').value = m.name;
487
+ document.getElementById('modelApiBase').value = m.apiBase || '';
488
+ document.getElementById('modelApiKey').value = ''; // 密钥不回填(服务端加密存储,防泄露)
489
+ document.getElementById('modelReasoning').value = m.reasoning || '';
490
+ document.getElementById('modelFormMsg').textContent = '正在编辑:' + m.name + '(密钥已加密存储,留空保持不变)';
491
+ document.getElementById('modelFormMsg').classList.remove('err');
492
+ document.getElementById('modelSaveBtn').dataset.editing = id;
493
+ }
494
+
495
+ function resetModelForm() {
496
+ document.getElementById('modelName').value = '';
497
+ document.getElementById('modelApiBase').value = '';
498
+ document.getElementById('modelApiKey').value = '';
499
+ document.getElementById('modelReasoning').value = '';
500
+ document.getElementById('modelFormMsg').textContent = '';
501
+ document.getElementById('modelFormMsg').classList.remove('err');
502
+ document.getElementById('modelSaveBtn').dataset.editing = '';
503
+ }
504
+
505
+ function renderModelList() {
506
+ const list = document.getElementById('modelList');
507
+ if (!state.models.length) {
508
+ renderEmpty(list, t('empty.no_models'));
509
+ return;
510
+ }
511
+ list.innerHTML = state.models.map((m) => (
512
+ '<div class="model-item" data-id="' + escapeHtml(m.id) + '">' +
513
+ '<div><div class="mi-name">' + escapeHtml(m.name) + '</div>' +
514
+ '<div class="mi-base">' + escapeHtml(m.apiBase || '(未填写 API 地址)') + (m.reasoning ? ' · 已配推理内容' : '') + '</div></div>' +
515
+ (m.default ? '<span class="mi-default">默认</span>' : '') +
516
+ '<div class="mi-actions">' +
517
+ (m.default ? '' : '<button data-act="default">设为默认</button>') +
518
+ '<button data-act="edit">编辑</button>' +
519
+ '<button class="danger" data-act="del">删除</button>' +
520
+ '</div>' +
521
+ '</div>'
522
+ )).join('');
523
+ list.querySelectorAll('.model-item').forEach((el) => {
524
+ const id = el.getAttribute('data-id');
525
+ el.querySelectorAll('button').forEach((b) => {
526
+ b.addEventListener('click', (e) => {
527
+ e.stopPropagation();
528
+ const act = b.getAttribute('data-act');
529
+ if (act === 'default') setDefaultModel(id);
530
+ else if (act === 'edit') editModel(id);
531
+ else if (act === 'del') deleteModel(id);
532
+ });
533
+ });
534
+ });
535
+ }
536
+
537
+ function openModels() {
538
+ renderModelList();
539
+ resetModelForm();
540
+ openModal('modelModal');
541
+ }
542
+
543
+ function renderModelSelect() {
544
+ const sel = document.getElementById('modelSelect');
545
+ const opts = ['<option value="">默认(系统路由)</option>'];
546
+ state.models.forEach((m) => {
547
+ opts.push('<option value="' + escapeHtml(m.id) + '">' + escapeHtml(m.name) + (m.default ? ' ★' : '') + '</option>');
548
+ });
549
+ sel.innerHTML = opts.join('');
550
+ sel.value = state.modelId || '';
551
+ }
552
+
553
+ function toggleDirectMode() {
554
+ state.directMode = !state.directMode;
555
+ document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
556
+ const computerTab = document.getElementById('computerTab');
557
+ if (computerTab) {
558
+ computerTab.style.display = state.directMode ? '' : 'none';
559
+ if (state.directMode) {
560
+ // 切换到电脑操作标签页
561
+ document.querySelectorAll('.right-tab').forEach(t => t.classList.remove('active'));
562
+ computerTab.classList.add('active');
563
+ document.querySelectorAll('.preview-panel').forEach(p => p.classList.remove('active'));
564
+ document.getElementById('computerPanel')?.classList.add('active');
565
+ }
566
+ }
567
+ toast(state.directMode ? '已开启「电脑操作」模式,可以用语言控制电脑' : '已关闭「电脑操作」模式');
568
+ }
569
+
570
+ function previewImage(path) {
571
+ document.getElementById('previewTitle').textContent = '图片: ' + path;
572
+ const box = document.getElementById('previewContent');
573
+ const img = document.createElement('img');
574
+ img.src = 'file://' + path;
575
+ img.style.maxWidth = '100%';
576
+ img.style.borderRadius = '8px';
577
+ img.addEventListener('error', () => {
578
+ box.innerHTML = '<div class="muted">无法直接显示本地图片,请用系统相册打开</div>';
579
+ });
580
+ box.innerHTML = '';
581
+ box.appendChild(img);
582
+ switchRightTab('preview');
583
+ }
584
+
585
+ async function previewFile(path) {
586
+ try {
587
+ const d = await api('/api/files/read', 'POST', { path });
588
+ showPreview('文件: ' + path, d.content, 'file');
589
+ switchRightTab('preview');
590
+ } catch (e) { toast('预览失败:' + e.message); }
591
+ }
592
+
593
+ function bindArtifacts(container) {
594
+ container.querySelectorAll('.artifact').forEach((el) => {
595
+ el.addEventListener('click', () => {
596
+ const kind = el.getAttribute('data-kind');
597
+ const path = el.getAttribute('data-path');
598
+ if (kind === 'url') openBrowser(path);
599
+ else previewFile(path);
600
+ });
601
+ });
602
+ container.querySelectorAll('.file-chip').forEach((el) => {
603
+ el.addEventListener('click', () => {
604
+ const path = el.getAttribute('data-path');
605
+ const mime = el.getAttribute('data-type') || '';
606
+ if (mime.startsWith('image/')) previewImage(path);
607
+ else previewFile(path);
608
+ });
609
+ });
610
+ }
611
+
612
+ function renderStagedFiles() {
613
+ const box = document.getElementById('stagedArea');
614
+ if (!box) return;
615
+ if (!state.stagedFiles.length) {
616
+ box.classList.remove('show');
617
+ box.innerHTML = '';
618
+ return;
619
+ }
620
+ box.classList.add('show');
621
+ box.innerHTML = state.stagedFiles.map((f) => {
622
+ const isImg = (f.mime || '').startsWith('image/');
623
+ const icon = isImg ? '🖼' : '📎';
624
+ return (
625
+ '<div class="staged-chip" data-id="' + escapeHtml(f.id) + '">' +
626
+ '<span>' + icon + '</span>' +
627
+ '<span class="staged-name">' + escapeHtml(f.name) + '</span>' +
628
+ '<span class="staged-size">' + formatSize(f.size) + '</span>' +
629
+ '<button class="staged-remove" data-act="remove" title="移除">×</button>' +
630
+ '</div>'
631
+ );
632
+ }).join('') + '<span class="staged-hint">💡 点发送时一起上传</span>';
633
+ box.querySelectorAll('.staged-remove').forEach((b) => {
634
+ b.addEventListener('click', (e) => {
635
+ e.stopPropagation();
636
+ const chip = b.closest('.staged-chip');
637
+ if (chip) removeStagedFile(chip.getAttribute('data-id'));
638
+ });
639
+ });
640
+ }
641
+
642
+ function showPreview(title, content, mode) {
643
+ document.getElementById('previewTitle').textContent = title;
644
+ const box = document.getElementById('previewContent');
645
+ const isCode = mode === 'code' || mode === 'file';
646
+ // 从标题提取文件路径(标题格式:"文件: /path/to/file")
647
+ const filePath = title.startsWith('文件: ') ? title.slice(4).trim() : '';
648
+ if (isCode) {
649
+ box.innerHTML =
650
+ '<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;align-items:center;">' +
651
+ '<button class="ghost-btn" id="previewEditBtn" style="font-size:11px;padding:3px 10px;">✏️ 编辑</button>' +
652
+ '<button class="ghost-btn" id="previewStageBtn" style="font-size:11px;padding:3px 10px;display:none;background:#2d5a3d;color:#fff;">📝 暂存变更</button>' +
653
+ '<button class="ghost-btn" id="previewCancelBtn" style="font-size:11px;padding:3px 10px;display:none;">取消</button>' +
654
+ '<span class="muted" id="previewEditHint" style="font-size:11px;"></span>' +
655
+ '<span class="muted" id="monacoStatus" style="font-size:10px;margin-left:auto;"></span>' +
656
+ '</div>' +
657
+ // P0-1: Monaco 编辑器容器(VSCode 同款内核,语法高亮+智能补全)
658
+ '<div id="monacoContainer" style="width:100%;height:500px;border:1px solid var(--border);border-radius:6px;overflow:hidden;"></div>';
659
+ const editBtn = document.getElementById('previewEditBtn');
660
+ const stageBtn = document.getElementById('previewStageBtn');
661
+ const cancelBtn = document.getElementById('previewCancelBtn');
662
+ const hint = document.getElementById('previewEditHint');
663
+ const monacoStatus = document.getElementById('monacoStatus');
664
+ let monacoEditor = null;
665
+ let originalContent = content;
666
+ let isEditing = false;
667
+
668
+ // 初始化 Monaco 只读视图(语法高亮)
669
+ async function initMonacoReadOnly() {
670
+ if (typeof FHMonaco === 'undefined') {
671
+ monacoStatus.textContent = 'Monaco 未加载';
672
+ return;
673
+ }
674
+ try {
675
+ monacoStatus.textContent = '加载编辑器…';
676
+ monacoEditor = await FHMonaco.createEditor('monacoContainer', content, filePath || 'untitled', {
677
+ readOnly: true,
678
+ domReadOnly: true,
679
+ minimap: { enabled: content.length > 500 },
680
+ });
681
+ monacoStatus.textContent = '';
682
+ } catch (e) {
683
+ monacoStatus.textContent = '编辑器加载失败: ' + e.message;
684
+ }
685
+ }
686
+
687
+ // 切换到编辑模式
688
+ async function enterEditMode() {
689
+ registerMonacoCompletions();
690
+ if (!monacoEditor) {
691
+ // 如果只读模式还没初始化,先创建可编辑实例
692
+ monacoEditor = await FHMonaco.createEditor('monacoContainer', originalContent, filePath || 'untitled', {
693
+ readOnly: false,
694
+ minimap: { enabled: true },
695
+ });
696
+ } else {
697
+ monacoEditor.updateOptions({ readOnly: false, domReadOnly: false });
698
+ }
699
+ isEditing = true;
700
+ editBtn.style.display = 'none';
701
+ stageBtn.style.display = 'inline-block';
702
+ cancelBtn.style.display = 'inline-block';
703
+ hint.textContent = filePath ? '编辑后点「暂存变更」· Tab 接受内联补全 · Ctrl+Space 选择补全' : '(未关联文件路径)';
704
+ monacoStatus.textContent = '编辑模式';
705
+ // 聚焦到编辑器末尾
706
+ monacoEditor.focus();
707
+ const model = monacoEditor.getModel();
708
+ const lastLine = model.getLineCount();
709
+ monacoEditor.setPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) });
710
+ }
711
+
712
+ // 退出编辑模式(取消)
713
+ function exitEditMode() {
714
+ if (monacoEditor) {
715
+ monacoEditor.setValue(originalContent);
716
+ monacoEditor.updateOptions({ readOnly: true, domReadOnly: true });
717
+ }
718
+ isEditing = false;
719
+ editBtn.style.display = 'inline-block';
720
+ stageBtn.style.display = 'none';
721
+ cancelBtn.style.display = 'none';
722
+ hint.textContent = '';
723
+ monacoStatus.textContent = '';
724
+ }
725
+
726
+ editBtn.addEventListener('click', enterEditMode);
727
+ cancelBtn.addEventListener('click', exitEditMode);
728
+ stageBtn.addEventListener('click', async () => {
729
+ if (!monacoEditor) return;
730
+ const newContent = monacoEditor.getValue();
731
+ if (newContent === originalContent) { toast('内容未变化'); return; }
732
+ const stagePath = filePath || prompt('请输入文件路径(用于暂存变更):');
733
+ if (!stagePath) return;
734
+ try {
735
+ await api('/api/changes/stage', 'POST', { path: stagePath, content: newContent });
736
+ toast('✅ 已暂存到变更面板');
737
+ originalContent = newContent;
738
+ exitEditMode();
739
+ switchRightTab('changes');
740
+ } catch (e) { toast('暂存失败:' + e.message); }
741
+ });
742
+
743
+ // 启动只读视图
744
+ initMonacoReadOnly();
745
+ } else {
746
+ box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
747
+ }
748
+ }
749
+
750
+ /* ========== P0-1: Monaco 补全 Provider 注册(内联 ghost text + 补全弹窗) ========== */
751
+ let _monacoCompletionsRegistered = false;
752
+
753
+ // P6-1: 补全接受后自动 lint——调 /api/lint 校验插入代码,异常追加 fhcode-lint 波浪线 + 提示
754
+ let _lintFeedbackShown = false;
755
+ async function lintAfterAccept(inserted) {
756
+ if (!inserted || !inserted.trim()) return;
757
+ try {
758
+ const d = await api('/api/lint', 'POST', { code: inserted, language: '' });
759
+ if (!d || !d.ok) return;
760
+ if (d.clean) {
761
+ _lintFeedbackShown = false;
762
+ return;
763
+ }
764
+ if (typeof FHMonaco !== 'undefined' && FHMonaco.showLintFeedback) {
765
+ FHMonaco.showLintFeedback(d.errors || []);
766
+ }
767
+ const errCount = (d.errors || []).filter((x) => x.severity === 1).length;
768
+ if (errCount > 0 && !_lintFeedbackShown) {
769
+ _lintFeedbackShown = true;
770
+ const bar = document.getElementById('statusMessage');
771
+ if (bar) bar.textContent = '⚠️ 补全已接受,但校验发现 ' + errCount + ' 个语法错误(已标注)';
772
+ setTimeout(() => { if (bar && bar.textContent.includes('校验发现')) bar.textContent = ''; }, 6000);
773
+ }
774
+ } catch (e) {
775
+ /* lint 反馈失败静默 */
776
+ }
777
+ }
778
+
779
+ function registerMonacoCompletions() {
780
+ if (_monacoCompletionsRegistered || typeof FHMonaco === 'undefined') return;
781
+ _monacoCompletionsRegistered = true;
782
+
783
+ // 内联补全(ghost text,Tab 接受)—— Monaco 原生支持,无需手动叠加
784
+ FHMonaco.registerInlineCompletions(async function ({ filePath, content, offset, token }) {
785
+ try {
786
+ const d = await api('/api/completion', 'POST', {
787
+ filePath: filePath,
788
+ fileContent: content,
789
+ cursorOffset: offset,
790
+ mode: 'quick',
791
+ });
792
+ const suggs = Array.isArray(d.suggestions) ? d.suggestions : [];
793
+ if (suggs.length > 0 && suggs[0].text) {
794
+ return { text: suggs[0].text };
795
+ }
796
+ return null;
797
+ } catch (e) {
798
+ return null;
799
+ }
800
+ }, {
801
+ getActiveEditor: function () { return (typeof FHMonaco !== 'undefined') ? FHMonaco.getActiveEditor() : null; },
802
+ // P6-1: 补全被接受后自动 lint 反馈
803
+ onAccept: function (inserted) { lintAfterAccept(inserted); },
804
+ });
805
+
806
+ // 补全项弹窗(Ctrl+Space 触发,↑↓选择,Enter接受)—— Monaco 原生支持
807
+ FHMonaco.registerCompletionItems(async function ({ filePath, content, offset, prefix, token }) {
808
+ try {
809
+ const d = await api('/api/completion', 'POST', {
810
+ filePath: filePath,
811
+ fileContent: content,
812
+ cursorOffset: offset,
813
+ mode: 'full',
814
+ });
815
+ const suggs = Array.isArray(d.suggestions) ? d.suggestions : [];
816
+ return suggs.map((s) => ({
817
+ label: (s.text || '').slice(0, 50) + ((s.text || '').length > 50 ? '…' : ''),
818
+ kind: s.kind === 'function' ? 'Function' : s.kind === 'import' ? 'Module' : s.kind === 'block' ? 'Snippet' : 'Text',
819
+ insertText: s.text || '',
820
+ detail: (s.kind || 'text') + (s.confidence ? ' · ' + Math.round(s.confidence * 100) + '%' : ''),
821
+ }));
822
+ } catch (e) {
823
+ return [];
824
+ }
825
+ });
826
+
827
+ // P4-2: 语义诊断波浪线(编辑防抖调 /api/lsp/diagnostics,setModelMarkers 标注)
828
+ FHMonaco.registerDiagnostics(async function ({ filePath }) {
829
+ try {
830
+ const rel = String(filePath).replace(/^\/+/, '');
831
+ if (!rel || rel === 'untitled') return { diagnostics: [] };
832
+ const q = 'cwd=' + encodeURIComponent('') + '&file=' + encodeURIComponent(rel);
833
+ const d = await api('/api/lsp/diagnostics?' + q, 'GET');
834
+ return Array.isArray(d.diagnostics) ? { diagnostics: d.diagnostics } : { diagnostics: [] };
835
+ } catch (e) {
836
+ return { diagnostics: [] };
837
+ }
838
+ }, function () { return (typeof monacoEditor !== 'undefined') ? monacoEditor : null; });
839
+ }
840
+
841
+ function switchRightTab(name) {
842
+ state.rightTab = name;
843
+ document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
844
+ document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
845
+ document.getElementById('previewPanel').classList.toggle('active', name === 'preview');
846
+ document.getElementById('computerPanel')?.classList.toggle('active', name === 'computer');
847
+ document.getElementById('changesPanel')?.classList.toggle('active', name === 'changes');
848
+ document.getElementById('designPanel')?.classList.toggle('active', name === 'design');
849
+ document.getElementById('gitPanel')?.classList.toggle('active', name === 'git');
850
+ document.getElementById('teamPanel')?.classList.toggle('active', name === 'team');
851
+ document.getElementById('capabilitiesPanel')?.classList.toggle('active', name === 'capabilities');
852
+ if (name === 'changes') {
853
+ bindChangesButtonsOnce();
854
+ loadChanges();
855
+ }
856
+ if (name === 'design') {
857
+ bindDesignButtonsOnce();
858
+ }
859
+ if (name === 'git') {
860
+ bindGitButtonsOnce();
861
+ loadGitStatus();
862
+ }
863
+ if (name === 'team') {
864
+ bindTeamButtonsOnce();
865
+ loadTeamData();
866
+ }
867
+ if (name === 'capabilities') {
868
+ bindCapabilitiesOnce();
869
+ loadCapabilitiesPlugins();
870
+ }
871
+ }
872
+
873
+ // 变更面板顶部按钮只绑定一次
874
+ let _changesButtonsBound = false;
875
+ let _conflictFiles = []; // 冲突文件路径列表
876
+ function bindChangesButtonsOnce() {
877
+ if (_changesButtonsBound) return;
878
+ _changesButtonsBound = true;
879
+ document.getElementById('changesRefreshBtn')?.addEventListener('click', () => { _conflictFiles = []; loadChanges(); });
880
+ document.getElementById('changesCommitBtn')?.addEventListener('click', async () => {
881
+ try {
882
+ const d = await api('/api/changes/commit', 'POST');
883
+ toast(d.success ? ' 已提交所有变更' : '⚠️ 部分提交失败:' + (d.error || ''));
884
+ _conflictFiles = [];
885
+ loadChanges();
886
+ } catch (e) { toast('提交失败:' + e.message); }
887
+ });
888
+ document.getElementById('changesDiscardBtn')?.addEventListener('click', async () => {
889
+ if (!confirm('确定丢弃所有暂存变更?')) return;
890
+ try {
891
+ await api('/api/changes/discard', 'POST');
892
+ toast('已丢弃所有变更');
893
+ _conflictFiles = [];
894
+ loadChanges();
895
+ } catch (e) { toast('丢弃失败:' + e.message); }
896
+ });
897
+ // P3: 检测冲突
898
+ document.getElementById('changesDetectBtn')?.addEventListener('click', async () => {
899
+ try {
900
+ const d = await api('/api/changes/detect-conflicts', 'POST');
901
+ _conflictFiles = Array.isArray(d.conflicts) ? d.conflicts.map(c => c.path || c) : [];
902
+ if (_conflictFiles.length > 0) {
903
+ toast('⚠️ 检测到 ' + _conflictFiles.length + ' 个文件冲突(已高亮)');
904
+ } else {
905
+ toast(' 无文件冲突');
906
+ }
907
+ loadChanges(); // 重新渲染以高亮冲突文件
908
+ } catch (e) { toast('冲突检测失败:' + e.message); }
909
+ });
910
+ }
911
+
912
+ /* ========== P3: 多文件变更面板 ========== */
913
+ let _autoConflictChecked = false; // P1-2: 每次会话仅自动检测一次
914
+ async function loadChanges() {
915
+ try {
916
+ const d = await api('/api/changes', 'GET');
917
+ renderChanges(d);
918
+ // P1-2 增强:加载后自动检测一次冲突(无需手动点击)
919
+ if (!_autoConflictChecked && Array.isArray(d.changes) && d.changes.length > 0) {
920
+ _autoConflictChecked = true;
921
+ try {
922
+ const dc = await api('/api/changes/detect-conflicts', 'POST');
923
+ _conflictFiles = Array.isArray(dc.conflicts) ? dc.conflicts.map(c => c.path || c) : [];
924
+ if (_conflictFiles.length > 0) renderChanges(d);
925
+ } catch { /* 自动检测失败不阻塞 */ }
926
+ }
927
+ } catch (e) {
928
+ document.getElementById('changesList').innerHTML = '<div class="muted" style="text-align:center;padding:20px;">加载失败:' + escapeHtml(e.message) + '</div>';
929
+ }
930
+ }
931
+
932
+ // P6-3: 多文件 diff 视图状态——每文件显示模式(inline/side)+ 折叠集合
933
+ const _diffModes = {};
934
+ const _collapsedPaths = new Set();
935
+
936
+ function renderChanges(data) {
937
+ const changes = Array.isArray(data.changes) ? data.changes : [];
938
+ const count = changes.length;
939
+ document.getElementById('changesCount').textContent = count;
940
+ // P1-2 增强:冲突计数 badge
941
+ const conflictBadge = document.getElementById('changesConflictCount');
942
+ if (conflictBadge) {
943
+ const n = _conflictFiles.filter(cf => changes.some(c => c.path === cf || c.path.endsWith(cf) || cf.endsWith(c.path))).length;
944
+ conflictBadge.style.display = n > 0 ? 'inline' : 'none';
945
+ conflictBadge.textContent = n + ' 冲突';
946
+ }
947
+ const list = document.getElementById('changesList');
948
+ if (!count) {
949
+ list.innerHTML = '<div class="muted" style="text-align:center;padding:30px;font-size:12px;">暂无暂存变更<br/><span style="font-size:11px;">AI 生成的文件修改会先暂存在这里,审批后才写入磁盘</span></div>';
950
+ return;
951
+ }
952
+ list.innerHTML = changes.map((c, idx) => {
953
+ const typeLabel = c.type === 'create' ? '🆕 新增' : c.type === 'delete' ? '🗑 删除' : '✏️ 修改';
954
+ const typeColor = c.type === 'create' ? '#2d8a4e' : c.type === 'delete' ? '#c0392b' : '#d4a017';
955
+ const isConflict = _conflictFiles.some(cf => c.path === cf || c.path.endsWith(cf) || cf.endsWith(c.path));
956
+ const conflictBorder = isConflict ? 'border:2px solid #c0392b;' : 'border:1px solid var(--border);';
957
+ const conflictBadge = isConflict ? '<span style="color:#c0392b;font-weight:700;margin-left:6px;">⚠️ 冲突</span>' : '';
958
+ const hunks = Array.isArray(c.hunks) ? c.hunks : [];
959
+ let hunksHtml = '';
960
+ if (hunks.length) {
961
+ const mode = _diffModes[c.path] || 'inline';
962
+ if (mode === 'side') {
963
+ // P6-3: 并排 diff 视图(左=原文件,右=新文件,行级对齐)
964
+ hunksHtml = '<div style="margin-top:6px;">' + hunks.map((h) => {
965
+ const rows = (h.lines || []).map((l) => {
966
+ const left = (l.type === 'del' || l.type === 'ctx')
967
+ ? '<div class="' + (l.type === 'del' ? 'diff-del' : 'diff-ctx') + '">' + escapeHtml(l.content) + '</div>'
968
+ : '<div class="diff-ctx" style="color:transparent;">·</div>';
969
+ const right = (l.type === 'add' || l.type === 'ctx')
970
+ ? '<div class="' + (l.type === 'add' ? 'diff-add' : 'diff-ctx') + '">' + escapeHtml(l.content) + '</div>'
971
+ : '<div class="diff-ctx" style="color:transparent;">·</div>';
972
+ return '<div style="display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--line, #222);">' + left + right + '</div>';
973
+ }).join('');
974
+ return '<div style="border:1px solid var(--border);border-radius:4px;overflow:hidden;margin-bottom:6px;">'
975
+ + '<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 6px;background:var(--bg);font-size:11px;">'
976
+ + '<span class="muted">@@ ' + escapeHtml(String(h.header || '')) + '</span>'
977
+ + '<span class="muted" style="font-size:10px;">← 原文件 | 新文件 →</span></div>'
978
+ + rows + '</div>';
979
+ }).join('') + '</div>';
980
+ } else {
981
+ hunksHtml = '<div style="margin-top:6px;">' + hunks.map((h, hi) => {
982
+ const lines = (h.lines || []).map(l => {
983
+ const cls = l.type === 'add' ? 'diff-add' : l.type === 'del' ? 'diff-del' : 'diff-ctx';
984
+ return '<div class="' + cls + '">' + escapeHtml(l.content) + '</div>';
985
+ }).join('');
986
+ return '<div class="diff-hunk" style="margin-bottom:6px;border:1px solid var(--border);border-radius:4px;overflow:hidden;">'
987
+ + '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 6px;background:var(--bg);font-size:11px;">'
988
+ + '<span class="muted">@@ ' + escapeHtml(String(h.header || '')) + '</span>'
989
+ + '<span style="display:flex;gap:4px;">'
990
+ + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="accept-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✓</button>'
991
+ + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="reject-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✕</button>'
992
+ + '</span></div>'
993
+ + '<div style="font-family:monospace;font-size:11px;line-height:1.5;">' + lines + '</div></div>';
994
+ }).join('') + '</div>';
995
+ }
996
+ }
997
+ const collapsed = _collapsedPaths.has(c.path);
998
+ return '<div class="change-item" style="padding:8px;margin-bottom:8px;' + conflictBorder + 'border-radius:6px;background:var(--card);">'
999
+ + '<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;">'
1000
+ + '<div style="flex:1;min-width:0;cursor:pointer;" data-act="toggle-collapse" data-idx="' + idx + '" title="点击折叠/展开">'
1001
+ + '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + (collapsed ? '▸ ' : '▾ ') + escapeHtml(c.path) + conflictBadge + '</div>'
1002
+ + '<div style="font-size:11px;margin-top:2px;"><span style="color:' + typeColor + ';">' + typeLabel + '</span>'
1003
+ + (c.additions != null ? ' · <span style="color:#2d8a4e;">+' + c.additions + '</span>' : '')
1004
+ + (c.deletions != null ? ' · <span style="color:#c0392b;">-' + c.deletions + '</span>' : '')
1005
+ + ' · <span class="muted">' + escapeHtml(c.status || 'pending') + '</span></div>'
1006
+ + '</div>'
1007
+ + '<div style="display:flex;gap:4px;flex-shrink:0;">'
1008
+ + (hunks.length ? '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;" data-act="toggle-mode" data-idx="' + idx + '">' + ((_diffModes[c.path] || 'inline') === 'side' ? '内联' : '并排') + '</button>' : '')
1009
+ + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;background:#2d5a3d;color:#fff;" data-act="accept" data-idx="' + idx + '">接受</button>'
1010
+ + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;" data-act="reject" data-idx="' + idx + '">拒绝</button>'
1011
+ + '</div></div>'
1012
+ + (collapsed ? '' : hunksHtml)
1013
+ + '</div>';
1014
+ }).join('');
1015
+ // 绑定事件
1016
+ list.querySelectorAll('[data-act]').forEach(btn => {
1017
+ btn.addEventListener('click', async () => {
1018
+ const act = btn.getAttribute('data-act');
1019
+ const idx = parseInt(btn.getAttribute('data-idx'));
1020
+ const change = changes[idx];
1021
+ if (!change) return;
1022
+ try {
1023
+ if (act === 'accept') {
1024
+ await api('/api/changes/accept', 'POST', { path: change.path });
1025
+ toast('已接受:' + change.path);
1026
+ } else if (act === 'reject') {
1027
+ await api('/api/changes/reject', 'POST', { path: change.path });
1028
+ toast('已拒绝:' + change.path);
1029
+ } else if (act === 'accept-hunk') {
1030
+ const hi = parseInt(btn.getAttribute('data-hunk'));
1031
+ await api('/api/changes/hunk/accept', 'POST', { path: change.path, hunkIndex: hi });
1032
+ toast('已接受 Hunk');
1033
+ } else if (act === 'reject-hunk') {
1034
+ const hi = parseInt(btn.getAttribute('data-hunk'));
1035
+ await api('/api/changes/hunk/reject', 'POST', { path: change.path, hunkIndex: hi });
1036
+ toast('已拒绝 Hunk');
1037
+ } else if (act === 'toggle-mode') {
1038
+ _diffModes[change.path] = (_diffModes[change.path] || 'inline') === 'side' ? 'inline' : 'side';
1039
+ } else if (act === 'toggle-collapse') {
1040
+ if (_collapsedPaths.has(change.path)) _collapsedPaths.delete(change.path);
1041
+ else _collapsedPaths.add(change.path);
1042
+ }
1043
+ loadChanges();
1044
+ } catch (e) { toast('操作失败:' + e.message); }
1045
+ });
1046
+ });
1047
+ }
1048
+
1049
+ async function showArtifacts(dir) {
1050
+ try {
1051
+ const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
1052
+ const files = (d.entries || []).filter((e) => e.type !== 'dir');
1053
+ const box = document.getElementById('taskDetail');
1054
+ const listHtml = files.length
1055
+ ? files.map((f) => '<div class="artifact-file" data-path="' + escapeHtml(f.path) + '">📄 ' + escapeHtml(f.name) + '</div>').join('')
1056
+ : '<div class="muted">该目录暂无文件产物。</div>';
1057
+ const hint = '<div class="muted" style="margin-bottom:8px;">产物文件(来自:' + escapeHtml(dir) + '),点击在右侧预览:</div>';
1058
+ const cur = box.innerHTML;
1059
+ box.innerHTML = cur + '<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">' + hint + '<div class="artifact-list">' + listHtml + '</div>';
1060
+ box.querySelectorAll('.artifact-file').forEach((el) => el.addEventListener('click', () => {
1061
+ previewFile(el.getAttribute('data-path'));
1062
+ toast('已打开:' + el.getAttribute('data-path'));
1063
+ }));
1064
+ switchRightTab('detail');
1065
+ } catch (e) { toast('读取产物失败:' + e.message); }
1066
+ }
1067
+
1068
+ function renderStepHtml(step) {
1069
+ const d = step.data || {};
1070
+ if (step.type === 'model.response') {
1071
+ const content = (d.content || '').toString();
1072
+ const toolCalls = Array.isArray(d.toolCalls) ? d.toolCalls : [];
1073
+ let inner = '<div class="reasoning-head">🧠 模型推理 · ' + escapeHtml(d.model || '') + '</div>';
1074
+ if (content) inner += '<div style="white-space:pre-wrap;word-break:break-word;">' + renderMarkdown(content) + '</div>';
1075
+ if (toolCalls.length) inner += '<div class="muted" style="margin-top:6px;">↳ 计划调用工具:' + toolCalls.map((t) => '<code>' + escapeHtml(t) + '</code>').join('、') + '</div>';
1076
+ return '<div class="step reasoning">' + inner + '</div>';
1077
+ } else if (step.type === 'tool.call') {
1078
+ const argsHtml = formatToolArgs(d.args);
1079
+ return '<div class="step toolcall"><div class="tc-name">🔧 调用 ' + escapeHtml(d.name || '') + '</div>' + argsHtml + '</div>';
1080
+ } else if (step.type === 'tool.result') {
1081
+ const ok = !!d.ok;
1082
+ const out = (d.output || '').toString();
1083
+ return '<div class="step result ' + (ok ? 'ok' : 'fail') + '"><div class="res-head">' + (ok ? '✅' : '❌') + ' ' + escapeHtml(d.name || '') + ' ' + (ok ? '成功' : '失败') + '</div>' + (out.trim() ? '<div class="res-out">' + escapeHtml(out).slice(0, 500) + '</div>' : '') + '</div>';
1084
+ } else if (step.type === 'self-heal') {
1085
+ return '<div class="step note">🩹 进入自愈:检测到 <b>' + escapeHtml(d.category || '') + '</b> 类错误,已注入反思重试(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
1086
+ } else if (step.type === 'context.compact') {
1087
+ return '<div class="step note">📦 上下文压缩:从 ' + (d.originalLength != null ? d.originalLength : '?') + ' 条压缩至 ' + (d.compressedLength != null ? d.compressedLength : '?') + ' 条,保留首条目标与近期对话</div>';
1088
+ }
1089
+ return '';
1090
+ }
1091
+
1092
+ // 消息操作按钮(复制、创建文档)
1093
+ function renderMsgActions() {
1094
+ return '<div class="msg-actions">'
1095
+ + '<button class="msg-action-btn" data-action="copy" title="复制整条消息">📋 复制</button>'
1096
+ + '<button class="msg-action-btn" data-action="create-doc" title="创建文档">📄 建文档</button>'
1097
+ + '</div>';
1098
+ }
1099
+
1100
+ function renderTaskThread(task) {
1101
+ const box = document.getElementById('messages');
1102
+ const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
1103
+ renderTaskHeader(task);
1104
+ if (!task) {
1105
+ box.innerHTML = '<div class="msg sys">从左侧「任务列表」选择历史任务查看对话,或在下方输入指令发起新任务。刷新页面会自动清空当前对话视图,历史对话已保存在任务列表中。</div>';
1106
+ return;
1107
+ }
1108
+ const finalAnswer = (task.result && task.result.finalAnswer || '').trim();
1109
+ let html = '';
1110
+ const conv = Array.isArray(task.conversation) ? task.conversation : [];
1111
+ const steps = Array.isArray(task.steps) ? task.steps : [];
1112
+
1113
+ // 统一处理:先显示用户消息,再从 steps 提取思考过程(实时更新),最后显示最终回复
1114
+ // 这样确保思考过程能实时显示,不会因为 conversation 有内容就跳过 steps
1115
+
1116
+ // 1. 显示用户消息
1117
+ let userMsgShown = false;
1118
+ if (conv.length > 0) {
1119
+ for (const m of conv) {
1120
+ if (m && m.role === 'user' && m.content) {
1121
+ html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(m.content) + '</div>';
1122
+ userMsgShown = true;
1123
+ break; // 只显示第一条用户消息,后续的在多轮对话中处理
1124
+ }
1125
+ }
1126
+ }
1127
+ if (!userMsgShown && task.goal) {
1128
+ html += '<div class="msg user">' + renderMsgActions() + linkifyArtifacts(task.goal) + '</div>';
1129
+ }
1130
+
1131
+ // 2. steps 提取思考过程(实时更新,这是关键)
1132
+ const thinkingTexts = [];
1133
+ for (const s of steps) {
1134
+ if (s.type === 'model.response' && s.data && s.data.content && s.data.content.trim()) {
1135
+ thinkingTexts.push(s.data.content.trim());
1136
+ } else if (s.type === 'self-heal') {
1137
+ thinkingTexts.push('刚才遇到点小问题,我调整一下思路再试试。');
1138
+ }
1139
+ }
1140
+ if (thinkingTexts.length > 0) {
1141
+ html += '<div class="msg assistant">'
1142
+ + renderMsgActions()
1143
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(thinkingTexts.join('\n\n')) + '</div>'
1144
+ + '</div>';
1145
+ }
1146
+
1147
+ // 3. 如果 conversation 中有完整的 assistant 文本回复(任务完成后),也显示出来
1148
+ if (conv.length > 0) {
1149
+ const assistantTexts = [];
1150
+ for (const m of conv) {
1151
+ if (m && m.role === 'assistant' && m.content && m.content.trim() && !(m.toolCalls && m.toolCalls.length > 0)) {
1152
+ assistantTexts.push(m.content.trim());
1153
+ }
1154
+ }
1155
+ // 避免和 steps 重复:只显示 steps 中没有的最终回复
1156
+ if (assistantTexts.length > 0) {
1157
+ const lastAssistantText = assistantTexts[assistantTexts.length - 1];
1158
+ const alreadyInSteps = thinkingTexts.some(t => t === lastAssistantText || lastAssistantText.includes(t) || t.includes(lastAssistantText));
1159
+ if (!alreadyInSteps && lastAssistantText !== finalAnswer) {
1160
+ html += '<div class="msg assistant">'
1161
+ + renderMsgActions()
1162
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(lastAssistantText) + '</div>'
1163
+ + '</div>';
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ // 终态:如果对话流里没有最终回复(旧任务或被中断),再显示 finalAnswer
1169
+ const hasFinalInConv = conv.some(m => m.role === 'assistant' && !(m.toolCalls || []).length && (m.content || '').trim());
1170
+ if (task.status === 'done' && finalAnswer && !hasFinalInConv) {
1171
+ html += '<div class="msg assistant">'
1172
+ + renderMsgActions()
1173
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
1174
+ + '</div>';
1175
+ } else if (task.status === 'failed') {
1176
+ if (finalAnswer && !hasFinalInConv) {
1177
+ html += '<div class="msg assistant">'
1178
+ + renderMsgActions()
1179
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
1180
+ + '</div>';
1181
+ }
1182
+ html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
1183
+ } else if (task.status === 'running') {
1184
+ // 计算已运行时间,让用户知道等了多久
1185
+ let waitTip = '';
1186
+ if (task.createdAt) {
1187
+ const elapsed = Math.floor((Date.now() - new Date(task.createdAt).getTime()) / 1000);
1188
+ if (elapsed > 5) {
1189
+ const mins = Math.floor(elapsed / 60);
1190
+ const secs = elapsed % 60;
1191
+ const timeStr = mins > 0 ? `${mins}分${secs}秒` : `${secs}秒`;
1192
+ waitTip = `<span style="color:var(--muted);font-size:12px;margin-left:8px;">已等待 ${timeStr}</span>`;
1193
+ }
1194
+ // 分级超时警告:让用户明确感知任务可能卡住,而非无限期"思考中"
1195
+ if (elapsed >= 300 && elapsed < 900) {
1196
+ // 5-15 分钟:橙色温和提醒
1197
+ waitTip += '<div style="color:#e65100;font-size:12px;margin-top:8px;line-height:1.6;background:#fff3e0;padding:8px 10px;border-radius:6px;border-left:3px solid #ff9800;">'
1198
+ + '⚠️ 已等待较久,模型可能正在处理复杂推理或执行耗时操作。'
1199
+ + '<br>若长时间无任何输出,可点击右上角「停止」后重新提交。'
1200
+ + '</div>';
1201
+ } else if (elapsed >= 900 && elapsed < 1500) {
1202
+ // 15-25 分钟:红色明确警告
1203
+ waitTip += '<div style="color:#c62828;font-size:12px;margin-top:8px;line-height:1.6;background:#ffebee;padding:8px 10px;border-radius:6px;border-left:3px solid #e53935;">'
1204
+ + '🚨 任务可能已卡住(已等待超过15分钟无输出)。'
1205
+ + '<br>常见原因:模型 API 无响应、网络中断、或工具调用死锁。'
1206
+ + '<br>建议点击下方按钮停止任务,检查模型配置后重新提交。'
1207
+ + '<br><button onclick="document.getElementById(\'tchStop\')?.click()" style="margin-top:6px;background:#e53935;color:#fff;border:none;padding:4px 14px;border-radius:5px;cursor:pointer;font-size:12px;">⏹ 停止当前任务</button>'
1208
+ + '</div>';
1209
+ } else if (elapsed >= 1500) {
1210
+ // 25 分钟以上:紧急警告,提示即将自动超时
1211
+ const remain = Math.max(0, 30 - Math.floor(elapsed / 60));
1212
+ waitTip += '<div style="color:#b71c1c;font-size:12px;margin-top:8px;line-height:1.6;background:#ffcdd2;padding:8px 10px;border-radius:6px;border-left:3px solid #c62828;font-weight:500;">'
1213
+ + `⛔ 任务即将到达 30 分钟自动超时限制(约 ${remain} 分钟后自动终止)。`
1214
+ + '<br>可立即点击下方按钮手动停止,或等待系统自动终止。'
1215
+ + '<br><button onclick="document.getElementById(\'tchStop\')?.click()" style="margin-top:6px;background:#c62828;color:#fff;border:none;padding:4px 14px;border-radius:5px;cursor:pointer;font-size:12px;">⏹ 立即停止任务</button>'
1216
+ + '</div>';
1217
+ } else if (elapsed > 60) {
1218
+ // 1-5 分钟:普通提示
1219
+ waitTip += '<div style="color:var(--muted);font-size:12px;margin-top:6px;line-height:1.5;">模型正在深度思考或执行复杂操作,请耐心等待...</div>';
1220
+ }
1221
+ }
1222
+ html += '<div class="thinking-indicator">'
1223
+ + '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
1224
+ + '<span>思考中...</span>'
1225
+ + waitTip
1226
+ + '</div>';
1227
+ } else if (task.status === 'queued') {
1228
+ html += '<div class="msg sys">任务已入队,等待执行…</div>';
1229
+ }
1230
+ box.innerHTML = html;
1231
+ if (nearBottom) box.scrollTop = box.scrollHeight;
1232
+ }
1233
+
1234
+ function renderTaskHeader(task) {
1235
+ const titleEl = document.getElementById('tchTitle');
1236
+ const statusEl = document.getElementById('tchStatus');
1237
+ const timeEl = document.getElementById('tchTime');
1238
+ const subEl = document.getElementById('tchSub');
1239
+ if (!task) {
1240
+ titleEl.textContent = '未选择任务';
1241
+ statusEl.className = 'badge'; statusEl.textContent = '—';
1242
+ timeEl.textContent = '';
1243
+ subEl.textContent = '从左侧任务列表选择,或在下方输入指令发起新任务';
1244
+ return;
1245
+ }
1246
+ titleEl.textContent = task.goal.length > 60 ? task.goal.slice(0, 60) + '…' : task.goal;
1247
+ statusEl.className = 'badge ' + task.status;
1248
+ statusEl.textContent = task.status;
1249
+ timeEl.textContent = '创建于 ' + fmtTime(task.createdAt);
1250
+ subEl.textContent = '对话流 + 内部执行过程追踪 · 共 ' + (task.steps ? task.steps.length : 0) + ' 个步骤';
1251
+ // 仅当任务正在运行/排队时显示「停止」按钮(解救卡死、无法继续的任务)
1252
+ const stopEl = document.getElementById('tchStop');
1253
+ if (stopEl) stopEl.style.display = (task && (task.status === 'running' || task.status === 'queued')) ? '' : 'none';
1254
+ }
1255
+
1256
+ function renderThinkingProcess(msgs) {
1257
+ // 收集所有思考文本,直接展示,不折叠、不显示工具调用
1258
+ const textParts = [];
1259
+ for (const m of msgs) {
1260
+ const text = (m.content || '').trim();
1261
+ if (text) textParts.push(text);
1262
+ }
1263
+ const textContent = textParts.join('\n\n');
1264
+ if (!textContent) return '';
1265
+ // 用普通 assistant 气泡样式展示思考过程,让用户看到模型在想什么
1266
+ return '<div class="msg assistant thinking-msg" style="opacity:0.85;">'
1267
+ + renderMsgActions()
1268
+ + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(textContent) + '</div>'
1269
+ + '</div>';
1270
+ }
1271
+
1272
+ function toggleArgs(el) {
1273
+ const args = el.parentElement.querySelector('.thinking-args');
1274
+ if (!args) return;
1275
+ args.classList.toggle('show');
1276
+ el.textContent = args.classList.contains('show') ? '收起参数' : '查看参数';
1277
+ }
1278
+
1279
+ function toggleThinking(header) {
1280
+ const body = header.parentElement.querySelector('.thinking-body');
1281
+ if (!body) return;
1282
+ const arrow = header.querySelector('.thinking-arrow');
1283
+ if (body.style.display === 'none' || body.style.display === '') {
1284
+ body.style.display = 'block';
1285
+ if (arrow) arrow.textContent = '▼';
1286
+ } else {
1287
+ body.style.display = 'none';
1288
+ if (arrow) arrow.textContent = '▶';
1289
+ }
1290
+ }
1291
+
1292
+ function renderTaskDetail(id) {
1293
+ const t = state.tasks.find((x) => x.id === id);
1294
+ const box = document.getElementById('taskDetail');
1295
+ if (!t) { box.innerHTML = '<div class="muted">任务不存在。</div>'; return; }
1296
+ const r = t.result;
1297
+ let ops = '';
1298
+ if (t.workspaceDir) {
1299
+ ops = '<div class="detail-ops">' +
1300
+ '<button class="secondary" data-op="browser">🌐 打开浏览器</button>' +
1301
+ '<button class="secondary" data-op="folder" data-path="' + escapeHtml(t.workspaceDir) + '">📂 打开资源管理器</button>' +
1302
+ '<button class="secondary" data-op="artifacts" data-path="' + escapeHtml(t.workspaceDir) + '">📑 查看产物文件</button>' +
1303
+ '</div>';
1304
+ }
1305
+ const rows = [
1306
+ ops,
1307
+ '<div><b>目标</b><br>' + escapeHtml(t.goal) + '</div>',
1308
+ '<div><b>状态</b> ' + statusBadge(t.status) + '</div>',
1309
+ t.agentType ? '<div><b>智能体</b> ' + escapeHtml(AGENT_TYPES[t.agentType]?.label || t.agentType) + '</div>' : '',
1310
+ r ? '<div><b>迭代</b> ' + r.iterations + ' · <b>成本</b> $' + r.costUsd.toFixed(6) + '</div>' : '',
1311
+ r && r.logFile ? '<div><b>日志</b> <code>' + escapeHtml(r.logFile) + '</code></div>' : '',
1312
+ r && r.finalAnswer ? '<div><b>结果</b><br><pre style="white-space:pre-wrap;word-break:break-word;">' + escapeHtml(r.finalAnswer) + '</pre></div>' : '',
1313
+ t.error ? '<div style="color:var(--err)"><b>错误</b><br>' + escapeHtml(t.error) + '</div>' : '',
1314
+ ].filter(Boolean).join('<hr style="border:none;border-top:1px solid var(--line);margin:10px 0;">');
1315
+ box.innerHTML = rows || '<div class="muted">无详情</div>';
1316
+ box.querySelectorAll('[data-op]').forEach((b) => b.addEventListener('click', () => handleDetailOp(t, b.getAttribute('data-op'), b.getAttribute('data-path'))));
1317
+ }
1318
+
1319
+ function renderSidebarTaskList() {
1320
+ const container = document.getElementById('taskListContainer');
1321
+ const section = document.getElementById('taskListSection');
1322
+ const badge = document.getElementById('taskCountBadge');
1323
+ if (!container || !section) return;
1324
+
1325
+ // 置顶任务排在前面,其余按时间倒序
1326
+ const sortedTasks = [...state.tasks].sort((a, b) => {
1327
+ if (state.pinnedTasks.has(a.id) && !state.pinnedTasks.has(b.id)) return -1;
1328
+ if (!state.pinnedTasks.has(a.id) && state.pinnedTasks.has(b.id)) return 1;
1329
+ return new Date(b.createdAt) - new Date(a.createdAt);
1330
+ });
1331
+
1332
+ const activeTasks = sortedTasks.filter(t => t.status === 'queued' || t.status === 'running' || t.status === 'done' || t.status === 'failed');
1333
+
1334
+ if (activeTasks.length === 0) {
1335
+ section.style.display = 'none';
1336
+ return;
1337
+ }
1338
+
1339
+ section.style.display = 'block';
1340
+ badge.textContent = activeTasks.length;
1341
+
1342
+ let html = '';
1343
+ activeTasks.slice(0, 15).forEach(t => {
1344
+ const icon = t.status === 'running' ? '⏳' : t.status === 'queued' ? '📥' : t.error ? '❌' : '✅';
1345
+ const title = t.goal.slice(0, 18) + (t.goal.length > 18 ? '…' : '');
1346
+ const isPinned = state.pinnedTasks.has(t.id);
1347
+ const activeClass = state.currentTaskId === t.id ? 'active' : '';
1348
+ html += `<div class="task-item ${activeClass}" data-task-id="${t.id}" data-pinned="${isPinned}">
1349
+ <span class="task-icon">${icon}</span>
1350
+ <span class="task-title">${title}</span>
1351
+ ${isPinned ? '<span class="task-pinned">📌</span>' : ''}
1352
+ <div class="task-actions">
1353
+ <button class="pin-btn" data-id="${t.id}" data-pinned="${isPinned}" title="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '📌' : '📍'}</button>
1354
+ ${t.status === 'done' || t.status === 'failed' ? '<button class="delete-btn" data-id="' + t.id + '" title="删除任务">🗑️</button>' : ''}
1355
+ </div>
1356
+ </div>`;
1357
+ });
1358
+
1359
+ container.innerHTML = html;
1360
+
1361
+ // 绑定点击事件
1362
+ container.querySelectorAll('.task-item[data-task-id]').forEach(el => {
1363
+ el.addEventListener('click', (e) => {
1364
+ // 如果是点击pin按钮,不切换任务
1365
+ if (e.target.classList.contains('pin-btn')) return;
1366
+ const id = el.getAttribute('data-task-id');
1367
+ selectTask(id);
1368
+ });
1369
+
1370
+ // 右键菜单 - 置顶/取消置顶
1371
+ el.addEventListener('contextmenu', (e) => {
1372
+ e.preventDefault();
1373
+ const id = el.getAttribute('data-task-id');
1374
+ togglePinTask(id);
1375
+ });
1376
+ });
1377
+
1378
+ // 绑定pin按钮
1379
+ container.querySelectorAll('.pin-btn').forEach(btn => {
1380
+ btn.addEventListener('click', (e) => {
1381
+ e.stopPropagation();
1382
+ const id = btn.getAttribute('data-id');
1383
+ togglePinTask(id);
1384
+ });
1385
+ });
1386
+
1387
+ // 绑定删除按钮
1388
+ container.querySelectorAll('.delete-btn').forEach(btn => {
1389
+ btn.addEventListener('click', (e) => {
1390
+ e.stopPropagation();
1391
+ const id = btn.getAttribute('data-id');
1392
+ deleteTask(id);
1393
+ });
1394
+ });
1395
+ }
1396
+
1397
+ function switchView(nav) {
1398
+ document.querySelectorAll('.topbar-nav-item').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1399
+ document.querySelectorAll('.sidebar-icon').forEach((n) => n.classList.toggle('active', n.getAttribute('data-nav') === nav));
1400
+ document.querySelectorAll('.view, .page-view').forEach((v) => v.classList.toggle('active', v.getAttribute('data-view') === nav));
1401
+ // 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
1402
+ const isChat = nav === 'chat';
1403
+ document.getElementById('chatFootbar').style.display = isChat ? 'flex' : 'none';
1404
+ // 切换到对话视图时,如果当前任务已完成或不存在,自动开启新对话
1405
+ if (isChat) {
1406
+ const cur = state.tasks.find((t) => t.id === state.currentTaskId);
1407
+ if (!cur || cur.status === 'done' || cur.status === 'failed') {
1408
+ startNewChat();
1409
+ }
1410
+ }
1411
+ }
1412
+
1413
+ // 开启新对话:清空当前任务选中,显示空状态,聚焦输入框
1414
+ function startNewChat() {
1415
+ state.currentTaskId = null;
1416
+ renderTaskThread(null);
1417
+ renderTaskDetail(null);
1418
+ renderSidebarTaskList();
1419
+ // 显示任务列表区域
1420
+ const taskListSection = document.getElementById('taskListSection');
1421
+ if (taskListSection) taskListSection.style.display = 'block';
1422
+ // 聚焦输入框
1423
+ setTimeout(() => {
1424
+ const input = document.getElementById('goalInput');
1425
+ if (input) input.focus();
1426
+ }, 100);
1427
+ }
1428
+
1429
+ function updateUserBar() {
1430
+ document.getElementById('topbarPhone').textContent = state.phone || '未登录';
1431
+ document.getElementById('sheetPhone').textContent = state.phone || '未登录';
1432
+ const a = AGENT_TYPES[state.agentType];
1433
+ document.getElementById('skillPill').textContent = (a?.icon || '🤖') + ' ' + (a?.label || '通用助手');
1434
+ }
1435
+
1436
+ function showWelcomeModal(welcomeTasks) {
1437
+ const modal = document.getElementById('welcomeGuideModal');
1438
+ if (!modal) return;
1439
+
1440
+ const listEl = document.getElementById('welcomeTaskList');
1441
+ if (listEl) {
1442
+ if (welcomeTasks.length > 0) {
1443
+ listEl.innerHTML = welcomeTasks.map((t, i) => `
1444
+ <div class="welcome-task-item" data-task-id="${t.taskId}" data-goal="${encodeURIComponent(t.goal)}">
1445
+ <div class="welcome-task-index">${i + 1}</div>
1446
+ <div class="welcome-task-content">
1447
+ <div class="welcome-task-title">${escapeHtml(t.title || '任务 ' + (i + 1))}</div>
1448
+ <div class="welcome-task-desc">${escapeHtml(t.description || t.goal.slice(0, 50) + '')}</div>
1449
+ </div>
1450
+ <button class="welcome-task-run" data-id="${t.taskId}">▶ 运行</button>
1451
+ </div>
1452
+ `).join('');
1453
+
1454
+ // 绑定运行按钮
1455
+ listEl.querySelectorAll('.welcome-task-run').forEach(btn => {
1456
+ btn.addEventListener('click', (e) => {
1457
+ e.stopPropagation();
1458
+ const taskId = btn.getAttribute('data-id');
1459
+ runWelcomeTask(taskId);
1460
+ });
1461
+ });
1462
+ } else {
1463
+ listEl.innerHTML = '<div class="empty-welcome">' + t('empty.preparing_welcome') + '</div>';
1464
+ }
1465
+ }
1466
+
1467
+ modal.classList.add('show');
1468
+ toast('🎉 欢迎!系统已为您准备了三个引导任务');
1469
+ }
1470
+
1471
+ function showWelcomeGuide(welcomeTasks = []) {
1472
+ // 显示欢迎引导弹窗
1473
+ showWelcomeModal(welcomeTasks);
1474
+ }
1475
+
1476
+ /* ========== P2-1: 设计稿转代码 ========== */
1477
+ let _designButtonsBound = false;
1478
+ let _designImageBase64 = '';
1479
+ let _designLastCode = '';
1480
+
1481
+ function bindDesignButtonsOnce() {
1482
+ if (_designButtonsBound) return;
1483
+ _designButtonsBound = true;
1484
+
1485
+ const dropZone = document.getElementById('designDropZone');
1486
+ const fileInput = document.getElementById('designFileInput');
1487
+ const removeBtn = document.getElementById('designRemoveImgBtn');
1488
+ const generateBtn = document.getElementById('designGenerateBtn');
1489
+ const refineBtn = document.getElementById('designRefineBtn');
1490
+ const copyBtn = document.getElementById('designCopyBtn');
1491
+
1492
+ if (!dropZone || !fileInput) return;
1493
+
1494
+ // 点击上传
1495
+ dropZone.addEventListener('click', () => fileInput.click());
1496
+
1497
+ // 文件选择
1498
+ fileInput.addEventListener('change', (e) => {
1499
+ const file = e.target.files?.[0];
1500
+ if (file) handleDesignFile(file);
1501
+ });
1502
+
1503
+ // 拖拽上传
1504
+ dropZone.addEventListener('dragover', (e) => {
1505
+ e.preventDefault();
1506
+ dropZone.style.borderColor = 'var(--accent, #2d5a3d)';
1507
+ });
1508
+ dropZone.addEventListener('dragleave', () => {
1509
+ dropZone.style.borderColor = 'var(--border)';
1510
+ });
1511
+ dropZone.addEventListener('drop', (e) => {
1512
+ e.preventDefault();
1513
+ dropZone.style.borderColor = 'var(--border)';
1514
+ const file = e.dataTransfer?.files?.[0];
1515
+ if (file) handleDesignFile(file);
1516
+ });
1517
+
1518
+ // 移除图片
1519
+ removeBtn?.addEventListener('click', () => {
1520
+ _designImageBase64 = '';
1521
+ document.getElementById('designImagePreview').style.display = 'none';
1522
+ document.getElementById('designDropZone').style.display = 'block';
1523
+ document.getElementById('designResult').style.display = 'none';
1524
+ });
1525
+
1526
+ // 生成代码
1527
+ generateBtn.addEventListener('click', () => generateDesignCode(false));
1528
+
1529
+ // 修正代码
1530
+ refineBtn?.addEventListener('click', () => generateDesignCode(true));
1531
+
1532
+ // 复制代码
1533
+ copyBtn?.addEventListener('click', () => {
1534
+ if (_designLastCode) {
1535
+ navigator.clipboard.writeText(_designLastCode).then(() => {
1536
+ toast('✅ 代码已复制到剪贴板');
1537
+ }).catch(() => {
1538
+ toast('复制失败,请手动复制');
1539
+ });
1540
+ }
1541
+ });
1542
+
1543
+ // 结果 tab 切换
1544
+ document.querySelectorAll('.design-result-tab').forEach(tab => {
1545
+ tab.addEventListener('click', () => {
1546
+ document.querySelectorAll('.design-result-tab').forEach(t => t.classList.remove('active'));
1547
+ tab.classList.add('active');
1548
+ const target = tab.getAttribute('data-tab');
1549
+ document.getElementById('designCodeView').style.display = target === 'code' ? 'block' : 'none';
1550
+ document.getElementById('designPreviewView').style.display = target === 'preview' ? 'block' : 'none';
1551
+ });
1552
+ });
1553
+ }
1554
+
1555
+ function handleDesignFile(file) {
1556
+ if (!file.type.startsWith('image/')) {
1557
+ toast('请上传图片文件');
1558
+ return;
1559
+ }
1560
+ if (file.size > 8 * 1024 * 1024) {
1561
+ toast('图片大小不能超过 8MB');
1562
+ return;
1563
+ }
1564
+ const reader = new FileReader();
1565
+ reader.onload = (e) => {
1566
+ _designImageBase64 = e.target.result;
1567
+ const img = document.getElementById('designPreviewImg');
1568
+ img.src = _designImageBase64;
1569
+ document.getElementById('designImagePreview').style.display = 'block';
1570
+ document.getElementById('designDropZone').style.display = 'none';
1571
+ document.getElementById('designResult').style.display = 'none';
1572
+ };
1573
+ reader.readAsDataURL(file);
1574
+ }
1575
+
1576
+ async function generateDesignCode(isRefine) {
1577
+ if (!_designImageBase64) {
1578
+ toast('请先上传设计稿图片');
1579
+ return;
1580
+ }
1581
+
1582
+ const framework = document.getElementById('designFramework').value;
1583
+ const instructions = document.getElementById('designInstructions').value;
1584
+ const feedback = isRefine ? document.getElementById('designFeedback').value : '';
1585
+
1586
+ if (isRefine && !feedback.trim()) {
1587
+ toast('请输入修正反馈');
1588
+ return;
1589
+ }
1590
+
1591
+ const statusEl = document.getElementById('designStatus');
1592
+ const statusText = document.getElementById('designStatusText');
1593
+ const generateBtn = document.getElementById('designGenerateBtn');
1594
+
1595
+ statusEl.style.display = 'block';
1596
+ statusText.textContent = isRefine ? '正在修正代码...' : '正在分析设计稿并生成代码...';
1597
+ generateBtn.disabled = true;
1598
+ generateBtn.style.opacity = '0.6';
1599
+
1600
+ try {
1601
+ const body = {
1602
+ image: _designImageBase64,
1603
+ framework,
1604
+ };
1605
+ if (instructions) body.instructions = instructions;
1606
+ if (isRefine) {
1607
+ body.previousCode = _designLastCode;
1608
+ body.feedback = feedback;
1609
+ }
1610
+
1611
+ const d = await api('/api/design-to-code', 'POST', body);
1612
+
1613
+ if (d.ok && d.code) {
1614
+ _designLastCode = d.code;
1615
+ document.getElementById('designCode').textContent = d.code;
1616
+ document.getElementById('designCodeLang').textContent = d.language || framework;
1617
+
1618
+ // 渲染预览 iframe
1619
+ if (d.previewHtml) {
1620
+ const iframe = document.getElementById('designPreviewFrame');
1621
+ iframe.srcdoc = d.previewHtml;
1622
+ }
1623
+
1624
+ document.getElementById('designResult').style.display = 'block';
1625
+ document.getElementById('designFeedback').value = '';
1626
+ toast(isRefine ? '✅ 代码已修正' : '✅ 代码生成成功');
1627
+ } else {
1628
+ toast('生成失败: ' + (d.error || '未知错误'));
1629
+ }
1630
+ } catch (e) {
1631
+ toast('请求失败: ' + (e.message || '网络错误'));
1632
+ } finally {
1633
+ statusEl.style.display = 'none';
1634
+ generateBtn.disabled = false;
1635
+ generateBtn.style.opacity = '1';
1636
+ }
1637
+ }
1638
+
1639
+ /* ========== P2-2: Git 集成 ========== */
1640
+ let _gitButtonsBound = false;
1641
+ let _gitStatus = null;
1642
+ let _gitBranches = [];
1643
+
1644
+ function bindGitButtonsOnce() {
1645
+ if (_gitButtonsBound) return;
1646
+ _gitButtonsBound = true;
1647
+
1648
+ document.getElementById('gitRefreshBtn')?.addEventListener('click', loadGitStatus);
1649
+ document.getElementById('gitPullBtn')?.addEventListener('click', gitPull);
1650
+ document.getElementById('gitPushBtn')?.addEventListener('click', gitPush);
1651
+ document.getElementById('gitInitBtn')?.addEventListener('click', gitInit);
1652
+ document.getElementById('gitCommitBtn')?.addEventListener('click', gitCommit);
1653
+ document.getElementById('gitAddAllBtn')?.addEventListener('click', () => gitAddAll('modified'));
1654
+ document.getElementById('gitResetAllBtn')?.addEventListener('click', gitResetAll);
1655
+ document.getElementById('gitBranchSelect')?.addEventListener('change', (e) => gitCheckout(e.target.value));
1656
+ document.getElementById('gitCommitMessage')?.addEventListener('keydown', (e) => {
1657
+ if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); gitCommit(); }
1658
+ });
1659
+ document.getElementById('gitLogToggle')?.addEventListener('click', () => {
1660
+ const list = document.getElementById('gitLogList');
1661
+ const toggle = document.getElementById('gitLogToggle');
1662
+ if (list.style.display === 'none') {
1663
+ list.style.display = 'block';
1664
+ toggle.textContent = '📜 提交历史 ▲';
1665
+ loadGitLog();
1666
+ } else {
1667
+ list.style.display = 'none';
1668
+ toggle.textContent = '📜 提交历史 ▼';
1669
+ }
1670
+ });
1671
+ document.getElementById('gitDiffCloseBtn')?.addEventListener('click', closeGitDiff);
1672
+ document.getElementById('gitDiffModal')?.addEventListener('click', (e) => {
1673
+ if (e.target.id === 'gitDiffModal') closeGitDiff();
1674
+ });
1675
+ }
1676
+
1677
+ async function loadGitStatus() {
1678
+ try {
1679
+ const d = await api('/api/git/status', 'GET');
1680
+ _gitStatus = d;
1681
+ if (!d.isRepo) {
1682
+ document.getElementById('gitNoRepo').style.display = 'block';
1683
+ document.getElementById('gitContent').style.display = 'none';
1684
+ document.getElementById('gitBranchName').textContent = '';
1685
+ return;
1686
+ }
1687
+ document.getElementById('gitNoRepo').style.display = 'none';
1688
+ document.getElementById('gitContent').style.display = 'block';
1689
+ document.getElementById('gitBranchName').textContent = `(${d.branch}${d.ahead ? '' + d.ahead : ''}${d.behind ? ' ↓' + d.behind : ''})`;
1690
+
1691
+ // 分类文件
1692
+ const staged = d.files.filter((f) => f.indexStatus !== '.' && f.indexStatus !== '?');
1693
+ const modified = d.files.filter((f) => f.workTreeStatus !== '.' && f.workTreeStatus !== '?' && f.indexStatus !== '?');
1694
+ const untracked = d.files.filter((f) => f.indexStatus === '?');
1695
+
1696
+ document.getElementById('gitStagedCount').textContent = staged.length;
1697
+ document.getElementById('gitModifiedCount').textContent = modified.length;
1698
+ document.getElementById('gitUntrackedCount').textContent = untracked.length;
1699
+ document.getElementById('gitAddAllBtn').style.display = modified.length ? 'inline-block' : 'none';
1700
+ document.getElementById('gitResetAllBtn').style.display = staged.length ? 'inline-block' : 'none';
1701
+
1702
+ renderGitFileList('gitStagedList', staged, 'staged');
1703
+ renderGitFileList('gitModifiedList', modified, 'modified');
1704
+ renderGitFileList('gitUntrackedList', untracked, 'untracked');
1705
+
1706
+ // 加载分支列表
1707
+ loadGitBranches();
1708
+ } catch (e) {
1709
+ toast('Git 状态加载失败: ' + (e.message || '网络错误'));
1710
+ }
1711
+ }
1712
+
1713
+ function renderGitFileList(containerId, files, type) {
1714
+ const container = document.getElementById(containerId);
1715
+ if (!files.length) {
1716
+ container.innerHTML = '<div class="muted" style="font-size:10px;padding:4px 8px;">无</div>';
1717
+ return;
1718
+ }
1719
+ container.innerHTML = files.map((f, idx) => {
1720
+ const statusColor = f.conflict ? '#c0392b' : type === 'staged' ? '#2d8a4e' : type === 'untracked' ? '#888' : '#c0392b';
1721
+ const statusLabel = f.indexStatus !== '.' && f.indexStatus !== '?' ? f.indexStatus : f.workTreeStatus;
1722
+ const actions = type === 'staged'
1723
+ ? `<button class="git-file-btn" data-act="reset" data-idx="${idx}" style="font-size:10px;padding:1px 5px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:var(--ink);">取消</button>`
1724
+ : `<button class="git-file-btn" data-act="add" data-idx="${idx}" style="font-size:10px;padding:1px 5px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:var(--ink);">暂存</button>`;
1725
+ return `<div style="display:flex;align-items:center;justify-content:space-between;padding:3px 8px;font-size:11px;border-bottom:1px solid var(--border);">
1726
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;" class="git-file-path" data-path="${f.path}" data-staged="${type === 'staged'}">
1727
+ <span style="color:${statusColor};font-weight:600;margin-right:4px;">${statusLabel}</span>${escapeHtml(f.path)}
1728
+ </span>
1729
+ ${actions}
1730
+ </div>`;
1731
+ }).join('');
1732
+
1733
+ // 绑定事件
1734
+ container.querySelectorAll('.git-file-btn').forEach((btn) => {
1735
+ btn.addEventListener('click', async () => {
1736
+ const act = btn.getAttribute('data-act');
1737
+ const idx = parseInt(btn.getAttribute('data-idx'));
1738
+ const file = files[idx];
1739
+ if (!file) return;
1740
+ if (act === 'add') {
1741
+ await gitAdd([file.path]);
1742
+ } else if (act === 'reset') {
1743
+ await gitReset([file.path]);
1744
+ }
1745
+ });
1746
+ });
1747
+ container.querySelectorAll('.git-file-path').forEach((el) => {
1748
+ el.addEventListener('click', () => {
1749
+ const path = el.getAttribute('data-path');
1750
+ const staged = el.getAttribute('data-staged') === 'true';
1751
+ showGitDiff(path, staged);
1752
+ });
1753
+ });
1754
+ }
1755
+
1756
+ async function gitAdd(paths) {
1757
+ try {
1758
+ const d = await api('/api/git/add', 'POST', { paths });
1759
+ if (d.ok) { toast(d.message); loadGitStatus(); }
1760
+ else { toast('暂存失败: ' + (d.error || d.message)); }
1761
+ } catch (e) { toast('暂存失败: ' + (e.message || '网络错误')); }
1762
+ }
1763
+
1764
+ async function gitAddAll(type) {
1765
+ if (!_gitStatus) return;
1766
+ const files = _gitStatus.files.filter((f) => f.workTreeStatus !== '.' && f.workTreeStatus !== '?' && f.indexStatus !== '?');
1767
+ const paths = files.map((f) => f.path);
1768
+ if (paths.length) await gitAdd(paths);
1769
+ }
1770
+
1771
+ async function gitReset(paths) {
1772
+ try {
1773
+ const d = await api('/api/git/reset', 'POST', { paths });
1774
+ if (d.ok) { toast(d.message); loadGitStatus(); }
1775
+ else { toast('取消暂存失败: ' + (d.error || d.message)); }
1776
+ } catch (e) { toast('取消暂存失败: ' + (e.message || '网络错误')); }
1777
+ }
1778
+
1779
+ async function gitResetAll() {
1780
+ if (!_gitStatus) return;
1781
+ const files = _gitStatus.files.filter((f) => f.indexStatus !== '.' && f.indexStatus !== '?');
1782
+ const paths = files.map((f) => f.path);
1783
+ if (paths.length) await gitReset(paths);
1784
+ }
1785
+
1786
+ async function gitCommit() {
1787
+ const message = document.getElementById('gitCommitMessage').value.trim();
1788
+ if (!message) { toast('请输入提交信息'); return; }
1789
+ try {
1790
+ const d = await api('/api/git/commit', 'POST', { message });
1791
+ if (d.ok) {
1792
+ toast('✅ 提交成功: ' + (d.hash || '').slice(0, 7));
1793
+ document.getElementById('gitCommitMessage').value = '';
1794
+ loadGitStatus();
1795
+ } else {
1796
+ toast('提交失败: ' + (d.error || d.message));
1797
+ }
1798
+ } catch (e) { toast('提交失败: ' + (e.message || '网络错误')); }
1799
+ }
1800
+
1801
+ async function loadGitBranches() {
1802
+ try {
1803
+ const d = await api('/api/git/branches', 'GET');
1804
+ _gitBranches = d.branches || [];
1805
+ const select = document.getElementById('gitBranchSelect');
1806
+ const localBranches = _gitBranches.filter((b) => !b.remote);
1807
+ select.innerHTML = localBranches.map((b) =>
1808
+ `<option value="${escapeHtml(b.name)}" ${b.current ? 'selected' : ''}>${b.current ? '★ ' : ''}${escapeHtml(b.name)}${b.ahead ? ' ↑' + b.ahead : ''}${b.behind ? ' ↓' + b.behind : ''}</option>`
1809
+ ).join('');
1810
+ } catch (e) { /* 静默失败 */ }
1811
+ }
1812
+
1813
+ async function gitCheckout(branch) {
1814
+ try {
1815
+ const d = await api('/api/git/checkout', 'POST', { branch });
1816
+ if (d.ok) { toast(d.message); loadGitStatus(); }
1817
+ else { toast('切换失败: ' + (d.error || d.message)); loadGitStatus(); }
1818
+ } catch (e) { toast('切换失败: ' + (e.message || '网络错误')); }
1819
+ }
1820
+
1821
+ async function gitPull() {
1822
+ try {
1823
+ const d = await api('/api/git/pull', 'POST', {});
1824
+ if (d.ok) { toast('✅ ' + d.message); loadGitStatus(); }
1825
+ else { toast('拉取失败: ' + (d.error || d.message)); }
1826
+ } catch (e) { toast('拉取失败: ' + (e.message || '网络错误')); }
1827
+ }
1828
+
1829
+ async function gitPush() {
1830
+ try {
1831
+ const d = await api('/api/git/push', 'POST', {});
1832
+ if (d.ok) { toast('' + d.message); loadGitStatus(); }
1833
+ else { toast('推送失败: ' + (d.error || d.message)); }
1834
+ } catch (e) { toast('推送失败: ' + (e.message || '网络错误')); }
1835
+ }
1836
+
1837
+ async function gitInit() {
1838
+ try {
1839
+ const d = await api('/api/git/init', 'POST', {});
1840
+ if (d.ok) { toast('✅ ' + d.message); loadGitStatus(); }
1841
+ else { toast('初始化失败: ' + (d.error || d.message)); }
1842
+ } catch (e) { toast('初始化失败: ' + (e.message || '网络错误')); }
1843
+ }
1844
+
1845
+ async function loadGitLog() {
1846
+ try {
1847
+ const d = await api('/api/git/log?count=20', 'GET');
1848
+ const list = document.getElementById('gitLogList');
1849
+ if (!d.commits || !d.commits.length) {
1850
+ list.innerHTML = '<div class="muted" style="font-size:10px;padding:4px;">无提交历史</div>';
1851
+ return;
1852
+ }
1853
+ list.innerHTML = d.commits.map((c) =>
1854
+ `<div style="padding:4px 8px;border-bottom:1px solid var(--border);">
1855
+ <div style="font-weight:600;color:var(--ink);">${escapeHtml(c.subject)}</div>
1856
+ <div style="font-size:10px;color:var(--muted);margin-top:2px;">
1857
+ <span style="color:#2d5a3d;font-family:monospace;">${c.shortHash}</span>
1858
+ · ${escapeHtml(c.author)} · ${new Date(c.date).toLocaleString('zh-CN')}
1859
+ </div>
1860
+ </div>`
1861
+ ).join('');
1862
+ } catch (e) { /* 静默失败 */ }
1863
+ }
1864
+
1865
+ async function showGitDiff(path, staged) {
1866
+ try {
1867
+ const d = await api('/api/git/diff', 'POST', { path, staged });
1868
+ const modal = document.getElementById('gitDiffModal');
1869
+ const title = document.getElementById('gitDiffTitle');
1870
+ const content = document.getElementById('gitDiffContent');
1871
+ title.textContent = (staged ? '📥 ' : '📝 ') + path;
1872
+ if (d.diffs && d.diffs.length) {
1873
+ const diff = d.diffs[0];
1874
+ let html = '';
1875
+ for (const hunk of diff.hunks) {
1876
+ html += `<div style="color:#888;background:var(--card);padding:2px 6px;margin:4px 0;font-size:10px;">${escapeHtml(hunk.header)}</div>`;
1877
+ for (const line of hunk.lines) {
1878
+ const color = line.type === 'add' ? '#2d8a4e' : line.type === 'remove' ? '#c0392b' : 'var(--ink)';
1879
+ const bg = line.type === 'add' ? 'rgba(45,138,78,0.1)' : line.type === 'remove' ? 'rgba(192,57,43,0.1)' : 'transparent';
1880
+ html += `<div style="color:${color};background:${bg};padding:0 6px;">${line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '}${escapeHtml(line.content)}</div>`;
1881
+ }
1882
+ }
1883
+ content.innerHTML = html || '<div class="muted">无差异</div>';
1884
+ } else {
1885
+ content.innerHTML = '<div class="muted">无差异或二进制文件</div>';
1886
+ }
1887
+ modal.style.display = 'flex';
1888
+ } catch (e) { toast('Diff 加载失败: ' + (e.message || '网络错误')); }
1889
+ }
1890
+
1891
+ function closeGitDiff() {
1892
+ document.getElementById('gitDiffModal').style.display = 'none';
1893
+ }
1894
+
1895
+ /* ========== P2-3: 团队协作 ========== */
1896
+ let _teamButtonsBound = false;
1897
+ let _teamData = null;
1898
+ let _teamTaskFilter = '';
1899
+
1900
+ function bindTeamButtonsOnce() {
1901
+ if (_teamButtonsBound) return;
1902
+ _teamButtonsBound = true;
1903
+
1904
+ document.getElementById('teamRefreshBtn')?.addEventListener('click', loadTeamData);
1905
+ document.getElementById('teamInviteBtn')?.addEventListener('click', teamInviteMember);
1906
+ document.getElementById('teamCreateTaskBtn')?.addEventListener('click', teamCreateTask);
1907
+
1908
+ // Tab 切换
1909
+ document.querySelectorAll('.team-tab').forEach((tab) => {
1910
+ tab.addEventListener('click', () => {
1911
+ document.querySelectorAll('.team-tab').forEach((t) => {
1912
+ t.classList.remove('active');
1913
+ t.style.color = 'var(--muted)';
1914
+ t.style.borderBottomColor = 'transparent';
1915
+ t.style.fontWeight = 'normal';
1916
+ });
1917
+ tab.classList.add('active');
1918
+ tab.style.color = 'var(--ink)';
1919
+ tab.style.borderBottomColor = '#2d5a3d';
1920
+ tab.style.fontWeight = '600';
1921
+ const target = tab.getAttribute('data-tab');
1922
+ document.getElementById('teamMembersTab').style.display = target === 'members' ? 'block' : 'none';
1923
+ document.getElementById('teamTasksTab').style.display = target === 'tasks' ? 'block' : 'none';
1924
+ });
1925
+ });
1926
+
1927
+ // 任务筛选
1928
+ document.querySelectorAll('.team-task-filter').forEach((btn) => {
1929
+ btn.addEventListener('click', () => {
1930
+ document.querySelectorAll('.team-task-filter').forEach((b) => b.classList.remove('active'));
1931
+ btn.classList.add('active');
1932
+ _teamTaskFilter = btn.getAttribute('data-status') || '';
1933
+ renderTeamTasks();
1934
+ });
1935
+ });
1936
+ }
1937
+
1938
+ async function loadTeamData() {
1939
+ try {
1940
+ const d = await api('/api/team', 'GET');
1941
+ _teamData = d;
1942
+ document.getElementById('teamName').textContent = d.team?.config?.name || '团队';
1943
+ document.getElementById('teamMemberCount').textContent = d.stats?.totalMembers || 0;
1944
+ document.getElementById('teamTaskCount').textContent = d.stats?.totalTasks || 0;
1945
+ renderTeamMembers();
1946
+ renderTeamTasks();
1947
+ renderTeamOverview(d);
1948
+ } catch (e) {
1949
+ toast('团队数据加载失败: ' + (e.message || '网络错误'));
1950
+ }
1951
+ }
1952
+
1953
+ // P7-4: agent 协作总览——任务状态漏斗 + 成员负载分布可视化
1954
+ function renderTeamOverview(d) {
1955
+ const box = document.getElementById('teamOverview');
1956
+ if (!box) return;
1957
+ const tasks = d?.team?.tasks || [];
1958
+ const members = d?.team?.members || [];
1959
+ if (!tasks.length) { box.style.display = 'none'; return; }
1960
+ box.style.display = 'block';
1961
+ const statusColors = { todo: '#7f8c8d', 'in-progress': '#3498db', review: '#e67e22', done: '#2d5a3d' };
1962
+ const statusLabels = { todo: '待办', 'in-progress': '进行中', review: '评审', done: '完成' };
1963
+ const order = ['todo', 'in-progress', 'review', 'done'];
1964
+ const counts = {};
1965
+ order.forEach((s) => { counts[s] = 0; });
1966
+ tasks.forEach((t) => { counts[t.status] = (counts[t.status] || 0) + 1; });
1967
+ const max = Math.max(1, ...Object.values(counts));
1968
+ const funnelHtml = '<div style="display:flex;flex-direction:column;gap:3px;margin-bottom:8px;">'
1969
+ + order.map((s) => {
1970
+ const n = counts[s] || 0;
1971
+ const pct = Math.round((n / max) * 100);
1972
+ return '<div style="display:flex;align-items:center;gap:6px;font-size:10px;">'
1973
+ + '<span style="width:40px;color:var(--muted);">' + statusLabels[s] + '</span>'
1974
+ + '<div style="flex:1;background:var(--bg);border-radius:3px;height:12px;overflow:hidden;">'
1975
+ + '<div style="width:' + pct + '%;height:100%;background:' + statusColors[s] + ';border-radius:3px;"></div></div>'
1976
+ + '<span style="width:24px;text-align:right;font-weight:600;">' + n + '</span>'
1977
+ + '</div>';
1978
+ }).join('') + '</div>';
1979
+ // 成员负载:按创建人统计任务数(top 5)
1980
+ const byMember = {};
1981
+ tasks.forEach((t) => { byMember[t.createdBy || '未指派'] = (byMember[t.createdBy || '未指派'] || 0) + 1; });
1982
+ const loadRows = Object.entries(byMember).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, n]) => {
1983
+ const pct = Math.round((n / Math.max(1, ...Object.values(byMember))) * 100);
1984
+ return '<div style="display:flex;align-items:center;gap:6px;font-size:10px;">'
1985
+ + '<span style="width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + escapeHtml(name) + '</span>'
1986
+ + '<div style="flex:1;background:var(--bg);border-radius:3px;height:10px;overflow:hidden;">'
1987
+ + '<div style="width:' + pct + '%;height:100%;background:#9b59b6;border-radius:3px;"></div></div>'
1988
+ + '<span style="width:24px;text-align:right;font-weight:600;">' + n + '</span>'
1989
+ + '</div>';
1990
+ }).join('');
1991
+ box.innerHTML = '<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px;">'
1992
+ + '<div style="font-size:11px;font-weight:600;margin-bottom:6px;">📊 协作总览</div>'
1993
+ + '<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">任务状态</div>' + funnelHtml
1994
+ + (loadRows ? '<div style="font-size:10px;color:var(--muted);margin:6px 0 4px;">成员负载(按创建人)</div>' + loadRows : '')
1995
+ + '</div>';
1996
+ }
1997
+
1998
+ function renderTeamMembers() {
1999
+ const container = document.getElementById('teamMembersList');
2000
+ const members = _teamData?.team?.members || [];
2001
+ if (!members.length) {
2002
+ container.innerHTML = '<div class="muted" style="font-size:11px;padding:20px;text-align:center;">暂无成员</div>';
2003
+ return;
2004
+ }
2005
+ const roleColors = { owner: '#c0392b', admin: '#e67e22', developer: '#2d5a3d', viewer: '#7f8c8d' };
2006
+ container.innerHTML = members.map((m) =>
2007
+ `<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-bottom:1px solid var(--border);font-size:11px;">
2008
+ <div style="flex:1;overflow:hidden;">
2009
+ <div style="font-weight:600;">${escapeHtml(m.name)} <span style="color:${roleColors[m.role] || '#888'};font-size:10px;">[${m.role}]</span></div>
2010
+ <div style="font-size:10px;color:var(--muted);">${escapeHtml(m.email)} · ${m.status}</div>
2011
+ </div>
2012
+ <div style="display:flex;gap:4px;flex-shrink:0;">
2013
+ <select class="team-role-select" data-id="${m.id}" style="font-size:10px;padding:2px 4px;border:1px solid var(--border);border-radius:3px;background:var(--card);color:var(--ink);" ${m.role === 'owner' ? 'disabled' : ''}>
2014
+ <option value="admin" ${m.role === 'admin' ? 'selected' : ''}>管理员</option>
2015
+ <option value="developer" ${m.role === 'developer' ? 'selected' : ''}>开发者</option>
2016
+ <option value="viewer" ${m.role === 'viewer' ? 'selected' : ''}>观察者</option>
2017
+ </select>
2018
+ <button class="team-remove-btn" data-id="${m.id}" style="font-size:10px;padding:2px 6px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:#c0392b;" ${m.role === 'owner' ? 'disabled style="opacity:0.3;"' : ''}>移除</button>
2019
+ </div>
2020
+ </div>`
2021
+ ).join('');
2022
+
2023
+ container.querySelectorAll('.team-role-select').forEach((sel) => {
2024
+ sel.addEventListener('change', async () => {
2025
+ const id = sel.getAttribute('data-id');
2026
+ const role = sel.value;
2027
+ try {
2028
+ await api(`/api/team/members/${id}/role`, 'PUT', { role });
2029
+ toast('角色已更新');
2030
+ loadTeamData();
2031
+ } catch (e) { toast('更新失败: ' + (e.message || '网络错误')); }
2032
+ });
2033
+ });
2034
+ container.querySelectorAll('.team-remove-btn').forEach((btn) => {
2035
+ btn.addEventListener('click', async () => {
2036
+ const id = btn.getAttribute('data-id');
2037
+ if (!confirm('确定移除该成员?')) return;
2038
+ try {
2039
+ await api(`/api/team/members/${id}`, 'DELETE');
2040
+ toast('成员已移除');
2041
+ loadTeamData();
2042
+ } catch (e) { toast('移除失败: ' + (e.message || '网络错误')); }
2043
+ });
2044
+ });
2045
+ }
2046
+
2047
+ async function teamInviteMember() {
2048
+ const name = document.getElementById('teamInviteName').value.trim();
2049
+ const email = document.getElementById('teamInviteEmail').value.trim();
2050
+ if (!name || !email) { toast('请输入姓名和邮箱'); return; }
2051
+ try {
2052
+ await api('/api/team/members/invite', 'POST', { name, email, role: 'developer' });
2053
+ toast('✅ 邀请已发送');
2054
+ document.getElementById('teamInviteName').value = '';
2055
+ document.getElementById('teamInviteEmail').value = '';
2056
+ loadTeamData();
2057
+ } catch (e) { toast('邀请失败: ' + (e.message || '网络错误')); }
2058
+ }
2059
+
2060
+ function renderTeamTasks() {
2061
+ const container = document.getElementById('teamTasksList');
2062
+ let tasks = _teamData?.team?.tasks || [];
2063
+ if (_teamTaskFilter) tasks = tasks.filter((t) => t.status === _teamTaskFilter);
2064
+ if (!tasks.length) {
2065
+ container.innerHTML = '<div class="muted" style="font-size:11px;padding:20px;text-align:center;">暂无任务</div>';
2066
+ return;
2067
+ }
2068
+ const statusColors = { todo: '#7f8c8d', 'in-progress': '#3498db', review: '#e67e22', done: '#2d5a3d' };
2069
+ const statusLabels = { todo: '待办', 'in-progress': '进行中', review: '评审', done: '完成' };
2070
+ container.innerHTML = tasks.map((t) =>
2071
+ `<div style="padding:8px;border-bottom:1px solid var(--border);font-size:11px;">
2072
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
2073
+ <span style="font-weight:600;flex:1;">${escapeHtml(t.title)}</span>
2074
+ <span style="font-size:10px;padding:2px 6px;border-radius:3px;background:${statusColors[t.status]}20;color:${statusColors[t.status]};font-weight:600;">${statusLabels[t.status]}</span>
2075
+ </div>
2076
+ ${t.description ? `<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">${escapeHtml(t.description.slice(0, 100))}</div>` : ''}
2077
+ <div style="display:flex;gap:4px;align-items:center;font-size:10px;color:var(--muted);">
2078
+ <span>👤 ${t.createdBy}</span>
2079
+ <span>·</span>
2080
+ <span>${new Date(t.updatedAt).toLocaleDateString('zh-CN')}</span>
2081
+ ${t.comments?.length ? `<span>· 💬 ${t.comments.length}</span>` : ''}
2082
+ <select class="team-task-status" data-id="${t.id}" style="margin-left:auto;font-size:10px;padding:2px 4px;border:1px solid var(--border);border-radius:3px;background:var(--card);color:var(--ink);">
2083
+ <option value="todo" ${t.status === 'todo' ? 'selected' : ''}>待办</option>
2084
+ <option value="in-progress" ${t.status === 'in-progress' ? 'selected' : ''}>进行中</option>
2085
+ <option value="review" ${t.status === 'review' ? 'selected' : ''}>评审</option>
2086
+ <option value="done" ${t.status === 'done' ? 'selected' : ''}>完成</option>
2087
+ </select>
2088
+ <button class="team-task-delete" data-id="${t.id}" style="font-size:10px;padding:2px 6px;background:none;border:1px solid var(--border);border-radius:3px;cursor:pointer;color:#c0392b;">删除</button>
2089
+ </div>
2090
+ </div>`
2091
+ ).join('');
2092
+
2093
+ container.querySelectorAll('.team-task-status').forEach((sel) => {
2094
+ sel.addEventListener('change', async () => {
2095
+ const id = sel.getAttribute('data-id');
2096
+ const status = sel.value;
2097
+ try {
2098
+ await api(`/api/team/tasks/${id}`, 'PUT', { status });
2099
+ toast('任务状态已更新');
2100
+ loadTeamData();
2101
+ } catch (e) { toast('更新失败: ' + (e.message || '网络错误')); }
2102
+ });
2103
+ });
2104
+ container.querySelectorAll('.team-task-delete').forEach((btn) => {
2105
+ btn.addEventListener('click', async () => {
2106
+ const id = btn.getAttribute('data-id');
2107
+ if (!confirm('确定删除该任务?')) return;
2108
+ try {
2109
+ await api(`/api/team/tasks/${id}`, 'DELETE');
2110
+ toast('任务已删除');
2111
+ loadTeamData();
2112
+ } catch (e) { toast('删除失败: ' + (e.message || '网络错误')); }
2113
+ });
2114
+ });
2115
+ }
2116
+
2117
+ async function teamCreateTask() {
2118
+ const title = document.getElementById('teamTaskTitle').value.trim();
2119
+ if (!title) { toast('请输入任务标题'); return; }
2120
+ try {
2121
+ await api('/api/team/tasks', 'POST', { title, createdBy: '当前用户', status: 'todo' });
2122
+ toast('✅ 任务已创建');
2123
+ document.getElementById('teamTaskTitle').value = '';
2124
+ loadTeamData();
2125
+ } catch (e) { toast('创建失败: ' + (e.message || '网络错误')); }
2126
+ }
2127
+
2128
+ /* ========== P0-3: 能力中心(v7.2 新能力前端接线)========== */
2129
+ let _capabilitiesBound = false;
2130
+ function bindCapabilitiesOnce() {
2131
+ if (_capabilitiesBound) return;
2132
+ _capabilitiesBound = true;
2133
+ document.getElementById('capVoiceBtn')?.addEventListener('click', runCapabilityVoice);
2134
+ document.getElementById('capVoiceInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runCapabilityVoice(); });
2135
+ document.getElementById('capKnowledgeSearchBtn')?.addEventListener('click', runCapabilityKnowledge);
2136
+ document.getElementById('capKnowledgeQuery')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runCapabilityKnowledge(); });
2137
+ document.getElementById('capPluginsBtn')?.addEventListener('click', loadCapabilitiesPlugins);
2138
+ }
2139
+
2140
+ async function runCapabilityVoice() {
2141
+ const input = document.getElementById('capVoiceInput');
2142
+ const out = document.getElementById('capVoiceResult');
2143
+ const text = (input?.value || '').trim();
2144
+ if (!text) { if (out) out.textContent = '请输入语音指令文本'; return; }
2145
+ if (out) out.textContent = '解析中…';
2146
+ try {
2147
+ const d = await apiVoiceParse(text);
2148
+ if (!d.ok) { if (out) out.textContent = '解析失败: ' + (d.error || ''); return; }
2149
+ const c = d.command || {};
2150
+ if (out) out.textContent = '→ ' + (c.type || '?') + (c.params && Object.keys(c.params).length ? ' 参数: ' + JSON.stringify(c.params) : '') + ' 置信度: ' + (c.confidence ?? '?');
2151
+ } catch (e) { if (out) out.textContent = '请求失败: ' + (e.message || '网络错误'); }
2152
+ }
2153
+
2154
+ async function runCapabilityKnowledge() {
2155
+ const input = document.getElementById('capKnowledgeQuery');
2156
+ const out = document.getElementById('capKnowledgeResult');
2157
+ const query = (input?.value || '').trim();
2158
+ if (!query) { if (out) out.innerHTML = '<div class="muted">请输入搜索关键词</div>'; return; }
2159
+ if (out) out.innerHTML = '<div class="muted">搜索中…</div>';
2160
+ try {
2161
+ const d = await apiKnowledgeSearch(query);
2162
+ if (!d.ok) { if (out) out.innerHTML = '<div>搜索失败: ' + (d.error || '') + '</div>'; return; }
2163
+ const list = d.results || [];
2164
+ if (!list.length) { if (out) out.innerHTML = '<div class="muted">未找到匹配资料</div>'; return; }
2165
+ if (out) out.innerHTML = list.map((r) => {
2166
+ const t = r.document?.title || r.title || '未命名';
2167
+ const id = r.document?.id || r.id || '';
2168
+ return '<div style="padding:3px 0;border-bottom:1px solid var(--border,#eee);">📄 ' + t +
2169
+ (id ? ' <span style="color:var(--muted);font-size:10px;">' + id.slice(0, 18) + '</span>' : '') + '</div>';
2170
+ }).join('');
2171
+ } catch (e) { if (out) out.innerHTML = '<div>请求失败: ' + (e.message || '网络错误') + '</div>'; }
2172
+ }
2173
+
2174
+ async function loadCapabilitiesPlugins() {
2175
+ const out = document.getElementById('capPluginsResult');
2176
+ if (!out) return;
2177
+ out.innerHTML = '<div class="muted">加载中…</div>';
2178
+ try {
2179
+ const d = await apiPluginsMarket('', '');
2180
+ if (!d.ok) { out.innerHTML = '<div>加载失败: ' + (d.error || '') + '</div>'; return; }
2181
+ const list = d.plugins || [];
2182
+ if (!list.length) { out.innerHTML = '<div class="muted">插件市场为空</div>'; return; }
2183
+ out.innerHTML = list.slice(0, 15).map((p) =>
2184
+ '<div style="padding:3px 0;border-bottom:1px solid var(--border,#eee);">🧩 ' + (p.name || p.id) +
2185
+ ' <span style="color:var(--muted);font-size:10px;">v' + (p.version || '?') + '</span>' +
2186
+ '<div style="font-size:10px;color:var(--muted);">' + (p.description || '').slice(0, 40) + '</div></div>'
2187
+ ).join('') + (list.length > 15 ? '<div class="muted" style="font-size:10px;margin-top:4px;">…共 ' + list.length + ' 个</div>' : '');
2188
+ } catch (e) { out.innerHTML = '<div>请求失败: ' + (e.message || '网络错误') + '</div>'; }
2189
+ }
2190
+
2191
+ /* ========== P1-1: ghost text 补全(记忆编辑器)========== */
2192
+ let _ghostTimer = null;
2193
+ let _ghostSuggestion = '';
2194
+ let _ghostBound = false;
2195
+
2196
+ function bindGhostTextOnce() {
2197
+ if (_ghostBound) return;
2198
+ _ghostBound = true;
2199
+ const editor = document.getElementById('memLongEditor');
2200
+ const bar = document.getElementById('memGhostBar');
2201
+ if (!editor || !bar) return;
2202
+ editor.addEventListener('input', () => {
2203
+ clearTimeout(_ghostTimer);
2204
+ _ghostTimer = setTimeout(() => requestGhost(editor), 350);
2205
+ });
2206
+ editor.addEventListener('keydown', (e) => {
2207
+ if (e.key === 'Tab' && _ghostSuggestion) {
2208
+ e.preventDefault();
2209
+ acceptGhost(editor);
2210
+ } else if (e.key === 'Escape') {
2211
+ clearGhost();
2212
+ }
2213
+ });
2214
+ editor.addEventListener('blur', () => setTimeout(clearGhost, 250));
2215
+ // 编辑框显示时绑定一次
2216
+ document.getElementById('memLongEditor')?.addEventListener('focus', () => { /* 已绑定 */ });
2217
+ }
2218
+
2219
+ async function requestGhost(editor) {
2220
+ const content = editor.value;
2221
+ const bar = document.getElementById('memGhostBar');
2222
+ if (!content.trim() || content.length < 10) { clearGhost(); return; }
2223
+ try {
2224
+ const d = await apiCompletion('memory-note.md', content, { language: 'markdown', mode: 'quick' });
2225
+ if (d.ok && Array.isArray(d.suggestions) && d.suggestions.length) {
2226
+ const t = d.suggestions[0].text || '';
2227
+ if (t && !content.endsWith(t)) {
2228
+ _ghostSuggestion = t;
2229
+ document.getElementById('memGhostText').textContent = t.slice(0, 200);
2230
+ bar.style.display = 'block';
2231
+ } else { clearGhost(); }
2232
+ } else { clearGhost(); }
2233
+ } catch { clearGhost(); }
2234
+ }
2235
+
2236
+ function acceptGhost(editor) {
2237
+ if (!_ghostSuggestion) return;
2238
+ const cur = editor.selectionStart ?? editor.value.length;
2239
+ const end = editor.selectionEnd ?? cur;
2240
+ editor.value = editor.value.slice(0, cur) + _ghostSuggestion + editor.value.slice(end);
2241
+ const pos = cur + _ghostSuggestion.length;
2242
+ editor.selectionStart = editor.selectionEnd = pos;
2243
+ try { editor.dispatchEvent(new Event('input')); } catch {}
2244
+ clearGhost();
2245
+ }
2246
+
2247
+ function clearGhost() {
2248
+ _ghostSuggestion = '';
2249
+ const bar = document.getElementById('memGhostBar');
2250
+ if (bar) bar.style.display = 'none';
2251
+ }