feihong-code 7.2.0 → 7.6.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 (204) hide show
  1. package/.github/workflows/ci.yml +54 -1
  2. package/.github/workflows/release.yml +30 -0
  3. package/.github/workflows/swebench-real.yml +84 -0
  4. package/CHANGELOG.md +41 -39
  5. package/CONTRIBUTING.md +9 -2
  6. package/README.md +56 -6
  7. package/README.self-evolve.md +279 -274
  8. package/dist/agent/change-manager.js +1 -1
  9. package/dist/agent/code-rag.js +1 -1
  10. package/dist/agent/code-review.js +1 -1
  11. package/dist/agent/code-writer.js +1 -1
  12. package/dist/agent/completion-engine.js +76 -45
  13. package/dist/agent/completion-engine.js.map +1 -1
  14. package/dist/agent/completion-postprocess.js +109 -0
  15. package/dist/agent/completion-postprocess.js.map +1 -0
  16. package/dist/agent/context-budget.js +121 -0
  17. package/dist/agent/context-budget.js.map +1 -0
  18. package/dist/agent/context-compactor.js +58 -0
  19. package/dist/agent/context-compactor.js.map +1 -1
  20. package/dist/agent/experience.js +1 -1
  21. package/dist/agent/layered-memory.js +1 -1
  22. package/dist/agent/lint.js +121 -0
  23. package/dist/agent/lint.js.map +1 -0
  24. package/dist/agent/orchestrator.js +106 -31
  25. package/dist/agent/orchestrator.js.map +1 -1
  26. package/dist/agent/parallel-orchestrator.js +1 -1
  27. package/dist/agent/planner.js +8 -3
  28. package/dist/agent/planner.js.map +1 -1
  29. package/dist/agent/prompts.js +55 -55
  30. package/dist/agent/quality-gate.js +1 -1
  31. package/dist/agent/repo-context.js +1 -1
  32. package/dist/agent/repo-reader.js +1 -1
  33. package/dist/agent/repo-underwriter.js +1 -1
  34. package/dist/agent/self-correction.js +25 -25
  35. package/dist/agent/self-heal.js +32 -27
  36. package/dist/agent/self-heal.js.map +1 -1
  37. package/dist/agent/self-improver.js +1 -1
  38. package/dist/agent/subagent-summary.js +1 -1
  39. package/dist/agent/subagent.js +1 -1
  40. package/dist/agent/swe-agent.js +1 -1
  41. package/dist/agent/swe-verifier.js +1 -1
  42. package/dist/agent/symbol-index.js +1 -1
  43. package/dist/agent/team.js +1 -1
  44. package/dist/agent/type-checker.js +1 -1
  45. package/dist/cli/commands.js +16 -0
  46. package/dist/cli/commands.js.map +1 -1
  47. package/dist/cli/index.js +66 -2
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/multi-pane-tui.js +217 -0
  50. package/dist/cli/multi-pane-tui.js.map +1 -0
  51. package/dist/cli/repl.js +1 -1
  52. package/dist/cli/run.js +168 -42
  53. package/dist/cli/run.js.map +1 -1
  54. package/dist/cli/self-evolve-cli.js +168 -0
  55. package/dist/cli/tui-run.js +79 -0
  56. package/dist/cli/tui-run.js.map +1 -0
  57. package/dist/cli/tui.js +1 -1
  58. package/dist/cli/tui.js.map +1 -1
  59. package/dist/cli/version.js +3 -3
  60. package/dist/cli/version.js.map +1 -1
  61. package/dist/enterprise/audit.js +1 -1
  62. package/dist/enterprise/index.js +1 -1
  63. package/dist/enterprise/policy.js +1 -1
  64. package/dist/enterprise/quota.js +1 -1
  65. package/dist/enterprise/tenant.js +1 -1
  66. package/dist/harness/executor.js +1 -1
  67. package/dist/harness/index.js +1 -1
  68. package/dist/harness/loader.js +1 -1
  69. package/dist/harness/types.js +1 -1
  70. package/dist/harness/verifier.js +52 -12
  71. package/dist/harness/verifier.js.map +1 -1
  72. package/dist/integrations/feishu-bridge.js +348 -0
  73. package/dist/integrations/feishu-bridge.js.map +1 -0
  74. package/dist/integrations/github-mcp.js +137 -0
  75. package/dist/integrations/github-mcp.js.map +1 -0
  76. package/dist/integrations/wechat-bridge.js +499 -0
  77. package/dist/integrations/wechat-bridge.js.map +1 -0
  78. package/dist/integrations/yuanbao-bridge.js +326 -0
  79. package/dist/integrations/yuanbao-bridge.js.map +1 -0
  80. package/dist/lsp/lsp-client.js +183 -0
  81. package/dist/lsp/lsp-client.js.map +1 -0
  82. package/dist/lsp/lsp-service.js +159 -0
  83. package/dist/lsp/lsp-service.js.map +1 -0
  84. package/dist/models/model-router.js +1 -1
  85. package/dist/models/model.dto.js +1 -1
  86. package/dist/models/providers/ollama.provider.js +1 -1
  87. package/dist/models/providers/openai-compatible.provider.js +1 -1
  88. package/dist/plugins/manager.js +50 -2
  89. package/dist/plugins/manager.js.map +1 -1
  90. package/dist/plugins/plugin-loader.js +1 -1
  91. package/dist/runtime/event-log.js +1 -1
  92. package/dist/runtime/git.js +1 -1
  93. package/dist/runtime/hooks.js +71 -4
  94. package/dist/runtime/hooks.js.map +1 -1
  95. package/dist/runtime/session-persist.js +1 -1
  96. package/dist/runtime/worktree.js +1 -1
  97. package/dist/self-evolve/manager.js +517 -401
  98. package/dist/self-evolve/self-heal-scheduler.js +160 -0
  99. package/dist/self-evolve/self-heal-scheduler.js.map +1 -0
  100. package/dist/shared/concurrency.js +1 -1
  101. package/dist/shared/config.js +4 -2
  102. package/dist/shared/config.js.map +1 -1
  103. package/dist/shared/errors.js +1 -1
  104. package/dist/shared/i18n.js +109 -101
  105. package/dist/shared/i18n.js.map +1 -1
  106. package/dist/shared/secure-store.js +12 -5
  107. package/dist/shared/secure-store.js.map +1 -1
  108. package/dist/shared/types.js +1 -1
  109. package/dist/skills/goal.js +1 -1
  110. package/dist/skills/grill.js +1 -1
  111. package/dist/skills/plan.js +1 -1
  112. package/dist/skills/pua-hooks.js +273 -0
  113. package/dist/skills/pua-hooks.js.map +1 -0
  114. package/dist/skills/self-heal.js +9 -3
  115. package/dist/skills/self-heal.js.map +1 -1
  116. package/dist/skills/skill-loader.js +1 -1
  117. package/dist/skills/skill-market.js +84 -8
  118. package/dist/skills/skill-market.js.map +1 -1
  119. package/dist/tools/analysis/code-analyzer.js +1 -1
  120. package/dist/tools/file/edit.tool.js +18 -3
  121. package/dist/tools/file/edit.tool.js.map +1 -1
  122. package/dist/tools/file/list.tool.js +1 -1
  123. package/dist/tools/file/read.tool.js +1 -1
  124. package/dist/tools/file/write.tool.js +1 -1
  125. package/dist/tools/generator/code-generator.js +41 -41
  126. package/dist/tools/generator/test-generator.js +10 -10
  127. package/dist/tools/index.js +1 -1
  128. package/dist/tools/mcp/index.js +1 -1
  129. package/dist/tools/mcp/mcp-client.js +1 -1
  130. package/dist/tools/safe-path.js +1 -1
  131. package/dist/tools/search/grep.tool.js +1 -1
  132. package/dist/tools/shell/exec.js +24 -2
  133. package/dist/tools/shell/exec.js.map +1 -1
  134. package/dist/tools/shell/run-shell.tool.js +1 -1
  135. package/dist/tools/skills/load-skill.tool.js +1 -1
  136. package/dist/tools/tool.registry.js +10 -1
  137. package/dist/tools/tool.registry.js.map +1 -1
  138. package/dist/tools/verify/build-check.tool.js +1 -1
  139. package/dist/tools/verify/test-run.tool.js +1 -1
  140. package/dist/tools/web/web.tool.js +1 -1
  141. package/dist/voice/voice-programming.js +32 -6
  142. package/dist/voice/voice-programming.js.map +1 -1
  143. package/dist/web/auth.js +34 -2
  144. package/dist/web/auth.js.map +1 -1
  145. package/dist/web/channels.js +1 -1
  146. package/dist/web/extra-apis.js +440 -0
  147. package/dist/web/extra-apis.js.map +1 -0
  148. package/dist/web/public/css/style.css +99 -72
  149. package/dist/web/public/index.html +44 -1
  150. package/dist/web/public/js/api.js +64 -2
  151. package/dist/web/public/js/app.js +151 -128
  152. package/dist/web/public/js/mock-api.js +25 -0
  153. package/dist/web/public/js/monaco-editor.js +161 -1
  154. package/dist/web/public/js/ui.js +292 -56
  155. package/dist/web/public/service-worker.js +5 -29
  156. package/dist/web/server.js +178 -61
  157. package/dist/web/server.js.map +1 -1
  158. package/dist/web/task-queue.js +6 -21
  159. package/dist/web/task-queue.js.map +1 -1
  160. package/docs/App/344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +1 -1
  161. package/docs/App/346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +1 -1
  162. package/docs/BENCHMARK_REPORT_en.md +62 -5
  163. package/docs/BENCHMARK_REPORT_zh.md +66 -9
  164. package/docs/DPA.md +65 -0
  165. package/docs/DPA/346/225/260/346/215/256/345/244/204/347/220/206/345/215/217/350/256/256/346/250/241/346/235/277_v1.0.md +69 -0
  166. package/docs/Deployment_Guide_EN.md +1 -1
  167. package/docs/SECURITY_WHITEPAPER.md +74 -0
  168. package/docs/SELF-EVOLVE-GUIDE.md +1 -1
  169. package/docs/SWE_BENCH_REPORT.md +73 -0
  170. package/docs/TECHNICAL_SPEC_en.md +1 -1
  171. package/docs/TECHNICAL_SPEC_zh.md +1 -1
  172. package/docs/Technical_Manual_EN.md +1 -1
  173. package/docs/UPDATE_NOTES_en.md +1 -1
  174. package/docs/UPDATE_NOTES_zh.md +1 -1
  175. package/docs/UPGRADE_7.2.0.md +309 -0
  176. package/docs/UPGRADE_GUIDE_7_5.md +86 -0
  177. package/docs/UPGRADE_GUIDE_7_6.md +153 -0
  178. package/docs/User_Manual_EN.md +1 -1
  179. package/docs/VOLTAGENT_SKILLS_CATALOG.md +95 -0
  180. package/docs/error-codes.md +1 -1
  181. package/docs/geo-feihong-code.html +391 -0
  182. package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +5 -3
  183. package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +140 -231
  184. package/docs//345/205/250/351/235/242/345/244/215/347/233/230_50/350/275/256/346/267/261/345/272/246/345/256/241/350/256/241_v7.2.0.md +208 -0
  185. package/docs//345/205/250/351/235/242/345/244/215/347/233/230_6/351/241/271/351/207/215/347/202/271/346/240/270/346/237/245_v7.2.0.md +152 -0
  186. package/docs//345/205/250/351/235/242/345/244/215/347/233/230_/350/277/220/350/241/214/346/227/266/345/256/236/346/265/213/347/211/210_v7.2.0.md +271 -0
  187. package/docs//345/220/210/350/247/204/350/256/244/350/257/201/345/207/206/345/244/207/346/270/205/345/215/225_v7.2.0.md +59 -0
  188. package/docs//345/256/211/345/205/250/347/231/275/347/232/256/344/271/246_v7.2.0.md +84 -0
  189. package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +240 -149
  190. package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +2 -2
  191. package/electron/main.js +4 -202
  192. package/package.json +182 -172
  193. package/templates/skills/api-design/SKILL.md +27 -0
  194. package/templates/skills/code-review/SKILL.md +35 -0
  195. package/templates/skills/dependency-upgrade/SKILL.md +29 -0
  196. package/templates/skills/doc-gen/SKILL.md +29 -0
  197. package/templates/skills/git-flow/SKILL.md +26 -0
  198. package/templates/skills/onboarding/SKILL.md +27 -0
  199. package/templates/skills/performance/SKILL.md +29 -0
  200. package/templates/skills/refactor/SKILL.md +28 -0
  201. package/templates/skills/security-audit/SKILL.md +28 -0
  202. package/templates/skills/test-gen/SKILL.md +30 -0
  203. package/docs/DEPLOYMENT.md +0 -382
  204. package/docs/MODEL_FINETUNING.md +0 -405
