feihong-code 0.6.1 → 7.2.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 (247) hide show
  1. package/.github/workflows/release.yml +173 -0
  2. package/CHANGELOG.md +83 -454
  3. package/dist/agent/browser-agent.js +354 -0
  4. package/dist/agent/browser-agent.js.map +1 -0
  5. package/dist/agent/change-manager.js +433 -0
  6. package/dist/agent/change-manager.js.map +1 -0
  7. package/dist/agent/code-rag.js +293 -0
  8. package/dist/agent/code-rag.js.map +1 -0
  9. package/dist/agent/code-writer.js +13 -1
  10. package/dist/agent/code-writer.js.map +1 -1
  11. package/dist/agent/completion-engine.js +522 -0
  12. package/dist/agent/completion-engine.js.map +1 -0
  13. package/dist/agent/context-compactor.js +188 -10
  14. package/dist/agent/context-compactor.js.map +1 -1
  15. package/dist/agent/custom-agent.js +368 -0
  16. package/dist/agent/custom-agent.js.map +1 -0
  17. package/dist/agent/design-to-code.js +218 -0
  18. package/dist/agent/design-to-code.js.map +1 -0
  19. package/dist/agent/event-driven-agent.js +359 -0
  20. package/dist/agent/event-driven-agent.js.map +1 -0
  21. package/dist/agent/git-integration.js +324 -0
  22. package/dist/agent/git-integration.js.map +1 -0
  23. package/dist/agent/layered-memory.js +329 -0
  24. package/dist/agent/layered-memory.js.map +1 -0
  25. package/dist/agent/multi-agent.js +359 -0
  26. package/dist/agent/multi-agent.js.map +1 -0
  27. package/dist/agent/orchestrator.js +160 -22
  28. package/dist/agent/orchestrator.js.map +1 -1
  29. package/dist/agent/prompts.js +20 -0
  30. package/dist/agent/prompts.js.map +1 -1
  31. package/dist/agent/quality-gate.js +139 -0
  32. package/dist/agent/quality-gate.js.map +1 -1
  33. package/dist/agent/self-correction.js +274 -0
  34. package/dist/agent/self-correction.js.map +1 -0
  35. package/dist/agent/self-heal.js +42 -12
  36. package/dist/agent/self-heal.js.map +1 -1
  37. package/dist/agent/solo-agent.js +321 -0
  38. package/dist/agent/solo-agent.js.map +1 -0
  39. package/dist/agent/symbol-index.js +258 -28
  40. package/dist/agent/symbol-index.js.map +1 -1
  41. package/dist/agent/team-collaboration.js +252 -0
  42. package/dist/agent/team-collaboration.js.map +1 -0
  43. package/dist/agent/type-checker.js +229 -0
  44. package/dist/agent/type-checker.js.map +1 -0
  45. package/dist/cli/run.js +1 -0
  46. package/dist/cli/run.js.map +1 -1
  47. package/dist/cli/version.js +1 -1
  48. package/dist/integrations/collaboration.js +219 -0
  49. package/dist/integrations/collaboration.js.map +1 -0
  50. package/dist/integrations/figma.js +307 -0
  51. package/dist/integrations/figma.js.map +1 -0
  52. package/dist/integrations/sso.js +327 -0
  53. package/dist/integrations/sso.js.map +1 -0
  54. package/dist/knowledge/library.js +301 -0
  55. package/dist/knowledge/library.js.map +1 -0
  56. package/dist/plugins/manager.js +332 -0
  57. package/dist/plugins/manager.js.map +1 -0
  58. package/dist/runtime/event-log.js.map +1 -1
  59. package/dist/tools/browser/browser.tool.js +217 -0
  60. package/dist/tools/browser/browser.tool.js.map +1 -0
  61. package/dist/tools/index.js +2 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/tools/shell/exec.js +50 -10
  64. package/dist/tools/shell/exec.js.map +1 -1
  65. package/dist/tools/shell/run-shell.tool.js +8 -2
  66. package/dist/tools/shell/run-shell.tool.js.map +1 -1
  67. package/dist/training/completion-evaluator.js +213 -0
  68. package/dist/training/completion-evaluator.js.map +1 -0
  69. package/dist/training/fim-data.js +248 -0
  70. package/dist/training/fim-data.js.map +1 -0
  71. package/dist/voice/voice-programming.js +223 -0
  72. package/dist/voice/voice-programming.js.map +1 -0
  73. package/dist/web/public/css/style.css +190 -7
  74. package/dist/web/public/index.html +403 -9
  75. package/dist/web/public/js/api.js +191 -4
  76. package/dist/web/public/js/app.js +850 -123
  77. package/dist/web/public/js/mock-api.js +234 -0
  78. package/dist/web/public/js/monaco-editor.js +203 -0
  79. package/dist/web/public/js/ui.js +1271 -54
  80. package/dist/web/public/manifest.json +59 -0
  81. package/dist/web/public/service-worker.js +173 -0
  82. package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  83. package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  84. package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  85. package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  86. package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  87. package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  88. package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  89. package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  90. package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  91. package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  92. package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  93. package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  94. package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  95. package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  96. package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  97. package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  98. package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  99. package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  100. package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  101. package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  102. package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  103. package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  104. package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  105. package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  106. package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  107. package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +1 -0
  108. package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +1 -0
  109. package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +1 -0
  110. package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  111. package/dist/web/public/vendor/monaco/vs/editor.js +1 -0
  112. package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  113. package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  114. package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  115. package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  116. package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  117. package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  118. package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  119. package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  120. package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  121. package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  122. package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  123. package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  124. package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +1 -0
  125. package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  126. package/dist/web/public/vendor/monaco/vs/index.js +1 -0
  127. package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  128. package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  129. package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  130. package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  131. package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  132. package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  133. package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  134. package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  135. package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +66 -0
  136. package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  137. package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +443 -0
  138. package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  139. package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +17 -0
  140. package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  141. package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  142. package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  143. package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  144. package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  145. package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  146. package/dist/web/public/vendor/monaco/vs/loader.js +1368 -0
  147. package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  148. package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  149. package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  150. package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  151. package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  152. package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  153. package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  154. package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  155. package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  156. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  157. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  158. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  159. package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  160. package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  161. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
  162. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +9 -0
  163. package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
  164. package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +9 -0
  165. package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
  166. package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +9 -0
  167. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
  168. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +9 -0
  169. package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
  170. package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +9 -0
  171. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
  172. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +9 -0
  173. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
  174. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +9 -0
  175. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
  176. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +9 -0
  177. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
  178. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
  179. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
  180. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +9 -0
  181. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
  182. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +9 -0
  183. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  184. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  185. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
  186. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
  187. package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +1 -0
  188. package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  189. package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  190. package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  191. package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  192. package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  193. package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  194. package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  195. package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  196. package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  197. package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  198. package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  199. package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  200. package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  201. package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  202. package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  203. package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  204. package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  205. package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  206. package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  207. package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  208. package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  209. package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  210. package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  211. package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  212. package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  213. package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  214. package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  215. package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  216. package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  217. package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  218. package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  219. package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  220. package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  221. package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  222. package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  223. package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  224. package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  225. package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  226. package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  227. package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  228. package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  229. package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  230. package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  231. package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  232. package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
  233. package/dist/web/server.js +2073 -210
  234. package/dist/web/server.js.map +1 -1
  235. package/dist/web/task-queue.js +25 -5
  236. package/dist/web/task-queue.js.map +1 -1
  237. package/docs/BENCHMARK_REPORT_en.md +329 -0
  238. package/docs/BENCHMARK_REPORT_zh.md +329 -0
  239. package/docs/DEPLOYMENT.md +382 -0
  240. package/docs/MODEL_FINETUNING.md +405 -0
  241. package/docs/TECHNICAL_SPEC_en.md +292 -0
  242. package/docs/TECHNICAL_SPEC_zh.md +292 -0
  243. package/docs/UPDATE_NOTES_en.md +159 -0
  244. package/docs/UPDATE_NOTES_zh.md +159 -0
  245. package/electron/main.js +602 -0
  246. package/package.json +67 -3
  247. package/tool-schema.json +1 -1
