feihong-code 0.6.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/.github/workflows/release.yml +173 -0
  2. package/CHANGELOG.md +83 -454
  3. package/dist/agent/browser-agent.js +354 -0
  4. package/dist/agent/browser-agent.js.map +1 -0
  5. package/dist/agent/change-manager.js +433 -0
  6. package/dist/agent/change-manager.js.map +1 -0
  7. package/dist/agent/code-rag.js +293 -0
  8. package/dist/agent/code-rag.js.map +1 -0
  9. package/dist/agent/code-writer.js +13 -1
  10. package/dist/agent/code-writer.js.map +1 -1
  11. package/dist/agent/completion-engine.js +522 -0
  12. package/dist/agent/completion-engine.js.map +1 -0
  13. package/dist/agent/context-compactor.js +188 -10
  14. package/dist/agent/context-compactor.js.map +1 -1
  15. package/dist/agent/custom-agent.js +368 -0
  16. package/dist/agent/custom-agent.js.map +1 -0
  17. package/dist/agent/design-to-code.js +218 -0
  18. package/dist/agent/design-to-code.js.map +1 -0
  19. package/dist/agent/event-driven-agent.js +359 -0
  20. package/dist/agent/event-driven-agent.js.map +1 -0
  21. package/dist/agent/git-integration.js +324 -0
  22. package/dist/agent/git-integration.js.map +1 -0
  23. package/dist/agent/layered-memory.js +329 -0
  24. package/dist/agent/layered-memory.js.map +1 -0
  25. package/dist/agent/multi-agent.js +359 -0
  26. package/dist/agent/multi-agent.js.map +1 -0
  27. package/dist/agent/orchestrator.js +160 -22
  28. package/dist/agent/orchestrator.js.map +1 -1
  29. package/dist/agent/prompts.js +20 -0
  30. package/dist/agent/prompts.js.map +1 -1
  31. package/dist/agent/quality-gate.js +139 -0
  32. package/dist/agent/quality-gate.js.map +1 -1
  33. package/dist/agent/self-correction.js +274 -0
  34. package/dist/agent/self-correction.js.map +1 -0
  35. package/dist/agent/self-heal.js +42 -12
  36. package/dist/agent/self-heal.js.map +1 -1
  37. package/dist/agent/solo-agent.js +321 -0
  38. package/dist/agent/solo-agent.js.map +1 -0
  39. package/dist/agent/symbol-index.js +258 -28
  40. package/dist/agent/symbol-index.js.map +1 -1
  41. package/dist/agent/team-collaboration.js +252 -0
  42. package/dist/agent/team-collaboration.js.map +1 -0
  43. package/dist/agent/type-checker.js +229 -0
  44. package/dist/agent/type-checker.js.map +1 -0
  45. package/dist/cli/run.js +1 -0
  46. package/dist/cli/run.js.map +1 -1
  47. package/dist/cli/version.js +1 -1
  48. package/dist/integrations/collaboration.js +219 -0
  49. package/dist/integrations/collaboration.js.map +1 -0
  50. package/dist/integrations/figma.js +307 -0
  51. package/dist/integrations/figma.js.map +1 -0
  52. package/dist/integrations/sso.js +327 -0
  53. package/dist/integrations/sso.js.map +1 -0
  54. package/dist/knowledge/library.js +301 -0
  55. package/dist/knowledge/library.js.map +1 -0
  56. package/dist/plugins/manager.js +332 -0
  57. package/dist/plugins/manager.js.map +1 -0
  58. package/dist/runtime/event-log.js.map +1 -1
  59. package/dist/tools/browser/browser.tool.js +217 -0
  60. package/dist/tools/browser/browser.tool.js.map +1 -0
  61. package/dist/tools/index.js +2 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/tools/shell/exec.js +50 -10
  64. package/dist/tools/shell/exec.js.map +1 -1
  65. package/dist/tools/shell/run-shell.tool.js +8 -2
  66. package/dist/tools/shell/run-shell.tool.js.map +1 -1
  67. package/dist/training/completion-evaluator.js +213 -0
  68. package/dist/training/completion-evaluator.js.map +1 -0
  69. package/dist/training/fim-data.js +248 -0
  70. package/dist/training/fim-data.js.map +1 -0
  71. package/dist/voice/voice-programming.js +223 -0
  72. package/dist/voice/voice-programming.js.map +1 -0
  73. package/dist/web/public/css/style.css +190 -7
  74. package/dist/web/public/index.html +403 -9
  75. package/dist/web/public/js/api.js +191 -4
  76. package/dist/web/public/js/app.js +850 -123
  77. package/dist/web/public/js/mock-api.js +234 -0
  78. package/dist/web/public/js/monaco-editor.js +203 -0
  79. package/dist/web/public/js/ui.js +1271 -54
  80. package/dist/web/public/manifest.json +59 -0
  81. package/dist/web/public/service-worker.js +173 -0
  82. package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  83. package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  84. package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  85. package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  86. package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  87. package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  88. package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  89. package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  90. package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  91. package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  92. package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  93. package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  94. package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  95. package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  96. package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  97. package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  98. package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  99. package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  100. package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  101. package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  102. package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  103. package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  104. package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  105. package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  106. package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  107. package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +1 -0
  108. package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +1 -0
  109. package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +1 -0
  110. package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  111. package/dist/web/public/vendor/monaco/vs/editor.js +1 -0
  112. package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  113. package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  114. package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  115. package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  116. package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  117. package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  118. package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  119. package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  120. package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  121. package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  122. package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  123. package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  124. package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +1 -0
  125. package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  126. package/dist/web/public/vendor/monaco/vs/index.js +1 -0
  127. package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  128. package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  129. package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  130. package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  131. package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  132. package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  133. package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  134. package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  135. package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +66 -0
  136. package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  137. package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +443 -0
  138. package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  139. package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +17 -0
  140. package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  141. package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  142. package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  143. package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  144. package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  145. package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  146. package/dist/web/public/vendor/monaco/vs/loader.js +1368 -0
  147. package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  148. package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  149. package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  150. package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  151. package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  152. package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  153. package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  154. package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  155. package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  156. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  157. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  158. package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  159. package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  160. package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  161. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
  162. package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +9 -0
  163. package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
  164. package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +9 -0
  165. package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
  166. package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +9 -0
  167. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
  168. package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +9 -0
  169. package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
  170. package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +9 -0
  171. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
  172. package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +9 -0
  173. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
  174. package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +9 -0
  175. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
  176. package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +9 -0
  177. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
  178. package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
  179. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
  180. package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +9 -0
  181. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
  182. package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +9 -0
  183. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  184. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  185. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
  186. package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
  187. package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +1 -0
  188. package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  189. package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  190. package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  191. package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  192. package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  193. package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  194. package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  195. package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  196. package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  197. package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  198. package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  199. package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  200. package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  201. package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  202. package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  203. package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  204. package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  205. package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  206. package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  207. package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  208. package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  209. package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  210. package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  211. package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  212. package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  213. package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  214. package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  215. package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  216. package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  217. package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  218. package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  219. package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  220. package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  221. package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  222. package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  223. package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  224. package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  225. package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  226. package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  227. package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  228. package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  229. package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  230. package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  231. package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  232. package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
  233. package/dist/web/server.js +2073 -210
  234. package/dist/web/server.js.map +1 -1
  235. package/dist/web/task-queue.js +25 -5
  236. package/dist/web/task-queue.js.map +1 -1
  237. package/docs/BENCHMARK_REPORT_en.md +329 -0
  238. package/docs/BENCHMARK_REPORT_zh.md +329 -0
  239. package/docs/DEPLOYMENT.md +382 -0
  240. package/docs/MODEL_FINETUNING.md +405 -0
  241. package/docs/TECHNICAL_SPEC_en.md +292 -0
  242. package/docs/TECHNICAL_SPEC_zh.md +292 -0
  243. package/docs/UPDATE_NOTES_en.md +159 -0
  244. package/docs/UPDATE_NOTES_zh.md +159 -0
  245. package/electron/main.js +602 -0
  246. package/package.json +67 -3
  247. package/tool-schema.json +1 -1
@@ -37,7 +37,7 @@
37
37
  'chat.screenshot': '📷 截图',
38
38
  'chat.upload_file': '📄 上传文件',