@@ -98,6 +98,7 @@
98
98
 
99
99
  // 长期记忆:编辑按钮
100
100
  document.getElementById('memEditLong')?.addEventListener('click', () => {
101
+ bindGhostTextOnce(); // P1-1: 编辑时启用 ghost text 补全
101
102
  const content = document.getElementById('memLongContent');
102
103
  const editor = document.getElementById('memLongEditor');
103
104
  const editBtn = document.getElementById('memEditLong');
@@ -748,6 +749,33 @@
748
749
 
749
750
  /* ========== P0-1: Monaco 补全 Provider 注册(内联 ghost text + 补全弹窗) ========== */
750
751
  let _monacoCompletionsRegistered = false;
752
+
753
+ // P6-1: 补全接受后自动 lint——调 /api/lint 校验插入代码,异常追加 fhcode-lint 波浪线 + 提示
754
+ let _lintFeedbackShown = false;
755
+ async function lintAfterAccept(inserted) {
756
+ if (!inserted || !inserted.trim()) return;
757
+ try {
758
+ const d = await api('/api/lint', 'POST', { code: inserted, language: '' });
759
+ if (!d || !d.ok) return;
760
+ if (d.clean) {
761
+ _lintFeedbackShown = false;
762
+ return;
763
+ }
764
+ if (typeof FHMonaco !== 'undefined' && FHMonaco.showLintFeedback) {
765
+ FHMonaco.showLintFeedback(d.errors || []);
766
+ }
767
+ const errCount = (d.errors || []).filter((x) => x.severity === 1).length;
768
+ if (errCount > 0 && !_lintFeedbackShown) {
769
+ _lintFeedbackShown = true;
770
+ const bar = document.getElementById('statusMessage');
771
+ if (bar) bar.textContent = '⚠️ 补全已接受,但校验发现 ' + errCount + ' 个语法错误(已标注)';
772
+ setTimeout(() => { if (bar && bar.textContent.includes('校验发现')) bar.textContent = ''; }, 6000);
773
+ }
774
+ } catch (e) {
775
+ /* lint 反馈失败静默 */
776
+ }
777
+ }
778
+
751
779
  function registerMonacoCompletions() {
752
780
  if (_monacoCompletionsRegistered || typeof FHMonaco === 'undefined') return;
753
781
  _monacoCompletionsRegistered = true;
@@ -769,6 +797,10 @@
769
797
  } catch (e) {
770
798
  return null;
771
799
  }
800
+ }, {
801
+ getActiveEditor: function () { return (typeof FHMonaco !== 'undefined') ? FHMonaco.getActiveEditor() : null; },
802
+ // P6-1: 补全被接受后自动 lint 反馈
803
+ onAccept: function (inserted) { lintAfterAccept(inserted); },
772
804
  });