@@ -48,6 +48,7 @@
48
48
  toast('✅ ' + (d.message || '总结完成'));
49
49
  loadMemoryStats();
50
50
  loadSummaryHistory();
51
+ loadLongTermMemory();
51
52
  } catch (e) {
52
53
  toast('总结失败:' + e.message);
53
54
  } finally {
@@ -55,6 +56,133 @@
55
56
  btn.textContent = '✨ 立即总结';
56
57
  }
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
+ });
58
186
  }
59
187
 
60
188
  // 每次进入都刷新数据(事件不重复绑定,数据实时刷新)
@@ -113,14 +241,125 @@
113
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>';
114
242
  }
115
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
+
116
281
  function renderAutoGrid(list) {
117
282
  const grid = document.getElementById('autoGrid');
118
283
  if (!list.length) { renderEmpty(grid, t('empty.no_automations')); return; }
119
- grid.innerHTML = list.map((a) => '<div class="tile" data-id="' + a.id + '"><div class="icon">⚡</div><div class="title">' + escapeHtml(a.name) + '</div><div class="desc">' + escapeHtml(a.goal).slice(0, 60) + (a.goal.length > 60 ? '…' : '') + '<br>已运行 ' + a.runCount + ' 次</div><div class="ops"><button class="run" data-id="' + a.id + '">▶ 运行</button><button class="ghost del" data-id="' + a.id + '">删除</button></div></div>').join('');
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('');
120
293
  grid.querySelectorAll('button.run').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); runAuto(b.getAttribute('data-id')); }));
