feihong-code 0.2.3 → 0.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 (180) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -37
  3. package/.github/ISSUE_TEMPLATE/config.yml +14 -14
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +28 -28
  5. package/.github/PULL_REQUEST_TEMPLATE.md +46 -46
  6. package/.github/SECURITY.md +67 -67
  7. package/.github/dependabot.yml +16 -0
  8. package/.github/workflows/ci.yml +1 -1
  9. package/AGENT-GUIDE.md +382 -237
  10. package/CHANGELOG.md +392 -0
  11. package/CODE_OF_CONDUCT.md +56 -56
  12. package/CONTRIBUTING.md +68 -68
  13. package/LICENSE +22 -22
  14. package/README.md +586 -522
  15. package/README.self-evolve.md +274 -0
  16. package/dist/agent/code-review.js +2 -0
  17. package/dist/agent/code-review.js.map +1 -1
  18. package/dist/agent/code-writer.js +74 -18
  19. package/dist/agent/code-writer.js.map +1 -1
  20. package/dist/agent/context-compactor.js +13 -4
  21. package/dist/agent/context-compactor.js.map +1 -1
  22. package/dist/agent/experience.js +319 -84
  23. package/dist/agent/experience.js.map +1 -1
  24. package/dist/agent/orchestrator.js +233 -71
  25. package/dist/agent/orchestrator.js.map +1 -1
  26. package/dist/agent/planner.js +30 -7
  27. package/dist/agent/planner.js.map +1 -1
  28. package/dist/agent/prompts.js +9 -0
  29. package/dist/agent/prompts.js.map +1 -1
  30. package/dist/agent/quality-gate.js +10 -4
  31. package/dist/agent/quality-gate.js.map +1 -1
  32. package/dist/agent/repo-context.js +181 -0
  33. package/dist/agent/repo-context.js.map +1 -0
  34. package/dist/agent/repo-reader.js +92 -99
  35. package/dist/agent/repo-reader.js.map +1 -1
  36. package/dist/agent/self-heal.js +80 -60
  37. package/dist/agent/self-heal.js.map +1 -1
  38. package/dist/agent/self-improver.js +65 -61
  39. package/dist/agent/self-improver.js.map +1 -1
  40. package/dist/agent/subagent-summary.js +31 -0
  41. package/dist/agent/subagent-summary.js.map +1 -0
  42. package/dist/agent/subagent.js +52 -2
  43. package/dist/agent/subagent.js.map +1 -1
  44. package/dist/agent/symbol-index.js +160 -0
  45. package/dist/agent/symbol-index.js.map +1 -0
  46. package/dist/agent/team.js +193 -0
  47. package/dist/agent/team.js.map +1 -0
  48. package/dist/cli/commands.js +124 -143
  49. package/dist/cli/commands.js.map +1 -1
  50. package/dist/cli/index.js +113 -106
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/cli/repl.js +82 -7
  53. package/dist/cli/repl.js.map +1 -1
  54. package/dist/cli/run.js +600 -95
  55. package/dist/cli/run.js.map +1 -1
  56. package/dist/cli/tui.js +145 -0
  57. package/dist/cli/tui.js.map +1 -0
  58. package/dist/cli/version.js +1 -1
  59. package/dist/enterprise/audit.js +145 -23
  60. package/dist/enterprise/audit.js.map +1 -1
  61. package/dist/enterprise/index.js +14 -11
  62. package/dist/enterprise/index.js.map +1 -1
  63. package/dist/enterprise/policy.js +22 -10
  64. package/dist/enterprise/policy.js.map +1 -1
  65. package/dist/harness/executor.js +127 -0
  66. package/dist/harness/executor.js.map +1 -0
  67. package/dist/harness/harness.js +87 -0
  68. package/dist/harness/harness.js.map +1 -0
  69. package/dist/harness/index.js +31 -0
  70. package/dist/harness/index.js.map +1 -0
  71. package/dist/harness/loader.js +138 -0
  72. package/dist/harness/loader.js.map +1 -0
  73. package/dist/harness/reporter.js +34 -0
  74. package/dist/harness/reporter.js.map +1 -0
  75. package/dist/harness/types.js +10 -0
  76. package/dist/harness/types.js.map +1 -0
  77. package/dist/harness/verifier.js +48 -0
  78. package/dist/harness/verifier.js.map +1 -0
  79. package/dist/hello.js +14 -0
  80. package/dist/hello.js.map +1 -0
  81. package/dist/memory/auto-summarize.js +208 -0
  82. package/dist/memory/auto-summarize.js.map +1 -0
  83. package/dist/memory/index.js +228 -0
  84. package/dist/memory/index.js.map +1 -0
  85. package/dist/models/model-router.js +100 -27
  86. package/dist/models/model-router.js.map +1 -1
  87. package/dist/models/model.dto.js +25 -3
  88. package/dist/models/model.dto.js.map +1 -1
  89. package/dist/models/providers/ollama.provider.js +12 -0
  90. package/dist/models/providers/ollama.provider.js.map +1 -1
  91. package/dist/models/providers/openai-compatible.provider.js +14 -1
  92. package/dist/models/providers/openai-compatible.provider.js.map +1 -1
  93. package/dist/plugins/plugin-loader.js +179 -0
  94. package/dist/plugins/plugin-loader.js.map +1 -0
  95. package/dist/runtime/event-log.js.map +1 -1
  96. package/dist/runtime/hooks.js +80 -0
  97. package/dist/runtime/hooks.js.map +1 -0
  98. package/dist/self-evolve/hook.js +60 -0
  99. package/dist/self-evolve/hook.js.map +1 -0
  100. package/dist/self-evolve/hook.ts +80 -0
  101. package/dist/self-evolve/manager.d.ts +8 -0
  102. package/dist/self-evolve/manager.js +401 -0
  103. package/dist/shared/config.js +35 -3
  104. package/dist/shared/config.js.map +1 -1
  105. package/dist/shared/errors.js +6 -2
  106. package/dist/shared/errors.js.map +1 -1
  107. package/dist/shared/i18n.js +535 -0
  108. package/dist/shared/i18n.js.map +1 -0
  109. package/dist/shared/secure-store.js +117 -0
  110. package/dist/shared/secure-store.js.map +1 -0
  111. package/dist/skills/grill.js +2 -1
  112. package/dist/skills/grill.js.map +1 -1
  113. package/dist/skills/self-heal.js +73 -0
  114. package/dist/skills/self-heal.js.map +1 -0
  115. package/dist/skills/skill-loader.js +131 -0
  116. package/dist/skills/skill-loader.js.map +1 -0
  117. package/dist/skills/skill-market.js +195 -0
  118. package/dist/skills/skill-market.js.map +1 -0
  119. package/dist/tools/analysis/code-analyzer.js +45 -18
  120. package/dist/tools/analysis/code-analyzer.js.map +1 -1
  121. package/dist/tools/index.js +5 -0
  122. package/dist/tools/index.js.map +1 -1
  123. package/dist/tools/mcp/index.js +84 -0
  124. package/dist/tools/mcp/index.js.map +1 -0
  125. package/dist/tools/mcp/mcp-client.js +194 -0
  126. package/dist/tools/mcp/mcp-client.js.map +1 -0
  127. package/dist/tools/sandbox.js +126 -0
  128. package/dist/tools/sandbox.js.map +1 -0
  129. package/dist/tools/shell/exec.js +72 -3
  130. package/dist/tools/shell/exec.js.map +1 -1
  131. package/dist/tools/shell/run-shell.tool.js +37 -7
  132. package/dist/tools/shell/run-shell.tool.js.map +1 -1
  133. package/dist/tools/skills/load-skill.tool.js +36 -0
  134. package/dist/tools/skills/load-skill.tool.js.map +1 -0
  135. package/dist/tools/tool.interface.js.map +1 -1
  136. package/dist/tools/tool.registry.js +69 -1
  137. package/dist/tools/tool.registry.js.map +1 -1
  138. package/dist/tools/web/web.tool.js +139 -0
  139. package/dist/tools/web/web.tool.js.map +1 -0
  140. package/dist/web/auth.js +178 -3
  141. package/dist/web/auth.js.map +1 -1
  142. package/dist/web/channels.js +171 -0
  143. package/dist/web/channels.js.map +1 -0
  144. package/dist/web/public/css/style.css +1546 -0
  145. package/dist/web/public/index.html +804 -37
  146. package/dist/web/public/js/api.js +309 -0
  147. package/dist/web/public/js/app.js +1472 -0
  148. package/dist/web/public/js/ui.js +832 -0
  149. package/dist/web/public/js/utils.js +170 -0
  150. package/dist/web/server.js +937 -11
  151. package/dist/web/server.js.map +1 -1
  152. package/dist/web/task-queue.js +470 -0
  153. package/dist/web/task-queue.js.map +1 -0
  154. package/dist/web/web-config.js +143 -0
  155. package/dist/web/web-config.js.map +1 -0
  156. package/docs/App/344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +299 -0
  157. package/docs/App/346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +554 -0
  158. package/docs/Deployment_Guide_EN.md +288 -0
  159. package/docs/SELF-EVOLVE-GUIDE.md +313 -0
  160. package/docs/Technical_Manual_EN.md +216 -0
  161. package/docs/User_Manual_EN.md +314 -0
  162. package/docs/error-codes.md +198 -0
  163. package/docs/screenshots/cli-demo.png +0 -0
  164. package/docs/screenshots/feature-comparison.png +0 -0
  165. package/docs/screenshots/web-console.png +0 -0
  166. package/docs/self-evolve-implementation.md +165 -0
  167. package/docs/self-evolve.md +166 -0
  168. package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +614 -614
  169. package/docs//344/274/201/344/270/232/351/203/250/347/275/262/344/270/216/345/220/210/350/247/204.md +267 -267
  170. package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +318 -358
  171. package/docs//345/270/270/350/247/201/351/227/256/351/242/230/344/270/216/346/225/205/351/232/234/346/216/222/346/237/245.md +130 -130
  172. package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +216 -303
  173. package/docs//346/236/266/346/236/204/344/270/216API.md +238 -238
  174. package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +196 -196
  175. package/docs//351/203/250/347/275/262/346/214/207/345/215/227.md +165 -165
  176. package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +288 -0
  177. package/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +127 -127
  178. package/docs//351/241/265/351/235/242/345/212/237/350/203/275/345/244/215/347/233/230/344/270/216/345/206/222/347/203/237/346/265/213/350/257/225/346/212/245/345/221/212.html +117 -0
  179. package/package.json +108 -109
  180. package/tool-schema.json +117 -46