773
805
 
774
806
  // 补全项弹窗(Ctrl+Space 触发,↑↓选择,Enter接受)—— Monaco 原生支持
@@ -791,6 +823,19 @@
791
823
  return [];
792
824
  }
793
825
  });
826
+
827
+ // P4-2: 语义诊断波浪线(编辑防抖调 /api/lsp/diagnostics,setModelMarkers 标注)
828
+ FHMonaco.registerDiagnostics(async function ({ filePath }) {
829
+ try {
830
+ const rel = String(filePath).replace(/^\/+/, '');
831
+ if (!rel || rel === 'untitled') return { diagnostics: [] };
832
+ const q = 'cwd=' + encodeURIComponent('') + '&file=' + encodeURIComponent(rel);
833
+ const d = await api('/api/lsp/diagnostics?' + q, 'GET');
834
+ return Array.isArray(d.diagnostics) ? { diagnostics: d.diagnostics } : { diagnostics: [] };
835
+ } catch (e) {
836
+ return { diagnostics: [] };
837
+ }
838
+ }, function () { return (typeof monacoEditor !== 'undefined') ? monacoEditor : null; });
794
839
  }
795
840
 
796
841
  function switchRightTab(name) {
@@ -803,6 +848,7 @@
803
848
  document.getElementById('designPanel')?.classList.toggle('active', name === 'design');
804
849
  document.getElementById('gitPanel')?.classList.toggle('active', name === 'git');
805
850
  document.getElementById('teamPanel')?.classList.toggle('active', name === 'team');
851
+ document.getElementById('capabilitiesPanel')?.classList.toggle('active', name === 'capabilities');
806
852
  if (name === 'changes') {
807
853
  bindChangesButtonsOnce();
808
854
  loadChanges();
@@ -818,6 +864,10 @@
818
864
  bindTeamButtonsOnce();
819
865
  loadTeamData();
820
866
  }
867
+ if (name === 'capabilities') {
868
+ bindCapabilitiesOnce();
869
+ loadCapabilitiesPlugins();
870
+ }
821
871
  }
