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
@@ -953,11 +953,12 @@
953
953
  const box = document.getElementById('messages');
954
954
  const el = document.createElement('div');
955
955
  el.className = 'msg ' + role;
956
- if (extras.html) el.innerHTML = extras.html;
956
+ const actionsHtml = (typeof renderMsgActions === 'function') ? renderMsgActions() : '';
957
+ if (extras.html) el.innerHTML = actionsHtml + extras.html;
957
958
  else if (extras.file) {
958
- el.innerHTML = '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
959
+ el.innerHTML = actionsHtml + '<span class="file-chip" data-path="' + escapeHtml(extras.file) + '" data-type="' + escapeHtml(extras.mime || 'file') + '">📎 ' + escapeHtml(extras.name) + '</span>';
959
960
  } else {
960
- el.innerHTML = linkifyArtifacts(text);
961
+ el.innerHTML = actionsHtml + linkifyArtifacts(text);
961
962
  }
962
963
  box.appendChild(el);
963
964
  box.scrollTop = box.scrollHeight;
@@ -1178,142 +1179,164 @@
1178
1179
  });
1179
1180
 
1180
1181
 
1181
- // 截图锁,防止重复触发
1182
- let screenshotRunning = false;
1182
+ // ========== 截图功能(极简重写版) ==========
1183
+ let _screenshotBusy = false;
1183
1184
 