39
39
  'chat.upload_image': '🖼 上传图片',
40
- 'chat.direct_mode': '🖥 直接操作电脑',
40
+ 'chat.direct_mode': '🖥 电脑操作',
41
41
  'chat.voice': '🎤 语音输入',
42
42
  'chat.agent_type': '🤖 选择智能体类型',
43
43
  'chat.agent_default': '🤖 通用助手',
@@ -49,6 +49,25 @@
49
49
  'call.voice': '📞 语音',
50
50
  'call.video': '🎥 视频',
51
51
  'call.hangup': '⛔ 挂断',
52
+ 'call.voice_call': '语音通话',
53
+ 'call.video_call': '视频通话',
54
+ 'call.voice_hint': '语音通话中,说话内容实时转文字',
55
+ 'call.video_hint': '视频通话中,可截取画面发送',
56
+ 'call.ended': '通话已结束,识别的文字已保留在输入框',
57
+ 'computer.tab': '🖥 电脑操作',
58
+ 'computer.capture': '📷 截取屏幕',
59
+ 'computer.mouse_pos': '坐标',
60
+ 'computer.click_left': '左键点击',
61
+ 'computer.click_right': '右键点击',
62
+ 'computer.double_click': '双击',
63
+ 'computer.type_hint': '输入文字后回车发送',
64
+ 'computer.screenshot_added': '摄像头画面已添加到输入框,可直接发送',
65
+ 'computer.screenshot_not_ready': '视频还没准备好,请稍等',
66
+ 'msg.copy': '📋 复制',
67
+ 'msg.create_doc': '📄 建文档',
68
+ 'msg.copied': '已复制到剪贴板',
69
+ 'msg.doc_downloaded': '文档已下载',
70
+ 'msg.no_content': '没有可复制的内容',
52
71
  'crop.tip': '按住鼠标拖拽选择截图区域 · 松开确认 · ESC 取消',
53
72
  'page.automations.sub': '把常用目标存成指令,一键发起任务',
54
73
  'btn.new_automation': '+ 新建指令',
@@ -218,7 +237,7 @@
218
237
  'chat.screenshot': '📷 Screenshot',
219
238
  'chat.upload_file': '📄 Upload File',
220
239
  'chat.upload_image': '🖼 Upload Image',
221
- 'chat.direct_mode': '🖥 Direct Control',
240
+ 'chat.direct_mode': '🖥 Computer Control',
222
241
  'chat.voice': '🎤 Voice Input',
223
242
  'chat.agent_type': '🤖 Agent Type',
224
243
  'chat.agent_default': '🤖 Assistant',
@@ -230,6 +249,25 @@
230
249
  'call.voice': '📞 Voice',
231
250
  'call.video': '🎥 Video',
232
251
  'call.hangup': '⛔ Hang Up',
252
+ 'call.voice_call': 'Voice Call',
253
+ 'call.video_call': 'Video Call',
254
+ 'call.voice_hint': 'Voice call in progress, speech transcribed in real-time',
255
+ 'call.video_hint': 'Video call in progress, capture frames to send',
256
+ 'call.ended': 'Call ended, transcribed text kept in input box',
257
+ 'computer.tab': '🖥 Computer Control',
258
+ 'computer.capture': '📷 Capture Screen',
259
+ 'computer.mouse_pos': 'Pos',
260
+ 'computer.click_left': 'Left Click',
261
+ 'computer.click_right': 'Right Click',
262
+ 'computer.double_click': 'Double Click',
263
+ 'computer.type_hint': 'Type text and press Enter to send',
264
+ 'computer.screenshot_added': 'Camera frame added to input box, ready to send',
265
+ 'computer.screenshot_not_ready': 'Video not ready yet, please wait',
266
+ 'msg.copy': '📋 Copy',
267
+ 'msg.create_doc': '📄 New Doc',
268
+ 'msg.copied': 'Copied to clipboard',
269
+ 'msg.doc_downloaded': 'Document downloaded',
270
+ 'msg.no_content': 'No content to copy',
233
271
  'crop.tip': 'Drag to select a screenshot area · Release to confirm · ESC to cancel',
234
272
  'page.automations.sub': 'Save common goals as shortcuts, launch tasks with one click',
235
273
  'btn.new_automation': '+ New Command',
@@ -440,6 +478,16 @@
440
478
  };
441
479
 
442
480
  (function initSession() {
481
+ // 提前检测登录状态,给 html 加 class,配合 CSS 避免登录页闪烁(感觉像打开两次)
482
+ try {
483
+ const urlToken = new URLSearchParams(location.search).get('token');
484
+ if (urlToken) localStorage.setItem('fhcode.token', urlToken);
485
+ const t = localStorage.getItem('fhcode.token');
486
+ const p = localStorage.getItem('fhcode.phone');
487
+ if (t && p) {
488
+ document.documentElement.classList.add('logged-in');
489
+ }
490
+ } catch(e) {}
443
491
  const urlToken = new URLSearchParams(location.search).get('token');
444
492
  if (urlToken) localStorage.setItem('fhcode.token', urlToken);
445
493
  const savedModel = localStorage.getItem('fhcode.model');
@@ -539,6 +587,40 @@
539
587
  }
540
588
  }
541
589
 
590
+ // 消息操作按钮事件委托(复制、创建文档)
591
+ document.getElementById('messages')?.addEventListener('click', (e) => {
592
+ const btn = e.target.closest('.msg-action-btn');
593
+ if (!btn) return;
594
+ e.stopPropagation();
595
+ const action = btn.getAttribute('data-action');
596
+ const msgEl = btn.closest('.msg');
597
+ if (!msgEl) return;
598
+ // 获取消息文本内容(排除操作按钮)
599
+ const actionsEl = msgEl.querySelector('.msg-actions');
600
+ let content = '';
601
+ if (actionsEl) {
602
+ // 临时移除操作按钮,获取文本,再放回去
603
+ const parent = actionsEl.parentNode;
604
+ const nextSibling = actionsEl.nextSibling;
605
+ parent.removeChild(actionsEl);
606
+ content = msgEl.textContent || '';
607
+ if (nextSibling) parent.insertBefore(actionsEl, nextSibling);
608
+ else parent.appendChild(actionsEl);
609
+ } else {
610
+ content = msgEl.textContent || '';
611
+ }
612
+ content = content.trim();
613
+ if (!content) {
614
+ toast('没有可复制的内容');
615
+ return;
616
+ }
617
+ if (action === 'copy') {
618
+ handleBubbleAction('copy', content);
619
+ } else if (action === 'create-doc') {
620
+ handleBubbleAction('doc', content);
621
+ }
622
+ });
623
+
542
624
  /* ========== 登录 ========== */
543
625
  document.getElementById('loginBtn').addEventListener('click', doLogin);
544
626
  document.getElementById('loginPhone').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
@@ -561,7 +643,7 @@
561
643
  }
562
644
  showWelcomeGuide([]);
563
645
  }
564
- await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats()]);
646
+ await Promise.allSettled([loadTasks(), loadAutomations(), loadTemplates(), loadMarket(), loadOffice(), loadWorkspace(), loadModels(), loadMemoryStats(), loadNodes(), loadSources()]);
565
647
  startRefresh();
566
648
  }
567
649
 
@@ -786,7 +868,7 @@
786
868
  // 允许只发送文件(无文本)
787
869
  if (!goal && !state.stagedFiles.length) return;
788
870
  if (!state.token) { toast('请先登录'); return; }
789
- if (state.directMode) goal = goal ? ('[直接操作电脑] ' + goal) : '[直接操作电脑](仅附件)';
871
+ if (state.directMode) goal = goal ? ('[电脑操作] ' + goal) : '[电脑操作](仅附件)';
790
872
  const btn = document.getElementById('sendBtn');
791
873
  btn.disabled = true;
792
874
  try {
@@ -883,6 +965,25 @@
883
965
  }
884
966
  document.getElementById('sendBtn').addEventListener('click', sendTask);