822
872
 
823
873
  // 变更面板顶部按钮只绑定一次
@@ -860,19 +910,40 @@
860
910
  }
861
911
 
862
912
  /* ========== P3: 多文件变更面板 ========== */
913
+ let _autoConflictChecked = false; // P1-2: 每次会话仅自动检测一次
863
914
  async function loadChanges() {
864
915
  try {
865
916
  const d = await api('/api/changes', 'GET');
866
917
  renderChanges(d);
918
+ // P1-2 增强:加载后自动检测一次冲突(无需手动点击)
919
+ if (!_autoConflictChecked && Array.isArray(d.changes) && d.changes.length > 0) {
920
+ _autoConflictChecked = true;
921
+ try {
922
+ const dc = await api('/api/changes/detect-conflicts', 'POST');
923
+ _conflictFiles = Array.isArray(dc.conflicts) ? dc.conflicts.map(c => c.path || c) : [];
924
+ if (_conflictFiles.length > 0) renderChanges(d);
925
+ } catch { /* 自动检测失败不阻塞 */ }
926
+ }
867
927
  } catch (e) {
868
928
  document.getElementById('changesList').innerHTML = '<div class="muted" style="text-align:center;padding:20px;">加载失败:' + escapeHtml(e.message) + '</div>';
869
929
  }
870
930
  }
871
931
 