1184
1185
  async function takeScreenshot() {
1185
- if (screenshotRunning) {
1186
- toast('正在截图中,请稍候...');
1187
- return;
1188
- }
1189
- screenshotRunning = true;
1186
+ if (_screenshotBusy) { toast('正在截图中,按 ESC 可取消'); return; }
1187
+ _screenshotBusy = true;
1188
+
1189
+ const overlay = document.getElementById('cropOverlay');
1190
+ const selection = document.getElementById('cropSelection');
1191
+ let stream = null;
1192
+ let cleanup = null;
1193
+
1190
1194
  try {
1191
- const overlay = document.getElementById('cropOverlay');
1192
- const selection = document.getElementById('cropSelection');
1193
- const video = document.getElementById('cropVideo');
1194
-
1195
- let srcImage = null; // 用于裁剪的源图像(Image 对象)
1196
- let stream = null; // 浏览器模式下的视频流
1197
-
1198
- // Electron 环境:用主进程截图,更稳定
1199
- if (window.isElectron && window.electronAPI && window.electronAPI.screenshot) {
1200
- toast('正在截取屏幕...');
1201
- const result = await window.electronAPI.screenshot.capture();
1202
- if (!result || !result.success) {
1203
- toast('截图失败:' + (result?.error || '未知错误'));
1204
- return;
1205
- }
1206
- // 加载截图为 Image 对象
1207
- srcImage = new Image();
1208
- srcImage.src = result.dataUrl;
1209
- await new Promise((resolve, reject) => {
1210
- srcImage.onload = resolve;
1211
- srcImage.onerror = () => reject(new Error('截图加载失败'));
1212
- });
1213
- // 把截图显示到 video 元素的位置(用一个 img 覆盖)
1214
- video.style.display = 'none';
1215
- let bgImg = document.getElementById('cropBgImg');
1216
- if (!bgImg) {
1217
- bgImg = document.createElement('img');
1218
- bgImg.id = 'cropBgImg';
1219
- bgImg.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;object-fit:cover;z-index:9998;';
1220
- overlay.appendChild(bgImg);
1221
- }
1222
- bgImg.src = result.dataUrl;
1223
- bgImg.style.display = 'block';
1224
- } else {
1225
- // 浏览器模式:用 getDisplayMedia
1226
- stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
1227
- video.srcObject = stream;
1228
- await video.play();
1229
- video.style.display = 'block';
1230
- const bgImg = document.getElementById('cropBgImg');
1231
- if (bgImg) bgImg.style.display = 'none';
1232
- }
1195
+ // 1. 获取屏幕共享流(请求高分辨率)
1196
+ toast('请选择要截取的屏幕或窗口...');
1197
+ stream = await navigator.mediaDevices.getDisplayMedia({
1198
+ video: { width: { ideal: 3840 }, height: { ideal: 2160 }, frameRate: { ideal: 30 } }
1199
+ });
1233
1200
 
1201
+ // 2. 用 video 播放流,等待元数据加载完成(确保 videoWidth/Height 有效)
1202
+ const video = document.createElement('video');
1203
+ video.style.display = 'none';
1204
+ video.srcObject = stream;
1205
+ video.muted = true;
1206
+ await new Promise((resolve, reject) => {
1207
+ video.onloadedmetadata = () => { video.play().then(resolve).catch(reject); };
1208
+ video.onerror = () => reject(new Error('视频加载失败'));
1209
+ setTimeout(() => reject(new Error('视频加载超时')), 5000);
1210
+ });
1211
+
1212
+ const vw = video.videoWidth, vh = video.videoHeight;
1213
+ if (!vw || !vh) throw new Error('屏幕画面尺寸无效');
1214
+
1215
+ // 3. 隐藏当前页面,避免截到自己(抓帧后立即恢复)
1216
+ const docEl = document.documentElement;
1217
+ const prevVis = docEl.style.visibility;
1218
+ docEl.style.visibility = 'hidden';
1219
+ await new Promise(r => requestAnimationFrame(r));
1220
+ await new Promise(r => setTimeout(r, 200));
1221
+
1222
+ // 4. canvas 抓帧
1223
+ const capCanvas = document.createElement('canvas');
1224
+ capCanvas.width = vw; capCanvas.height = vh;
1225
+ capCanvas.getContext('2d').drawImage(video, 0, 0, vw, vh);
1226
+ const fullDataUrl = capCanvas.toDataURL('image/png');
1227
+
1228
+ // 5. 停止流 + 恢复页面
1229
+ stream.getTracks().forEach(t => t.stop());
1230
+ stream = null;
1231
+ docEl.style.visibility = prevVis;
1232
+
1233
+ // 6. 加载为 Image 对象供裁剪
1234
+ const srcImg = new Image();
1235
+ srcImg.src = fullDataUrl;
1236
+ await new Promise((resolve, reject) => {
1237
+ srcImg.onload = resolve;
1238
+ srcImg.onerror = () => reject(new Error('截图加载失败'));
1239
+ });
1240
+
1241
+ // 7. 显示裁剪 overlay
1242
+ let bgImg = document.getElementById('cropBgImg');
1243
+ if (!bgImg) {
1244
+ bgImg = document.createElement('img');
1245
+ bgImg.id = 'cropBgImg';
1246
+ bgImg.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;object-fit:scale-down;object-position:center;background:#000;z-index:9998;pointer-events:none;';
1247
+ overlay.appendChild(bgImg);
1248
+ }
1249
+ bgImg.src = fullDataUrl;
1250
+ bgImg.style.display = 'block';
1234
1251
  overlay.style.display = 'block';
1235
1252
  selection.style.display = 'none';
1236
- let startX = 0, startY = 0, drawing = false;
1237
-
1238
- const onDown = (e) => {
1239
- drawing = true;
1240
- startX = e.clientX; startY = e.clientY;
1241
- selection.style.display = 'block';
1242
- selection.style.left = startX + 'px';
1243
- selection.style.top = startY + 'px';
1244
- selection.style.width = '0px';
1245
- selection.style.height = '0px';
1246
- };
1247
- const onMove = (e) => {
1248
- if (!drawing) return;
1249
- const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
1250
- const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
1251
- selection.style.left = x + 'px';
1252
- selection.style.top = y + 'px';
1253
- selection.style.width = w + 'px';
1254
- selection.style.height = h + 'px';
1255
- };
1256
- const finish = async (e) => {
1257
- if (!drawing) return;
1258
- drawing = false;
1259
- window.removeEventListener('mousedown', onDown);
1260
- window.removeEventListener('mousemove', onMove);
1261
- window.removeEventListener('mouseup', finish);
1262
- const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
1263
- const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
1264
- overlay.style.display = 'none';
1265
- if (w < 8 || h < 8) {
1266
- if (stream) stream.getTracks().forEach((t) => t.stop());
1267
- return;
1268
- }
1269
- try {
1270
- let srcW, srcH, source;
1271
- if (srcImage) {
1272
- // Electron 模式:从 Image 裁剪
1273
- srcW = srcImage.naturalWidth;
1274
- srcH = srcImage.naturalHeight;
1275
- source = srcImage;
1276
- } else {
1277
- // 浏览器模式:从 video 裁剪
1278
- srcW = video.videoWidth;
1279
- srcH = video.videoHeight;
1280
- source = video;
1281
- }
1282
- const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
1283
- const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
1284
- const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
1285
- const canvas = document.createElement('canvas');
1286
- canvas.width = sw; canvas.height = sh;
1287
- canvas.getContext('2d').drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
1288
- if (stream) stream.getTracks().forEach((t) => t.stop());
1289
- const dataUrl = canvas.toDataURL('image/png');
1290
- stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl });
1291
- toast('截图已添加到输入框,可直接输入文字发送');
1292
- } catch (err) { toast('截图失败:' + err.message); }
1293
- screenshotRunning = false;
1294
- };
1295
- const onKey = (e) => {
1296
- if (e.key === 'Escape') {
1297
- overlay.style.display = 'none';
1298
- if (stream) stream.getTracks().forEach((t) => t.stop());
1253
+
1254
+ // 8. 等待用户拖拽选择区域
1255
+ const result = await new Promise((resolve) => {
1256
+ let sx = 0, sy = 0, dragging = false;
1257
+
1258
+ const onDown = (e) => {
1259
+ dragging = true;
1260
+ sx = e.clientX; sy = e.clientY;
1261
+ selection.style.display = 'block';
1262
+ selection.style.left = sx + 'px';
1263
+ selection.style.top = sy + 'px';
1264
+ selection.style.width = '0px';
1265
+ selection.style.height = '0px';
1266
+ };
1267
+ const onMove = (e) => {
1268
+ if (!dragging) return;
1269
+ const x = Math.min(e.clientX, sx), y = Math.min(e.clientY, sy);
1270
+ const w = Math.abs(e.clientX - sx), h = Math.abs(e.clientY - sy);
1271
+ selection.style.left = x + 'px';
1272
+ selection.style.top = y + 'px';
1273
+ selection.style.width = w + 'px';
1274
+ selection.style.height = h + 'px';
1275
+ };
1276
+ const onUp = (e) => {
1277
+ if (!dragging) return;
1278
+ dragging = false;
1279
+ const x = Math.min(e.clientX, sx), y = Math.min(e.clientY, sy);
1280
+ const w = Math.abs(e.clientX - sx), h = Math.abs(e.clientY - sy);
1281
+ cleanup();
1282
+ if (w < 10 || h < 10) { resolve(null); return; }
1283
+ resolve({ x, y, w, h });
1284
+ };
1285
+ const onKey = (e) => {
1286
+ if (e.key === 'Escape') { cleanup(); resolve(null); }
1287
+ };
1288
+
1289
+ cleanup = () => {
1299
1290
  window.removeEventListener('mousedown', onDown);
1300
1291
  window.removeEventListener('mousemove', onMove);
1301
- window.removeEventListener('mouseup', finish);
1292
+ window.removeEventListener('mouseup', onUp);
1302
1293
  window.removeEventListener('keydown', onKey);
1303
- screenshotRunning = false;
1304
- }
1305
- };
1306
- window.addEventListener('mousedown', onDown);
1307
- window.addEventListener('mousemove', onMove);
1308
- window.addEventListener('mouseup', finish);
1309
- window.addEventListener('keydown', onKey);
1310
- } catch (e) {
1311
- if (e.name === 'NotAllowedError') {
1312
- toast('已取消截图。也可以用 Win+Shift+S 截图后直接粘贴到输入框');
1294
+ overlay.style.display = 'none';
1295
+ selection.style.display = 'none';
1296
+ };
1297
+
1298
+ window.addEventListener('mousedown', onDown);
1299
+ window.addEventListener('mousemove', onMove);
1300
+ window.addEventListener('mouseup', onUp);
1301
+ window.addEventListener('keydown', onKey);
1302
+ });
1303
+
1304
+ // 9. 用户取消
1305
+ if (!result) {
1306
+ toast('已取消截图');
1307
+ _screenshotBusy = false;
1308
+ return;
1309
+ }
1310
+
1311
+ // 10. 裁剪选中区域
1312
+ const scaleX = srcImg.naturalWidth / window.innerWidth;
1313
+ const scaleY = srcImg.naturalHeight / window.innerHeight;
1314
+ const cropCanvas = document.createElement('canvas');
1315
+ cropCanvas.width = Math.round(result.w * scaleX);
1316
+ cropCanvas.height = Math.round(result.h * scaleY);
1317
+ cropCanvas.getContext('2d').drawImage(
1318
+ srcImg,
1319
+ Math.round(result.x * scaleX), Math.round(result.y * scaleY),
1320
+ Math.round(result.w * scaleX), Math.round(result.h * scaleY),
1321
+ 0, 0, cropCanvas.width, cropCanvas.height
1322
+ );
1323
+ const croppedUrl = cropCanvas.toDataURL('image/png');
1324
+
1325
+ // 11. 添加到输入框
1326
+ stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl: croppedUrl });
1327
+ toast('截图已添加到输入框');
1328
+ } catch (err) {
1329
+ if (err.name === 'NotAllowedError') {
1330
+ toast('已取消截图。也可用 Win+Shift+S 截图后粘贴');
1313
1331
  } else {
1314
- toast('截图失败:' + e.message);
1332
+ console.error('[screenshot] 失败:', err);
1333
+ toast('截图失败:' + err.message);
1315
1334
  }
1316
- screenshotRunning = false;
1335
+ if (stream) { try { stream.getTracks().forEach(t => t.stop()); } catch(e){} }
1336
+ if (cleanup) { try { cleanup(); } catch(e){} }
1337
+ else { overlay.style.display = 'none'; selection.style.display = 'none'; }
1338
+ } finally {
1339
+ _screenshotBusy = false;
1317
1340
  }