package/dist/cli/run.js CHANGED
@@ -1,11 +1,48 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.getEnterprise = getEnterprise;
37
+ exports.streamRenderer = streamRenderer;
38
+ exports.executeTask = executeTask;
4
39
  exports.runGoal = runGoal;
5
40
  exports.isOfflineByDefault = isOfflineByDefault;
6
41
  exports.runPlanSkill = runPlanSkill;
7
42
  exports.runGrillSkill = runGrillSkill;
43
+ exports.runReviewCmd = runReviewCmd;
8
44
  exports.runGoalSkill = runGoalSkill;
45
+ exports.runSelfHealSkill = runSelfHealSkill;
9
46
  exports.runParallelGoal = runParallelGoal;
10
47
  exports.runSessions = runSessions;
11
48
  exports.runResume = runResume;
@@ -16,6 +53,10 @@ exports.runPolicyCmd = runPolicyCmd;
16
53
  exports.runAudit = runAudit;
17
54
  exports.runAuditVerify = runAuditVerify;
18
55
  exports.runTenants = runTenants;
56
+ exports.runDoctor = runDoctor;
57
+ exports.runPluginCmd = runPluginCmd;
58
+ exports.runTeamCmd = runTeamCmd;
59
+ exports.runSkillMarketCmd = runSkillMarketCmd;
19
60
  exports.runModelStats = runModelStats;
20
61
  exports.runExperiences = runExperiences;
21
62
  exports.runServe = runServe;
@@ -23,6 +64,7 @@ exports.runCodeWrite = runCodeWrite;
23
64
  exports.runQualityGate = runQualityGate;
24
65
  exports.runSelfImprove = runSelfImprove;
25
66
  exports.runSwe = runSwe;
67
+ exports.runHarness = runHarness;
26
68
  exports.defaultApproverFor = defaultApproverFor;
27
69
  exports.interactiveApprover = interactiveApprover;
28
70
  /**
@@ -40,11 +82,15 @@ const os_1 = require("os");
40
82
  const path_1 = require("path");
41
83
  const readline_1 = require("readline");
42
84
  const logger_1 = require("../shared/logger");
85
+ const i18n_1 = require("../shared/i18n");
43
86
  const config_1 = require("../shared/config");
44
87
  const errors_1 = require("../shared/errors");
45
88
  const model_router_1 = require("../models/model-router");
46
89
  const mock_provider_1 = require("../models/providers/mock.provider");
90
+ const openai_compatible_provider_1 = require("../models/providers/openai-compatible.provider");
91
+ const ollama_provider_1 = require("../models/providers/ollama.provider");
47
92
  const tools_1 = require("../tools");
93
+ const mcp_1 = require("../tools/mcp");
48
94
  const exec_1 = require("../tools/shell/exec");
49
95
  const event_log_1 = require("../runtime/event-log");
50
96
  const session_store_1 = require("../runtime/session-store");
@@ -52,16 +98,28 @@ const session_persist_1 = require("../runtime/session-persist");
52
98
  const git_1 = require("../runtime/git");
53
99
  const enterprise_1 = require("../enterprise");
54
100
  const server_1 = require("../web/server");
101
+ const auto_summarize_1 = require("../memory/auto-summarize");
102
+ const plugin_loader_1 = require("../plugins/plugin-loader");
103
+ const team_1 = require("../agent/team");
104
+ const skill_market_1 = require("../skills/skill-market");
105
+ const skill_loader_1 = require("../skills/skill-loader");
55
106
  const orchestrator_1 = require("../agent/orchestrator");
56
107
  const parallel_orchestrator_1 = require("../agent/parallel-orchestrator");
57
108
  const plan_1 = require("../skills/plan");
58
109
  const grill_1 = require("../skills/grill");
59
110
  const goal_1 = require("../skills/goal");
111
+ const self_heal_1 = require("../skills/self-heal");
60
112
  const experience_1 = require("../agent/experience");
61
113
  const code_writer_1 = require("../agent/code-writer");
62
114
  const quality_gate_1 = require("../agent/quality-gate");
63
115
  const self_improver_1 = require("../agent/self-improver");
64
116
  const swe_agent_1 = require("../agent/swe-agent");
117
+ const subagent_summary_1 = require("../agent/subagent-summary");
118
+ const harness_1 = require("../harness/harness");
119
+ const loader_1 = require("../harness/loader");
120
+ const executor_1 = require("../harness/executor");
121
+ const verifier_1 = require("../harness/verifier");
122
+ const reporter_1 = require("../harness/reporter");
65
123
  /** 离线演示脚本:写文件 → 总结,跑通完整链路 */