932
+ // P6-3: 多文件 diff 视图状态——每文件显示模式(inline/side)+ 折叠集合
933
+ const _diffModes = {};
934
+ const _collapsedPaths = new Set();
935
+
872
936
  function renderChanges(data) {
873
937
  const changes = Array.isArray(data.changes) ? data.changes : [];
874
938
  const count = changes.length;
875
939
  document.getElementById('changesCount').textContent = count;
940
+ // P1-2 增强:冲突计数 badge
941
+ const conflictBadge = document.getElementById('changesConflictCount');
942
+ if (conflictBadge) {
943
+ const n = _conflictFiles.filter(cf => changes.some(c => c.path === cf || c.path.endsWith(cf) || cf.endsWith(c.path))).length;
944
+ conflictBadge.style.display = n > 0 ? 'inline' : 'none';
945
+ conflictBadge.textContent = n + ' 冲突';
946
+ }
876
947
  const list = document.getElementById('changesList');
877
948
  if (!count) {
878
949
  list.innerHTML = '<div class="muted" style="text-align:center;padding:30px;font-size:12px;">暂无暂存变更<br/><span style="font-size:11px;">AI 生成的文件修改会先暂存在这里,审批后才写入磁盘</span></div>';
@@ -887,35 +958,58 @@
887
958
  const hunks = Array.isArray(c.hunks) ? c.hunks : [];
888
959
  let hunksHtml = '';
889
960
  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>';
961
+ const mode = _diffModes[c.path] || 'inline';
962
+ if (mode === 'side') {
963
+ // P6-3: 并排 diff 视图(左=原文件,右=新文件,行级对齐)
964
+ hunksHtml = '<div style="margin-top:6px;">' + hunks.map((h) => {
965
+ const rows = (h.lines || []).map((l) => {
966
+ const left = (l.type === 'del' || l.type === 'ctx')
967
+ ? '<div class="' + (l.type === 'del' ? 'diff-del' : 'diff-ctx') + '">' + escapeHtml(l.content) + '</div>'
968
+ : '<div class="diff-ctx" style="color:transparent;">·</div>';
969
+ const right = (l.type === 'add' || l.type === 'ctx')
970
+ ? '<div class="' + (l.type === 'add' ? 'diff-add' : 'diff-ctx') + '">' + escapeHtml(l.content) + '</div>'
971
+ : '<div class="diff-ctx" style="color:transparent;">·</div>';
972
+ return '<div style="display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--line, #222);">' + left + right + '</div>';
973
+ }).join('');
974
+ return '<div style="border:1px solid var(--border);border-radius:4px;overflow:hidden;margin-bottom:6px;">'
975
+ + '<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 6px;background:var(--bg);font-size:11px;">'
976
+ + '<span class="muted">@@ ' + escapeHtml(String(h.header || '')) + '</span>'
977
+ + '<span class="muted" style="font-size:10px;">← 原文件 | 新文件 →</span></div>'
978
+ + rows + '</div>';
979
+ }).join('') + '</div>';
980
+ } else {
981
+ hunksHtml = '<div style="margin-top:6px;">' + hunks.map((h, hi) => {
982
+ const lines = (h.lines || []).map(l => {
983
+ const cls = l.type === 'add' ? 'diff-add' : l.type === 'del' ? 'diff-del' : 'diff-ctx';
984
+ return '<div class="' + cls + '">' + escapeHtml(l.content) + '</div>';
985
+ }).join('');
986
+ return '<div class="diff-hunk" style="margin-bottom:6px;border:1px solid var(--border);border-radius:4px;overflow:hidden;">'
987
+ + '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 6px;background:var(--bg);font-size:11px;">'
988
+ + '<span class="muted">@@ ' + escapeHtml(String(h.header || '')) + '</span>'
989
+ + '<span style="display:flex;gap:4px;">'
990
+ + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="accept-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✓</button>'
991
+ + '<button class="ghost-btn" style="font-size:10px;padding:1px 6px;" data-act="reject-hunk" data-idx="' + idx + '" data-hunk="' + hi + '">✕</button>'
992
+ + '</span></div>'
993
+ + '<div style="font-family:monospace;font-size:11px;line-height:1.5;">' + lines + '</div></div>';
994
+ }).join('') + '</div>';
995
+ }
904
996
  }
997
+ const collapsed = _collapsedPaths.has(c.path);
905
998
  return '<div class="change-item" style="padding:8px;margin-bottom:8px;' + conflictBorder + 'border-radius:6px;background:var(--card);">'
906
999
  + '<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>'
1000
+ + '<div style="flex:1;min-width:0;cursor:pointer;" data-act="toggle-collapse" data-idx="' + idx + '" title="点击折叠/展开">'
1001
+ + '<div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + (collapsed ? '' : '▾ ') + escapeHtml(c.path) + conflictBadge + '</div>'
909
1002
  + '<div style="font-size:11px;margin-top:2px;"><span style="color:' + typeColor + ';">' + typeLabel + '</span>'
910
1003
  + (c.additions != null ? ' · <span style="color:#2d8a4e;">+' + c.additions + '</span>' : '')
911
1004
  + (c.deletions != null ? ' · <span style="color:#c0392b;">-' + c.deletions + '</span>' : '')
912
1005
  + ' · <span class="muted">' + escapeHtml(c.status || 'pending') + '</span></div>'
913
1006
  + '</div>'
914
1007
  + '<div style="display:flex;gap:4px;flex-shrink:0;">'
1008
+ + (hunks.length ? '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;" data-act="toggle-mode" data-idx="' + idx + '">' + ((_diffModes[c.path] || 'inline') === 'side' ? '内联' : '并排') + '</button>' : '')
915
1009
  + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;background:#2d5a3d;color:#fff;" data-act="accept" data-idx="' + idx + '">接受</button>'
916
1010
  + '<button class="ghost-btn" style="font-size:11px;padding:3px 8px;" data-act="reject" data-idx="' + idx + '">拒绝</button>'
917
1011
  + '</div></div>'
918
- + hunksHtml
1012
+ + (collapsed ? '' : hunksHtml)
919
1013
  + '</div>';
920
1014
  }).join('');
921
1015
  // 绑定事件
@@ -940,6 +1034,11 @@
940
1034
  const hi = parseInt(btn.getAttribute('data-hunk'));
941
1035
  await api('/api/changes/hunk/reject', 'POST', { path: change.path, hunkIndex: hi });
942
1036
  toast('已拒绝 Hunk');
1037
+ } else if (act === 'toggle-mode') {
1038
+ _diffModes[change.path] = (_diffModes[change.path] || 'inline') === 'side' ? 'inline' : 'side';
1039
+ } else if (act === 'toggle-collapse') {
1040
+ if (_collapsedPaths.has(change.path)) _collapsedPaths.delete(change.path);
1041
+ else _collapsedPaths.add(change.path);
943
1042
  }
944
1043
  loadChanges();
945
1044
  } catch (e) { toast('操作失败:' + e.message); }
@@ -983,6 +1082,10 @@
983
1082
  const out = (d.output || '').toString();