1318
1341
  }
1319
1342
 
@@ -12,6 +12,22 @@
12
12
  */
13
13
  (function () {
14
14
  if (window.__feihongMockInstalled) return;
15
+
16
+ // ========== 环境检测:仅在安卓离线模式下激活 Mock ==========
17
+ // Web 控制台通过 http://localhost:8080 加载,有真实 Node.js 后端,不应拦截 fetch。
18
+ // 安卓 WebView 通过 file:// 或 content:// 加载本地 HTML,无后端,才需要 Mock。
19
+ var proto = (location.protocol || '').toLowerCase();
20
+ var isAndroid = /android/i.test(navigator.userAgent || '');
21
+ var isLocalFile = proto === 'file:' || proto === 'content:';
22
+ // 仅在「安卓 + 本地文件协议」下激活 Mock;其他情况(Web 控制台、远程服务器)直接放行
23
+ if (!(isAndroid && isLocalFile)) {
24
+ console.log('[MockAPI] 检测到 HTTP 环境,跳过 Mock 拦截(mode=passthrough)');
25
+ window.__feihongMockInstalled = true;
26
+ window.FeiHongApp = window.FeiHongApp || {};
27
+ window.FeiHongApp.isMock = function () { return false; };
28
+ return;
29
+ }
30
+
15
31
  window.__feihongMockInstalled = true;
16
32
 
17
33
  // ========== 远程服务器配置 ==========
@@ -70,6 +86,15 @@
70
86
  return ok({ product: '飞虹 Code', version: '7.0.0', enterprise: true, signature: '安卓版 · 离线演示模式', mode: 'mock', time: new Date().toISOString() });
71
87
  }
72
88
 
89
+ // 安全:加密公钥(Mock 模式下返回固定测试公钥,仅供前端加密流程演示)
90
+ if (lower === '/api/security/public-key') {
91
+ return ok({
92
+ ok: true,
93
+ publicKey: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmock-public-key-for-demo-only\n-----END PUBLIC KEY-----',
94
+ algorithm: 'RSA-OAEP-2048-SHA256'
95
+ });
96
+ }
97
+
73
98
  // 模型
74
99
  if (lower === '/api/models' && method === 'GET') return ok({ models: MODELS });
75
100
  if (lower === '/api/models' && method === 'POST') {
@@ -10,9 +10,15 @@
10
10
  let monacoInstance = null;
11
11
  let initPromise = null;
12
12
  const editors = new Map(); // containerId -> editor instance
13
+ let activeEditor = null; // 最近创建的编辑器(供 lint/诊断反馈定位)
13
14
  let inlineCompletionProvider = null;
14
15
  let completionItemProvider = null;
15
16
 
17
+ /** 获取当前活跃编辑器(P6-1 lint 反馈定位) */
18
+ function getActiveEditor() {
19
+ return activeEditor;
20
+ }
21
+
16
22
  /** 根据文件扩展名推断 Monaco language id */
17
23
  function detectLanguage(filePath) {
18
24
  const ext = filePath.split('.').pop().toLowerCase();
@@ -98,6 +104,7 @@
98
104
 
99
105
  const key = el.id || container;
100
106
  editors.set(key, editor);
107
+ activeEditor = editor; // P6-1: 记录最近活跃编辑器供 lint 反馈定位
101
108
  return editor;
102
109
  }
103
110
 
@@ -121,11 +128,18 @@
121
128
  * 注册内联补全 Provider(ghost text,Tab 接受)
122
129
  * @param {function} fetchFn - 异步补全函数,参数 {filePath, content, offset},返回 {text: string}
123
130
  */
124
- function registerInlineCompletions(fetchFn) {
131
+ /**
132
+ * 注册行内补全 Provider(ghost text)
133
+ * @param {function} fetchFn - 异步补全函数,参数 {filePath, content, offset, token},返回 {text} 或 null
134
+ * @param {{getActiveEditor?:function, onAccept?:function}} [opts] - P6-1: getActiveEditor 取当前编辑器;onAccept 在补全被接受(插入)时回调插入文本
135
+ */
136
+ function registerInlineCompletions(fetchFn, opts) {
137
+ const o = opts || {};
125
138
  initMonaco().then((monaco) => {
126
139
  if (inlineCompletionProvider) {
127
140
  inlineCompletionProvider.dispose();
128
141
  }
142
+ let lastText = '';
129
143
  inlineCompletionProvider = monaco.languages.registerInlineCompletionsProvider('*', {
130
144
  provideInlineCompletions: async function (model, position, context, token) {
131
145
  try {
@@ -139,6 +153,7 @@
139
153
  insertText: result.text,
140
154
  filterText: result.text.slice(0, 20),
141
155
  };
156
+ lastText = result.text;
142
157
  return { items: [item] };
143
158
  } catch (e) {
144
159
  return { items: [] };
@@ -146,6 +161,28 @@
146
161
  },
147
162
  freeInlineCompletions: function () {},
148
163
  });
164
+
165
+ // P6-1: accept 检测——编辑器内容变化且插入文本匹配最近补全 → onAccept(插入文本)
166
+ const bindAccept = function () {
167
+ const editor = typeof o.getActiveEditor === 'function' ? o.getActiveEditor() : null;
168
+ if (!editor || editor.__fhInlineAcceptBound) return;
169
+ editor.__fhInlineAcceptBound = true;
170
+ editor.onDidChangeModelContent((e) => {
171
+ if (!lastText) return;
172
+ for (const ch of e.changes || []) {
173
+ const ins = ch.text || '';
174
+ if (ins && (ins.startsWith(lastText) || lastText.startsWith(ins))) {
175
+ const accepted = ins.length >= lastText.length ? ins : lastText;
176
+ if (typeof o.onAccept === 'function') {
177
+ try { o.onAccept(accepted); } catch { /* lint 回调失败不阻塞编辑 */ }
178
+ }
179
+ lastText = '';
180
+ break;
181
+ }
182
+ }
183
+ });
184
+ };
185
+ bindAccept();
149
186
  });
150
187
  }
151
188
 
@@ -190,6 +227,126 @@
190
227
  });
191
228
  }