66
124
  function buildDemoSteps() {
67
125
  return [
@@ -114,22 +172,72 @@ function getSessionHome(offline) {
114
172
  const home = process.env.FH_HOME ? expandHome(process.env.FH_HOME) : joinHome();
115
173
  return (0, path_1.join)(home, 'sessions');
116
174
  }
117
- async function runGoal(goal, opts = {}) {
175
+ /** P0-1:把编排器事件流增量渲染到 stdout(流式输出)。抑制换行噪音:仅打印关键节点。 */
176
+ function renderStreamEvent(ev) {
177
+ switch (ev.type) {
178
+ case 'model.response':
179
+ // 有文本内容时直接流式打印(模拟 token 流);仅工具调用时打印调用摘要
180
+ if (ev.content.trim())
181
+ console.log(`🧠 ${ev.content.trim().slice(0, 300)}`);
182
+ else if (ev.toolCalls.length > 0)
183
+ console.log(`🔧 ${(0, i18n_1.t)('stream.toolCalling', { tools: ev.toolCalls.join(', ') })}`);
184
+ break;
185
+ case 'tool.call':
186
+ break; // 已在上层 model.response 摘要,避免重复
187
+ case 'tool.result':
188
+ console.log(ev.ok ? ` ✅ ${ev.name} ${(0, i18n_1.t)('stream.toolOk')}` : ` ❌ ${ev.name} ${(0, i18n_1.t)('stream.toolFail')} — ${ev.output.slice(0, 120)}`);
189
+ break;
190
+ case 'self-heal':
191
+ console.log(`🩹 ${(0, i18n_1.t)('stream.selfHeal', { category: ev.category })}`);
192
+ break;
193
+ case 'context.compact':
194
+ console.log(`📦 ${(0, i18n_1.t)('stream.compact', { from: ev.originalLength, to: ev.compressedLength })}`);
195
+ break;
196
+ case 'session.end':
197
+ console.log(`🏁 ${(0, i18n_1.t)('stream.done', { iter: ev.iterations, cost: '$' + ev.costUsd.toFixed(6) })}`);
198
+ break;
199
+ }
200
+ }
201
+ /** 流式输出事件渲染器(供 runGoal / runSwe 复用) */
202
+ function streamRenderer() {
203
+ return renderStreamEvent;
204
+ }
205
+ /**
206
+ * P4-1 服务端可复用执行函数:装配编排器并执行目标,返回结构化结果。
207
+ * 不打印任何 console 输出(供 Web 任务队列等非 CLI 场景调用)。
208
+ * runGoal 在其上叠加展示层。
209
+ */
210
+ async function executeTask(goal, opts = {}) {
118
211
  const runId = (0, crypto_1.randomUUID)();
119
212
  (0, logger_1.setRunId)(runId);
120
213
  const security = { shellAllowlist: [], requireApproval: true };
121
214
  const offline = opts.offline ?? isOfflineByDefault();
215
+ // Web 控制台可传入安全配置覆盖默认值
216
+ if (opts.security) {
217
+ security.shellAllowlist = opts.security.shellAllowlist ?? security.shellAllowlist;
218
+ security.requireApproval = opts.security.requireApproval ?? security.requireApproval;
219
+ security.sandboxMode = opts.security.sandboxMode ?? security.sandboxMode;
220
+ }
122
221
  // M4:企业上下文(租户隔离 / RBAC / 审计 / 配额),配额超限在此 fail-fast
123
222
  const rt = getEnterprise();
124
223
  if (rt)
125
224
  (0, enterprise_1.assertQuota)(rt);
126
225
  // 离线模式用临时工作区,避免污染用户目录;并 git init 以支持 diff/rollback 演示
127
- const cwd = offline ? (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'fhcode-demo-')) : process.cwd();
226
+ const targetCwd = opts.workspaceDir || process.cwd();
227
+ const cwd = offline ? (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'fhcode-demo-')) : targetCwd;
128
228
  if (offline) {
129
229
  await (0, exec_1.runCommand)('git init -q', cwd).catch(() => undefined);
130
230
  }
231
+ // 注意:不再使用 process.chdir(targetCwd) —— 所有工具通过 ctx.cwd 显式传参,
232
+ // 全局切换 cwd 会导致并发任务互相覆盖工作目录。移除后可安全提高并发数。
131
233
  let router;
132
- if (offline) {
234
+ let pluginSkillDirs = [];
235
+ // Web 控制台直接注入模型配置(绕过 loadConfig 缓存)
236
+ if (opts.modelProviders && opts.modelProviders.length > 0) {
237
+ const providers = opts.modelProviders.map((p) => p.type === 'ollama' ? new ollama_provider_1.OllamaProvider({ id: p.id, type: 'ollama', baseURL: p.baseURL, model: p.model || 'default', tags: ['code-gen', 'reasoning'] }) : new openai_compatible_provider_1.OpenAICompatibleProvider({ id: p.id, type: 'openai-compatible', baseURL: p.baseURL, model: p.model || 'gpt-4o', apiKey: p.apiKey, tags: ['code-gen', 'reasoning'] }));
238
+ router = new model_router_1.ModelRouter(providers, 'cost', 0);
239
+ }
240
+ else if (offline) {
133
241
  router = new model_router_1.ModelRouter([new mock_provider_1.ScriptedMockProvider(buildDemoSteps())], 'cost', 0);
134
242
  }
135
243
  else {
@@ -137,11 +245,35 @@ async function runGoal(goal, opts = {}) {
137
245
  router = model_router_1.ModelRouter.fromConfig(cfg);
138
246
  security.shellAllowlist = cfg.security.shellAllowlist;
139
247
  security.requireApproval = cfg.security.requireApproval;
248
+ security.sandboxMode = cfg.security.sandboxMode;
249
+ security.networkRules = { networkAllow: cfg.security.networkAllow, networkDeny: cfg.security.networkDeny };
250
+ security.hooks = cfg.hooks;
251
+ pluginSkillDirs = cfg.plugins.skillDirs;
140
252
  }
141
253
  const tools = (0, tools_1.createDefaultRegistry)();
254
+ // P0-3:附加 MCP 外部工具(真实模式且有配置时)
255
+ let mcpClients = [];
256
+ if (!offline) {
257
+ const cfg = (0, config_1.loadConfig)();
258
+ mcpClients = await (0, mcp_1.attachMcpTools)(tools, cfg.mcp.servers);
259
+ }
142
260
  const logDir = getSessionHome(offline);
143
261
  const eventLog = new event_log_1.EventLog(runId, logDir);
144
262
  const session = new session_store_1.SessionStore(runId, cwd);
263
+ // M3 多轮续接:把上一轮历史消息补入会话,使 session.snapshot() 返回完整对话(供下一轮 resume 持久化)
264
+ if (opts.resume && opts.resume.messages.length > 0) {
265
+ for (const m of opts.resume.messages)
266
+ session.append(m);
267
+ }
268
+ // 用户附件上下文:把本轮附件路径以系统提示形式注入到本次目标的前缀,
269
+ // 让模型知道有哪些文件可读取;具体处理由 LLM 决定(如读取图片/读文件等)。
270
+ let effectiveGoal = goal;
271
+ if (opts.attachments && opts.attachments.length > 0) {
272
+ const list = opts.attachments.map((p, i) => ` ${i + 1}. ${p}`).join('\n');
273
+ effectiveGoal =
274
+ `[用户附件] 本轮上传了 ${opts.attachments.length} 个文件,请按需读取:\n${list}\n\n` +
275
+ goal;
276
+ }
145
277
  const approve = opts.approve ??
146
278
  (process.stdin.isTTY ? interactiveApprover() : defaultApproverFor(security));
147
279
  const guard = rt
@@ -158,9 +290,11 @@ async function runGoal(goal, opts = {}) {
158
290
  guard,
159
291
  maxCostUsd: rt?.maxCostUsd ?? 0,
160
292
  persist: (cp) => (0, session_persist_1.saveCheckpoint)(logDir, cp),
293
+ onEvent: opts.renderer ?? (opts.stream ? streamRenderer() : undefined),
294
+ pluginSkillDirs,
295
+ signal: opts.signal,
161
296
  });
162
297
  if (rt) {
163
- console.log(`[飞虹 Code] 身份 tenant=${rt.tenant.tenantId} user=${rt.tenant.userId} role=${rt.tenant.role}`);
164
298
  rt.audit.record({
165
299
  tenantId: rt.tenant.tenantId,
166
300
  userId: rt.tenant.userId,
@@ -172,31 +306,44 @@ async function runGoal(goal, opts = {}) {
172
306
  reason: offline ? '离线模式' : '真实模式',
173
307
  });
174
308
  }
175
- console.log(`[飞虹 Code] 开始任务 (runId=${runId.slice(0, 8)}${offline ? ', 离线模式' : ''})`);
176
- const result = await orchestrator.run(goal);
177
- if (rt) {
178
- rt.audit.record({
179
- tenantId: rt.tenant.tenantId,
180
- userId: rt.tenant.userId,
181
- role: rt.tenant.role,
182
- runId,
183
- action: 'session:end',
184
- resource: `iterations=${result.iterations}`,
185
- decision: 'info',
186
- reason: `cost=$${result.costUsd.toFixed(6)}`,
187
- });
309
+ try {
310
+ const result = await orchestrator.run(effectiveGoal, opts.resume);
311
+ if (rt) {
312
+ rt.audit.record({
313
+ tenantId: rt.tenant.tenantId,
314
+ userId: rt.tenant.userId,
315
+ role: rt.tenant.role,
316
+ runId,
317
+ action: 'session:end',
318
+ resource: `iterations=${result.iterations}`,
319
+ decision: 'info',
320
+ reason: `cost=$${result.costUsd.toFixed(6)}`,
321
+ });
322
+ }
323
+ // M3 多轮续接:把本轮完整消息历史带回调用方(任务队列持久化后供下一轮 resume)
324
+ const history = session.snapshot().messages;
325
+ return { ...result, messages: history };
326
+ }
327
+ finally {
328
+ // P0-3:任务结束关闭 MCP 子进程
329
+ await (0, mcp_1.closeMcpClients)(mcpClients);
188
330
  }
189
- console.log('\n===== 执行结果 =====');
331
+ }
332
+ async function runGoal(goal, opts = {}) {
333
+ const result = await executeTask(goal, opts);
334
+ const offline = opts.offline ?? isOfflineByDefault();
335
+ const logDir = getSessionHome(offline);
336
+ console.log('\n' + (0, i18n_1.t)('run.resultTitle'));
190
337
  console.log(result.finalAnswer);
191
- console.log(`\n迭代 ${result.iterations} · 成本 $${result.costUsd.toFixed(6)} · 日志 ${result.logFile}`);
192
- console.log(`会话检查点: ${(0, path_1.join)(logDir, `${runId}.session.json`)}(可用 fhcode sessions / resume / diff 管理)`);
338
+ console.log('\n' + (0, i18n_1.t)('run.summary', { iter: result.iterations, cost: '$' + result.costUsd.toFixed(6), log: result.logFile }));
339
+ console.log((0, i18n_1.t)('run.checkpoint', { path: (0, path_1.join)(logDir, `${result.runId}.session.json`) }));
193
340
  if (offline) {
194
341
  // 演示文件可能因策略拒绝而未生成,据实播报,避免误导
195
- const demoFile = (0, path_1.join)(cwd, 'demo-output.txt');
196
- logger_1.logger.info('offline-run done', { cwd, demoFile });
342
+ const demoFile = (0, path_1.join)(process.cwd(), 'demo-output.txt');
343
+ logger_1.logger.info('offline-run done', { cwd: process.cwd(), demoFile });
197
344
  console.log((0, fs_1.existsSync)(demoFile)
198
- ? `(离线模式演示文件已写入: ${demoFile})`
199
- : `(离线模式未产生演示文件,工作区: ${cwd};如为策略拒绝可用 fhcode audit 查看原因)`);
345
+ ? (0, i18n_1.t)('run.offlineFileYes', { file: demoFile })
346
+ : (0, i18n_1.t)('run.offlineFileNo', { cwd: process.cwd() }));
200
347
  }
201
348
  }
202
349
  /** 默认是否离线:未配置 FH_PROVIDERS 或为空数组时离线 */
@@ -243,6 +390,24 @@ function runGrillSkill(target) {
243
390
  ];
244
391
  return lines.filter(Boolean).join('\n');
245
392
  }
393
+ /**
394
+ * M1.1a `fhcode review [路径] [--json]`:红队式代码审查的结构化版本。
395
+ * - 文本模式:与 /grill 输出一致(人类可读)
396
+ * - --json 模式:输出完整结构(scanned/findings/summary),供 IDE 内联评审等消费
397
+ */
398
+ function runReviewCmd(path, asJson) {
399
+ const result = (0, grill_1.runGrill)(process.cwd(), path || '.');
400
+ if (asJson) {
401
+ console.log(JSON.stringify(result, null, 2));
402
+ return;
403
+ }
404
+ console.log(`【review】${result.summary}`);
405
+ for (const f of result.findings) {
406
+ console.log(` [${f.severity.toUpperCase()}] ${f.file}:${f.line} (${f.rule}) ${f.detail}`);
407
+ }
408
+ if (result.findings.length === 0)
409
+ console.log(' 未发现明显问题。');
410
+ }
246
411
  /** /goal 技能:分解并保存高层目标(M4 起写入租户隔离目录 tenants/<id>/goals) */
247
412
  function runGoalSkill(title) {
248
413
  const goal = (0, goal_1.decomposeGoalToGoal)(title);
@@ -268,10 +433,36 @@ function runGoalSkill(title) {
268
433
  }
269
434
  return `【/goal】已保存\n${(0, goal_1.renderGoal)(goal)}\n文件: ${file}`;
270
435
  }
436
+ /** /self-heal 技能:系统化自我修复错误(分类 → 根因 → 修复建议 → 验证步骤) */
437
+ function runSelfHealSkill(errorText) {
438
+ const out = (0, self_heal_1.runSelfHeal)(errorText);
439
+ if (getEnterprise()) {
440
+ // 审计留痕(脱敏由 audit.ts 处理),便于追溯自愈诊断过程
441
+ try {
442
+ const rt = getEnterprise();
443
+ if (rt) {
444
+ rt.audit.record({
445
+ tenantId: rt.tenant.tenantId,
446
+ userId: rt.tenant.userId,
447
+ role: rt.tenant.role,
448
+ runId: (0, crypto_1.randomUUID)(),
449
+ action: 'skill:self-heal',
450
+ resource: out.category,
451
+ decision: out.known ? 'info' : 'deny',
452
+ reason: out.known ? '自动分类完成' : '未能自动分类,需人工介入',
453
+ });
454
+ }
455
+ }
456
+ catch {
457
+ /* 审计失败不阻断技能输出 */
458
+ }
459
+ }
460
+ return out.text;
461
+ }
271
462
  /** --parallel 并行多子代理执行(离线用 Mock;真实模式接入 FH_PROVIDERS 路由) */
272
463
  async function runParallelGoal(goal) {
273
464
  const offline = isOfflineByDefault();
274
- console.log(`[飞虹 Code] 并行模式 (offline=${offline})`);
465
+ console.log((0, i18n_1.t)('run.parallelMode', { offline: offline ? (0, i18n_1.t)('run.modeOffline') : (0, i18n_1.t)('run.modeLive') }));
275
466
  if (!offline) {
276
467
  const cfg = (0, config_1.loadConfig)();
277
468
  const router = model_router_1.ModelRouter.fromConfig(cfg);
@@ -284,18 +475,18 @@ async function runParallelGoal(goal) {
284
475
  router,
285
476
  approve: defaultApproverFor(security),
286
477
  });
287
- console.log('\n===== 并行执行结果 =====');
478
+ console.log('\n' + (0, i18n_1.t)('run.parallelResult'));
288
479
  console.log(result.summary);
289
- console.log(`仓库根: ${result.repoRoot} · 工作树已清理: ${result.worktrees.length}`);
480
+ console.log((0, i18n_1.t)('run.parallelRepo', { root: result.repoRoot, trees: result.worktrees.length }));
290
481
  return;
291
482
  }
292
483
  const result = await (0, parallel_orchestrator_1.runParallel)(goal, {
293
484
  offline: true,
294
485
  mockFor: (task) => (0, parallel_orchestrator_1.defaultParallelMock)(task),
295
486
  });
296
- console.log('\n===== 并行执行结果 =====');
487
+ console.log('\n' + (0, i18n_1.t)('run.parallelResult'));
297
488
  console.log(result.summary);
298
- console.log(`仓库根: ${result.repoRoot} · 工作树已清理: ${result.worktrees.length}`);
489
+ console.log((0, i18n_1.t)('run.parallelRepo', { root: result.repoRoot, trees: result.worktrees.length }));
299
490
  }
300
491
  /* ===================== M3:会话管理(resume / diff / rollback) ===================== */
301
492
  /** 按完整 id 或前缀解析会话检查点(sessions 列表默认展示 8 位前缀,便于直接引用) */
@@ -318,13 +509,20 @@ async function runSessions() {
318
509
  const home = getSessionHome(offline);
319
510
  const cps = await (0, session_persist_1.listCheckpoints)(home);
320
511
  if (cps.length === 0) {
321
- console.log('(无历史会话)');
512
+ console.log((0, i18n_1.t)('run.noSessions'));
322
513
  return;
323
514
  }
324
- console.log(`历史会话(${offline ? '离线' : '真实'}模式,目录 ${home}):`);
515
+ console.log((0, i18n_1.t)('run.sessionList', { mode: offline ? (0, i18n_1.t)('mode.offline') : (0, i18n_1.t)('mode.live'), home }));
325
516
  for (const cp of cps) {
326
- console.log(`- ${cp.runId.slice(0, 8)} | ${cp.status} | 迭代${cp.iterations} | $${cp.costUsd.toFixed(6)} | 文件${cp.touchedFiles.length} | ${cp.updatedAt}`);
327
- console.log(` 目标: ${cp.goal}`);
517
+ console.log((0, i18n_1.t)('run.sessionItem', {
518
+ id: cp.runId.slice(0, 8),
519
+ status: cp.status,
520
+ iter: cp.iterations,
521
+ cost: '$' + cp.costUsd.toFixed(6),
522
+ files: cp.touchedFiles.length,
523
+ time: cp.updatedAt,
524
+ }));
525
+ console.log((0, i18n_1.t)('run.sessionGoal', { goal: cp.goal }));
328
526
  }
329
527
  }
330
528
  /** 从检查点恢复中断的会话并续跑 */
@@ -333,10 +531,10 @@ async function runResume(runId) {
333
531
  const home = getSessionHome(offline);
334
532
  const cp = await resolveCheckpoint(home, runId);
335
533
  if (cp.status === 'done') {
336
- console.log(`会话 ${runId.slice(0, 8)} 已完成,无需恢复。`);
534
+ console.log((0, i18n_1.t)('run.resumeDone', { id: runId.slice(0, 8) }));
337
535
  return;
338
536
  }
339
- console.log(`[飞虹 Code] 恢复会话 ${runId.slice(0, 8)} (状态: ${cp.status}, 已迭代 ${cp.iterations})`);
537
+ console.log((0, i18n_1.t)('run.resumeStart', { id: runId.slice(0, 8), status: cp.status, iter: cp.iterations }));
340
538
  const security = { shellAllowlist: [], requireApproval: true };
341
539
  let router;
342
540
  if (offline) {
@@ -382,9 +580,9 @@ async function runResume(runId) {
382
580
  costUsd: cp.costUsd,
383
581
  touchedFiles: cp.touchedFiles,
384
582
  });
385
- console.log('\n===== 恢复执行结果 =====');
583
+ console.log('\n' + (0, i18n_1.t)('run.resultTitle'));
386
584
  console.log(result.finalAnswer);
387
- console.log(`迭代 ${result.iterations} · 成本 $${result.costUsd.toFixed(6)} · 日志 ${result.logFile}`);
585
+ console.log((0, i18n_1.t)('run.summary', { iter: result.iterations, cost: '$' + result.costUsd.toFixed(6), log: result.logFile }));
388
586
  }
389
587
  /** 展示会话作用域的 diff(缺省为本工作区全量 diff) */
390
588
  async function runDiff(id) {
@@ -392,11 +590,11 @@ async function runDiff(id) {
392
590
  const home = getSessionHome(offline);
393
591
  if (id) {
394
592
  const cp = await resolveCheckpoint(home, id);
395
- console.log(`[飞虹 Code] 会话 ${id.slice(0, 8)} 的变更 (cwd=${cp.cwd}):`);
593
+ console.log((0, i18n_1.t)('run.diffSession', { id: id.slice(0, 8), cwd: cp.cwd }));
396
594
  console.log(await (0, git_1.gitDiff)(cp.cwd, cp.touchedFiles));
397
595
  }
398
596
  else {
399
- console.log(`[飞虹 Code] 当前目录 (${process.cwd()}) 工作区变更:`);
597
+ console.log((0, i18n_1.t)('run.diffCwd', { cwd: process.cwd() }));
400
598
  console.log(await (0, git_1.gitDiff)(process.cwd()));
401
599
  }
402
600
  }
@@ -423,14 +621,14 @@ async function runRollback(id, yes) {
423
621
  throw new errors_1.AppError('角色 viewer 无权执行回滚操作', 'RBAC_DENIED', 403);
424
622
  }
425
623
  }
426
- console.log(`[飞虹 Code] 回滚会话 ${id.slice(0, 8)} ${cp.touchedFiles.length} 个文件 (cwd=${cp.cwd})`);
624
+ console.log((0, i18n_1.t)('run.rollbackStart', { id: id.slice(0, 8), n: cp.touchedFiles.length, cwd: cp.cwd }));
427
625
  const res = await (0, git_1.gitRollback)(cp.cwd, cp.touchedFiles, { yes });
428
626
  if (res.reverted.length)
429
- console.log(`已还原(已跟踪): ${res.reverted.join(', ')}`);
627
+ console.log((0, i18n_1.t)('run.rollbackReverted', { files: res.reverted.join(', ') }));
430
628
  if (res.removed.length)
431
- console.log(`已删除(未跟踪): ${res.removed.join(', ')}`);
629
+ console.log((0, i18n_1.t)('run.rollbackRemoved', { files: res.removed.join(', ') }));
432
630
  if (res.errors.length)
433
- console.log(`注意: ${res.errors.join('; ')}`);
631
+ console.log((0, i18n_1.t)('run.rollbackNote', { errors: res.errors.join('; ') }));
434
632
  if (yes)
435
633
  await (0, session_persist_1.updateStatus)(home, id, 'done');
436
634
  }