984
1083
  return '<div class="step result ' + (ok ? 'ok' : 'fail') + '"><div class="res-head">' + (ok ? '✅' : '❌') + ' ' + escapeHtml(d.name || '') + ' ' + (ok ? '成功' : '失败') + '</div>' + (out.trim() ? '<div class="res-out">' + escapeHtml(out).slice(0, 500) + '</div>' : '') + '</div>';
985
1084
  } else if (step.type === 'self-heal') {
1085
+ // 区分「普通反思重试」与「连续失败换路线继续」
1086
+ if (d.strategy === 'bypass-and-continue') {
1087
+ return '<div class="step note">🔄 连续失败已自动换路线:放弃当前方案,正在尝试<b>其他方式继续任务</b>(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
1088
+ }
986
1089
  return '<div class="step note">🩹 进入自愈:检测到 <b>' + escapeHtml(d.category || '') + '</b> 类错误,已注入反思重试(第 ' + (d.iteration != null ? d.iteration : '?') + ' 轮)</div>';
987
1090
  } else if (step.type === 'context.compact') {
988
1091
  return '<div class="step note">📦 上下文压缩:从 ' + (d.originalLength != null ? d.originalLength : '?') + ' 条压缩至 ' + (d.compressedLength != null ? d.compressedLength : '?') + ' 条,保留首条目标与近期对话</div>';
@@ -1082,48 +1185,10 @@
1082
1185
  }
1083
1186
  html += '<div class="msg assistant error-msg">任务遇到问题:' + escapeHtml(task.error || '未知错误') + '</div>';
1084
1187
  } 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
- }
1188
+ // 仅显示思考动画,不显示已等待时间
1123
1189
  html += '<div class="thinking-indicator">'
1124
1190
  + '<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
1125
1191
  + '<span>思考中...</span>'
1126
- + waitTip
1127
1192
  + '</div>';
1128
1193
  } else if (task.status === 'queued') {
1129
1194
  html += '<div class="msg sys">任务已入队,等待执行…</div>';
@@ -1845,11 +1910,57 @@
1845
1910
  document.getElementById('teamTaskCount').textContent = d.stats?.totalTasks || 0;
1846
1911
  renderTeamMembers();
1847
1912
  renderTeamTasks();
1913
+ renderTeamOverview(d);
1848
1914
  } catch (e) {
1849
1915
  toast('团队数据加载失败: ' + (e.message || '网络错误'));
1850
1916
  }
1851
1917
  }
1852
1918
 
1919
+ // P7-4: 多 agent 协作总览——任务状态漏斗 + 成员负载分布可视化
1920
+ function renderTeamOverview(d) {
1921
+ const box = document.getElementById('teamOverview');
1922
+ if (!box) return;
1923
+ const tasks = d?.team?.tasks || [];
1924
+ const members = d?.team?.members || [];
1925
+ if (!tasks.length) { box.style.display = 'none'; return; }
1926
+ box.style.display = 'block';
1927
+ const statusColors = { todo: '#7f8c8d', 'in-progress': '#3498db', review: '#e67e22', done: '#2d5a3d' };
1928
+ const statusLabels = { todo: '待办', 'in-progress': '进行中', review: '评审', done: '完成' };
1929
+ const order = ['todo', 'in-progress', 'review', 'done'];
1930
+ const counts = {};
1931
+ order.forEach((s) => { counts[s] = 0; });
1932
+ tasks.forEach((t) => { counts[t.status] = (counts[t.status] || 0) + 1; });
1933
+ const max = Math.max(1, ...Object.values(counts));
1934
+ const funnelHtml = '<div style="display:flex;flex-direction:column;gap:3px;margin-bottom:8px;">'
1935
+ + order.map((s) => {
1936
+ const n = counts[s] || 0;
1937
+ const pct = Math.round((n / max) * 100);
1938
+ return '<div style="display:flex;align-items:center;gap:6px;font-size:10px;">'
1939
+ + '<span style="width:40px;color:var(--muted);">' + statusLabels[s] + '</span>'
1940
+ + '<div style="flex:1;background:var(--bg);border-radius:3px;height:12px;overflow:hidden;">'
1941
+ + '<div style="width:' + pct + '%;height:100%;background:' + statusColors[s] + ';border-radius:3px;"></div></div>'
1942
+ + '<span style="width:24px;text-align:right;font-weight:600;">' + n + '</span>'
1943
+ + '</div>';
1944
+ }).join('') + '</div>';
1945
+ // 成员负载:按创建人统计任务数(top 5)
1946
+ const byMember = {};
1947
+ tasks.forEach((t) => { byMember[t.createdBy || '未指派'] = (byMember[t.createdBy || '未指派'] || 0) + 1; });
1948
+ const loadRows = Object.entries(byMember).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, n]) => {
1949
+ const pct = Math.round((n / Math.max(1, ...Object.values(byMember))) * 100);
1950
+ return '<div style="display:flex;align-items:center;gap:6px;font-size:10px;">'
1951
+ + '<span style="width:70px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + escapeHtml(name) + '</span>'
1952
+ + '<div style="flex:1;background:var(--bg);border-radius:3px;height:10px;overflow:hidden;">'
1953
+ + '<div style="width:' + pct + '%;height:100%;background:#9b59b6;border-radius:3px;"></div></div>'
1954
+ + '<span style="width:24px;text-align:right;font-weight:600;">' + n + '</span>'
1955
+ + '</div>';
1956
+ }).join('');
1957
+ box.innerHTML = '<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px;">'
1958
+ + '<div style="font-size:11px;font-weight:600;margin-bottom:6px;">📊 协作总览</div>'
1959
+ + '<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">任务状态</div>' + funnelHtml
1960
+ + (loadRows ? '<div style="font-size:10px;color:var(--muted);margin:6px 0 4px;">成员负载(按创建人)</div>' + loadRows : '')
1961
+ + '</div>';
1962
+ }
1963
+
1853
1964
  function renderTeamMembers() {
1854
1965
  const container = document.getElementById('teamMembersList');
1855
1966
  const members = _teamData?.team?.members || [];
@@ -1979,3 +2090,128 @@
1979
2090
  loadTeamData();
1980
2091
  } catch (e) { toast('创建失败: ' + (e.message || '网络错误')); }