121
294
  grid.querySelectorAll('button.del').forEach((b) => b.addEventListener('click', (e) => { e.stopPropagation(); delAuto(b.getAttribute('data-id')); }));
122
295
  }
123
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
+
124
363
  function renderWorkspaceBar() {
125
364
  const el = document.getElementById('workspaceCwdBottom');
126
365
  if (el) el.textContent = state.workspaceDir || '(未设置)';
@@ -313,7 +552,18 @@
313
552
  function toggleDirectMode() {
314
553
  state.directMode = !state.directMode;
315
554
  document.getElementById('directModePill').style.display = state.directMode ? 'inline-flex' : 'none';
316
- toast(state.directMode ? '已开启「直接操作电脑」模式' : '已关闭「直接操作电脑」模式');
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 ? '已开启「电脑操作」模式,可以用语言控制电脑' : '已关闭「电脑操作」模式');
317
567
  }
318
568
 
319
569
  function previewImage(path) {
@@ -391,18 +641,310 @@
391
641
  function showPreview(title, content, mode) {
392
642
  document.getElementById('previewTitle').textContent = title;
393
643
  const box = document.getElementById('previewContent');
394
- if (mode === 'code' || mode === 'file') {
395
- box.innerHTML = '<pre>' + escapeHtml(content) + '</pre>';
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();
396
744
  } else {
397
745
  box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
398
746
  }
399
747
  }
400
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
+
401
796
  function switchRightTab(name) {
402
797
  state.rightTab = name;
403
798
  document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
404
799
  document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
405
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
+ });
406
948
  }
407
949
 
408
950
  async function showArtifacts(dir) {
@@ -448,6 +990,14 @@
448
990
  return '';
449
991
  }