885
967
  document.getElementById('goalInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendTask(); });
968
+ // 粘贴图片:用户用 Win+Shift+S 截图后可直接粘贴到输入框
969
+ document.getElementById('goalInput').addEventListener('paste', (e) => {
970
+ const items = e.clipboardData?.items;
971
+ if (!items) return;
972
+ for (const item of items) {
973
+ if (item.type.startsWith('image/')) {
974
+ e.preventDefault();
975
+ const file = item.getAsFile();
976
+ if (!file) continue;
977
+ const reader = new FileReader();
978
+ reader.onload = () => {
979
+ stageFile({ name: 'paste-' + Date.now() + '.png', mime: file.type || 'image/png', dataUrl: String(reader.result) });
980
+ toast('图片已粘贴到输入框,可直接输入文字发送');
981
+ };
982
+ reader.readAsDataURL(file);
983
+ break;
984
+ }
985
+ }
986
+ });
886
987
  document.getElementById('tchRefresh').addEventListener('click', () => { refreshCurrentThread(); toast('已刷新当前任务'); });
887
988
  document.getElementById('tchStop').addEventListener('click', async () => {
888
989
  if (!state.currentTaskId) return;
@@ -894,6 +995,7 @@
894
995
  });
895
996
  document.getElementById('tchNewTask').addEventListener('click', () => { try { openNewTaskModal(); } catch { toast('新建任务面板不可用'); } });
896
997
  document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
998
+ document.getElementById('voiceBtn')?.addEventListener('click', startVoice);
897
999
 
898
1000
  /* ========== 输入框右键菜单(复制/剪切/粘贴/全选) ========== */
899
1001
  const goalInput = document.getElementById('goalInput');
@@ -1047,8 +1149,8 @@
1047
1149
  } catch (e) { toast('创建失败:' + e.message); }
1048
1150
  finally { btn.disabled = false; }
1049
1151
  }
1050
- document.getElementById('newTaskBtn')?.addEventListener('click', openNewTaskModal);
1051
- document.getElementById('tchNewTask')?.addEventListener('click', openNewTaskModal);
1152
+ document.getElementById('newTaskBtn')?.addEventListener('click', () => { switchView('chat'); startNewChat(); });
1153
+ document.getElementById('tchNewTask')?.addEventListener('click', startNewChat);
1052
1154
  document.getElementById('ntCreateBtn')?.addEventListener('click', createNewTask);
1053
1155
  document.getElementById('ntGoal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) createNewTask(); });
1054
1156
 
@@ -1076,17 +1178,63 @@
1076
1178
  });
1077
1179
 
1078
1180
 
1181
+ // 截图锁,防止重复触发
1182
+ let screenshotRunning = false;
1183
+
1079
1184
  async function takeScreenshot() {
1185
+ if (screenshotRunning) {
1186
+ toast('正在截图中,请稍候...');
1187
+ return;
1188
+ }
1189
+ screenshotRunning = true;
1080
1190
  try {
1081
1191
  const overlay = document.getElementById('cropOverlay');
1082
1192
  const selection = document.getElementById('cropSelection');
1083
1193
  const video = document.getElementById('cropVideo');
1084
- const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
1085
- video.srcObject = stream;
1086
- await video.play();
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
+ }
1233
+
1087
1234
  overlay.style.display = 'block';
1088
1235
  selection.style.display = 'none';
1089
1236
  let startX = 0, startY = 0, drawing = false;
1237
+
1090
1238
  const onDown = (e) => {
1091
1239
  drawing = true;
1092
1240
  startX = e.clientX; startY = e.clientY;
@@ -1114,36 +1262,59 @@
1114
1262
  const x = Math.min(e.clientX, startX), y = Math.min(e.clientY, startY);
1115
1263
  const w = Math.abs(e.clientX - startX), h = Math.abs(e.clientY - startY);
1116
1264
  overlay.style.display = 'none';
1117
- if (w < 8 || h < 8) { stream.getTracks().forEach((t) => t.stop()); return; }
1265
+ if (w < 8 || h < 8) {
1266
+ if (stream) stream.getTracks().forEach((t) => t.stop());
1267
+ return;
1268
+ }
1118
1269
  try {
1119
- const srcW = video.videoWidth, srcH = video.videoHeight;
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
+ }
1120
1282
  const scaleX = srcW / window.innerWidth, scaleY = srcH / window.innerHeight;
1121
1283
  const sx = Math.round(x * scaleX), sy = Math.round(y * scaleY);
1122
1284
  const sw = Math.round(w * scaleX), sh = Math.round(h * scaleY);
1123
1285
  const canvas = document.createElement('canvas');
1124
1286
  canvas.width = sw; canvas.height = sh;
1125
- canvas.getContext('2d').drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh);
1126
- stream.getTracks().forEach((t) => t.stop());
1287
+ canvas.getContext('2d').drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
1288
+ if (stream) stream.getTracks().forEach((t) => t.stop());
1127
1289
  const dataUrl = canvas.toDataURL('image/png');
1128
1290
  stageFile({ name: 'screenshot.png', mime: 'image/png', dataUrl });
1129
- toast('截图已加入暂存,点发送时一起发给 AI');
1291
+ toast('截图已添加到输入框,可直接输入文字发送');
1130
1292
  } catch (err) { toast('截图失败:' + err.message); }
1293
+ screenshotRunning = false;
1131
1294
  };
1132
1295
  const onKey = (e) => {
1133
1296
  if (e.key === 'Escape') {
1134
1297
  overlay.style.display = 'none';
1135
- stream.getTracks().forEach((t) => t.stop());
1298
+ if (stream) stream.getTracks().forEach((t) => t.stop());
1136
1299
  window.removeEventListener('mousedown', onDown);
1137
1300
  window.removeEventListener('mousemove', onMove);
1138
1301
  window.removeEventListener('mouseup', finish);
1139
1302
  window.removeEventListener('keydown', onKey);
1303
+ screenshotRunning = false;
1140
1304
  }
1141
1305
  };
1142
1306
  window.addEventListener('mousedown', onDown);
1143
1307
  window.addEventListener('mousemove', onMove);
1144
1308
  window.addEventListener('mouseup', finish);
1145
1309
  window.addEventListener('keydown', onKey);
1146
- } catch (e) { toast('截图失败:' + e.message); }
1310
+ } catch (e) {
1311
+ if (e.name === 'NotAllowedError') {
1312
+ toast('已取消截图。也可以用 Win+Shift+S 截图后直接粘贴到输入框');
1313
+ } else {
1314
+ toast('截图失败:' + e.message);
1315
+ }
1316
+ screenshotRunning = false;
1317
+ }
1147
1318
  }
1148
1319
 
