feihong-code 7.0.0 → 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 (236) hide show
  1. package/.github/workflows/release.yml +173 -0
  2. package/CHANGELOG.md +50 -0
  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 +127 -16
  28. package/dist/agent/orchestrator.js.map +1 -1
  29. package/dist/agent/quality-gate.js +139 -0
  30. package/dist/agent/quality-gate.js.map +1 -1
  31. package/dist/agent/self-correction.js +274 -0
  32. package/dist/agent/self-correction.js.map +1 -0
  33. package/dist/agent/self-heal.js +40 -10
  34. package/dist/agent/self-heal.js.map +1 -1
  35. package/dist/agent/solo-agent.js +321 -0
  36. package/dist/agent/solo-agent.js.map +1 -0
  37. package/dist/agent/symbol-index.js +258 -28
  38. package/dist/agent/symbol-index.js.map +1 -1
  39. package/dist/agent/team-collaboration.js +252 -0
  40. package/dist/agent/team-collaboration.js.map +1 -0
  41. package/dist/agent/type-checker.js +229 -0
  42. package/dist/agent/type-checker.js.map +1 -0
  43. package/dist/cli/run.js +1 -0
  44. package/dist/cli/run.js.map +1 -1
  45. package/dist/integrations/collaboration.js +219 -0
  46. package/dist/integrations/collaboration.js.map +1 -0
  47. package/dist/integrations/figma.js +307 -0
  48. package/dist/integrations/figma.js.map +1 -0
  49. package/dist/integrations/sso.js +327 -0
  50. package/dist/integrations/sso.js.map +1 -0
  51. package/dist/knowledge/library.js +301 -0
  52. package/dist/knowledge/library.js.map +1 -0
  53. package/dist/plugins/manager.js +332 -0
  54. package/dist/plugins/manager.js.map +1 -0
  55. package/dist/runtime/event-log.js.map +1 -1
  56. package/dist/tools/browser/browser.tool.js +217 -0
  57. package/dist/tools/browser/browser.tool.js.map +1 -0
  58. package/dist/tools/index.js +2 -0
  59. package/dist/tools/index.js.map +1 -1
  60. package/dist/tools/shell/exec.js +50 -10
  61. package/dist/tools/shell/exec.js.map +1 -1
  62. package/dist/tools/shell/run-shell.tool.js +8 -2
  63. package/dist/tools/shell/run-shell.tool.js.map +1 -1
  64. package/dist/training/completion-evaluator.js +213 -0
  65. package/dist/training/completion-evaluator.js.map +1 -0
  66. package/dist/training/fim-data.js +248 -0
  67. package/dist/training/fim-data.js.map +1 -0
  68. package/dist/voice/voice-programming.js +223 -0
  69. package/dist/voice/voice-programming.js.map +1 -0
  70. package/dist/web/public/css/style.css +58 -0
  71. package/dist/web/public/index.html +210 -0
  72. package/dist/web/public/js/api.js +115 -1
  73. package/dist/web/public/js/mock-api.js +234 -0
  74. package/dist/web/public/js/monaco-editor.js +203 -0
  75. package/dist/web/public/js/ui.js +926 -4
  76. package/dist/web/public/manifest.json +59 -0
  77. package/dist/web/public/service-worker.js +173 -0
  78. package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  79. package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  80. package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  81. package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  82. package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  83. package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  84. package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  85. package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  86. package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  87. package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  88. package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  89. package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  90. package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  91. package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  92. package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  93. package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  94. package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  95. package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  96. package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  97. package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  98. package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  99. package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  100. package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  101. package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  102. package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  103. package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +1 -0
  104. package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +1 -0
  105. package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +1 -0
  106. package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  107. package/dist/web/public/vendor/monaco/vs/editor.js +1 -0
  108. package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  109. package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  110. package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  111. package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  112. package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  113. package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  114. package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  115. package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  116. package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  117. package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  118. package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  119. package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  120. package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +1 -0
  121. package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  122. package/dist/web/public/vendor/monaco/vs/index.js +1 -0
  123. package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  124. package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  125. package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  126. package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  127. package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  128. package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  129. package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  130. package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  131. package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +66 -0
  132. package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  133. package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +443 -0
  134. package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  135. package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +17 -0
  136. package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  137. package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  138. package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  139. package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  140. package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  141. package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  142. package/dist/web/public/vendor/monaco/vs/loader.js +1368 -0
  143. package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  144. package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  145. package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  146. package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  147. package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  148. package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  149. package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  150. package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  151. package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  152. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  153. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  154. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  155. package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  156. package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  157. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
  158. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +9 -0
  159. package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
  160. package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +9 -0
  161. package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
  162. package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +9 -0
  163. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
  164. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +9 -0
  165. package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
  166. package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +9 -0
  167. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
  168. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +9 -0
  169. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
  170. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +9 -0
  171. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
  172. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +9 -0
  173. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
  174. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
  175. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
  176. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +9 -0
  177. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
  178. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +9 -0
  179. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  180. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  181. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
  182. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
  183. package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +1 -0
  184. package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  185. package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  186. package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  187. package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  188. package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  189. package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  190. package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  191. package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  192. package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  193. package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  194. package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  195. package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  196. package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  197. package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  198. package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  199. package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  200. package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  201. package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  202. package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  203. package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  204. package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  205. package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  206. package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  207. package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  208. package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  209. package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  210. package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  211. package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  212. package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  213. package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  214. package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  215. package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  216. package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  217. package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  218. package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  219. package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  220. package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  221. package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  222. package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  223. package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  224. package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  225. package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  226. package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  227. package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  228. package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
  229. package/dist/web/server.js +1019 -0
  230. package/dist/web/server.js.map +1 -1
  231. package/dist/web/task-queue.js +22 -2
  232. package/dist/web/task-queue.js.map +1 -1
  233. package/docs/DEPLOYMENT.md +382 -0
  234. package/docs/MODEL_FINETUNING.md +405 -0
  235. package/electron/main.js +202 -4
  236. package/package.json +6 -1