450
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
+
451
1001
  function renderTaskThread(task) {
452
1002
  const box = document.getElementById('messages');
453
1003
  const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
@@ -461,82 +1011,119 @@
461
1011
  const conv = Array.isArray(task.conversation) ? task.conversation : [];
462
1012
  const steps = Array.isArray(task.steps) ? task.steps : [];
463
1013
 
464
- if (conv.length > 0) {
465
- let i = 0;
466
- while (i < conv.length) {
467
- const m = conv[i];
468
- if (!m) { i++; continue; }
469
- // 过滤 tool 消息和 system 消息,用户只看得到人和助手的对话
470
- if (m.role === 'tool' || m.role === 'system') { i++; continue; }
471
-
472
- if (m.role === 'user') {
473
- html += '<div class="msg user">' + linkifyArtifacts(m.content || '') + '</div>';
474
- i++;
475
- continue;
476
- }
1014
+ // 统一处理:先显示用户消息,再从 steps 提取思考过程(实时更新),最后显示最终回复
1015
+ // 这样确保思考过程能实时显示,不会因为 conversation 有内容就跳过 steps
477
1016
 
478
- if (m.role === 'assistant') {
479
- // 收集连续 assistant 消息
480
- const assistantMsgs = [];
481
- while (i < conv.length && conv[i] && conv[i].role === 'assistant') {
482
- assistantMsgs.push(conv[i]);
483
- i++;
484
- }
485
- const hasToolCalls = assistantMsgs.some(msg => (msg.toolCalls || []).length > 0);
486
- const allText = assistantMsgs.map(msg => (msg.content || '').trim()).filter(Boolean).join('\n\n');
487
- if (hasToolCalls && allText) {
488
- // 有工具调用且有思考文本 → 展示思考过程(直接显示,不折叠)
489
- html += renderThinkingProcess(assistantMsgs);
490
- } else if (allText) {
491
- // 纯文本回复 → 普通气泡,这就是最终回复
492
- html += '<div class="msg assistant">'
493
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(allText) + '</div>'
494
- + '</div>';
495
- }
496
- } else {
497
- i++;
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; // 只显示第一条用户消息,后续的在多轮对话中处理
498
1025
  }
499
1026
  }
500
- } else if (steps.length > 0) {
501
- // 任务运行中 conversation 还没生成,从 steps 里提取思考文本实时显示
502
- if (task.goal) {
503
- html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
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('刚才遇到点小问题,我调整一下思路再试试。');
504
1039
  }
505
- const thinkingTexts = [];
506
- for (const s of steps) {
507
- if (s.type === 'model.response' && s.data && s.data.content && s.data.content.trim()) {
508
- thinkingTexts.push(s.data.content.trim());
509
- } else if (s.type === 'self-heal') {
510
- thinkingTexts.push('刚才遇到点小问题,我调整一下思路再试试。');
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());
511
1054
  }
512
1055
  }
513
- if (thinkingTexts.length > 0) {
514
- html += '<div class="msg assistant" style="opacity:0.9;">'
515
- + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(thinkingTexts.join('\n\n')) + '</div>'
516
- + '</div>';
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
+ }
517
1066
  }
518
- } else if (task.goal) {
519
- // 首轮尚未产生对话流时,至少呈现用户原始指令
520
- html += '<div class="msg user">' + linkifyArtifacts(task.goal) + '</div>';
521
1067
  }
522
1068
 
523
1069
  // 终态:如果对话流里没有最终回复(旧任务或被中断),再显示 finalAnswer
524
1070
  const hasFinalInConv = conv.some(m => m.role === 'assistant' && !(m.toolCalls || []).length && (m.content || '').trim());
525
1071
  if (task.status === 'done' && finalAnswer && !hasFinalInConv) {
526
1072
  html += '<div class="msg assistant">'
1073
+ + renderMsgActions()
527
1074
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
528
1075
  + '</div>';
529
1076
  } else if (task.status === 'failed') {
530
1077
  if (finalAnswer && !hasFinalInConv) {
531
1078
  html += '<div class="msg assistant">'
1079
+ + renderMsgActions()
532
1080
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(finalAnswer) + '</div>'
533
1081
  + '</div>';
534
1082
  }
535
1083
  html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
536
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
+ }
537
1123
  html += '<div class="thinking-indicator">'
538
1124
  + '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
539
1125
  + '<span>思考中...</span>'
1126
+ + waitTip
540
1127
  + '</div>';
541
1128
  } else if (task.status === 'queued') {
542
1129
  html += '<div class="msg sys">任务已入队,等待执行…</div>';
@@ -578,6 +1165,7 @@
578
1165
  if (!textContent) return '';
579
1166
  // 用普通 assistant 气泡样式展示思考过程,让用户看到模型在想什么
580
1167
  return '<div class="msg assistant thinking-msg" style="opacity:0.85;">'
1168
+ + renderMsgActions()
581
1169
  + '<div style="white-space:pre-wrap;word-break:break-word;line-height:1.7;">' + renderPlainText(textContent) + '</div>'
582
1170
  + '</div>';
583
1171
  }
@@ -714,6 +1302,29 @@
714
1302
  // 切换时显示/隐藏底部工具栏(任务上下文头部始终可见)
715
1303
  const isChat = nav === 'chat';
716
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);
717
1328
  }
718
1329
 
719
1330
  function updateUserBar() {
@@ -762,3 +1373,609 @@
762
1373
  // 显示欢迎引导弹窗
763
1374
  showWelcomeModal(welcomeTasks);
764
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
+ }