1149
1320
  function handleUpload(input, mimePrefix) {
@@ -1225,48 +1396,336 @@
1225
1396
  document.getElementById('imReset').addEventListener('click', resetInlineForm);
1226
1397
  document.getElementById('modelManageClose')?.addEventListener('click', () => { document.getElementById('modelManage').style.display = 'none'; });
1227
1398
 
1228
- /* ========== 通话控件 ========== */
1399
+ /* ========== 通话控件(语音/视频通话 + 实时语音识别) ========== */
1229
1400
  let callStream = null;
1401
+ let callKind = null;
1402
+
1230
1403
  async function startCall(kind) {
1231
1404
  const status = document.getElementById('callStatus');
1232
1405
  try {
1233
- const constraints = kind === 'video' ? { audio: true, video: true } : { audio: true };
1406
+ const constraints = kind === 'video'
1407
+ ? { audio: true, video: { width: 640, height: 480 } }
1408
+ : { audio: true };
1234
1409
  callStream = await navigator.mediaDevices.getUserMedia(constraints);
1235
- status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '…';
1410
+ callKind = kind;
1411
+
1412
+ // 视频通话:显示摄像头画面
1413
+ if (kind === 'video') {
1414
+ const videoWindow = document.getElementById('videoCallWindow');
1415
+ const localVideo = document.getElementById('localVideo');
1416
+ localVideo.srcObject = callStream;
1417
+ videoWindow.style.display = 'block';
1418
+ }
1419
+
1420
+ status.textContent = (kind === 'video' ? '视频通话中' : '语音通话中') + '(语音实时转文字)';
1236
1421
  document.getElementById('hangupCallBtn').style.display = 'inline-block';
1237
- toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话(演示)');
1422
+ document.getElementById('voiceCallBtn').style.display = 'none';
1423
+ document.getElementById('videoCallBtn').style.display = 'none';
1424
+
1425
+ // 开启语音识别(复用语音输入模块)
1426
+ if (!voiceState.isListening) {
1427
+ voiceState.finalText = '';
1428
+ voiceState.interimText = '';
1429
+ voiceState.originalText = document.getElementById('goalInput').value;
1430
+ voiceState.manuallyStopped = false;
1431
+ voiceState.restartCount = 0;
1432
+ updateVoiceUI(true);
1433
+ startRecognition();
1434
+ }
1435
+
1436
+ toast('已发起' + (kind === 'video' ? '视频' : '语音') + '通话,说话内容会实时转成文字');
1238
1437
  } catch (e) {
1239
1438
  status.textContent = '';
1240
- toast('无法发起通话:' + e.message);
1439
+ if (e.name === 'NotAllowedError') {
1440
+ toast('请允许浏览器使用麦克风和摄像头权限');
1441
+ } else if (e.name === 'NotFoundError') {
1442
+ toast('未找到麦克风或摄像头设备');
1443
+ } else {
1444
+ toast('无法发起通话:' + e.message);
1445
+ }
1241
1446
  }
1242
1447
  }
1448
+
1243
1449
  function hangupCall() {
1244
- if (callStream) { callStream.getTracks().forEach((t) => t.stop()); callStream = null; }
1450
+ // 先保存识别的文字到输入框(确保内容不会丢失)
1451
+ if (voiceState.finalText) {
1452
+ updateVoiceInput();
1453
+ }
1454
+
1455
+ // 停止媒体流
1456
+ if (callStream) {
1457
+ callStream.getTracks().forEach((t) => t.stop());
1458
+ callStream = null;
1459
+ }
1460
+ callKind = null;
1461
+
1462
+ // 隐藏视频窗口
1463
+ const videoWindow = document.getElementById('videoCallWindow');
1464
+ const localVideo = document.getElementById('localVideo');
1465
+ if (videoWindow) videoWindow.style.display = 'none';
1466
+ if (localVideo) localVideo.srcObject = null;
1467
+
1468
+ // 停止语音识别
1469
+ if (voiceState.isListening) {
1470
+ stopVoiceInput();
1471
+ }
1472
+
1245
1473
  document.getElementById('callStatus').textContent = '';
1246
1474
  document.getElementById('hangupCallBtn').style.display = 'none';
1247
- toast('已挂断');
1475
+ document.getElementById('voiceCallBtn').style.display = 'inline-block';
1476
+ document.getElementById('videoCallBtn').style.display = 'inline-block';
1477
+ toast('通话已结束,识别的文字已保留在输入框');
1248
1478
  }
1479
+
1249
1480
  document.getElementById('voiceCallBtn').addEventListener('click', () => startCall('voice'));
1250
1481
  document.getElementById('videoCallBtn').addEventListener('click', () => startCall('video'));
1251
1482
  document.getElementById('hangupCallBtn').addEventListener('click', hangupCall);
1483
+ document.getElementById('videoCallClose')?.addEventListener('click', hangupCall);
1252
1484
 
1253
- function startVoice() {
1485
+ // 视频通话截图:把当前摄像头画面截取下来放到输入框
1486
+ document.getElementById('videoSnapshotBtn')?.addEventListener('click', () => {
1487
+ const video = document.getElementById('localVideo');
1488
+ if (!video || !video.videoWidth) {
1489
+ toast('视频还没准备好,请稍等');
1490
+ return;
1491
+ }
1492
+ try {
1493
+ const canvas = document.createElement('canvas');
1494
+ canvas.width = video.videoWidth;
1495
+ canvas.height = video.videoHeight;
1496
+ canvas.getContext('2d').drawImage(video, 0, 0);
1497
+ const dataUrl = canvas.toDataURL('image/png');
1498
+ stageFile({
1499
+ name: 'camera-' + Date.now() + '.png',
1500
+ mime: 'image/png',
1501
+ dataUrl: dataUrl,
1502
+ });
1503
+ toast('摄像头画面已添加到输入框,可直接发送');
1504
+ } catch (e) {
1505
+ toast('截图失败:' + e.message);
1506
+ }
1507
+ });
1508
+
1509
+ /* ========== 语音输入模块 ========== */
1510
+ const voiceState = {
1511
+ recognition: null,
1512
+ finalText: '',
1513
+ interimText: '',
1514
+ originalText: '',
1515
+ isListening: false,
1516
+ manuallyStopped: false,
1517
+ restartTimer: null,
1518
+ restartCount: 0,
1519
+ maxRestarts: 10000, // 基本无限制,长时间通话也不会停
1520
+ };
1521
+
1522
+ // 优化语音识别文本:去掉多余空格、自动加标点、清理重复
1523
+ function optimizeVoiceText(text) {
1524
+ if (!text) return '';
1525
+ let result = text.trim();
1526
+ // 去掉多余空格
1527
+ result = result.replace(/\s+/g, ' ');
1528
+ // 去掉首尾空格
1529
+ result = result.trim();
1530
+ // 中文标点优化:如果末尾是中文且没有标点,自动加句号
1531
+ if (result && /[\u4e00-\u9fa5]$/.test(result) && !/[。!?,、;:""''()]$/.test(result)) {
1532
+ result += '。';
1533
+ }
1534
+ return result;
1535
+ }
1536
+
1537
+ // 更新输入框显示
1538
+ function updateVoiceInput() {
1539
+ const input = document.getElementById('goalInput');
1540
+ const finalOptimized = optimizeVoiceText(voiceState.finalText);
1541
+ const displayText = voiceState.originalText
1542
+ ? (voiceState.originalText + ' ' + finalOptimized + voiceState.interimText)
1543
+ : (finalOptimized + voiceState.interimText);
1544
+ input.value = displayText;
1545
+ input.scrollTop = input.scrollHeight;
1546
+ }
1547
+
1548
+ // 更新UI状态
1549
+ function updateVoiceUI(listening) {
1550
+ const badge = document.getElementById('voiceBadge');
1551
+ const voiceBtn = document.getElementById('voiceBtn');
1552
+ if (listening) {
1553
+ badge.style.display = 'inline-flex';
1554
+ badge.textContent = '● 正在听…(再次点击停止)';
1555
+ if (voiceBtn) {
1556
+ voiceBtn.style.background = 'var(--brand)';
1557
+ voiceBtn.style.color = '#fff';
1558
+ voiceBtn.textContent = '● 听…';
1559
+ }
1560
+ } else {
1561
+ badge.style.display = 'none';
1562
+ if (voiceBtn) {
1563
+ voiceBtn.style.background = '';
1564
+ voiceBtn.style.color = '';
1565
+ voiceBtn.textContent = '🎤 语音';
1566
+ }
1567
+ }
1568
+ }
1569
+
1570
+ // 创建新的语音识别对象
1571
+ function createRecognition() {
1254
1572
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1255
- if (!SpeechRecognition) { toast('当前浏览器不支持语音输入'); return; }
1573
+ if (!SpeechRecognition) return null;
1574
+
1256
1575
  const rec = new SpeechRecognition();
1257
1576
  rec.lang = 'zh-CN';
1258
- rec.continuous = false;
1259
- rec.interimResults = false;
1260
- const badge = document.getElementById('voiceBadge');
1261
- badge.style.display = 'inline-flex';
1577
+ rec.continuous = true;
1578
+ rec.interimResults = true;
1579
+ rec.maxAlternatives = 1;
1580
+
1581
+ // 识别结果处理
1262
1582
  rec.onresult = (e) => {
1263
- const text = e.results[0][0].transcript;
1264
- const input = document.getElementById('goalInput');
1265
- input.value = (input.value ? input.value + ' ' : '') + text;
1583
+ let interim = '';
1584
+ let finalDelta = '';
1585
+ for (let i = e.resultIndex; i < e.results.length; i++) {
1586
+ const transcript = e.results[i][0].transcript;
1587
+ if (e.results[i].isFinal) {
1588
+ finalDelta += transcript;
1589
+ } else {
1590
+ interim += transcript;
1591
+ }
1592
+ }
1593
+ if (finalDelta) {
1594
+ voiceState.finalText += finalDelta;
1595
+ voiceState.restartCount = 0; // 有新结果,重置重启计数
1596
+ }
1597
+ voiceState.interimText = interim;
1598
+ updateVoiceInput();
1266
1599
  };
1267
- rec.onerror = (e) => { toast('语音识别失败:' + e.error); badge.style.display = 'none'; };
1268
- rec.onend = () => { badge.style.display = 'none'; };
1269
- rec.start();
1600
+
1601
+ // 错误处理
1602
+ rec.onerror = (e) => {
1603
+ console.warn('[语音识别] 错误:', e.error);
1604
+ if (e.error === 'not-allowed' || e.error === 'service-not-allowed') {
1605
+ toast('请允许浏览器使用麦克风权限(点击地址栏左侧锁图标)');
1606
+ voiceState.manuallyStopped = true;
1607
+ stopVoiceInput();
1608
+ } else if (e.error === 'network') {
1609
+ toast('语音识别网络错误!Chrome用的是Google服务,国内建议用Edge浏览器,或检查网络/代理');
1610
+ voiceState.manuallyStopped = true;
1611
+ stopVoiceInput();
1612
+ } else if (e.error === 'audio-capture') {
1613
+ toast('未检测到麦克风设备,请检查麦克风连接和系统设置');
1614
+ voiceState.manuallyStopped = true;
1615
+ stopVoiceInput();
1616
+ } else if (e.error === 'no-speech') {
1617
+ // 没检测到语音,继续等待,不停止
1618
+ console.log('[语音识别] 未检测到语音,继续等待...');
1619
+ toast('没听到声音,请检查麦克风是否开启,离麦克风近一点说话');
1620
+ } else if (e.error === 'aborted') {
1621
+ // 手动停止,不处理
1622
+ } else {
1623
+ console.warn('[语音识别] 其他错误:', e.error);
1624
+ toast('语音识别出错: ' + e.error);
1625
+ }
1626
+ };
1627
+
1628
+ // 识别结束处理
1629
+ rec.onend = () => {
1630
+ voiceState.recognition = null;
1631
+ voiceState.interimText = '';
1632
+
1633
+ // 如果不是手动停止,自动重启(基本无限制)
1634
+ if (!voiceState.manuallyStopped && voiceState.restartCount < voiceState.maxRestarts) {
1635
+ voiceState.restartCount++;
1636
+ // 只在调试时打印,避免控制台太多日志
1637
+ if (voiceState.restartCount % 50 === 0) {
1638
+ console.log(`[语音识别] 自动重启 (${voiceState.restartCount})`);
1639
+ }
1640
+ voiceState.restartTimer = setTimeout(() => {
1641
+ if (!voiceState.manuallyStopped) {
1642
+ startRecognition();
1643
+ }
1644
+ }, 50); // 50ms快速重启,减少停顿感
1645
+ } else if (voiceState.manuallyStopped) {
1646
+ // 手动停止,最终处理
1647
+ updateVoiceUI(false);
1648
+ if (voiceState.finalText) {
1649
+ updateVoiceInput();
1650
+ toast('语音输入完成,文字已在输入框中,可直接发送');
1651
+ }
1652
+ } else {
1653
+ // 超过最大重启次数(基本不会发生)
1654
+ updateVoiceUI(false);
1655
+ toast('语音输入已自动停止,可再次点击开启');
1656
+ }
1657
+ };
1658
+
1659
+ return rec;
1660
+ }
1661
+
1662
+ // 启动识别
1663
+ function startRecognition() {
1664
+ const rec = createRecognition();
1665
+ if (!rec) {
1666
+ toast('当前浏览器不支持语音输入,请使用 Chrome 或 Edge 浏览器');
1667
+ return false;
1668
+ }
1669
+ try {
1670
+ rec.start();
1671
+ voiceState.recognition = rec;
1672
+ voiceState.isListening = true;
1673
+ return true;
1674
+ } catch (e) {
1675
+ console.error('[语音识别] 启动失败:', e);
1676
+ return false;
1677
+ }
1678
+ }
1679
+
1680
+ // 停止语音输入
1681
+ function stopVoiceInput() {
1682
+ voiceState.manuallyStopped = true;
1683
+ voiceState.isListening = false;
1684
+ if (voiceState.restartTimer) {
1685
+ clearTimeout(voiceState.restartTimer);
1686
+ voiceState.restartTimer = null;
1687
+ }
1688
+ if (voiceState.recognition) {
1689
+ try { voiceState.recognition.stop(); } catch(e) {}
1690
+ voiceState.recognition = null;
1691
+ }
1692
+ updateVoiceUI(false);
1693
+ }
1694
+
1695
+ // 开始/停止语音输入(入口函数)
1696
+ function startVoice() {
1697
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1698
+ if (!SpeechRecognition) {
1699
+ toast('当前环境不支持语音输入,请使用 Chrome 或 Edge 浏览器打开网页版');
1700
+ return;
1701
+ }
1702
+
1703
+ // 如果正在识别,就停止
1704
+ if (voiceState.isListening) {
1705
+ stopVoiceInput();
1706
+ toast('已停止语音输入');
1707
+ return;
1708
+ }
1709
+
1710
+ // Electron 桌面版特殊提示
1711
+ if (window.isElectron) {
1712
+ toast('桌面版语音输入可能不稳定,建议用网页版(Edge浏览器)');
1713
+ }
1714
+
1715
+ // 开始识别
1716
+ voiceState.finalText = '';
1717
+ voiceState.interimText = '';
1718
+ voiceState.originalText = document.getElementById('goalInput').value;
1719
+ voiceState.manuallyStopped = false;
1720
+ voiceState.restartCount = 0;
1721
+
1722
+ updateVoiceUI(true);
1723
+ toast('语音输入已开启,请说话,再次点击停止');
1724
+
1725
+ if (!startRecognition()) {
1726
+ updateVoiceUI(false);
1727
+ toast('启动语音输入失败,请刷新页面重试');
1728
+ }
1270
1729
  }
1271
1730
 
1272
1731
  /* ========== 智能体选择 ========== */
@@ -1298,92 +1757,219 @@
1298
1757
  });