192
229
 
230
+ /**
231
+ * 注册语义诊断 Provider(P4-2:编辑时错误波浪线)
232
+ * @param {function} fetchFn - 异步诊断函数,参数 {filePath, content},返回 [{line, column, endLine, endColumn, message, severity}]
233
+ * @param {function} [getEditor] - 获取当前编辑器实例(默认遍历已注册编辑器)
234
+ */
235
+ function registerDiagnostics(fetchFn, getActiveEditor) {
236
+ initMonaco().then((monaco) => {
237
+ // P5-2: 诊断 hover 提示——hover 到有 marker 的行时展示错误/警告详情
238
+ const langSet = [
239
+ 'typescript', 'javascript', 'json', 'markdown', 'html', 'css', 'scss', 'less',
240
+ 'python', 'java', 'c', 'cpp', 'csharp', 'go', 'rust', 'php', 'ruby', 'swift',
241
+ 'kotlin', 'scala', 'shell', 'sql', 'yaml', 'xml', 'protobuf', 'dockerfile',
242
+ 'ini', 'plaintext',
243
+ ];
244
+ const hoverDisp = monaco.languages.registerHoverProvider(langSet, {
245
+ provideHover(model, position) {
246
+ let editor;
247
+ try { editor = typeof getActiveEditor === 'function' ? getActiveEditor() : null; } catch { editor = null; }
248
+ if (!editor || editor.isDisposed?.()) return null;
249
+ const cur = editor.getModel();
250
+ if (!cur || model.uri.toString() !== cur.uri.toString()) return null;
251
+ const markers = monaco.editor.getModelMarkers({ resource: model.uri })
252
+ .filter((x) => x.startLineNumber <= position.lineNumber && position.lineNumber <= x.endLineNumber);
253
+ if (markers.length === 0) return null;
254
+ const md = markers.map((x) => {
255
+ const sevName = x.severity === monaco.MarkerSeverity.Error ? '错误' : x.severity === monaco.MarkerSeverity.Warning ? '警告' : '提示';
256
+ const icon = x.severity === monaco.MarkerSeverity.Error ? '⛔' : x.severity === monaco.MarkerSeverity.Warning ? '⚠️' : 'ℹ️';
257
+ return '**' + icon + ' ' + sevName + '** (L' + x.startLineNumber + ':' + x.startColumn + ') \n' + x.message;
258
+ }).join('\n\n---\n\n');
259
+ return {
260
+ contents: [{ value: '#### 飞虹 Code 诊断\n\n' + md, isTrusted: true }],
261
+ range: new monaco.Range(position.lineNumber, 1, position.lineNumber, 1),
262
+ };
263
+ },
264
+ });
265
+ hoverDisp; // 引用保持存活(Monaco 内部维护)
266
+
267
+ // 防抖 + 单飞,避免编辑时高频请求
268
+ let timer = null;
269
+ let inflight = false;
270
+ let queued = false;
271
+ const run = async function () {
272
+ if (inflight) { queued = true; return; }
273
+ const editor = typeof getActiveEditor === 'function' ? getActiveEditor() : null;
274
+ if (!editor || editor.isDisposed?.()) return;
275
+ const model = editor.getModel();
276
+ if (!model) return;
277
+ inflight = true;
278
+ try {
279
+ const filePath = model.uri.path || 'untitled';
280
+ const content = model.getValue();
281
+ const result = await fetchFn({ filePath, content });
282
+ if (!result || !Array.isArray(result.diagnostics)) {
283
+ monaco.editor.setModelMarkers(model, 'fhcode', []);
284
+ return;
285
+ }
286
+ const markers = result.diagnostics.map((d) => {
287
+ const sev = d.severity === 1 || d.severity === 'error'
288
+ ? monaco.MarkerSeverity.Error
289
+ : d.severity === 2 || d.severity === 'warning'
290
+ ? monaco.MarkerSeverity.Warning
291
+ : monaco.MarkerSeverity.Info;
292
+ return {
293
+ severity: sev,
294
+ message: d.message || '',
295
+ startLineNumber: d.line ?? 1,
296
+ startColumn: d.column ?? 1,
297
+ endLineNumber: d.endLine ?? (d.line ?? 1),
298
+ endColumn: d.endColumn ?? (d.column ?? 1) + (d.message?.length || 8),
299
+ };
300
+ });
301
+ monaco.editor.setModelMarkers(model, 'fhcode', markers);
302
+ } catch {
303
+ /* 诊断失败静默(不阻塞编辑) */
304
+ } finally {
305
+ inflight = false;
306
+ if (queued) { queued = false; timer = setTimeout(run, 200); }
307
+ }
308
+ };
309
+ const schedule = function () {
310
+ clearTimeout(timer);
311
+ timer = setTimeout(run, 350);
312
+ };
313
+ // 绑定现有编辑器(createEditor 后由 ui 层调用 attachDiagnostics 触发监听)
314
+ const editor = typeof getActiveEditor === 'function' ? getActiveEditor() : null;
315
+ if (editor && !editor.__fhDiagnosticsBound) {
316
+ editor.__fhDiagnosticsBound = true;
317
+ editor.onDidChangeModelContent(schedule);
318
+ editor.onDidChangeModel(() => schedule());
319
+ }
320
+ return { run, schedule };
321
+ });
322
+ }
323
+
324
+ /**
325
+ * P6-1: 补全接受后的 lint 反馈——把 /api/lint 结果追加为编辑器波浪线(owner: fhcode-lint)
326
+ * @param {Array} issues - [{severity, line, column, message}]
327
+ */
328
+ function showLintFeedback(issues) {
329
+ initMonaco().then((monaco) => {
330
+ const editor = activeEditor;
331
+ if (!editor || editor.isDisposed?.()) return;
332
+ const model = editor.getModel();
333
+ if (!model) return;
334
+ if (!Array.isArray(issues) || issues.length === 0) {
335
+ monaco.editor.setModelMarkers(model, 'fhcode-lint', []);
336
+ return;
337
+ }
338
+ const markers = issues.map((d) => ({
339
+ severity: d.severity === 1 ? monaco.MarkerSeverity.Error : d.severity === 2 ? monaco.MarkerSeverity.Warning : monaco.MarkerSeverity.Info,
340
+ message: '补全校验:' + (d.message || ''),
341
+ startLineNumber: d.line ?? 1,
342
+ startColumn: d.column ?? 1,
343
+ endLineNumber: d.line ?? 1,
344
+ endColumn: (d.column ?? 1) + (d.message?.length || 20),
345
+ }));
346
+ monaco.editor.setModelMarkers(model, 'fhcode-lint', markers);
347
+ });
348
+ }
349
+
193
350
  /** 暴露全局 API */
194
351
  global.FHMonaco = {
195
352
  init: initMonaco,
@@ -197,7 +354,10 @@
197
354
  getEditor: getEditor,
198
355
  disposeEditor: disposeEditor,
199
356
  detectLanguage: detectLanguage,
357
+ getActiveEditor: getActiveEditor,
200
358
  registerInlineCompletions: registerInlineCompletions,
201
359
  registerCompletionItems: registerCompletionItems,
360
+ registerDiagnostics: registerDiagnostics,
361
+ showLintFeedback: showLintFeedback,
202
362
  };
203
363
  })(window);