@@ -641,18 +641,310 @@
641
641
  function showPreview(title, content, mode) {
642
642
  document.getElementById('previewTitle').textContent = title;
643
643
  const box = document.getElementById('previewContent');
644
- if (mode === 'code' || mode === 'file') {
645
- 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();
646
744
  } else {
647
745
  box.innerHTML = '<div style="white-space:pre-wrap;word-break:break-word;">' + content + '</div>';
648
746
  }
649
747
  }
650
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
+
651
796
  function switchRightTab(name) {
652
797
  state.rightTab = name;
653
798
  document.querySelectorAll('.right-tab').forEach((t) => t.classList.toggle('active', t.getAttribute('data-tab') === name));
654
799
  document.getElementById('detailPanel').classList.toggle('active', name === 'detail');
655
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
+ });
656
948
  }
657
949
 
658
950
  async function showArtifacts(dir) {
@@ -800,8 +1092,32 @@
800
1092
  const timeStr = mins > 0 ? `${mins}分${secs}秒` : `${secs}秒`;
801
1093
  waitTip = `<span style="color:var(--muted);font-size:12px;margin-left:8px;">已等待 ${timeStr}</span>`;
802
1094
  }
803
- if (elapsed > 60) {
804
- waitTip += '<div style="color:var(--muted);font-size:12px;margin-top:6px;line-height:1.5;">模型正在深度思考或执行复杂操作,请耐心等待...<br>如果等待超过3分钟,可以尝试点击「停止」后重新提交。</div>';
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>';
805
1121
  }
806
1122
  }
807
1123
  html += '<div class="thinking-indicator">'
@@ -1057,3 +1373,609 @@
1057
1373
  // 显示欢迎引导弹窗
1058
1374
  showWelcomeModal(welcomeTasks);
1059
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
+ }