1981
2092
  }
2093
+
2094
+ /* ========== P0-3: 能力中心(v7.2 新能力前端接线)========== */
2095
+ let _capabilitiesBound = false;
2096
+ function bindCapabilitiesOnce() {
2097
+ if (_capabilitiesBound) return;
2098
+ _capabilitiesBound = true;
2099
+ document.getElementById('capVoiceBtn')?.addEventListener('click', runCapabilityVoice);
2100
+ document.getElementById('capVoiceInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runCapabilityVoice(); });
2101
+ document.getElementById('capKnowledgeSearchBtn')?.addEventListener('click', runCapabilityKnowledge);
2102
+ document.getElementById('capKnowledgeQuery')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runCapabilityKnowledge(); });
2103
+ document.getElementById('capPluginsBtn')?.addEventListener('click', loadCapabilitiesPlugins);
2104
+ }
2105
+
2106
+ async function runCapabilityVoice() {
2107
+ const input = document.getElementById('capVoiceInput');
2108
+ const out = document.getElementById('capVoiceResult');
2109
+ const text = (input?.value || '').trim();
2110
+ if (!text) { if (out) out.textContent = '请输入语音指令文本'; return; }
2111
+ if (out) out.textContent = '解析中…';
2112
+ try {
2113
+ const d = await apiVoiceParse(text);
2114
+ if (!d.ok) { if (out) out.textContent = '解析失败: ' + (d.error || ''); return; }
2115
+ const c = d.command || {};
2116
+ if (out) out.textContent = '→ ' + (c.type || '?') + (c.params && Object.keys(c.params).length ? ' 参数: ' + JSON.stringify(c.params) : '') + ' 置信度: ' + (c.confidence ?? '?');
2117
+ } catch (e) { if (out) out.textContent = '请求失败: ' + (e.message || '网络错误'); }
2118
+ }
2119
+
2120
+ async function runCapabilityKnowledge() {
2121
+ const input = document.getElementById('capKnowledgeQuery');
2122
+ const out = document.getElementById('capKnowledgeResult');
2123
+ const query = (input?.value || '').trim();
2124
+ if (!query) { if (out) out.innerHTML = '<div class="muted">请输入搜索关键词</div>'; return; }
2125
+ if (out) out.innerHTML = '<div class="muted">搜索中…</div>';
2126
+ try {
2127
+ const d = await apiKnowledgeSearch(query);
2128
+ if (!d.ok) { if (out) out.innerHTML = '<div>搜索失败: ' + (d.error || '') + '</div>'; return; }
2129
+ const list = d.results || [];
2130
+ if (!list.length) { if (out) out.innerHTML = '<div class="muted">未找到匹配资料</div>'; return; }
2131
+ if (out) out.innerHTML = list.map((r) => {
2132
+ const t = r.document?.title || r.title || '未命名';
2133
+ const id = r.document?.id || r.id || '';
2134
+ return '<div style="padding:3px 0;border-bottom:1px solid var(--border,#eee);">📄 ' + t +
2135
+ (id ? ' <span style="color:var(--muted);font-size:10px;">' + id.slice(0, 18) + '</span>' : '') + '</div>';
2136
+ }).join('');
2137
+ } catch (e) { if (out) out.innerHTML = '<div>请求失败: ' + (e.message || '网络错误') + '</div>'; }
2138
+ }
2139
+
2140
+ async function loadCapabilitiesPlugins() {
2141
+ const out = document.getElementById('capPluginsResult');
2142
+ if (!out) return;
2143
+ out.innerHTML = '<div class="muted">加载中…</div>';
2144
+ try {
2145
+ const d = await apiPluginsMarket('', '');
2146
+ if (!d.ok) { out.innerHTML = '<div>加载失败: ' + (d.error || '') + '</div>'; return; }
2147
+ const list = d.plugins || [];
2148
+ if (!list.length) { out.innerHTML = '<div class="muted">插件市场为空</div>'; return; }
2149
+ out.innerHTML = list.slice(0, 15).map((p) =>
2150
+ '<div style="padding:3px 0;border-bottom:1px solid var(--border,#eee);">🧩 ' + (p.name || p.id) +
2151
+ ' <span style="color:var(--muted);font-size:10px;">v' + (p.version || '?') + '</span>' +
2152
+ '<div style="font-size:10px;color:var(--muted);">' + (p.description || '').slice(0, 40) + '</div></div>'
2153
+ ).join('') + (list.length > 15 ? '<div class="muted" style="font-size:10px;margin-top:4px;">…共 ' + list.length + ' 个</div>' : '');
2154
+ } catch (e) { out.innerHTML = '<div>请求失败: ' + (e.message || '网络错误') + '</div>'; }
2155
+ }
2156
+
2157
+ /* ========== P1-1: ghost text 补全(记忆编辑器)========== */
2158
+ let _ghostTimer = null;
2159
+ let _ghostSuggestion = '';
2160
+ let _ghostBound = false;
2161
+
2162
+ function bindGhostTextOnce() {
2163
+ if (_ghostBound) return;
2164
+ _ghostBound = true;
2165
+ const editor = document.getElementById('memLongEditor');
2166
+ const bar = document.getElementById('memGhostBar');
2167
+ if (!editor || !bar) return;
2168
+ editor.addEventListener('input', () => {
2169
+ clearTimeout(_ghostTimer);
2170
+ _ghostTimer = setTimeout(() => requestGhost(editor), 350);
2171
+ });
2172
+ editor.addEventListener('keydown', (e) => {
2173
+ if (e.key === 'Tab' && _ghostSuggestion) {
2174
+ e.preventDefault();
2175
+ acceptGhost(editor);
2176
+ } else if (e.key === 'Escape') {
2177
+ clearGhost();
2178
+ }
2179
+ });
2180
+ editor.addEventListener('blur', () => setTimeout(clearGhost, 250));
2181
+ // 编辑框显示时绑定一次
2182
+ document.getElementById('memLongEditor')?.addEventListener('focus', () => { /* 已绑定 */ });
2183
+ }
2184
+
2185
+ async function requestGhost(editor) {
2186
+ const content = editor.value;
2187
+ const bar = document.getElementById('memGhostBar');
2188
+ if (!content.trim() || content.length < 10) { clearGhost(); return; }
2189
+ try {
2190
+ const d = await apiCompletion('memory-note.md', content, { language: 'markdown', mode: 'quick' });
2191
+ if (d.ok && Array.isArray(d.suggestions) && d.suggestions.length) {
2192
+ const t = d.suggestions[0].text || '';
2193
+ if (t && !content.endsWith(t)) {
2194
+ _ghostSuggestion = t;
2195
+ document.getElementById('memGhostText').textContent = t.slice(0, 200);
2196
+ bar.style.display = 'block';
2197
+ } else { clearGhost(); }
2198
+ } else { clearGhost(); }
2199
+ } catch { clearGhost(); }
2200
+ }
2201
+
2202
+ function acceptGhost(editor) {
2203
+ if (!_ghostSuggestion) return;
2204
+ const cur = editor.selectionStart ?? editor.value.length;
2205
+ const end = editor.selectionEnd ?? cur;
2206
+ editor.value = editor.value.slice(0, cur) + _ghostSuggestion + editor.value.slice(end);
2207
+ const pos = cur + _ghostSuggestion.length;
2208
+ editor.selectionStart = editor.selectionEnd = pos;
2209
+ try { editor.dispatchEvent(new Event('input')); } catch {}
2210
+ clearGhost();
2211
+ }
2212
+
2213
+ function clearGhost() {
2214
+ _ghostSuggestion = '';
2215
+ const bar = document.getElementById('memGhostBar');
2216
+ if (bar) bar.style.display = 'none';
2217
+ }
@@ -3,9 +3,9 @@
3
3
  * 支持离线缓存、PWA 安装、移动端适配