@@ -438,7 +636,7 @@ async function runRollback(id, yes) {
438
636
  function requireEnterprise() {
439
637
  const rt = getEnterprise();
440
638
  if (!rt) {
441
- throw new errors_1.AppError('企业模式已关闭(FH_ENTERPRISE=false),该命令不可用。', 'ENTERPRISE_DISABLED', 400);
639
+ throw new errors_1.AppError((0, i18n_1.t)('err.enterpriseDisabled'), 'ENTERPRISE_DISABLED', 400);
442
640
  }
443
641
  return rt;
444
642
  }
@@ -456,29 +654,36 @@ function runAudit(limit = 20) {
456
654
  const rt = requireEnterprise();
457
655
  const all = (0, enterprise_1.readAudit)(rt.tenant.auditDir);
458
656
  if (all.length === 0) {
459
- console.log(`(租户 ${rt.tenant.tenantId} 暂无审计记录,目录 ${rt.tenant.auditDir})`);
657
+ console.log((0, i18n_1.t)('audit.empty', { tenant: rt.tenant.tenantId, dir: rt.tenant.auditDir }));
460
658
  return;
461
659
  }
462
660
  const rows = all.slice(-limit);
463
- console.log(`审计记录 ${rows.length}/${all.length} 条(租户 ${rt.tenant.tenantId}):`);
661
+ console.log((0, i18n_1.t)('audit.header', { rows: rows.length, all: all.length, tenant: rt.tenant.tenantId }));
464
662
  for (const r of rows) {
465
- console.log(`#${String(r.seq).padStart(4, '0')} ${r.ts} [${r.decision.toUpperCase()}] ${r.action}` +
466
- ` by ${r.userId}(${r.role}) run=${String(r.runId).slice(0, 8)}`);
467
- console.log(` 资源: ${r.resource}`);
663
+ console.log((0, i18n_1.t)('audit.row', {
664
+ seq: String(r.seq).padStart(4, '0'),
665
+ ts: r.ts,
666
+ decision: r.decision.toUpperCase(),
667
+ action: r.action,
668
+ user: r.userId,
669
+ role: r.role,
670
+ run: String(r.runId).slice(0, 8),
671
+ }));
672
+ console.log((0, i18n_1.t)('audit.resource', { resource: r.resource }));
468
673
  if (r.reason)
469
- console.log(` 理由: ${r.reason}`);
674
+ console.log((0, i18n_1.t)('audit.reason', { reason: r.reason }));
470
675
  }
471
- console.log(`链尾哈希: ${all[all.length - 1].hash.slice(0, 16)}…`);
676
+ console.log((0, i18n_1.t)('audit.chainTail', { hash: all[all.length - 1].hash.slice(0, 16) }));
472
677
  }
473
678
  /** fhcode audit verify:校验哈希链完整性 */
474
679
  function runAuditVerify() {
475
680
  const rt = requireEnterprise();
476
681
  const res = (0, enterprise_1.verifyAudit)(rt.tenant.auditDir);
477
682
  if (res.ok) {
478
- console.log(`✅ 审计链完整:${res.total} 条记录,哈希链自洽未被篡改。`);
683
+ console.log((0, i18n_1.t)('audit.verifyOk', { total: res.total }));
479
684
  return;
480
685
  }
481
- console.log(`❌ 审计链校验失败:共 ${res.total} 条,断点在第 ${res.brokenAt} 条`);
686
+ console.log((0, i18n_1.t)('audit.verifyFail', { total: res.total, brokenAt: res.brokenAt ?? 0 }));
482
687
  console.log(` ${res.detail}`);
483
688
  process.exitCode = 2;
484
689
  }
@@ -487,33 +692,263 @@ function runTenants() {
487
692
  requireEnterprise();
488
693
  const list = (0, enterprise_1.listTenants)();
489
694
  if (list.length === 0) {
490
- console.log('(暂无租户数据,执行一次任务后自动创建)');
695
+ console.log((0, i18n_1.t)('tenants.empty'));
491
696
  return;
492
697
  }
493
- console.log('租户用量汇总:');
494
- console.log(' 租户ID 会话数 累计成本 审计条数 最近活跃');
495
- for (const t of list) {
496
- console.log(` ${t.tenantId.padEnd(20)} ${String(t.sessions).padStart(5)} $${t.costUsd.toFixed(6).padStart(10)} ${String(t.auditRecords).padStart(7)} ${t.lastActiveAt}`);
698
+ console.log((0, i18n_1.t)('tenants.header'));
699
+ console.log((0, i18n_1.t)('tenants.tableHeader'));
700
+ for (const tenant of list) {
701
+ console.log((0, i18n_1.t)('tenants.row', {
702
+ id: tenant.tenantId.padEnd(20),
703
+ sessions: String(tenant.sessions).padStart(5),
704
+ cost: '$' + tenant.costUsd.toFixed(6).padStart(10),
705
+ audit: String(tenant.auditRecords).padStart(7),
706
+ last: tenant.lastActiveAt,
707
+ }));
497
708
  }
498
709
  }
499
710
  /* ===================== M6:自我进化 ===================== */
711
+ /** 探测 baseURL 连通性:任何 HTTP 响应(含 4xx/5xx)都视为可达,连接失败视为不可达 */
712
+ async function probeUrl(base) {
713
+ const controller = new AbortController();
714
+ const timer = setTimeout(() => controller.abort(), 5000);
715
+ try {
716
+ await fetch(base.replace(/\/+$/, ''), { method: 'GET', signal: controller.signal });
717
+ return true;
718
+ }
719
+ catch {
720
+ return false;
721
+ }
722
+ finally {
723
+ clearTimeout(timer);
724
+ }
725
+ }
726
+ /**
727
+ * fhcode doctor:环境自检(版本 / git / 配置 / provider / 路径可写 / 网络连通)。
728
+ * 全部通过输出 ✅,异常项以 ⚠️ 列明,帮助快速定位接入问题。
729
+ */
730
+ async function runDoctor() {
731
+ const checks = [];
732
+ // 1. Node 版本(engines >= 18)
733
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
734
+ checks.push({
735
+ name: (0, i18n_1.t)('doctor.node'),
736
+ ok: nodeMajor >= 18,
737
+ detail: `Node ${process.version}(要求 >=18)`,
738
+ });
739
+ // 2. git 可用性(diff/rollback/并行 worktree 依赖)
740
+ const git = await (0, exec_1.runCommand)('git --version', process.cwd(), 5000).catch(() => null);
741
+ checks.push({
742
+ name: (0, i18n_1.t)('doctor.git'),
743
+ ok: !!git && git.code === 0,
744
+ detail: git && git.code === 0 ? (git.stdout || git.stderr).trim() : (0, i18n_1.t)('doctor.gitMissing'),
745
+ });
746
+ // 3. 配置加载 + provider 明细 + 网络连通
747
+ try {
748
+ const cfg = (0, config_1.loadConfig)();
749
+ if (cfg.models.providers.length === 0) {
750
+ checks.push({ name: (0, i18n_1.t)('doctor.config'), ok: true, detail: (0, i18n_1.t)('doctor.configEmpty') });
751
+ }
752
+ else {
753
+ checks.push({ name: (0, i18n_1.t)('doctor.config'), ok: true, detail: `${cfg.models.providers.length} providers` });
754
+ for (const p of cfg.models.providers) {
755
+ checks.push({
756
+ name: `${(0, i18n_1.t)('doctor.provider')} ${p.id}`,
757
+ ok: !!p.baseURL,
758
+ detail: `${p.type} @ ${p.baseURL || '(缺 baseURL)'}${p.model ? ` · model=${p.model}` : ''}`,
759
+ });
760
+ }
761
+ }
762
+ // 网络探测:仅真实模式且有 provider 时执行;离线模式直接跳过
763
+ if (!isOfflineByDefault() && cfg.models.providers.length > 0) {
764
+ const base = cfg.models.providers[0].baseURL;
765
+ if (base) {
766
+ const reachable = await probeUrl(base);
767
+ checks.push({
768
+ name: (0, i18n_1.t)('doctor.network'),
769
+ ok: reachable,
770
+ detail: reachable ? `${base} 可达` : `${base} 不可达`,
771
+ });
772
+ }
773
+ }
774
+ else {
775
+ checks.push({ name: (0, i18n_1.t)('doctor.network'), ok: true, detail: (0, i18n_1.t)('doctor.networkOffline') });
776
+ }
777
+ }
778
+ catch (e) {
779
+ checks.push({
780
+ name: (0, i18n_1.t)('doctor.config'),
781
+ ok: false,
782
+ detail: e instanceof Error ? e.message : String(e),
783
+ });
784
+ }
785
+ // 4. 主目录可写(会话/审计/经验/统计落盘依赖)
786
+ const homeDir = (0, config_1.resolveHomeDir)();
787
+ let homeOk = true;
788
+ let homeDetail = homeDir;
789
+ try {
790
+ (0, fs_1.mkdirSync)(homeDir, { recursive: true });
791
+ const probe = (0, path_1.join)(homeDir, '.doctor-probe');
792
+ (0, fs_1.writeFileSync)(probe, 'ok');
793
+ (0, fs_1.rmSync)(probe, { force: true });
794
+ }
795
+ catch (e) {
796
+ homeOk = false;
797
+ homeDetail = `${homeDir}(${e instanceof Error ? e.message : String(e)})`;
798
+ }
799
+ checks.push({ name: (0, i18n_1.t)('doctor.home'), ok: homeOk, detail: homeDetail });
800
+ // 5. 沙箱模式(P0-2)
801
+ try {
802
+ const cfg = (0, config_1.loadConfig)();
803
+ const sandboxDetail = `${cfg.security.sandboxMode}` +
804
+ (cfg.security.networkDeny.length > 0 ? ` · deny: ${cfg.security.networkDeny.join(',')}` : '') +
805
+ (cfg.security.networkAllow.length > 0 ? ` · allow: ${cfg.security.networkAllow.join(',')}` : '');
806
+ checks.push({ name: (0, i18n_1.t)('doctor.sandbox'), ok: true, detail: sandboxDetail });
807
+ }
808
+ catch {
809
+ checks.push({ name: (0, i18n_1.t)('doctor.sandbox'), ok: false, detail: (0, i18n_1.t)('doctor.sandboxUnavailable') });
810
+ }
811
+ // 输出
812
+ console.log((0, i18n_1.t)('doctor.title'));
813
+ const failed = checks.filter((c) => !c.ok);
814
+ for (const c of checks) {
815
+ console.log(` ${c.ok ? '✅' : '⚠️'} ${c.name}: ${c.detail}`);
816
+ }
817
+ console.log(failed.length === 0 ? (0, i18n_1.t)('doctor.allOk') : (0, i18n_1.t)('doctor.issues', { n: failed.length }));
818
+ }
819
+ /* ===================== P3-3:插件管理 ===================== */
820
+ /** fhcode plugin install <source> / plugin list:插件打包分发管理 */
821
+ async function runPluginCmd(action, source) {
822
+ if (action === 'install') {
823
+ if (!source) {
824
+ console.error((0, i18n_1.t)('plugin.installUsage'));
825
+ return;
826
+ }
827
+ try {
828
+ const { name, dir } = await (0, plugin_loader_1.installPlugin)(source);
829
+ console.log((0, i18n_1.t)('plugin.installed', { name, dir }));
830
+ }
831
+ catch (e) {
832
+ console.error((0, i18n_1.t)('plugin.installFailed') + (e instanceof Error ? e.message : String(e)));
833
+ process.exitCode = 1;
834
+ }
835
+ return;
836
+ }
837
+ // list
838
+ const plugins = (0, plugin_loader_1.listPlugins)(process.cwd());
839
+ if (plugins.length === 0) {
840
+ console.log((0, i18n_1.t)('plugin.empty'));
841
+ return;
842
+ }
843
+ console.log((0, i18n_1.t)('plugin.listTitle'));
844
+ for (const p of plugins) {
845
+ console.log(` ${p.name.padEnd(24)} v${p.version} ${p.description ?? ''}`);
846
+ }
847
+ }
848
+ /* ===================== P4-2:Agent teams ===================== */
849
+ /** fhcode team "<目标>":多 agent 协作执行(共享任务清单 + 消息总线) */
850
+ async function runTeamCmd(goal) {
851
+ const offline = isOfflineByDefault();
852
+ console.log((0, i18n_1.t)('team.start', { mode: offline ? (0, i18n_1.t)('run.modeOffline') : (0, i18n_1.t)('run.modeLive') }));
853
+ // 目标拆解为任务清单(复用 planner 的并列连词拆分;拆不开则单任务)
854
+ const { decomposeGoal } = await Promise.resolve().then(() => __importStar(require('../agent/planner')));
855
+ const tasks = decomposeGoal(goal).map((t) => t.goal);
856
+ if (tasks.length === 0)
857
+ tasks.push(goal);
858
+ const report = await (0, team_1.runTeam)(tasks, {
859
+ runSubTask: async (focusedGoal) => {
860
+ const result = await executeTask(focusedGoal, { offline });
861
+ return { ok: result.ok, finalAnswer: result.finalAnswer, iterations: result.iterations };
862
+ },
863
+ pollIntervalMs: offline ? 0 : 100,
864
+ });
865
+ console.log('\n' + (0, i18n_1.t)('team.reportTitle'));
866
+ console.log(report.summary);
867
+ }
868
+ /* ===================== Skills 市场 ===================== */
869
+ /** 默认市场源(agentskills.io 官方规范端点;可用 --repo 或 FH_SKILL_MARKET 覆盖) */
870
+ const DEFAULT_MARKET = process.env.FH_SKILL_MARKET || 'https://agentskills.io';
871
+ /** fhcode skill-market search <关键词> | install <技能名> | list */
872
+ async function runSkillMarketCmd(action, query, market) {
873
+ const base = market || DEFAULT_MARKET;
874
+ if (action === 'list') {
875
+ // 列出本地已安装技能(复用技能发现,含打包/仓库/用户级)
876
+ const skills = (0, skill_loader_1.discoverSkills)(process.cwd());
877
+ if (skills.length === 0) {
878
+ console.log((0, i18n_1.t)('skillMarket.localEmpty'));
879
+ return;
880
+ }
881
+ console.log((0, i18n_1.t)('skillMarket.localTitle', { n: skills.length }));
882
+ for (const s of skills) {
883
+ console.log(` ${s.name.padEnd(24)} ${s.description.slice(0, 60)}`);
884
+ }
885
+ return;
886
+ }
887
+ // search / install 需联网拉索引
888
+ let index;
889
+ try {
890
+ index = await (0, skill_market_1.fetchMarketIndex)(base);
891
+ }
892
+ catch (e) {
893
+ console.error((0, i18n_1.t)('skillMarket.fetchFailed', { base }) + (e instanceof Error ? e.message : String(e)));
894
+ process.exitCode = 1;
895
+ return;
896
+ }
897
+ if (!(0, skill_market_1.isSchemaSupported)(index.schema)) {
898
+ console.warn((0, i18n_1.t)('skillMarket.schemaWarn', { schema: index.schema ?? '?' }));
899
+ }
900
+ if (action === 'search') {
901
+ const results = (0, skill_market_1.searchMarket)(index, query ?? '');
902
+ if (results.length === 0) {
903
+ console.log((0, i18n_1.t)('skillMarket.searchEmpty', { q: query ?? '' }));
904
+ return;
905
+ }
906
+ console.log((0, i18n_1.t)('skillMarket.searchTitle', { q: query ?? '', n: results.length, base }));
907
+ for (const s of results) {
908
+ console.log(` ${s.name.padEnd(28)} [${s.type}] ${s.description.slice(0, 70)}`);
909
+ }
910
+ console.log((0, i18n_1.t)('skillMarket.installHint'));
911
+ return;
912
+ }
913
+ // install
914
+ if (!query) {
915
+ console.error((0, i18n_1.t)('skillMarket.installUsage'));
916
+ process.exitCode = 1;
917
+ return;
918
+ }
919
+ const skill = index.skills.find((s) => s.name === query);
920
+ if (!skill) {
921
+ console.error((0, i18n_1.t)('skillMarket.notFound', { name: query }));
922
+ process.exitCode = 1;
923
+ return;
924
+ }
925
+ const destDir = (0, path_1.join)((0, config_1.resolveHomeDir)(), 'skills');
926
+ try {
927
+ const target = await (0, skill_market_1.installMarketSkill)(index, skill, destDir);
928
+ console.log((0, i18n_1.t)('skillMarket.installed', { name: skill.name, dir: target }));
929
+ }
930
+ catch (e) {
931
+ console.error((0, i18n_1.t)('skillMarket.installFailed') + (e instanceof Error ? e.message : String(e)));
932
+ process.exitCode = 1;
933
+ }
934
+ }
500
935
  /** fhcode model-stats:显示各模型性能统计 */
501
936
  function runModelStats() {
502
- const homeDir = (0, path_1.join)((0, os_1.homedir)(), '.feihong-code');
937
+ const homeDir = (0, config_1.resolveHomeDir)();
503
938
  const statsFile = (0, path_1.join)(homeDir, 'model-stats.jsonl');
504
939
  if (!(0, fs_1.existsSync)(statsFile)) {
505
- console.log('(暂无模型性能数据,执行任务后自动生成)');
940
+ console.log((0, i18n_1.t)('modelStats.empty'));
506
941
  return;
507
942
  }
508
943
  const router = new model_router_1.ModelRouter([], 'cost', 0, statsFile);
509
944
  router.loadStats(homeDir).then(() => {
510
945
  const stats = router.getStats();
511
946
  if (stats.length === 0) {
512
- console.log('(无模型统计记录)');
947
+ console.log((0, i18n_1.t)('modelStats.noRecords'));
513
948
  return;
514
949
  }
515
- console.log('模型性能统计(M6):');
516
- console.log(' 提供者ID 模型 总调用 成功 失败 成功率 平均延迟 总成本');
950
+ console.log((0, i18n_1.t)('modelStats.title'));
951
+ console.log((0, i18n_1.t)('modelStats.tableHeader'));
517
952
  for (const s of stats) {
518
953
  console.log(` ${s.providerId.padEnd(16)} ${s.model.padEnd(18)} ${String(s.totalCalls).padStart(5)} ${String(s.successfulCalls).padStart(5)} ${String(s.failedCalls).padStart(5)} ${s.successRate.toFixed(2).padStart(6)} ${s.avgLatencyMs.toFixed(0).padStart(8)}ms $${s.totalCostUsd.toFixed(6)}`);
519
954
  }
@@ -521,19 +956,19 @@ function runModelStats() {
521
956
  }
522
957
  /** fhcode experiences [路径]:列出经验库 */
523
958
  function runExperiences(path) {
524
- const experienceDir = path || (0, path_1.join)(require('os').homedir(), '.feihong-code', 'experiences');
959
+ const experienceDir = path || (0, path_1.join)((0, config_1.resolveHomeDir)(), 'experiences');
525
960
  (0, experience_1.listExperiences)(experienceDir).then((experiences) => {
526
961
  if (experiences.length === 0) {
527
- console.log('(暂无经验记录,完成任务后自动积累)');
962
+ console.log((0, i18n_1.t)('exp.empty'));
528
963
  return;
529
964
  }
530
- console.log(`经验库 (${experiences.length} 条,来源: ${experienceDir}):`);
531
- console.log(' ID 类型 标题 成功率 使用次数');
965
+ console.log((0, i18n_1.t)('exp.header', { n: experiences.length, dir: experienceDir }));
966
+ console.log((0, i18n_1.t)('exp.tableHeader'));
532
967
  for (const exp of experiences.slice(0, 10)) {
533
968
  console.log(` ${exp.id.padEnd(30)} ${exp.type.padEnd(16)} ${exp.title.slice(0, 25).padEnd(25)} ${(exp.metadata.successRate * 100).toFixed(0).padStart(4)}% ${String(exp.metadata.sessionCount).padStart(4)}`);
534
969
  }
535
970
  if (experiences.length > 10) {
536
- console.log(` ... ${experiences.length} 条,显示前 10 条`);
971
+ console.log((0, i18n_1.t)('exp.more', { n: experiences.length }));
537
972
  }
538
973
  });
539
974
  }
@@ -541,9 +976,11 @@ function runExperiences(path) {
541
976
  /** fhcode serve:启动 Web 管理控制台。无 FH_WEB_PORT 用 8080;无 FH_WEB_TOKEN 自动生成。 */
542
977
  function runServe(port) {
543
978
  const handle = (0, server_1.startWebServer)({ port });
544
- console.log(`[飞虹 Code] Web 控制台: ${handle.url}`);
545
- console.log(`[飞虹 Code] 访问令牌 (FH_WEB_TOKEN): ${handle.token}`);
546
- console.log(`[飞虹 Code] 按 Ctrl+C 停止`);
979
+ console.log((0, i18n_1.t)('serve.url', { url: handle.url }));
980
+ console.log((0, i18n_1.t)('serve.token', { token: handle.token }));
981
+ console.log((0, i18n_1.t)('serve.stop'));
982
+ // 启动每日记忆总结定时器(每天 00:00 UTC)
983
+ (0, auto_summarize_1.scheduleDailySummary)();
547
984
  // 注意:app.listen 保持事件循环运行,进程持续存活直到收到 SIGINT;本函数返回后 main() 结束不影响服务。
548
985
  }
549
986
  /* ===================== M8:自主编程能力 ===================== */
@@ -586,9 +1023,9 @@ export function calculateTieredCommission(plan: CommissionPlan, amount: number):
586
1023
  }
587
1024
  `;
588
1025
  const result = await writer.run(goal, sampleCode, 'generated/commission.ts');
589
- console.log(`\n===== M8 自主编写结果 =====`);
1026
+ console.log('\n' + (0, i18n_1.t)('codewrite.resultTitle'));
590
1027
  console.log(result.summary);
591
- console.log(`生成文件: ${result.finalFiles.join(', ')}`);
1028
+ console.log((0, i18n_1.t)('codewrite.files', { files: result.finalFiles.join(', ') }));
592
1029
  }
593
1030
  /** fhcode quality-gate [路径]:质量门禁审查 */
594
1031
  function runQualityGate(path) {
@@ -598,30 +1035,44 @@ function runQualityGate(path) {
598
1035
  console.log(gate.report(results));
599
1036
  const failed = results.filter((r) => !r.passed);
600
1037
  if (failed.length > 0) {
601
- console.log(`\n⚠️ ${failed.length} 个文件未通过门禁,请修复后再提交`);
1038
+ console.log('\n' + (0, i18n_1.t)('quality.failed', { n: failed.length }));
602
1039
  }
603
1040
  }
604
- /** fhcode self-improve:自我改进统计 */
605
- function runSelfImprove() {
1041
+ /** fhcode self-improve:自我改进统计 + 经验库概览 + 学习提示预览 */
1042
+ async function runSelfImprove() {
606
1043
  const improver = (0, self_improver_1.createSelfImprover)();
607
1044
  const records = improver.loadImprovements();
608
1045
  const stats = improver.getStats();
609
- console.log('===== M8 自我改进统计 =====');
610
- console.log(`总反思次数: ${stats.totalReflections}`);
611
- console.log(`成功率: ${(stats.successRate * 100).toFixed(1)}%`);
612
- console.log(`平均耗时: ${stats.avgDurationMs.toFixed(0)}ms`);
1046
+ console.log((0, i18n_1.t)('selfimp.title'));
1047
+ console.log((0, i18n_1.t)('selfimp.reflections', { n: stats.totalReflections }));
1048
+ console.log((0, i18n_1.t)('selfimp.successRate', { p: (stats.successRate * 100).toFixed(1) }));
1049
+ console.log((0, i18n_1.t)('selfimp.avgDuration', { ms: stats.avgDurationMs.toFixed(0) }));
1050
+ // 经验库概览(与 orchestrator 共用同一库,体现回流闭环)
1051
+ const exps = await (0, experience_1.listExperiences)(improver.experienceStoreDir);
1052
+ const totalWeight = exps.reduce((s, e) => s + e.metadata.sessionCount, 0);
1053
+ console.log('\n' + (0, i18n_1.t)('selfimp.expLib', { n: exps.length, w: totalWeight }));
1054
+ if (exps.length > 0) {
1055
+ console.log('\n' + (0, i18n_1.t)('selfimp.topExp'));
1056
+ for (const e of exps.slice(0, 6)) {
1057
+ console.log((0, i18n_1.t)('selfimp.expItem', { count: e.metadata.sessionCount, type: e.type, title: e.title }));
1058
+ }
1059
+ }
613
1060
  if (records.length > 0) {
614
- console.log(`\n最近改进记录:`);
1061
+ console.log('\n' + (0, i18n_1.t)('selfimp.recent'));
615
1062
  for (const rec of records.slice(-5).reverse()) {
616
- console.log(` ${rec.timestamp.slice(0, 19)} | ${rec.success ? '✅' : '❌'} | 模式: ${rec.patterns.length} 条`);
1063
+ console.log((0, i18n_1.t)('selfimp.record', { ts: rec.timestamp.slice(0, 19), ok: rec.success ? '✅' : '❌', n: rec.patterns.length }));
617
1064
  for (const imp of rec.improvements.slice(0, 3)) {
618
- console.log(` → ${imp}`);
1065
+ console.log((0, i18n_1.t)('selfimp.improvement', { imp }));
619
1066
  }
620
1067
  }
621
1068
  }
622
1069
  else {
623
- console.log('\n(暂无改进记录,完成任务后自动生成)');
1070
+ console.log('\n' + (0, i18n_1.t)('selfimp.noRecords'));
624
1071
  }
1072
+ // 学习提示预览:模拟一次任务,展示将注入模型的经验
1073
+ console.log('\n' + (0, i18n_1.t)('selfimp.learnPreview', { goal: '实现一个 REST API 功能' }));
1074
+ const learned = await improver.getLearnedPrompt('实现一个 REST API 功能');
1075
+ console.log(learned || (0, i18n_1.t)('selfimp.noLearned'));
625
1076
  }
626
1077
  /**
627
1078
  * fhcode swe "<目标>":全自动软件工程 Agent
@@ -643,11 +1094,7 @@ async function runSwe(goal, opts = {}) {
643
1094
  if (!offline) {
644
1095
  const cfg = (0, config_1.loadConfig)();
645
1096
  if (!cfg.models.providers.length) {
646
- console.error('[飞虹 Code] 未配置真实模型供应商,无法进入真实模式。请通过以下任一方式接入:\n' +
647
- ' 1) 环境变量: FH_MODEL_NAME=<模型> FH_MODEL_TYPE=ollama|openai-compatible FH_MODEL_BASE_URL=<地址> [FH_MODEL_API_KEY=<令牌>]\n' +
648
- ' 2) 配置文件: ./fhcode.config.json 的 models.providers 数组\n' +
649
- ' 3) FH_PROVIDERS 环境变量(JSON 数组)\n' +
650
- '本地 Ollama 示例: FH_MODEL_NAME=qwen2.5-coder:1.5b FH_MODEL_TYPE=ollama fhcode swe "..."');
1097
+ console.error((0, i18n_1.t)('swe.noProvider'));
651
1098
  return;
652
1099
  }
653
1100
  }
@@ -664,8 +1111,17 @@ async function runSwe(goal, opts = {}) {
664
1111
  router = model_router_1.ModelRouter.fromConfig(cfg);
665
1112
  security.shellAllowlist = cfg.security.shellAllowlist;
666
1113
  security.requireApproval = cfg.security.requireApproval;
1114
+ security.sandboxMode = cfg.security.sandboxMode;
1115
+ security.networkRules = { networkAllow: cfg.security.networkAllow, networkDeny: cfg.security.networkDeny };
1116
+ security.hooks = cfg.hooks;
667
1117
  }
668
1118
  const tools = (0, tools_1.createDefaultRegistry)();
1119
+ // P0-3:附加 MCP 外部工具(真实模式且有配置时),子任务结束即关闭
1120
+ let mcpClients = [];
1121
+ if (!offline) {
1122
+ const cfg = (0, config_1.loadConfig)();
1123
+ mcpClients = await (0, mcp_1.attachMcpTools)(tools, cfg.mcp.servers);
1124
+ }
669
1125
  const logDir = getSessionHome(offline);
670
1126
  const eventLog = new event_log_1.EventLog(runId, logDir);
671
1127
  const session = new session_store_1.SessionStore(runId, cwd);
@@ -682,19 +1138,24 @@ async function runSwe(goal, opts = {}) {
682
1138
  security,
683
1139
  approve,
684
1140
  guard,
685
- maxIterations: opts.maxIterations ?? 6,
1141
+ maxIterations: opts.maxIterations ?? 15,
686
1142
  maxCostUsd: rt?.maxCostUsd ?? 0,
1143
+ // P1-1:子任务用低成本模型分担(编排器主模型保持 code-gen,worker 加 cheap 优先)
1144
+ tags: ['code-gen', 'cheap'],
687
1145
  persist: (cp) => (0, session_persist_1.saveCheckpoint)(logDir, cp),
688
1146
  });
689
1147
  const result = await orchestrator.run(focusedGoal);
1148
+ await (0, mcp_1.closeMcpClients)(mcpClients);
1149
+ // P2-2:子代理结果摘要化回主上下文(隔离中间大输出)
1150
+ const summarized = (0, subagent_summary_1.summarizeSubTaskAnswer)(result.finalAnswer);
690
1151
  return {
691
1152
  ok: result.ok,
692
- finalAnswer: result.finalAnswer,
1153
+ finalAnswer: summarized.text,
693
1154
  iterations: result.iterations,
694
1155
  touchedFiles: [],
695
1156
  };
696
1157
  };
697
- console.log(`[飞虹 Code] 启动全自动软件工程 Agent (offline=${offline}, 仓库=${cwd})`);
1158
+ console.log((0, i18n_1.t)('swe.start', { offline: offline ? (0, i18n_1.t)('run.modeOffline') : (0, i18n_1.t)('run.modeLive'), cwd }));
698
1159
  const report = await (0, swe_agent_1.runSweAgent)(goal, {
699
1160
  cwd,
700
1161
  runSubTask,
@@ -703,7 +1164,7 @@ async function runSwe(goal, opts = {}) {
703
1164
  verifyOnly: !!opts.verifyOnly,
704
1165
  planOnly: !!opts.planOnly,
705
1166
  });
706
- console.log('\n===== 全自动软件工程 Agent 报告 =====');
1167
+ console.log('\n' + (0, i18n_1.t)('swe.reportTitle'));
707
1168
  console.log(report.summary);
708
1169
  if (rt) {
709
1170
  rt.audit.record({
@@ -718,10 +1179,54 @@ async function runSwe(goal, opts = {}) {
718
1179
  });
719
1180
  }
720
1181
  }
1182
+ /** fhcode harness [--split lite|verified] [--limit N] [--offset N] [--mode mock|real] [--report 路径] [--json] */
1183
+ async function runHarness(opts) {
1184
+ console.log((0, i18n_1.t)('harness.start', { mode: opts.mode, split: opts.split, limit: String(opts.limit) }));
1185
+ // 真实模式就绪检查:未配置任何模型供应商时给出明确接入指引
1186
+ if (opts.mode === 'real') {
1187
+ const cfg = (0, config_1.loadConfig)();
1188
+ if (!cfg.models.providers.length) {
1189
+ console.error((0, i18n_1.t)('harness.noProvider'));
1190
+ return;
1191
+ }
1192
+ }
1193
+ const loader = new loader_1.SwebenchLoader({ split: opts.split });
1194
+ const executor = opts.mode === 'real' ? new executor_1.RealModelExecutor() : new executor_1.MockOrchestratorExecutor();
1195
+ const verifier = new verifier_1.FileExistsVerifier();
1196
+ const harness = new harness_1.Harness({
1197
+ loader,
1198
+ executor,
1199
+ verifier,
1200
+ reporter: opts.json ? new reporter_1.JsonReporter() : new reporter_1.MarkdownReporter(),
1201
+ limit: opts.limit,
1202
+ offset: opts.offset,
1203
+ onProgress: (r, i, total) => {
1204
+ console.log(` [${i}/${total}] ${r.ok ? '✅' : '❌'} ${r.instance_id} iter=${r.iterations} tools=${r.toolCalls}`);
1205
+ },
1206
+ });
1207
+ const { report, rendered } = await harness.run();
1208
+ if (opts.report) {
1209
+ (0, fs_1.writeFileSync)(opts.report, rendered, 'utf8');
1210
+ console.log((0, i18n_1.t)('harness.reportWritten', { path: opts.report }));
1211
+ }
1212
+ else {
1213
+ console.log('\n' + rendered);
1214
+ }
1215
+ console.log((0, i18n_1.t)('harness.summary', {
1216
+ completed: String(report.summary.completed),
1217
+ total: String(report.summary.total),
1218
+ rate: String(Math.round(report.summary.rate * 100)),
1219
+ }));
1220
+ // 有失败实例 → 退出码非零(供 CI 门禁复用)
1221
+ if (report.summary.completed < report.summary.total)
1222
+ process.exitCode = 1;
1223
+ console.log((0, i18n_1.t)('app.signature'));
1224
+ }
721
1225
  /* ===================== 审批器 ===================== */
722
1226
  function expandHome(p) {
1227
+ // 统一用 os.homedir()(Windows 上 process.env.HOME 可能缺失,导致 ~ 展开为空路径)
723
1228
  if (p.startsWith('~'))
724
- return (0, path_1.join)(process.env.HOME || process.cwd(), p.slice(1));
1229
+ return (0, path_1.join)((0, os_1.homedir)(), p.slice(1));
725
1230
  return p;
726
1231
  }
727
1232
  function joinHome() {
@@ -752,7 +1257,7 @@ function defaultApproverFor(security) {
752
1257
  function interactiveApprover() {
753
1258
  return (action) => new Promise((resolve) => {
754
1259
  const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
755
- const ask = () => rl.question(`[审批] 是否允许执行: ${action}\n 输入 y/yes 允许,其他拒绝: `, (ans) => {
1260
+ const ask = () => rl.question((0, i18n_1.t)('approve.prompt', { action }), (ans) => {
756
1261
  rl.close();
757
1262
  resolve(/^(y|yes|是)$/i.test(ans.trim()));
758
1263
  });