1299
1758
 
1300
1759
  let fpCurrent = '';
1301
- async function openFolderPicker() {
1302
- console.log('[folderPicker] 打开文件夹选择器');
1303
- console.log('[folderPicker] 当前 state.workspaceDir:', state.workspaceDir);
1304
- console.log('[folderPicker] 当前 state.token:', state.token ? '已登录' : '未登录');
1305
-
1306
- // 如果 workspaceDir 为空,尝试从 localStorage 恢复或询问用户
1307
- let initialDir = state.workspaceDir;
1308
- if (!initialDir) {
1309
- // 尝试从 localStorage 恢复
1310
- const savedCwd = localStorage.getItem('fhcode.workspaceDir');
1311
- if (savedCwd) {
1312
- initialDir = savedCwd;
1313
- console.log('[folderPicker] 从 localStorage 恢复工作区:', initialDir);
1314
- } else {
1315
- // 使用默认路径
1316
- initialDir = '.';
1317
- console.log('[folderPicker] 使用默认路径:', initialDir);
1318
- }
1319
- }
1760
+ let fpSelected = '';
1320
1761
 
1321
- fpCurrent = initialDir || '.';
1322
- const fpPathEl = document.getElementById('fpPath');
1323
- if (fpPathEl) fpPathEl.textContent = fpCurrent || '(根目录)';
1762
+ async function openFolderPicker() {
1763
+ fpCurrent = '';
1764
+ fpSelected = '';
1765
+ document.getElementById('fpDrivesView').style.display = '';
1766
+ document.getElementById('fpFoldersView').style.display = 'none';
1767
+ document.getElementById('fpMkdirBar').style.display = 'none';
1768
+ document.getElementById('fpRenameBar').style.display = 'none';
1769
+ document.getElementById('fpConfirm').disabled = true;
1770
+ document.getElementById('fpRenameFolder').disabled = true;
1771
+ document.getElementById('fpTitle').textContent = '📂 选择驱动器';
1772
+ openModal('folderPickerModal');
1773
+ await loadDrives();
1774
+ }
1324
1775
 
1776
+ async function loadDrives() {
1325
1777
  try {
1326
- console.log('[folderPicker] 加载目录列表:', fpCurrent);
1327
- await loadFolderPicker(fpCurrent);
1328
- console.log('[folderPicker] 目录加载成功');
1778
+ const d = await api('/api/drives');
1779
+ const drives = d.drives || [];
1780
+ const grid = document.getElementById('fpDrivesGrid');
1781
+ grid.innerHTML = drives.map(drv => {
1782
+ const label = drv.replace('\\', '');
1783
+ return '<div class="fp-drive-card" data-drive="' + escapeHtml(drv) + '" style="padding:16px;text-align:center;border:1px solid var(--line);border-radius:8px;cursor:pointer;transition:all .15s;">'
1784
+ + '<div style="font-size:28px;margin-bottom:6px;">💽</div>'
1785
+ + '<div style="font-weight:600;font-size:14px;">' + escapeHtml(label) + '</div>'
1786
+ + '<div style="font-size:11px;color:var(--ink-2);margin-top:2px;">本地磁盘</div>'
1787
+ + '</div>';
1788
+ }).join('');
1789
+ grid.querySelectorAll('.fp-drive-card').forEach(el => {
1790
+ el.addEventListener('click', () => selectDrive(el.getAttribute('data-drive')));
1791
+ el.addEventListener('mouseenter', () => { el.style.borderColor = 'var(--brand)'; el.style.background = 'var(--brand-soft)'; });
1792
+ el.addEventListener('mouseleave', () => { el.style.borderColor = 'var(--line)'; el.style.background = ''; });
1793
+ });
1329
1794
  } catch (e) {
1330
- console.error('[folderPicker] 加载失败:', e);
1331
- toast('读取目录失败:' + e.message);
1332
- return;
1795
+ toast('加载驱动器失败:' + e.message);
1333
1796
  }
1334
- openModal('folderPickerModal');
1335
- console.log('[folderPicker] 模态框已打开');
1336
1797
  }
1337
- async function loadFolderPicker(dir) {
1338
- console.log('[folderPicker] 加载:', dir);
1339
- const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
1340
- fpCurrent = d.cwd;
1341
- const fpPathEl = document.getElementById('fpPath');
1342
- if (fpPathEl) fpPathEl.textContent = fpCurrent;
1343
- const tree = document.getElementById('fpTree');
1344
- if (!tree) {
1345
- console.error('[folderPicker] fpTree 元素不存在');
1798
+
1799
+ async function selectDrive(drive) {
1800
+ fpCurrent = drive;
1801
+ fpSelected = '';
1802
+ document.getElementById('fpDrivesView').style.display = 'none';
1803
+ document.getElementById('fpFoldersView').style.display = '';
1804
+ document.getElementById('fpTitle').textContent = '📂 选择文件夹';
1805
+ document.getElementById('fpConfirm').disabled = true;
1806
+ document.getElementById('fpRenameFolder').disabled = true;
1807
+ await loadFolderPicker(drive);
1808
+ }
1809
+
1810
+ function backToDrives() {
1811
+ fpCurrent = '';
1812
+ fpSelected = '';
1813
+ document.getElementById('fpDrivesView').style.display = '';
1814
+ document.getElementById('fpFoldersView').style.display = 'none';
1815
+ document.getElementById('fpMkdirBar').style.display = 'none';
1816
+ document.getElementById('fpRenameBar').style.display = 'none';
1817
+ document.getElementById('fpTitle').textContent = '📂 选择驱动器';
1818
+ document.getElementById('fpConfirm').disabled = true;
1819
+ document.getElementById('fpRenameFolder').disabled = true;
1820
+ }
1821
+
1822
+ async function upDir() {
1823
+ if (!fpCurrent) return;
1824
+ // 驱动器根目录不能再往上
1825
+ if (/^[A-Za-z]:\\$/.test(fpCurrent)) {
1826
+ backToDrives();
1346
1827
  return;
1347
1828
  }
1348
- const parentPath = getDirectoryName(d.cwd);
1349
- // 如果在驱动器根目录,不显示"..",改为显示驱动器列表
1350
- const isDriveRoot = /^[A-Za-z]:\\$/.test(d.cwd);
1351
- let upButtonHtml = '';
1352
- if (!isDriveRoot && parentPath && parentPath !== d.cwd) {
1353
- upButtonHtml = '<div class="file-item" data-path="' + escapeHtml(parentPath) + '" data-type="up">⬆ ..</div>';
1354
- } else if (isDriveRoot) {
1355
- // 动态获取可用驱动器列表
1356
- const drives = await getDrivesList();
1357
- upButtonHtml = '<div class="file-item drive-switcher">' +
1358
- '<span>🖥 切换驱动器</span>' +
1359
- drives.map(d => '<div class="drive-option" data-drive="' + d + '">' + d + '</div>').join('') +
1360
- '</div>';
1829
+ const parent = getDirectoryName(fpCurrent);
1830
+ if (parent && parent !== fpCurrent) {
1831
+ fpCurrent = parent;
1832
+ fpSelected = '';
1833
+ document.getElementById('fpConfirm').disabled = true;
1834
+ document.getElementById('fpRenameFolder').disabled = true;
1835
+ await loadFolderPicker(fpCurrent);
1836
+ }
1837
+ }
1838
+
1839
+ async function loadFolderPicker(dir) {
1840
+ try {
1841
+ const d = await api('/api/workspace/list?path=' + encodeURIComponent(dir));
1842
+ fpCurrent = d.cwd;
1843
+ const pathEl = document.getElementById('fpPath');
1844
+ pathEl.textContent = d.cwd;
1845
+ pathEl.title = d.cwd;
1846
+ const tree = document.getElementById('fpTree');
1847
+ // 只显示文件夹,不显示文件
1848
+ const dirs = (d.entries || []).filter(e => e.type === 'dir');
1849
+ if (dirs.length === 0) {
1850
+ tree.innerHTML = '<div style="padding:20px;text-align:center;color:var(--ink-2);font-size:13px;">此目录下没有文件夹</div>';
1851
+ } else {
1852
+ tree.innerHTML = dirs.map(e => {
1853
+ const selected = fpSelected === e.path ? 'background:var(--brand-soft);border-color:var(--brand);' : '';
1854
+ return '<div class="file-item fp-folder-item" data-path="' + escapeHtml(e.path) + '" data-name="' + escapeHtml(e.name) + '" style="' + selected + 'padding:8px 10px;border:1px solid transparent;border-radius:6px;cursor:pointer;display:flex;align-items:center;gap:8px;">'
1855
+ + '<span style="font-size:16px;">📁</span>'
1856
+ + '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(e.name) + '</span>'
1857
+ + '</div>';
1858
+ }).join('');
1859
+ }
1860
+ tree.querySelectorAll('.fp-folder-item').forEach(el => {
1861
+ el.addEventListener('click', (e) => {
1862
+ e.stopPropagation();
1863
+ const path = el.getAttribute('data-path');
1864
+ const name = el.getAttribute('data-name');
1865
+ // 单击选中
1866
+ fpSelected = path;
1867
+ document.getElementById('fpConfirm').disabled = false;
1868
+ document.getElementById('fpRenameFolder').disabled = false;
1869
+ document.getElementById('fpRenameName').value = name;
1870
+ // 高亮选中项
1871
+ tree.querySelectorAll('.fp-folder-item').forEach(item => {
1872
+ item.style.background = '';
1873
+ item.style.borderColor = 'transparent';
1874
+ });
1875
+ el.style.background = 'var(--brand-soft)';
1876
+ el.style.borderColor = 'var(--brand)';
1877
+ });
1878
+ el.addEventListener('dblclick', () => {
1879
+ const path = el.getAttribute('data-path');
1880
+ fpSelected = '';
1881
+ document.getElementById('fpConfirm').disabled = true;
1882
+ document.getElementById('fpRenameFolder').disabled = true;
1883
+ loadFolderPicker(path);
1884
+ });
1885
+ });
1886
+ } catch (e) {
1887
+ toast('读取目录失败:' + e.message);
1361
1888
  }
1362
- tree.innerHTML = upButtonHtml +
1363
- d.entries.map((e) => '<div class="file-item ' + (e.type === 'dir' ? 'dir' : '') + '" data-path="' + escapeHtml(e.path) + '" data-type="' + e.type + '">' + (e.type === 'dir' ? '📁' : '📄') + ' ' + escapeHtml(e.name) + '</div>').join('');
1364
- console.log('[folderPicker] 目录渲染完成, 条目数:', d.entries.length);
1365
1889
  }
1366
- // 驱动器列表缓存
1367
- let _drivesCache = null;
1368
1890
 
1369
1891
  document.getElementById('fpConfirm')?.addEventListener('click', async () => {
1370
- console.log('[folderPicker] 点击确认, 当前选择:', fpCurrent);
1892
+ if (!fpSelected) { toast('请先选择一个文件夹'); return; }
1371
1893
  try {
1372
- console.log('[folderPicker] 提交切换工作区...');
1373
- const result = await api('/api/workspace', 'POST', { cwd: fpCurrent });
1374
- console.log('[folderPicker] API 返回:', result);
1375
- state.workspaceDir = fpCurrent;
1376
- // 持久化到 localStorage
1377
- localStorage.setItem('fhcode.workspaceDir', fpCurrent);
1894
+ const result = await api('/api/workspace', 'POST', { cwd: fpSelected });
1895
+ state.workspaceDir = result.cwd;
1896
+ localStorage.setItem('fhcode.workspaceDir', result.cwd);
1378
1897
  renderWorkspaceBar();
1379
1898
  closeModal('folderPickerModal');
1380
- toast('已切换工作区:' + fpCurrent);
1899
+ toast('已切换工作区:' + result.cwd);
1381
1900
  } catch (e) {
1382
- console.error('[folderPicker] 切换失败:', e);
1383
1901
  toast('设置失败:' + e.message);
1384
1902
  }
1385
1903
  });
1386
1904
 
1905
+ // 返回驱动器选择
1906
+ document.getElementById('fpBackToDrives')?.addEventListener('click', backToDrives);
1907
+ // 返回上级目录
1908
+ document.getElementById('fpUpDir')?.addEventListener('click', upDir);
1909
+
1910
+ // 新建文件夹
1911
+ document.getElementById('fpNewFolder')?.addEventListener('click', () => {
1912
+ document.getElementById('fpMkdirBar').style.display = 'flex';
1913
+ document.getElementById('fpRenameBar').style.display = 'none';
1914
+ document.getElementById('fpMkdirName').value = '';
1915
+ setTimeout(() => document.getElementById('fpMkdirName').focus(), 50);
1916
+ });
1917
+ document.getElementById('fpMkdirCancel')?.addEventListener('click', () => {
1918
+ document.getElementById('fpMkdirBar').style.display = 'none';
1919
+ });
1920
+ document.getElementById('fpMkdirConfirm')?.addEventListener('click', async () => {
1921
+ const name = document.getElementById('fpMkdirName').value.trim();
1922
+ if (!name) { toast('请输入文件夹名称'); return; }
1923
+ try {
1924
+ const result = await api('/api/workspace/mkdir', 'POST', { parent: fpCurrent, name });
1925
+ toast('文件夹已创建:' + name);
1926
+ document.getElementById('fpMkdirBar').style.display = 'none';
1927
+ fpSelected = result.path;
1928
+ await loadFolderPicker(fpCurrent);
1929
+ document.getElementById('fpConfirm').disabled = false;
1930
+ document.getElementById('fpRenameFolder').disabled = false;
1931
+ } catch (e) {
1932
+ toast('创建失败:' + e.message);
1933
+ }
1934
+ });
1935
+ document.getElementById('fpMkdirName')?.addEventListener('keydown', (e) => {
1936
+ if (e.key === 'Enter') document.getElementById('fpMkdirConfirm').click();
1937
+ if (e.key === 'Escape') document.getElementById('fpMkdirCancel').click();
1938
+ });
1939
+
1940
+ // 重命名文件夹
1941
+ document.getElementById('fpRenameFolder')?.addEventListener('click', () => {
1942
+ if (!fpSelected) { toast('请先选择一个文件夹'); return; }
1943
+ document.getElementById('fpRenameBar').style.display = 'flex';
1944
+ document.getElementById('fpMkdirBar').style.display = 'none';
1945
+ const name = fpSelected.split(/[\\/]/).filter(Boolean).pop() || '';
1946
+ document.getElementById('fpRenameName').value = name;
1947
+ setTimeout(() => document.getElementById('fpRenameName').focus(), 50);
1948
+ });
1949
+ document.getElementById('fpRenameCancel')?.addEventListener('click', () => {
1950
+ document.getElementById('fpRenameBar').style.display = 'none';
1951
+ });
1952
+ document.getElementById('fpRenameConfirm')?.addEventListener('click', async () => {
1953
+ const newName = document.getElementById('fpRenameName').value.trim();
1954
+ if (!newName) { toast('请输入新名称'); return; }
1955
+ if (!fpSelected) { toast('请先选择一个文件夹'); return; }
1956
+ try {
1957
+ const result = await api('/api/workspace/rename', 'POST', { path: fpSelected, newName });
1958
+ toast('文件夹已重命名为:' + newName);
1959
+ document.getElementById('fpRenameBar').style.display = 'none';
1960
+ fpSelected = result.path;
1961
+ await loadFolderPicker(fpCurrent);
1962
+ document.getElementById('fpConfirm').disabled = false;
1963
+ document.getElementById('fpRenameFolder').disabled = false;
1964
+ } catch (e) {
1965
+ toast('重命名失败:' + e.message);
1966
+ }
1967
+ });
1968
+ document.getElementById('fpRenameName')?.addEventListener('keydown', (e) => {
1969
+ if (e.key === 'Enter') document.getElementById('fpRenameConfirm').click();
1970
+ if (e.key === 'Escape') document.getElementById('fpRenameCancel').click();
1971
+ });
1972
+
1387
1973
  /* ========== 自动化、模板、市场、办公助理 ========== */
1388
1974
  document.getElementById('newAutoBtn')?.addEventListener('click', () => openModal('autoModal'));
1389
1975
  document.getElementById('autoSaveBtn')?.addEventListener('click', async () => {
@@ -1423,30 +2009,76 @@
1423
2009
  document.getElementById('marketSearch')?.addEventListener('input', () => { clearTimeout(marketTimer); marketTimer = setTimeout(loadMarket, 400); });
1424
2010
  document.getElementById('marketSource')?.addEventListener('change', loadMarket);
1425
2011
 
2012
+ /* ========== 节点管理 & 自定义来源 ========== */
2013
+ document.getElementById('addNodeBtn')?.addEventListener('click', () => openNodeModal());
2014
+ document.getElementById('addSourceBtn')?.addEventListener('click', () => openSourceModal());
2015
+ document.getElementById('nodeSaveBtn')?.addEventListener('click', async () => {
2016
+ const id = document.getElementById('nodeEditId').value || '';
2017
+ const name = document.getElementById('nodeName').value.trim();
2018
+ const type = document.getElementById('nodeType').value;
2019
+ const url = document.getElementById('nodeUrl').value.trim();
2020
+ const apiKey = document.getElementById('nodeApiKey').value;
2021
+ const capabilities = [];
2022
+ if (document.getElementById('nodeCapTemplates').checked) capabilities.push('templates');
2023
+ if (document.getElementById('nodeCapSkills').checked) capabilities.push('skills');
2024
+ if (document.getElementById('nodeCapOffice').checked) capabilities.push('office');
2025
+ if (!name || !url) { toast('请填写名称和地址'); return; }
2026
+ if (!capabilities.length) { toast('请至少选择一种能力'); return; }
2027
+ const data = { name, type, url, capabilities };
2028
+ if (apiKey) data.apiKey = apiKey;
2029
+ if (id) data.id = id;
2030
+ await saveNode(data);
2031
+ closeModal('nodeModal');
2032
+ });
2033
+ document.getElementById('sourceSaveBtn')?.addEventListener('click', async () => {
2034
+ const id = document.getElementById('sourceEditId').value || '';
2035
+ const name = document.getElementById('sourceName').value.trim();
2036
+ const type = document.getElementById('sourceType').value;
2037
+ const url = document.getElementById('sourceUrl').value.trim();
2038
+ if (!name || !url) { toast('请填写名称和地址'); return; }
2039
+ const data = { name, type, url };
2040
+ if (id) data.id = id;
2041
+ await saveSource(data);
2042
+ closeModal('sourceModal');
2043
+ // 刷新相关数据
2044
+ if (type === 'templates') await loadTemplates();
2045
+ else if (type === 'skills') await loadMarket();
2046
+ else if (type === 'office') await loadOffice();
2047
+ });
2048
+
2049
+ function openNodeModal(node) {
2050
+ document.getElementById('nodeModalTitle').textContent = node ? '编辑节点' : '添加节点';
2051
+ document.getElementById('nodeEditId').value = node?.id || '';
2052
+ document.getElementById('nodeName').value = node?.name || '';
2053
+ document.getElementById('nodeType').value = node?.type || 'http';
2054
+ document.getElementById('nodeUrl').value = node?.url || '';
2055
+ document.getElementById('nodeApiKey').value = '';
2056
+ document.getElementById('nodeCapTemplates').checked = node?.capabilities?.includes('templates') ?? true;
2057
+ document.getElementById('nodeCapSkills').checked = node?.capabilities?.includes('skills') ?? true;
2058
+ document.getElementById('nodeCapOffice').checked = node?.capabilities?.includes('office') ?? true;
2059
+ openModal('nodeModal');
2060
+ }
2061
+ function editNode(id) {
2062
+ const node = (window._nodesCache || []).find((n) => n.id === id);
2063
+ openNodeModal(node || { id });
2064
+ }
2065
+ function openSourceModal(source) {
2066
+ document.getElementById('sourceModalTitle').textContent = source ? '编辑自定义来源' : '添加自定义来源';
2067
+ document.getElementById('sourceEditId').value = source?.id || '';
2068
+ document.getElementById('sourceName').value = source?.name || '';
2069
+ document.getElementById('sourceType').value = source?.type || 'templates';
2070
+ document.getElementById('sourceUrl').value = source?.url || '';
2071
+ openModal('sourceModal');
2072
+ }
2073
+ function editSource(id) {
2074
+ const source = (window._sourcesCache || []).find((s) => s.id === id);
2075
+ openSourceModal(source || { id });
2076
+ }
2077
+
1426
2078
 
1427
2079
  document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', () => closeModal(el.getAttribute('data-close'))));
1428
2080
  document.querySelectorAll('.mask').forEach((m) => m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('show'); }));
1429
- // 文件夹选择器点击事件(只绑定一次,避免累积)
1430
- document.getElementById('fpTree')?.addEventListener('click', async (e) => {
1431
- const driveOption = e.target.closest('.drive-option');
1432
- if (driveOption) {
1433
- e.stopPropagation();
1434
- const drive = driveOption.getAttribute('data-drive');
1435
- console.log('[folderPicker] 选择驱动器:', drive);
1436
- try { await loadFolderPicker(drive); } catch (err) { toast('读取失败:' + err.message); }
1437
- return;
1438
- }
1439
- const item = e.target.closest('.file-item');
1440
- if (!item) return;
1441
- const type = item.getAttribute('data-type');
1442
- const path = item.getAttribute('data-path');
1443
- console.log('[folderPicker] 点击:', type, path);
1444
- if (type === 'dir' || type === 'up') {
1445
- try { await loadFolderPicker(path); } catch (e) { toast('读取失败:' + e.message); }
1446
- } else if (type) {
1447
- previewFile(path);
1448
- }
1449
- });
2081
+ // 文件夹选择器事件已在 loadFolderPicker / loadDrives 内直接绑定,此处保留空壳避免旧引用报错
1450
2082
  function startRefresh() {
1451
2083
  setInterval(async () => {
1452
2084
  await loadTasks();
@@ -1457,9 +2089,104 @@
1457
2089
  await refreshCurrentThread();
1458
2090
  }
1459
2091
  }
1460
- }, 3000);
2092
+ }, 1000); // 1秒轮询,确保思考过程实时显示
2093
+ }
2094
+
2095
+ /* ========== 电脑操作(鼠标/键盘/截图) ========== */
2096
+ let selectedMousePos = { x: 0, y: 0 };
2097
+
2098
+ // 截取屏幕
2099
+ async function captureScreen() {
2100
+ try {
2101
+ const d = await api('/api/computer/screenshot', 'POST', {});
2102
+ if (d.ok && d.image) {
2103
+ const preview = document.getElementById('screenPreview');
2104
+ preview.innerHTML = `<img id="screenImg" src="${d.image}" alt="屏幕截图" />`;
2105
+ const img = document.getElementById('screenImg');
2106
+ // 点击图片获取坐标
2107
+ img.addEventListener('click', (e) => {
2108
+ const rect = img.getBoundingClientRect();
2109
+ const scaleX = (d.width || 1920) / rect.width;
2110
+ const scaleY = (d.height || 1080) / rect.height;
2111
+ selectedMousePos.x = Math.round((e.clientX - rect.left) * scaleX);
2112
+ selectedMousePos.y = Math.round((e.clientY - rect.top) * scaleY);
2113
+ document.getElementById('mousePos').textContent = `坐标: ${selectedMousePos.x}, ${selectedMousePos.y}`;
2114
+ toast(`已选择坐标: ${selectedMousePos.x}, ${selectedMousePos.y},点击操作按钮执行`);
2115
+ });
2116
+ toast('屏幕截图已更新,点击图片选择坐标');
2117
+ } else {
2118
+ toast('截图失败: ' + (d.error || '未知错误'));
2119
+ }
2120
+ } catch (e) {
2121
+ toast('截图失败: ' + e.message);
2122
+ }
1461
2123
  }