4
4
  */
5
5
 
6
- const CACHE_NAME = 'feihong-code-v1';
7
- const STATIC_CACHE = 'feihong-static-v1';
8
- const RUNTIME_CACHE = 'feihong-runtime-v1';
6
+ const CACHE_NAME = 'feihong-code-v2';
7
+ const STATIC_CACHE = 'feihong-static-v2';
8
+ const RUNTIME_CACHE = 'feihong-runtime-v2';
9
9
 
10
10
  // 静态资源缓存列表
11
11
  const STATIC_ASSETS = [
@@ -60,33 +60,9 @@ self.addEventListener('fetch', (event) => {
60
60
  return;
61
61
  }
62
62
 
63
- // API 请求:网络优先,失败时回退到缓存
63
+ // API 请求:直接透传,不缓存(API 响应是动态的,缓存会导致公钥/认证/模型配置等数据过期)
64
64
  if (url.pathname.startsWith('/api/')) {
65
- event.respondWith(
66
- fetch(request)
67
- .then((response) => {
68
- // 缓存成功的 API 响应
69
- if (response.status === 200) {
70
- const clone = response.clone();
71
- caches.open(RUNTIME_CACHE).then((cache) => cache.put(request, clone));
72
- }
73
- return response;
74
- })
75
- .catch(() => {
76
- return caches.match(request).then((cached) => {
77
- if (cached) return cached;
78
- // 返回离线错误
79
- return new Response(JSON.stringify({
80
- ok: false,
81
- error: '网络连接失败,显示离线数据',
82
- offline: true,
83
- }), {
84
- status: 503,
85
- headers: { 'Content-Type': 'application/json' },
86
- });
87
- });
88
- })
89
- );
65
+ // 安全相关 API 强制不缓存,直接走网络
90
66
  return;
91
67
  }
92
68