1462
2124
 
2125
+ // 鼠标点击
2126
+ async function mouseClick(button, doubleClick = false) {
2127
+ try {
2128
+ const d = await api('/api/computer/mouse/click', 'POST', {
2129
+ button,
2130
+ x: selectedMousePos.x,
2131
+ y: selectedMousePos.y,
2132
+ double: doubleClick,
2133
+ });
2134
+ if (d.ok) {
2135
+ toast(`${doubleClick ? '双击' : (button === 'right' ? '右键点击' : '左键点击')} (${selectedMousePos.x}, ${selectedMousePos.y})`);
2136
+ } else {
2137
+ toast('操作失败: ' + (d.error || '未知错误'));
2138
+ }
2139
+ } catch (e) {
2140
+ toast('操作失败: ' + e.message);
2141
+ }
2142
+ }
2143
+
2144
+ // 输入文字
2145
+ async function typeText(text) {
2146
+ try {
2147
+ const d = await api('/api/computer/keyboard/type', 'POST', { text });
2148
+ if (d.ok) {
2149
+ toast('已输入文字');
2150
+ } else {
2151
+ toast('输入失败: ' + (d.error || '未知错误'));
2152
+ }
2153
+ } catch (e) {
2154
+ toast('输入失败: ' + e.message);
2155
+ }
2156
+ }
2157
+
2158
+ // 按键
2159
+ async function pressKey(key) {
2160
+ try {
2161
+ const d = await api('/api/computer/keyboard/press', 'POST', { key });
2162
+ if (d.ok) {
2163
+ toast('已按键: ' + key);
2164
+ } else {
2165
+ toast('按键失败: ' + (d.error || '未知错误'));
2166
+ }
2167
+ } catch (e) {
2168
+ toast('按键失败: ' + e.message);
2169
+ }
2170
+ }
2171
+
2172
+ // 绑定电脑操作事件
2173
+ document.getElementById('screenCaptureBtn')?.addEventListener('click', captureScreen);
2174
+ document.getElementById('clickLeftBtn')?.addEventListener('click', () => mouseClick('left'));
2175
+ document.getElementById('clickRightBtn')?.addEventListener('click', () => mouseClick('right'));
2176
+ document.getElementById('doubleClickBtn')?.addEventListener('click', () => mouseClick('left', true));
2177
+ document.getElementById('typeTextInput')?.addEventListener('keydown', (e) => {
2178
+ if (e.key === 'Enter') {
2179
+ const text = e.target.value.trim();
2180
+ if (text) {
2181
+ typeText(text);
2182
+ e.target.value = '';
2183
+ }
2184
+ }
2185
+ });
2186
+ document.querySelectorAll('.computer-actions [data-key]').forEach((btn) => {
2187
+ btn.addEventListener('click', () => pressKey(btn.getAttribute('data-key') || ''));
2188
+ });
2189
+
1463
2190
  /* ========== 记忆系统 ========== */
1464
2191
 
1465
2192