@huicai/cli 2026.814.0 → 2026.1021.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 (493) hide show
  1. package/LICENSE +391 -32
  2. package/README.md +139 -8
  3. package/bin/huicai.js +155 -74
  4. package/bin/update.js +97 -0
  5. package/package.json +27 -8
  6. package/skills/ab-testing/trigger-cases.json +30 -0
  7. package/skills/academic-writing/trigger-cases.json +30 -0
  8. package/skills/accounting-ops/trigger-cases.json +30 -0
  9. package/skills/ai-agent/trigger-cases.json +30 -0
  10. package/skills/ai-aiops/trigger-cases.json +30 -0
  11. package/skills/ai-coding/SKILL.md +2 -0
  12. package/skills/ai-coding/trigger-cases.json +30 -0
  13. package/skills/ai-compliance/trigger-cases.json +30 -0
  14. package/skills/ai-eval/trigger-cases.json +30 -0
  15. package/skills/ai-llm-app/trigger-cases.json +30 -0
  16. package/skills/ai-mlops/trigger-cases.json +30 -0
  17. package/skills/ai-multimodal/trigger-cases.json +30 -0
  18. package/skills/ai-prompt/trigger-cases.json +30 -0
  19. package/skills/ai-rag/trigger-cases.json +30 -0
  20. package/skills/anomaly-detection/trigger-cases.json +30 -0
  21. package/skills/api-design/trigger-cases.json +31 -0
  22. package/skills/api-integration/trigger-cases.json +31 -0
  23. package/skills/arch-design/trigger-cases.json +12 -0
  24. package/skills/arch-diagram/SKILL.md +118 -0
  25. package/skills/arch-diagram/trigger-cases.json +30 -0
  26. package/skills/arch-distributed/trigger-cases.json +30 -0
  27. package/skills/arch-multitenant/trigger-cases.json +30 -0
  28. package/skills/audio-brief/SKILL.md +65 -0
  29. package/skills/audio-brief/trigger-cases.json +30 -0
  30. package/skills/audit-assist/trigger-cases.json +30 -0
  31. package/skills/audit-chain/trigger-cases.json +31 -0
  32. package/skills/banking-credit/trigger-cases.json +30 -0
  33. package/skills/brand-design/trigger-cases.json +30 -0
  34. package/skills/business-plan/trigger-cases.json +30 -0
  35. package/skills/capital-ops/trigger-cases.json +30 -0
  36. package/skills/cicd-pipeline/trigger-cases.json +31 -0
  37. package/skills/code-archaeology/SKILL.md +55 -0
  38. package/skills/code-archaeology/trigger-cases.json +9 -0
  39. package/skills/code-quality/SKILL.md +138 -0
  40. package/skills/code-quality/references/languages.md +107 -0
  41. package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
  42. package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
  43. package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
  44. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
  45. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
  46. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
  47. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
  48. package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
  49. package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
  50. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
  51. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
  52. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
  53. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
  54. package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
  55. package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
  56. package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
  57. package/skills/code-quality/scripts/code_battery.py +1072 -0
  58. package/skills/code-quality/scripts/code_check.py +1269 -0
  59. package/skills/code-quality/scripts/code_csharp.py +371 -0
  60. package/skills/code-quality/scripts/code_fix.py +201 -0
  61. package/skills/code-quality/scripts/code_java.py +695 -0
  62. package/skills/code-quality/scripts/code_jsts.py +313 -0
  63. package/skills/code-quality/scripts/code_lens.py +1079 -0
  64. package/skills/code-quality/scripts/code_python.py +536 -0
  65. package/skills/code-quality/scripts/code_testplan.py +239 -0
  66. package/skills/code-quality/trigger-cases.json +45 -0
  67. package/skills/code-review/SKILL.md +24 -0
  68. package/skills/code-review/trigger-cases.json +35 -0
  69. package/skills/commit-and-pr/trigger-cases.json +31 -0
  70. package/skills/content-creation/trigger-cases.json +30 -0
  71. package/skills/corporate-governance/trigger-cases.json +30 -0
  72. package/skills/crawl-chain/trigger-cases.json +31 -0
  73. package/skills/customer-service/trigger-cases.json +30 -0
  74. package/skills/data-analysis/trigger-cases.json +30 -0
  75. package/skills/data-chain/SKILL.md +1 -1
  76. package/skills/data-chain/trigger-cases.json +31 -0
  77. package/skills/data-governance/trigger-cases.json +30 -0
  78. package/skills/data-report-pro/trigger-cases.json +30 -0
  79. package/skills/data-streaming/trigger-cases.json +30 -0
  80. package/skills/data-wrangling/SKILL.md +1 -1
  81. package/skills/data-wrangling/trigger-cases.json +30 -0
  82. package/skills/db-es/trigger-cases.json +30 -0
  83. package/skills/db-migration/trigger-cases.json +30 -0
  84. package/skills/db-mysql/trigger-cases.json +30 -0
  85. package/skills/db-postgres/trigger-cases.json +30 -0
  86. package/skills/db-redis/trigger-cases.json +30 -0
  87. package/skills/debug-root-cause/trigger-cases.json +30 -0
  88. package/skills/dependency-audit/SKILL.md +56 -0
  89. package/skills/dependency-audit/trigger-cases.json +7 -0
  90. package/skills/dependency-upgrade/trigger-cases.json +31 -0
  91. package/skills/dev-cpp/trigger-cases.json +30 -0
  92. package/skills/dev-cross-platform/trigger-cases.json +30 -0
  93. package/skills/dev-csharp/SKILL.md +23 -0
  94. package/skills/dev-csharp/trigger-cases.json +30 -0
  95. package/skills/dev-frontend/SKILL.md +19 -0
  96. package/skills/dev-frontend/trigger-cases.json +30 -0
  97. package/skills/dev-go/trigger-cases.json +30 -0
  98. package/skills/dev-harmony/trigger-cases.json +30 -0
  99. package/skills/dev-java/SKILL.md +20 -0
  100. package/skills/dev-java/trigger-cases.json +30 -0
  101. package/skills/dev-mobile/trigger-cases.json +30 -0
  102. package/skills/dev-node/trigger-cases.json +30 -0
  103. package/skills/dev-php/trigger-cases.json +30 -0
  104. package/skills/dev-python/SKILL.md +21 -0
  105. package/skills/dev-python/trigger-cases.json +30 -0
  106. package/skills/dev-rust/trigger-cases.json +30 -0
  107. package/skills/dev-toolchain/trigger-cases.json +31 -0
  108. package/skills/doc-chain/trigger-cases.json +31 -0
  109. package/skills/doc-compare/trigger-cases.json +30 -0
  110. package/skills/docx-author/SKILL.md +127 -25
  111. package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
  112. package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
  113. package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
  114. package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
  115. package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
  116. package/skills/docx-author/scripts/docx_battery.py +586 -0
  117. package/skills/docx-author/scripts/docx_build.py +995 -0
  118. package/skills/docx-author/scripts/docx_check.py +1306 -0
  119. package/skills/docx-author/scripts/docx_doc.py +14 -154
  120. package/skills/docx-author/scripts/docx_layout.py +490 -0
  121. package/skills/docx-author/scripts/zh_prose.py +639 -0
  122. package/skills/docx-author/trigger-cases.json +58 -0
  123. package/skills/echo-brief/SKILL.md +84 -0
  124. package/skills/echo-brief/references/echo-format.md +68 -0
  125. package/skills/echo-brief/trigger-cases.json +40 -0
  126. package/skills/ecommerce-ops/trigger-cases.json +30 -0
  127. package/skills/education-training/trigger-cases.json +30 -0
  128. package/skills/ehs-safety/trigger-cases.json +30 -0
  129. package/skills/email-writing/trigger-cases.json +30 -0
  130. package/skills/eng-ai/trigger-cases.json +30 -0
  131. package/skills/eng-architecture/trigger-cases.json +31 -0
  132. package/skills/eng-automation/trigger-cases.json +31 -0
  133. package/skills/eng-collaboration/trigger-cases.json +30 -0
  134. package/skills/eng-complex-env/trigger-cases.json +30 -0
  135. package/skills/eng-data-platform/trigger-cases.json +31 -0
  136. package/skills/eng-database/trigger-cases.json +31 -0
  137. package/skills/eng-develop/SKILL.md +9 -1
  138. package/skills/eng-develop/references/dod-checklist.md +3 -0
  139. package/skills/eng-develop/references/engineering-workflow.md +37 -0
  140. package/skills/eng-develop/trigger-cases.json +30 -0
  141. package/skills/eng-docs/SKILL.md +24 -0
  142. package/skills/eng-docs/trigger-cases.json +30 -0
  143. package/skills/eng-efficiency/trigger-cases.json +30 -0
  144. package/skills/eng-infra/trigger-cases.json +30 -0
  145. package/skills/eng-integration/trigger-cases.json +31 -0
  146. package/skills/eng-legacy/trigger-cases.json +31 -0
  147. package/skills/eng-observability/trigger-cases.json +31 -0
  148. package/skills/eng-ops/trigger-cases.json +31 -0
  149. package/skills/eng-performance/trigger-cases.json +31 -0
  150. package/skills/eng-planning/trigger-cases.json +31 -0
  151. package/skills/eng-quality/SKILL.md +8 -0
  152. package/skills/eng-quality/trigger-cases.json +31 -0
  153. package/skills/eng-release/trigger-cases.json +31 -0
  154. package/skills/eng-reliability/trigger-cases.json +31 -0
  155. package/skills/eng-requirements/trigger-cases.json +12 -0
  156. package/skills/eng-research/trigger-cases.json +30 -0
  157. package/skills/eng-security/trigger-cases.json +31 -0
  158. package/skills/eng-ux/trigger-cases.json +30 -0
  159. package/skills/engineering-chain/trigger-cases.json +30 -0
  160. package/skills/env-offline/trigger-cases.json +31 -0
  161. package/skills/env-private-deploy/trigger-cases.json +31 -0
  162. package/skills/env-xinchuang/trigger-cases.json +31 -0
  163. package/skills/equipment-tpm/trigger-cases.json +30 -0
  164. package/skills/esg-report/trigger-cases.json +30 -0
  165. package/skills/event-management/trigger-cases.json +30 -0
  166. package/skills/excel-analysis-chain/SKILL.md +17 -4
  167. package/skills/excel-analysis-chain/trigger-cases.json +31 -0
  168. package/skills/explain-codebase/trigger-cases.json +11 -0
  169. package/skills/file-ingest/trigger-cases.json +30 -0
  170. package/skills/finance-analysis/trigger-cases.json +31 -0
  171. package/skills/finance-chain/trigger-cases.json +31 -0
  172. package/skills/financial-modeling/trigger-cases.json +31 -0
  173. package/skills/find-simplifications/SKILL.md +43 -0
  174. package/skills/find-simplifications/trigger-cases.json +23 -0
  175. package/skills/foreign-trade/trigger-cases.json +30 -0
  176. package/skills/funnel-cohort/trigger-cases.json +30 -0
  177. package/skills/gov-document/trigger-cases.json +30 -0
  178. package/skills/gui-change-evidence/SKILL.md +47 -0
  179. package/skills/gui-change-evidence/trigger-cases.json +23 -0
  180. package/skills/hr-chain/trigger-cases.json +31 -0
  181. package/skills/hr-operations/trigger-cases.json +30 -0
  182. package/skills/info-disclosure/trigger-cases.json +31 -0
  183. package/skills/infra-gateway/trigger-cases.json +31 -0
  184. package/skills/infra-k8s/trigger-cases.json +31 -0
  185. package/skills/infra-mq/trigger-cases.json +30 -0
  186. package/skills/insurance-advisory/trigger-cases.json +30 -0
  187. package/skills/internal-audit/trigger-cases.json +30 -0
  188. package/skills/ip-management/trigger-cases.json +31 -0
  189. package/skills/ipo-listing/trigger-cases.json +31 -0
  190. package/skills/it-audit/trigger-cases.json +31 -0
  191. package/skills/itsm-ops/trigger-cases.json +31 -0
  192. package/skills/knowledge-sop/trigger-cases.json +31 -0
  193. package/skills/legal-chain/trigger-cases.json +31 -0
  194. package/skills/legal-contract/trigger-cases.json +30 -0
  195. package/skills/litigation-support/trigger-cases.json +31 -0
  196. package/skills/logistics-warehouse/trigger-cases.json +30 -0
  197. package/skills/long-doc-analysis/trigger-cases.json +31 -0
  198. package/skills/management-accounting/trigger-cases.json +30 -0
  199. package/skills/manufacturing-chain/trigger-cases.json +31 -0
  200. package/skills/manufacturing-quality/trigger-cases.json +30 -0
  201. package/skills/marketing-analytics/trigger-cases.json +31 -0
  202. package/skills/marketing-chain/trigger-cases.json +31 -0
  203. package/skills/meeting-minutes/trigger-cases.json +30 -0
  204. package/skills/metrics-system/trigger-cases.json +31 -0
  205. package/skills/negotiation-prep/trigger-cases.json +30 -0
  206. package/skills/obs-prometheus/trigger-cases.json +31 -0
  207. package/skills/office-automation/trigger-cases.json +31 -0
  208. package/skills/okr-review/trigger-cases.json +30 -0
  209. package/skills/ops-fleet/trigger-cases.json +31 -0
  210. package/skills/ops-linux/trigger-cases.json +31 -0
  211. package/skills/optimization-solver/trigger-cases.json +30 -0
  212. package/skills/patent-disclosure/SKILL.md +105 -0
  213. package/skills/patent-disclosure/references/figures.md +110 -0
  214. package/skills/patent-disclosure/references/outline.md +162 -0
  215. package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
  216. package/skills/patent-disclosure/trigger-cases.json +31 -0
  217. package/skills/patent-drafting/SKILL.md +108 -0
  218. package/skills/patent-drafting/trigger-cases.json +31 -0
  219. package/skills/patent-portfolio/SKILL.md +81 -0
  220. package/skills/patent-portfolio/trigger-cases.json +31 -0
  221. package/skills/pdf-toolkit/trigger-cases.json +31 -0
  222. package/skills/perf-optimize/trigger-cases.json +31 -0
  223. package/skills/pivot-crosstab/trigger-cases.json +30 -0
  224. package/skills/policy-analysis/trigger-cases.json +31 -0
  225. package/skills/portfolio-analysis/trigger-cases.json +31 -0
  226. package/skills/ppt-chain/SKILL.md +14 -6
  227. package/skills/ppt-chain/trigger-cases.json +31 -0
  228. package/skills/pptx-author/SKILL.md +86 -33
  229. package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
  230. package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
  231. package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
  232. package/skills/pptx-author/scripts/pptx_battery.py +178 -0
  233. package/skills/pptx-author/scripts/pptx_build.py +119 -0
  234. package/skills/pptx-author/scripts/pptx_check.py +1182 -0
  235. package/skills/pptx-author/scripts/pptx_deck.py +736 -145
  236. package/skills/pptx-author/scripts/pptx_layout.py +384 -0
  237. package/skills/pptx-author/trigger-cases.json +15 -0
  238. package/skills/pr-comms/trigger-cases.json +30 -0
  239. package/skills/procurement-supply/trigger-cases.json +31 -0
  240. package/skills/product-design/trigger-cases.json +31 -0
  241. package/skills/production-planning/trigger-cases.json +31 -0
  242. package/skills/project-management/trigger-cases.json +31 -0
  243. package/skills/project-proposal/trigger-cases.json +31 -0
  244. package/skills/prose-standard/SKILL.md +53 -0
  245. package/skills/prose-standard/trigger-cases.json +23 -0
  246. package/skills/qa-api-automation/trigger-cases.json +31 -0
  247. package/skills/qa-mobile-automation/trigger-cases.json +31 -0
  248. package/skills/qa-ui-automation/trigger-cases.json +31 -0
  249. package/skills/quant-analysis/trigger-cases.json +31 -0
  250. package/skills/rag-triage/SKILL.md +64 -0
  251. package/skills/rag-triage/evals.json +57 -0
  252. package/skills/rag-triage/references/triage-playbook.md +75 -0
  253. package/skills/rag-triage/trigger-cases.json +31 -0
  254. package/skills/real-estate-property/trigger-cases.json +30 -0
  255. package/skills/refactor-safely/SKILL.md +1 -1
  256. package/skills/refactor-safely/trigger-cases.json +30 -0
  257. package/skills/regression-forecast/trigger-cases.json +31 -0
  258. package/skills/release-checklist/trigger-cases.json +31 -0
  259. package/skills/report-craft/SKILL.md +76 -0
  260. package/skills/report-craft/references/feasibility.md +118 -0
  261. package/skills/report-craft/references/review-checklist.md +51 -0
  262. package/skills/report-craft/references/visuals.md +114 -0
  263. package/skills/report-craft/references/writing.md +80 -0
  264. package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
  265. package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
  266. package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
  267. package/skills/report-craft/scripts/chart_render.py +1222 -0
  268. package/skills/report-craft/scripts/fin_model.py +602 -0
  269. package/skills/report-craft/scripts/project_facts.py +580 -0
  270. package/skills/report-craft/scripts/report_battery.py +349 -0
  271. package/skills/report-craft/trigger-cases.json +15 -0
  272. package/skills/retail-store-ops/trigger-cases.json +30 -0
  273. package/skills/risk-compliance/trigger-cases.json +31 -0
  274. package/skills/scope-creep/SKILL.md +45 -0
  275. package/skills/scope-creep/trigger-cases.json +7 -0
  276. package/skills/sec-auth/trigger-cases.json +31 -0
  277. package/skills/sec-compliance/trigger-cases.json +31 -0
  278. package/skills/securities-chain/trigger-cases.json +31 -0
  279. package/skills/securities-research/trigger-cases.json +31 -0
  280. package/skills/security-audit/trigger-cases.json +31 -0
  281. package/skills/slides-html/SKILL.md +25 -10
  282. package/skills/slides-html/trigger-cases.json +11 -0
  283. package/skills/sql-analysis/SKILL.md +110 -0
  284. package/skills/sql-analysis/trigger-cases.json +34 -0
  285. package/skills/sql-tuning/trigger-cases.json +31 -0
  286. package/skills/statistical-testing/trigger-cases.json +31 -0
  287. package/skills/strategy-planning/trigger-cases.json +30 -0
  288. package/skills/supply-chain/trigger-cases.json +31 -0
  289. package/skills/tax-compliance/trigger-cases.json +30 -0
  290. package/skills/tech-doc/SKILL.md +262 -0
  291. package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
  292. package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
  293. package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
  294. package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
  295. package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
  296. package/skills/tech-doc/scripts/archive_battery.py +860 -0
  297. package/skills/tech-doc/scripts/archive_build.py +599 -0
  298. package/skills/tech-doc/scripts/archive_check.py +1307 -0
  299. package/skills/tech-doc/scripts/archive_spec.py +499 -0
  300. package/skills/tech-doc/scripts/doc_templates.py +624 -0
  301. package/skills/tech-doc/trigger-cases.json +57 -0
  302. package/skills/tech-research/trigger-cases.json +31 -0
  303. package/skills/tender-bidding/trigger-cases.json +31 -0
  304. package/skills/translation-pro/trigger-cases.json +30 -0
  305. package/skills/treasury-mgmt/trigger-cases.json +31 -0
  306. package/skills/trim-cot-leakage/SKILL.md +58 -0
  307. package/skills/trim-cot-leakage/trigger-cases.json +23 -0
  308. package/skills/web-craft/SKILL.md +103 -0
  309. package/skills/web-craft/references/craft-checklist.md +75 -0
  310. package/skills/web-craft/references/design-direction.md +122 -0
  311. package/skills/web-craft/references/h5-mobile.md +100 -0
  312. package/skills/web-craft/references/motion.md +136 -0
  313. package/skills/web-craft/references/recipes.md +74 -0
  314. package/skills/web-craft/templates/wedding-invitation.html +1180 -0
  315. package/skills/web-craft/trigger-cases.json +14 -0
  316. package/skills/web-scraping/trigger-cases.json +34 -0
  317. package/skills/web-search/SKILL.md +11 -10
  318. package/skills/web-search/trigger-cases.json +31 -0
  319. package/skills/weekly-report/SKILL.md +134 -0
  320. package/skills/weekly-report/trigger-cases.json +11 -0
  321. package/skills/write-tests/trigger-cases.json +31 -0
  322. package/skills/xlsx-analysis/SKILL.md +143 -77
  323. package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
  324. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
  325. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
  326. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
  327. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
  328. package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
  329. package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
  330. package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
  331. package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
  332. package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
  333. package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
  334. package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
  335. package/skills/xlsx-analysis/trigger-cases.json +59 -0
  336. package/squads/hc-defense.md +47 -0
  337. package/squads/hc-rdarchive.md +110 -0
  338. package/squads/hc-redteam.md +50 -0
  339. package/squads/hc-weekly.md +125 -0
  340. package/src/account.js +1 -1
  341. package/src/acp.js +1 -0
  342. package/src/advisor.js +1 -1
  343. package/src/agent.js +1 -1
  344. package/src/api.js +1 -1
  345. package/src/archaeology.js +1 -0
  346. package/src/ask-ui.js +1 -0
  347. package/src/assets-import.js +1 -1
  348. package/src/autofix.js +1 -1
  349. package/src/automation-tools.js +1 -1
  350. package/src/automation.js +1 -1
  351. package/src/autorun.js +1 -1
  352. package/src/banner.js +1 -1
  353. package/src/capability-probe.js +1 -0
  354. package/src/checkpoints.js +1 -1
  355. package/src/code-tools.js +1 -0
  356. package/src/codeindex.js +1 -1
  357. package/src/codemode.js +1 -0
  358. package/src/command-bridge.js +1 -0
  359. package/src/command-ui.js +1 -0
  360. package/src/commands.js +1 -1
  361. package/src/compaction.js +1 -1
  362. package/src/computer.js +1 -1
  363. package/src/config.js +1 -1
  364. package/src/consult.js +1 -1
  365. package/src/context-report.js +1 -1
  366. package/src/context-window.js +1 -0
  367. package/src/context.js +1 -1
  368. package/src/crew.js +1 -0
  369. package/src/crush.js +1 -0
  370. package/src/daemon.js +1 -1
  371. package/src/decisions.js +1 -1
  372. package/src/depaudit.js +1 -0
  373. package/src/device.js +1 -1
  374. package/src/diagram-draw.js +1 -0
  375. package/src/diagram-layouts.js +1 -0
  376. package/src/diagram-raster.js +1 -0
  377. package/src/diagram.js +1 -0
  378. package/src/dictation.js +1 -0
  379. package/src/diffview.js +1 -1
  380. package/src/doctor.js +1 -1
  381. package/src/docx-tools.js +1 -0
  382. package/src/engineering.js +1 -0
  383. package/src/errkind.js +1 -1
  384. package/src/evidence.js +1 -0
  385. package/src/executors.js +1 -1
  386. package/src/expert-catalog.js +1 -1
  387. package/src/experts-store.js +1 -1
  388. package/src/experts.js +1 -1
  389. package/src/external-agents.js +1 -0
  390. package/src/forks.js +1 -1
  391. package/src/fs-observe.js +1 -0
  392. package/src/gitflow.js +1 -1
  393. package/src/goalround.js +1 -0
  394. package/src/hooks.js +1 -1
  395. package/src/ide.js +1 -1
  396. package/src/input.js +1 -1
  397. package/src/insights.js +1 -1
  398. package/src/jobs.js +1 -0
  399. package/src/kbsync.js +1 -1
  400. package/src/lsp.js +1 -0
  401. package/src/mcp-apps.js +1 -0
  402. package/src/mcp.js +1 -1
  403. package/src/memory.js +1 -1
  404. package/src/notify.js +1 -1
  405. package/src/permissions.js +1 -1
  406. package/src/plan-limits.js +1 -0
  407. package/src/policy.js +1 -0
  408. package/src/pptx-tools.js +1 -0
  409. package/src/presence.js +1 -1
  410. package/src/privatedir.js +1 -0
  411. package/src/pyruntime.js +1 -1
  412. package/src/radar.js +1 -0
  413. package/src/render.js +1 -1
  414. package/src/repl.js +1 -1
  415. package/src/report-tools.js +1 -0
  416. package/src/routing.js +1 -0
  417. package/src/sandbox-probe.js +1 -0
  418. package/src/sandbox-win-launcher.mjs +156 -0
  419. package/src/sandbox.js +1 -0
  420. package/src/scan.js +1 -1
  421. package/src/schedule.js +1 -1
  422. package/src/scopecreep.js +1 -0
  423. package/src/sdk.js +1 -0
  424. package/src/selection.js +1 -0
  425. package/src/session-events.js +1 -0
  426. package/src/session.js +1 -1
  427. package/src/sessionindex.js +1 -0
  428. package/src/sessionlog.js +1 -0
  429. package/src/skill-catalog.js +1 -0
  430. package/src/skill-lint.js +1 -0
  431. package/src/skill-optimize.js +1 -0
  432. package/src/skill-scan.js +1 -0
  433. package/src/skills.js +1 -1
  434. package/src/spill.js +1 -0
  435. package/src/squad.js +1 -1
  436. package/src/subagent.js +1 -1
  437. package/src/surface-state.js +1 -0
  438. package/src/tapd.js +1 -0
  439. package/src/terminal-select.js +1 -0
  440. package/src/terminal.js +1 -0
  441. package/src/test-runner.js +1 -0
  442. package/src/todo.js +1 -1
  443. package/src/toolpipe.js +1 -0
  444. package/src/tools.js +1 -1
  445. package/src/toon.js +1 -0
  446. package/src/transcript.js +1 -1
  447. package/src/tui/app.js +1 -1
  448. package/src/tui/command-menu.js +1 -0
  449. package/src/tui/progress.js +1 -0
  450. package/src/tui/scroll.js +1 -0
  451. package/src/tui/welcome.js +1 -0
  452. package/src/tui/workbench.js +1 -0
  453. package/src/ui-spec.js +1 -0
  454. package/src/uitree.js +1 -0
  455. package/src/uninstall.js +1 -1
  456. package/src/vision.js +1 -1
  457. package/src/visual.js +1 -1
  458. package/src/voice-form.js +1 -0
  459. package/src/voice.js +1 -0
  460. package/src/watch.js +1 -1
  461. package/src/web-audit.js +1 -0
  462. package/src/web-cdp.js +1 -0
  463. package/src/web-inspect.js +1 -0
  464. package/src/websearch.js +1 -0
  465. package/src/weekly.js +1 -0
  466. package/src/workflow.js +1 -0
  467. package/src/worklog.js +1 -0
  468. package/src/xlsx-tools.js +1 -0
  469. package/ui/command-picker.js +79 -0
  470. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  471. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  472. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  473. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  474. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  475. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  476. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  477. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  478. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  479. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  480. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  481. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  482. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  483. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  484. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  485. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  486. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  487. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  488. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  489. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  490. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  491. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  492. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  493. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
package/src/render.js CHANGED
@@ -1 +1 @@
1
- import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;case"dispatch_member":{const t=String(e.task??"").split("\n")[0];return`dispatch_member ${e.squad??""}·${e.member??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_expert":{const t=String(e.query??"").split("\n")[0];return`consult_expert ${e.expert??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_experts":return`consult_experts(并行咨询 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个平台专家)`;case"install_packages":return`install_packages ${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"(全部依赖)"}${e.global?" -g":""}${e.dev?" -D":""}`;case"setup_environment":return"setup_environment"+(Array.isArray(e.commands)&&e.commands.length?`(${e.commands.length} 条配置命令)`:"(自动配置运行环境)");case"install_plugin":return`install_plugin ${e.target??""} ${e.id??""}`;case"open_browser":return`open_browser ${e.url??""}`;case"open_path":return`open_path ${e.path??""}`;case"open_app":return`open_app ${e.name??""}${e.target?" "+e.target:""}`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,r){const n=[];for(const r of String(e??"").split("\n"))n.push(t.red("- "+r));for(const e of String(r??"").split("\n"))n.push(t.green("+ "+e));return n.join("\n")}export function diffLines(t,e){const r=t=>null==t||""===t?[]:String(t).split("\n"),n=r(t),s=r(e),a=n.length,i=s.length;if(a*i>4e6)return[...n.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const p=Array.from({length:a+1},()=>new Uint32Array(i+1));for(let t=a-1;t>=0;t--)for(let e=i-1;e>=0;e--)p[t][e]=n[t]===s[e]?p[t+1][e+1]+1:Math.max(p[t+1][e],p[t][e+1]);const o=[];let l=0,c=0;for(;l<a&&c<i;)n[l]===s[c]?(o.push({type:" ",text:n[l]}),l++,c++):p[l+1][c]>=p[l][c+1]?(o.push({type:"-",text:n[l]}),l++):(o.push({type:"+",text:s[c]}),c++);for(;l<a;)o.push({type:"-",text:n[l++]});for(;c<i;)o.push({type:"+",text:s[c++]});return o}export function renderFileDiff(e,r,n={}){const{path:s="",context:a=3,maxLines:i=80}=n,p=diffLines(e,r);let o=0,l=0;for(const t of p)"+"===t.type?o++:"-"===t.type&&l++;if(0===o&&0===l)return"";const c=[];let u=0,h=0;for(const t of p)" "===t.type?(u++,h++,c.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,c.push({type:"-",text:t.text,no:u})):(h++,c.push({type:"+",text:t.text,no:h}));const g=new Array(c.length).fill(!1);c.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-a);t<=Math.min(c.length-1,e+a);t++)g[t]=!0});const f=String(Math.max(u,h)).length,_=t=>String(null==t?"":t).padStart(f),y=[(s?t.bold(s)+" ":"")+t.green("+"+o)+" "+t.red("−"+l)];let d=!1,$=0,x=!1;for(let e=0;e<c.length;e++){if(!g[e]){d||(y.push(t.gray(" "+" ".repeat(f)+" ⋯")),d=!0);continue}if(d=!1,$>=i){x=!0;break}const r=c[e];"+"===r.type?y.push(t.green(" "+_(r.no)+" + ")+t.green(r.text)):"-"===r.type?y.push(t.red(" "+_(r.no)+" - ")+t.red(r.text)):y.push(t.gray(" "+_(r.no)+" "+r.text)),$++}return x&&y.push(t.gray(` …(差异较大,仅显示前 ${i} 行;共 +${o} −${l})`)),y.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,r=6){const n=String(e).split("\n");return n.length<=r?e:n.slice(0,r).join("\n")+t.gray(`\n …(共 ${n.length} 行)`)}
1
+ import{c as t}from"./colors.js";import{defaultDiagramPath as e}from"./diagram.js";import{defaultChartPath as r}from"./report-tools.js";export function toolLabel(t,n={}){switch(t){case"read_file":return`read_file ${n.path??""}`;case"list_files":return`list_files ${n.path??"."}`;case"glob":return`glob ${n.pattern??""}`;case"grep":return`grep ${JSON.stringify(n.pattern??"")}${n.path?" in "+n.path:""}`;case"search_code":return`search_code ${JSON.stringify(n.query??"")}`;case"write_file":return`write_file ${n.path??""}`;case"edit_file":return`edit_file ${n.path??""}`;case"run_bash":return`run_bash: ${String(n.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${n.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(n.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(n.tasks)?n.tasks.length:"?"} 个子任务)`;case"dispatch_member":{const t=String(n.task??"").split("\n")[0];return`dispatch_member ${n.squad??""}·${n.member??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_expert":{const t=String(n.query??"").split("\n")[0];return`consult_expert ${n.expert??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_experts":return`consult_experts(并行咨询 ${Array.isArray(n.tasks)?n.tasks.length:"?"} 个平台专家)`;case"install_packages":return`install_packages ${Array.isArray(n.packages)&&n.packages.length?n.packages.join(" "):"(全部依赖)"}${n.global?" -g":""}${n.dev?" -D":""}`;case"setup_environment":return"setup_environment"+(Array.isArray(n.commands)&&n.commands.length?`(${n.commands.length} 条配置命令)`:"(自动配置运行环境)");case"install_plugin":return`install_plugin ${n.target??""} ${n.id??""}`;case"open_browser":return`open_browser ${n.url??""}`;case"render_diagram":return`render_diagram ${n.path||e(n.spec)}`;case"render_chart":return`render_chart ${n.path||r(n.spec)}`;case"run_program":{const t=String(n.description??"").split("\n")[0];return`run_program:${t?t.length>40?t.slice(0,40)+"…":t:"写程序编排工具调用"}`}case"open_path":return`open_path ${n.path??""}`;case"open_app":return`open_app ${n.name??""}${n.target?" "+n.target:""}`;default:return`${t}(${Object.keys(n).join(", ")})`}}export function renderEditDiff(e,r){const n=[];for(const r of String(e??"").split("\n"))n.push(t.red("- "+r));for(const e of String(r??"").split("\n"))n.push(t.green("+ "+e));return n.join("\n")}export function diffLines(t,e){const r=t=>null==t||""===t?[]:String(t).split("\n"),n=r(t),s=r(e),a=n.length,i=s.length;if(a*i>4e6)return[...n.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const p=Array.from({length:a+1},()=>new Uint32Array(i+1));for(let t=a-1;t>=0;t--)for(let e=i-1;e>=0;e--)p[t][e]=n[t]===s[e]?p[t+1][e+1]+1:Math.max(p[t+1][e],p[t][e+1]);const o=[];let c=0,l=0;for(;c<a&&l<i;)n[c]===s[l]?(o.push({type:" ",text:n[c]}),c++,l++):p[c+1][l]>=p[c][l+1]?(o.push({type:"-",text:n[c]}),c++):(o.push({type:"+",text:s[l]}),l++);for(;c<a;)o.push({type:"-",text:n[c++]});for(;l<i;)o.push({type:"+",text:s[l++]});return o}export function renderFileDiff(e,r,n={}){const{path:s="",context:a=3,maxLines:i=80}=n,p=diffLines(e,r);let o=0,c=0;for(const t of p)"+"===t.type?o++:"-"===t.type&&c++;if(0===o&&0===c)return"";const l=[];let u=0,g=0;for(const t of p)" "===t.type?(u++,g++,l.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,l.push({type:"-",text:t.text,no:u})):(g++,l.push({type:"+",text:t.text,no:g}));const h=new Array(l.length).fill(!1);l.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-a);t<=Math.min(l.length-1,e+a);t++)h[t]=!0});const f=String(Math.max(u,g)).length,_=t=>String(null==t?"":t).padStart(f),d=[(s?t.bold(s)+" ":"")+t.green("+"+o)+" "+t.red("−"+c)];let m=!1,y=0,$=!1;for(let e=0;e<l.length;e++){if(!h[e]){m||(d.push(t.gray(" "+" ".repeat(f)+" ⋯")),m=!0);continue}if(m=!1,y>=i){$=!0;break}const r=l[e];"+"===r.type?d.push(t.green(" "+_(r.no)+" + ")+t.green(r.text)):"-"===r.type?d.push(t.red(" "+_(r.no)+" - ")+t.red(r.text)):d.push(t.gray(" "+_(r.no)+" "+r.text)),y++}return $&&d.push(t.gray(` …(差异较大,仅显示前 ${i} 行;共 +${o} −${c})`)),d.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,r=6){const n=String(e).split("\n");return n.length<=r?e:n.slice(0,r).join("\n")+t.gray(`\n …(共 ${n.length} 行)`)}
package/src/repl.js CHANGED
@@ -1 +1 @@
1
- import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as r,saveSession as n,turnCount as i}from"./session.js";import{ensureSessionTitle as c}from"./title.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as l}from"./context.js";import{runVerifyLoop as u}from"./autofix.js";import{runAutoFlow as p}from"./autorun.js";import{loadMcpTools as m,closeMcp as d,makeMcpConnectTools as g}from"./mcp.js";import{makeAutomationTools as f}from"./automation-tools.js";import{makeNotifyTool as w,notifyTurnDone as h}from"./notify.js";import{makePresenceTools as y,acquireWakeLock as j,releaseWakeLock as x}from"./presence.js";import{makeComputerTools as b}from"./computer.js";import{createVisualBridge as k}from"./visual.js";import{autoSyncPlatformSkills as T}from"./skills.js";import{makeSubagentTool as I,makeDispatchAgentsTool as S}from"./subagent.js";import{makeDispatchMemberTool as v}from"./squad.js";import{makeConsultTool as $}from"./consult.js";import{makeExpertTools as q}from"./experts.js";import{listWorkspaceFiles as P}from"./executors.js";import{makeAdvisorTool as E}from"./advisor.js";import{makeRecallTool as R,readMemoryRaw as _}from"./memory.js";import{detectScanners as A,makeScanTool as C}from"./scan.js";import{runSlash as L}from"./commands.js";import{handleAccountError as M,accountErrorKind as O}from"./account.js";import{expandAtMentions as W,buildUserContent as D,runBangCommand as H,loadHistory as V,appendHistory as J}from"./input.js";import{makeSkillTools as N}from"./skills.js";import{renderWelcome as Q}from"./banner.js";export async function buildExtraTools(s,t,e={}){const r="function"==typeof e.onSub?e.onSub:null;let n={tools:[],clients:[],registry:{servers:{}}};try{n=await m(t)}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const i=[...N(t),...n.tools],c=I({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});c&&i.push(c);const a=S({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});a&&i.push(a);const l=v({config:s,cwd:t,extraTools:n.tools,onSub:r});l&&i.push(l);const u=$({config:s});u&&i.push(u);try{i.push(...await q({config:s}))}catch{}const p=E({config:s});p&&i.push(p),!1!==s.memoryEnabled&&_(t)&&i.push(R({cwd:t})),A(t).length&&i.push(C({cwd:t})),i.push(...g({registry:n.registry,clients:n.clients,cwd:t,register:s=>i.push(...s)})),i.push(...f({cwd:t,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint}));const d=w({enabled:!1!==s.notifications});d&&i.push(d),i.push(...y({signalRef:e.signalRef}));const h="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?k({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...b({config:s,injectVisual:h,omit:e.omitComputer||[],enabled:e.computerUse})),T(s,{onInfo:t=>{s.debug&&process.stdout.write(o.gray(" · "+t)+"\n")}}).catch(()=>{}),{extraTools:i,clients:n.clients,mcpRegistry:n.registry}}export async function startRepl({config:i,cwd:m,yolo:g,initialPrompt:f,session:w,version:y}){const b=s.createInterface({input:process.stdin,output:process.stdout,history:V(m).reverse(),historySize:200,completer:s=>{const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const t="@"+o[1],e=t.toLowerCase();return[P(m).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(e)).slice(0,40),t]}}),k=s=>new Promise(o=>b.question(s,o));let T=!1,I=t({yolo:g,ask:k,cwd:m});const S={prompt:0,completion:0},v={list:[]},$=a();let q=null,E=null;const R={config:i,cwd:m,permissions:I,usage:S,session:w,version:y};b.on("SIGINT",()=>{q?q.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(Q({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,m,I,y),z(w);const _=[],A={current:null};let{extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const N=()=>{c(R.session,i).then(s=>{s&&n(R.session)}).catch(()=>{})},Y=async s=>{const{text:t,mentions:r,images:c}=await W(s,m,i,{onDocStart:(s,t,e)=>process.stdout.write(o.gray(` · 正在解析文档 @${s}${e>1?`(${t}/${e})`:""}…`)+"\n")});r.length&&process.stdout.write(o.gray(" · 已展开引用:"+r.join("、")+(c.length?`(含 ${c.length} 张图片)`:""))+"\n");const a=$.snapshot({session:R.session,label:s});R.session.messages.push({role:"user",content:D(t,c)}),N(),q=new AbortController,A.current=q.signal,R.session.compaction||(R.session.compaction={}),i.sessionId=R.session.id;const l="auto"===I.getMode?.()&&!1!==i.wakeLockInAuto?j({reason:"自动模式任务执行中"}):null,u=Date.now();let p;try{p=await e({config:i,messages:R.session.messages,permissions:I,cwd:m,signal:q.signal,extraTools:C,todoState:v,compaction:R.session.compaction,fileJournal:a,pullQueued:()=>_.shift()||null})}finally{l?.ok&&x()}return p?.usage&&(S.prompt+=p.usage.prompt,S.completion+=p.usage.completion),p?.lastPromptTokens&&(R.session.lastPromptTokens=p.lastPromptTokens),q=null,A.current=null,p?.aborted||p?.error||h({durationMs:Date.now()-u,cwd:m,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==i.notifications}).catch(()=>{}),n(R.session),N(),p?.error&&"auth"===M(p,i)&&(T=!0),p},F=()=>({...R,session:R.session,extraTools:C,checkpoints:$,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(f&&(process.stdout.write(o.cyan("\nhuicai> ")+f+"\n"),await Y(f));!T;){let s;try{s=(await k(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(J(m,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:r}=await H(t,m);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),R.session.messages.push({role:"user",content:r}),n(R.session);continue}if(s.startsWith("/")){const e=await L(s,F());if("exit"===e.action)break;"clear"===e.action&&(R.session=r({cwd:m,model:i.model,messages:[R.session.messages[0]]}),v.list=[],$.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&z(R.session),"cd"===e.action&&(m=e.cwd,R.cwd=m,I=t({yolo:I.isYolo(),ask:k,cwd:m}),R.permissions=I,d(O),({extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A})),R.session=r({cwd:m,model:i.model,messages:[{role:"system",content:l(m,{memory:!1!==i.memoryEnabled,config:i})}]}),v.list=[],$.list.length=0,i.sessionId=R.session.id,process.stdout.write(o.gray("已切换到 "+m+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await u({command:e.command,maxIters:e.maxIters,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await p({goal:e.goal,config:i,permissions:I,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(E&&(clearInterval(E),E=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),E=setInterval(()=>{q||Y(e.prompt)},1e3*e.seconds))),e.prompt&&await Y(e.prompt);continue}await Y(s)}}return E&&clearInterval(E),b.close(),d(O),T?{relogin:!0}:(process.stdout.write(o.gray("\n再见。\n")),{})}function Y(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function z(s){const t=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=i(s.messages);if(0===e)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const r=t.length>40?t.length-40:0;r&&process.stdout.write(o.gray(` (较早 ${r} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of t.slice(-40)){const t=Y(s.content).trim();if(!t)continue;const e=t.length>800?t.slice(0,800)+"…":t,r="user"===s.role?o.bold(" 你 › "):o.gray(" 助手 › ");process.stdout.write(r+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(o.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:r,yolo:c,prompt:a,session:l,persist:u,outputFormat:p,maxTurns:m}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const g="json"===p,f=t({yolo:c,ask:null,cwd:r}),w=[],{extraTools:h,clients:y}=await buildExtraTools(s,r,{queueInject:s=>w.push(s)}),{text:b,images:k}=await W(a,r,s);l.messages.push({role:"user",content:D(b,k)}),l.compaction||(l.compaction={}),s.sessionId=l.id;const T=c&&!1!==s.wakeLockInAuto?j({reason:"无人值守任务执行中"}):null;let I;try{I=await e({config:s,messages:l.messages,permissions:f,cwd:r,extraTools:h,maxIters:m,compaction:l.compaction,io:g?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{T?.ok&&x()}I?.lastPromptTokens&&(l.lastPromptTokens=I.lastPromptTokens),u&&n(l),d(y);const S=I?.error?O(I):"";if(S&&(M(I,s,s=>process.stderr.write(s)),"auth"===S&&process.stderr.write(o.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),g){const o=[...l.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!I?.error,...I?.error?{error:I.error}:{},...S?{error_kind:S}:{},result:o?String(o.content):"",usage:{input_tokens:I?.usage?.prompt??0,output_tokens:I?.usage?.completion??0},session_id:l.id,model:s.model,num_turns:i(l.messages)})+"\n")}I?.error&&process.exit(1)}
1
+ import{terminalSelect as s}from"./terminal-select.js";import{questionSelection as o}from"./selection.js";import{parseAskAnswer as e}from"./ask-ui.js";import t from"node:readline";import{syncPlanUser as r}from"./plan-limits.js";import{c as n}from"./colors.js";import{createPermissions as i}from"./permissions.js";import{runTurn as a}from"./agent.js";import{createSession as c,saveSession as l,turnCount as u}from"./session.js";import{syncSessionLog as p,rewindSessionLog as m}from"./sessionlog.js";import{ensureSessionTitle as d}from"./title.js";import{createCheckpoints as f}from"./checkpoints.js";import{buildSystemPrompt as g}from"./context.js";import{runVerifyLoop as w}from"./autofix.js";import{runAutoFlow as h}from"./autorun.js";import{loadMcpTools as j,closeMcp as y,makeMcpConnectTools as b}from"./mcp.js";import{makeJobTools as k,onSettled as x,describe as I}from"./jobs.js";import{makeTerminalTools as T,closeAllTerminals as S,dropSessionTerminals as v}from"./terminal.js";import{makeGoalTools as $,runGoalRounds as q,setRoundKind as O,getGoal as P,viewGoal as U}from"./goalround.js";import{makeCodeModeTool as C}from"./codemode.js";import{makeWorkflowTool as _}from"./workflow.js";import{makeLspTool as M,setProvider as A}from"./lsp.js";import{ensureVerified as E}from"./sandbox.js";import{makeAutomationTools as R}from"./automation-tools.js";import{makeNotifyTool as H,notifyTurnDone as L}from"./notify.js";import{makePresenceTools as N,acquireWakeLock as W,releaseWakeLock as B}from"./presence.js";import{makeComputerTools as G}from"./computer.js";import{makeDiagramTools as V}from"./diagram.js";import{makePptxTools as D}from"./pptx-tools.js";import{makeDocxTools as Y}from"./docx-tools.js";import{makeReportTools as J}from"./report-tools.js";import{makeXlsxTools as K}from"./xlsx-tools.js";import{makeCodeTools as Q}from"./code-tools.js";import{makeTestTools as z}from"./test-runner.js";import{makeEngineeringTools as F}from"./engineering.js";import{makeWebTools as X}from"./web-inspect.js";import{browserAvailable as Z}from"./web-cdp.js";import{makeUiTools as ss}from"./ui-spec.js";import{makeAskUiTool as os}from"./ask-ui.js";import{makeSurfaceTools as es}from"./surface-state.js";import{makeVoiceFormTool as ts}from"./voice-form.js";import{makeDepAuditTool as rs}from"./depaudit.js";import{makeScopeCheckTool as ns}from"./scopecreep.js";import{makeArchaeologyTool as is}from"./archaeology.js";import{streamChat as as}from"./api.js";import{execSync as cs}from"node:child_process";import{createVisualBridge as ls}from"./visual.js";import{autoSyncPlatformSkills as us}from"./skills.js";import{makeSubagentTool as ps,makeDispatchAgentsTool as ms,makeContinuableAgentTools as ds}from"./subagent.js";import{makeDispatchMemberTool as fs}from"./squad.js";import{makeConsultTool as gs}from"./consult.js";import{makeExpertTools as ws}from"./experts.js";import{listWorkspaceFiles as hs}from"./executors.js";import{makeAdvisorTool as js}from"./advisor.js";import{makeRecallTool as ys,readMemoryRaw as bs}from"./memory.js";import{detectScanners as ks,makeScanTool as xs}from"./scan.js";import{runSlash as Is,listCommands as Ts}from"./commands.js";import{handleAccountError as Ss,accountErrorKind as vs}from"./account.js";import{expandAtMentions as $s,buildUserContent as qs,runBangCommand as Os,loadHistory as Ps,appendHistory as Us}from"./input.js";import{makeSkillTools as Cs}from"./skills.js";import{makeSessionEventTools as _s}from"./session-events.js";import{makeExternalAgentTools as Ms}from"./external-agents.js";import{makeWebSearchTools as As}from"./websearch.js";import{renderWelcome as Es}from"./banner.js";import{createCrewBudget as Rs,withBudget as Hs,makePlanBudgetTool as Ls,makeBudgetStatusTool as Ns}from"./crew.js";import{makeEvidenceTools as Ws}from"./evidence.js";import{makeTapdTools as Bs}from"./tapd.js";import{makeWeeklyTools as Gs}from"./weekly.js";export async function buildExtraTools(s,o,e={}){const t="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[],registry:{servers:{}}};try{r=await j(o)}catch(s){process.stdout.write(n.yellow("MCP初始化失败(忽略):"+s.message)+"\n")}const i=[...Cs(o),...r.tools],a=s.crewBudget?Rs(s.crewBudgetPlan||{}):null,c=(s,o,e)=>a?Hs(s,a,o,e):s,l=ps({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});l&&i.push(c(l,"dispatch"));const u=ms({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});u&&i.push(c(u,"dispatch",s=>(s?.tasks||[]).length||1)),i.push(...ds({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t,queueInject:"function"==typeof e.queueInject?e.queueInject:null}));const p=fs({config:s,cwd:o,extraTools:r.tools,onSub:t});p&&i.push(p),i.push(...As({config:s})),i.push(...Ms({config:s,cwd:o,onSub:t}));const m=gs({config:s});m&&i.push(m);try{i.push(...await ws({config:s}))}catch{}const d=js({config:s});if(d?i.push(c(d,"advisor")):a&&a.degrade("顾问","未配置advisorModel"),a&&(i.push(Ls(a,e.onBudgetDeclared||null)),i.push(Ns(a))),!1!==s.memoryEnabled&&bs(o)&&i.push(ys({cwd:o})),ks(o).length&&i.push(xs({cwd:o})),i.push(...k({sessionId:()=>s.sessionId||""})),"function"==typeof e.queueInject){x(o=>{if(o.sessionId!==String(s.sessionId||""))return;const t="agent"===o.kind?`它已转为空闲,可用send_message继续对话;想看这一段的细节用job_output("${o.id}")。`:`用job_output("${o.id}") 读它的输出(默认只回新增)。`;e.queueInject(`【后台作业完成】${I(o)}\n${t}`)})}i.push(_({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t})),i.push(C()),"function"==typeof e.sessionOf&&i.push(...$({sessionOf:e.sessionOf})),e.lspProvider&&A(e.lspProvider),i.push(M({cwd:o})),i.push(...await T({sessionId:()=>s.sessionId||"",cwd:o,config:s})),i.push(...b({registry:r.registry,clients:r.clients,cwd:o,register:s=>i.push(...s)})),i.push(...R({cwd:o,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint})),i.push(...V({cwd:o})),i.push(...D({cwd:o,config:s})),i.push(...Y({cwd:o,config:s})),i.push(...J({cwd:o,config:s})),i.push(...K({cwd:o,config:s})),i.push(...Q({cwd:o,config:s})),i.push(...z({cwd:o,config:s})),i.push(...F()),i.push(...Ws({cwd:o})),i.push(...Bs({cwd:o})),i.push(...Gs({cwd:o})),i.push(...ss({cwd:o})),i.push(os({ask:e.ask,askUi:e.askUi})),e.sessionId&&i.push(...es({sessionId:e.sessionId,emit:e.onSurface})),i.push(...rs({cwd:o})),i.push(..._s({cwd:o,config:s})),i.push(...is({cwd:o})),i.push(...ns({cwd:o,runGit:(s,o)=>{try{return cs(`git ${o.join(" ")}`,{cwd:s,encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:33554432})}catch{return null}},goalOf:()=>{try{const s="function"==typeof e.sessionOf?P(e.sessionOf()):null;return s&&"active"===s.phase?s.objective:""}catch{return""}}})),i.push(...ts(async(o,e)=>{let t="";try{await as({baseUrl:s.baseUrl,apiKey:s.apiKey,model:s.model,messages:[{role:"system",content:o},{role:"user",content:e}],temperature:0,client:s.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:s=>{t+=s}})}catch{return""}return t}));const f=H({enabled:!1!==s.notifications});f&&i.push(f),i.push(...N({signalRef:e.signalRef}));const g="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?ls({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...G({config:s,injectVisual:g,cwd:o,omit:e.omitComputer||[],enabled:e.computerUse})),Z()&&i.push(...X({cwd:o,config:s,injectVisual:g})),us(s,{onInfo:o=>{s.debug&&process.stdout.write(n.gray(" · "+o)+"\n")}}).catch(()=>{}),{extraTools:i,clients:r.clients,mcpRegistry:r.registry,crew:a}}export async function startRepl({config:u,cwd:j,yolo:b,initialPrompt:k,session:x,version:I,...T}){E().catch(()=>{}),r(u).catch(()=>{});const $=t.createInterface({input:process.stdin,output:process.stdout,history:Ps(j).reverse(),historySize:200,completer:s=>{if(/^\/\S*$/.test(s)){const o=Ts(j).flatMap(s=>[s.name,...s.aliases||[]]);return[o.map(s=>"/"+s).filter(o=>o.startsWith(s)),s]}const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const e="@"+o[1],t=e.toLowerCase();return[hs(j).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(t)).slice(0,40),e]}});let U,C=null,_=!1,M=null,A=!1;T.updater?.check.then(s=>{C=s,s&&A&&!$.line&&U?.()});let R=!1;const H=s=>new Promise((o,e)=>{if($.closed)return e(new Error("终端已关闭"));const t=new AbortController,r=()=>{U=null,e(new Error("终端已关闭"))},n=s=>{$.off("close",r),U=null,o(s)};U=()=>{t.abort(),n("")},$.once("close",r),$.question(s,{signal:t.signal},n)});let N=Promise.resolve();const G=process.stdin.isTTY&&process.stdout.isTTY?o=>{const e=z?.signal,t=N.then(async()=>{R=!0;try{return await s(o,{rl:$,signal:e,onInterrupt:()=>z?.abort()})}finally{R=!1}});return N=t.catch(()=>null),t}:void 0,V=T.askUi||(G?async s=>{const t=await G(o(s));return"date_range"===s.kind&&t?.value?e(s,t.value):t||{value:null}}:void 0);let D=!1,Y=i({yolo:b,ask:H,select:G,cwd:j});const J={prompt:0,completion:0},K={list:[]},Q=f();let z=null,F=null;const X={config:u,cwd:j,permissions:Y,usage:J,session:x,version:I,crewOf:()=>ts};$.on("SIGINT",()=>{R?(z?.abort(),U?.(),process.stdout.write("\n")):z?z.abort():process.stdout.write("\n"+n.gray("输入 /exit退出。")+"\n")}),function(s,o,e,t){process.stdout.write(Es({model:s.model,cwd:o,version:t,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:e.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(u,j,Y,I),Ds(x);const Z=[],ss={current:null};let{extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,ask:H,askUi:V,sessionId:X.session?.id,onSurface:T.onSurface,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const rs=()=>{d(X.session,u).then(s=>{s&&l(X.session)}).catch(()=>{})},ns=async s=>{const{text:o,mentions:e,images:t}=await $s(s,j,u,{onDocStart:(s,o,e)=>process.stdout.write(n.gray(` · 正在解析文档 @${s}${e>1?`(${o}/${e})`:""}…`)+"\n")});e.length&&process.stdout.write(n.gray(" · 已展开引用:"+e.join("、")+(t.length?`(含 ${t.length} 张图片)`:""))+"\n");const r=Q.snapshot({session:X.session,label:s});X.session.messages.push({role:"user",content:qs(o,t)}),rs(),z=new AbortController,ss.current=z.signal,X.session.compaction||(X.session.compaction={}),u.sessionId=X.session.id,u.goal=P(X.session)?.objective||"";const i="auto"===Y.getMode?.()&&!1!==u.wakeLockInAuto?W({reason:"自动模式任务执行中"}):null,c=Date.now();let m;try{m=await a({config:u,messages:X.session.messages,permissions:Y,cwd:j,signal:z.signal,extraTools:os,todoState:K,compaction:X.session.compaction,fileJournal:r,pullQueued:()=>Z.shift()||null})}finally{i?.ok&&B()}return m?.usage&&(J.prompt+=m.usage.prompt,J.completion+=m.usage.completion),m?.lastPromptTokens&&(X.session.lastPromptTokens=m.lastPromptTokens),m?.lastUsage&&(X.session.lastUsage=m.lastUsage),z=null,ss.current=null,m?.aborted||m?.error||L({durationMs:Date.now()-c,cwd:j,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==u.notifications}).catch(()=>{}),p(X.session),l(X.session),rs(),m?.error&&"auth"===Ss(m,u)&&(D=!0),m},is=async s=>{O(X.session.id,"human");const o=await ns(s);return o?.aborted||o?.error||D?o:P(X.session)?(await q({session:X.session,doTurn:ns,stats:()=>({usedTokens:J.prompt+J.completion,budgetTokens:Number(u.budgetTokens)||0}),onInfo:s=>process.stdout.write(n.gray(" ◆ "+s)+"\n"),aborted:()=>D}),l(X.session),o):o},as=()=>({...X,signal:z?.signal,select:G,resumeSession:s=>{F&&(clearInterval(F),F=null),v(X.session.id),X.session=s,u.sessionId=s.id,s.model&&(u.model=s.model);const o=Y.getMode();Y=i({yolo:!1,ask:H,select:G,cwd:j}),Y.setMode(o),X.permissions=Y,Z.length=0,K.list=[],Q.list.length=0,Ds(s)},session:X.session,extraTools:os,checkpoints:Q,onRewind:s=>m(X.session,s),print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(k&&(process.stdout.write(n.cyan("\nhuicai> ")+k+"\n"),await is(k));!D;){if(C&&!_&&G){_=!0;if("update"===await G(T.updater.menu(C))){process.stdout.write("正在更新 HuiCai AI,请稍候…\n"),z=new AbortController;try{M=await T.updater.install(C,{signal:z.signal});break}catch(s){process.stdout.write(s.message+" 输入 /update 可重试。\n")}finally{z=null}}}let s;try{A=!0,s=(await H(n.cyan("\nhuicai> "))).trim()}catch{break}finally{A=!1}if(""!==s){if(Us(j,s),"/exit"===s||"/quit"===s)break;if("/update"===s&&C&&T.updater)_=!1;else{if(s.startsWith("!")&&s.length>1){const o=s.slice(1).trim(),{output:e,record:t}=await Os(o,j);process.stdout.write(n.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),X.session.messages.push({role:"user",content:t}),l(X.session);continue}if(s.startsWith("/")){let o;z=new AbortController;try{o=await Is(s,as())}catch(s){process.stdout.write((z.signal.aborted?"操作已取消":"命令失败:"+s.message)+"\n");continue}finally{z=null}if($.closed)break;if("exit"===o.action)break;if("clear"===o.action){F&&(clearInterval(F),F=null);const s=Y.getMode();Y=i({yolo:!1,ask:H,select:G,cwd:j}),Y.setMode(s),X.permissions=Y,l(X.session),v(X.session.id),X.session=c({cwd:j,model:u.model,messages:[X.session.messages[0]]}),u.sessionId=X.session.id,Z.length=0,K.list=[],Q.list.length=0,process.stdout.write(n.gray("已开启新会话(旧会话已保存,可用huicai --resume恢复)。\n"))}"refresh"===o.action&&Ds(X.session),"cd"===o.action&&(j=o.cwd,X.cwd=j,Y=i({yolo:Y.isYolo(),ask:H,select:G,cwd:j}),X.permissions=Y,y(es),({extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{ask:H,askUi:V,sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"})),X.session=c({cwd:j,model:u.model,messages:[{role:"system",content:g(j,{memory:!1!==u.memoryEnabled,config:u})}]}),K.list=[],Q.list.length=0,u.sessionId=X.session.id,process.stdout.write(n.gray("已切换到 "+j+"(新会话,旧会话已保存)。\n"))),"autofix"===o.action&&await w({command:o.command,maxIters:o.maxIters,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(s)+"\n")}),"autorun"===o.action&&await h({goal:o.goal,config:u,permissions:Y,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(" · "+s)+"\n")}),"loop"===o.action&&(F&&(clearInterval(F),F=null),o.stop?process.stdout.write(n.gray("已停止循环。\n")):(process.stdout.write(n.gray(`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)。\n`)),F=setInterval(()=>{z||ns(o.prompt)},1e3*o.seconds))),o.prompt&&await is(o.prompt);continue}await is(s)}}}return F&&clearInterval(F),$.close(),y(es),S(),T.updater?.dispose(),M?{updated:M}:D?{relogin:!0}:(process.stdout.write(n.gray("\n再见。\n")),{})}function Vs(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function Ds(s){const o=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=u(s.messages);if(0===e)return;process.stdout.write(n.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const t=o.length>40?o.length-40:0;t&&process.stdout.write(n.gray(` (较早 ${t} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of o.slice(-40)){const o=Vs(s.content).trim();if(!o)continue;const e=o.length>800?o.slice(0,800)+"…":o,t="user"===s.role?n.bold(" 你 › "):n.gray(" 助手 › ");process.stdout.write(t+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(n.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:e,prompt:t,session:r,persist:c,outputFormat:m,maxTurns:d}){t||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const f="json"===m,g=i({yolo:e,ask:null,cwd:o}),w=[],h={current:null},j=new AbortController;h.current=j.signal;let b=!1;const k=()=>{b&&process.exit(130),b=!0,process.stderr.write(n.gray("\n已请求中断(再按一次Ctrl-C强制退出)…\n")),j.abort()};process.on("SIGINT",k);const{extraTools:x,clients:I}=await buildExtraTools(s,o,{sessionOf:()=>r,queueInject:s=>w.push(s),signalRef:h,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});let T=t;if(t.trim().startsWith("/")){const{runSlash:e}=await import("./commands.js"),n=[],i=await e(t.trim(),{cwd:o,config:s,session:r,permissions:g,print:s=>n.push(String(s))}).catch(s=>({handled:!1,error:s}));if(n.length&&process.stdout.write(n.join("\n")+"\n"),i&&i.prompt)T=i.prompt;else if(i&&i.handled)return y(I),process.off("SIGINT",k),{text:n.join("\n")}}const{text:v,images:$}=await $s(T,o,s);r.messages.push({role:"user",content:qs(v,$)}),r.compaction||(r.compaction={}),s.sessionId=r.id;const q=e&&!1!==s.wakeLockInAuto?W({reason:"无人值守任务执行中"}):null;let O;try{O=await a({config:s,messages:r.messages,permissions:g,cwd:o,extraTools:x,signal:j.signal,maxIters:d,compaction:r.compaction,io:f?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{q?.ok&&B()}O?.lastPromptTokens&&(r.lastPromptTokens=O.lastPromptTokens),O?.lastUsage&&(r.lastUsage=O.lastUsage),c&&(l(r),p(r)),process.removeListener("SIGINT",k),y(I),S();const P=O?.error?vs(O):"";if(P&&(Ss(O,s,s=>process.stderr.write(s)),"auth"===P&&process.stderr.write(n.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),f){const o=[...r.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!O?.error,...O?.error?{error:O.error}:{},...P?{error_kind:P}:{},result:o?String(o.content):"",usage:{input_tokens:O?.usage?.prompt??0,output_tokens:O?.usage?.completion??0},session_id:r.id,model:s.model,num_turns:u(r.messages)})+"\n")}O?.error&&process.exit(1)}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:os";import r from"node:path";import{BUNDLED_SKILLS_DIR as n}from"./skills.js";import{ensureRuntime as i,execFileCapture as s,runtimeStatus as o,venvPython as a}from"./pyruntime.js";import{probeSystemPython as c}from"./code-tools.js";export function reportEngineDir(){return r.join(n,"report-craft","scripts")}export function defaultChartPath(e){let t=e;if("string"==typeof t)try{t=JSON.parse(t)}catch{t=null}return r.join("docs","charts",function(e){return String(e||"").trim().replace(/[\\/:*?"<>|\s]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"chart"}(t?.title||t?.type||"chart")+".png")}async function p(t,n,p,{config:u={},timeoutMs:l=12e4,needsMpl:m=!1}={}){const d=r.join(reportEngineDir(),n);if(!e.existsSync(d))throw new Error("报告引擎缺失:"+d);let h="";if(o(u).usable&&e.existsSync(a())?h=a():m||(h=c()),!h&&(await i({log:()=>{},config:u}),h=a(),!e.existsSync(h)))throw new Error("托管Python未就绪(设置 → 智能 → 立即准备,或先调用一次run_python)");const f=await s(h,["-X","utf8",d,...p],{cwd:t,timeoutMs:l,env:{PYTHONIOENCODING:"utf-8",MPLBACKEND:"Agg"}});if(f.killed)throw new Error("执行超时被终止");return function(e){const t=String(e||"").trim().split("\n");for(let e=t.length-1;e>=0;e--){const r=t[e].trim();if(r.startsWith("{"))try{return JSON.parse(r)}catch{}}throw new Error("引擎输出异常:"+String(e||"").slice(-400))}(f.output)}function u(t,n,i){e.mkdirSync(t,{recursive:!0});const s=r.join(t,`.report-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2,7)}.json`);return e.writeFileSync(s,JSON.stringify(n)),i(s).finally(()=>{try{e.rmSync(s,{force:!0})}catch{}})}function l(e,t){if(null==e)return null;if("object"==typeof e)return e;try{return JSON.parse(String(e))}catch{throw new Error(`${t} 不是合法JSON`)}}const m=["图型与字段(categories与每个series.values一一对应;unit写单位,引擎标在左上):","· bar柱状 / hbar条形(类别名长或 >8 个用hbar)/ stacked·hstacked堆叠:{categories, series:[{name, values}]},",' highlight:"系列名" 其余退为灰(讲清一件事时比 8 种颜色更有力),labels:"auto|all|none"',"· line折线 / area面积:同上;**不支持双纵轴**(两种量纲请拆两张图或换算成指数)","· pie环形(构成):{categories, series:[{values}]} 只一个系列,>6 块自动并「其他」","· waterfall瀑布(逐项累加到总额,投资构成/利润桥):{items:[{name, value}], total_label}","· tornado龙卷风(敏感性):{base, metric, items:[{name, low, high, low_label, high_label}]}",'· gantt甘特(实施进度):{tasks:[{name, start:"2026-10", end:"2027-01", group}], milestones:[{name, date}], today:"编制日"}','· matrix风险矩阵:{items:[{id:"R1", name, p:1-5, i:1-5}]}(p=可能性,i=影响)','通用:size:"half" 半栏宽(两图并排)、height_cm、note(图下注)。图题不画进图里——由文档题注负责。'].join("\n");export function makeReportTools({cwd:n,config:i={}}){return[{readOnly:!1,timeoutMs:24e4,spec:{type:"function",function:{name:"render_chart",description:"把数据画成出版级图表并落盘(PNG 300dpi,按Word版心物理尺寸出图 + 同名SVG)。**你只给数据,配色、字号、标注、图例、防重叠由引擎负责**——报告里的数据图不要用run_python手写matplotlib。结构类的图(架构/流程/模块/时序)用render_diagram,这里只画数字。一张图只讲一个结论。\n"+m+"\n返回图下数据表(markdown),正文或附表可直接用;插入Word用create_doc的figure块或Markdown `![题注](图路径)`。",parameters:{type:"object",properties:{path:{type:"string",description:"PNG输出路径(相对项目根,如docs/charts/累计现金流量.png);省略则docs/charts/<title>.png"},spec:{type:"object",description:"图表数据:必含type与该图型的字段(见说明)。建议给title(只用于命名文件)。",additionalProperties:!0,properties:{type:{type:"string",description:"bar/hbar/stacked/hstacked/line/area/pie/waterfall/tornado/gantt/matrix"},title:{type:"string"},unit:{type:"string",description:"数值单位,如 万元 / % / 人天"}}},spec_path:{type:"string",description:"可选:从JSON文件读spec(fin_model给的图表spec很长时可先落盘)"}},required:[]}}},run:async t=>{try{let s=l(t?.spec,"spec");if(!s&&t?.spec_path){const i=r.resolve(n,String(t.spec_path));if(!e.existsSync(i))return`spec文件不存在:${t.spec_path}`;s=JSON.parse(e.readFileSync(i,"utf8"))}if(!s||!s.type)return"render_chart需要spec(含type)。\n"+m;const o=function(t,n,i){const s=String(n||"").trim()||i;let o=r.resolve(t,s);if(o!==t&&!o.startsWith(t+r.sep))throw new Error("输出路径越出工作目录:"+s);return/\.svg$/i.test(o)?o=o.replace(/\.svg$/i,".png"):/\.png$/i.test(o)||(o+=".png"),e.mkdirSync(r.dirname(o),{recursive:!0}),o}(n,t?.path,defaultChartPath(s)),a=await u(r.dirname(o),s,e=>p(n,"chart_render.py",[e,o],{config:i,timeoutMs:23e4,needsMpl:!0}));if(!a.ok)return`出图失败:${a.error}`;const c=e=>r.relative(n,e).split(r.sep).join("/"),d=[`已出图:${c(a.png)}(${a.width_cm}×${a.height_cm}cm,300dpi,插入Word后即版心宽、图中字号 9 磅)+ ${c(a.svg)}`,`插入文档:Markdown写 ![图题](${c(a.png)})(草稿在子目录时写相对草稿的路径也行)。图题用名词短语(如「项目累计净现金流量」),这张图说明了什么写在引出它的正文句里。`];return a.table_md&&d.push("图中数据(需要时作附表或正文引用,数值与图一致):\n"+a.table_md),a.warnings?.length&&d.push("提醒:\n"+a.warnings.map(e=>"· "+e).join("\n")),d.join("\n")}catch(e){return`出图失败:${e.message}`}}},{readOnly:!0,timeoutMs:6e4,spec:{type:"function",function:{name:"fin_model",description:'可研 / 立项 / 商业计划的财务测算:给假设,返回投资估算表、项目投资现金流量表、财务评价指标(所得税前/后NPV、IRR、静态与动态投资回收期、总投资收益率)、情景分析、敏感性分析(含临界点)、一段可直接引用的结论句、测算口径说明,以及现成的图表spec(交给render_chart出图)。**报告里的财务数字一律用它算,不要心算**——回收期要扣经营成本、节约额不能超过被节约的支出、口径按《建设项目经济评价方法与参数》(年末习惯法、含建设期)。\n假设字段:years运营期年数、construction_years建设期(默认 1)、start_year计算期首年、discount_rate基准收益率(0.08)、tax_rate所得税率(0 只算税前)、investment:[{name, amount, basis}]、revenue:[{name, values:[逐年] | price+volume:[逐年] | per_year+growth, basis}]、costs:[{name, values|per_year+growth|pct_of_revenue, basis}]、savings:[{name, base_values:[被节约的支出逐年], rate, basis}]、scenarios:{"悲观":{"revenue":0.7,"costs":1.1}}(乘数:悲观情景收入 ≤1、成本 ≥1)、unit(默认 万元)。**按pct_of_revenue列的成本在情景与敏感性里自动随收入变动**,不必为了「跟随收入」去调低costs;costs乘数再乘在全部经营成本上。逐年数组只写运营期(不含建设期)。',parameters:{type:"object",properties:{assumptions:{type:"object",description:"测算假设(字段见说明);每项尽量写basis(测算依据),会原样进口径说明",additionalProperties:!0},assumptions_path:{type:"string",description:"可选:从JSON文件读假设"}},required:[]}}},run:async s=>{try{let o=l(s?.assumptions,"assumptions");if(!o&&s?.assumptions_path){const t=r.resolve(n,String(s.assumptions_path));if(!e.existsSync(t))return`假设文件不存在:${s.assumptions_path}`;o=JSON.parse(e.readFileSync(t,"utf8"))}if(!o)return"fin_model需要assumptions(投资、收入/节约、经营成本、年限、折现率)。";const a=JSON.stringify(o);let c;if(a.length<24e3)c=await p(n,"fin_model.py",[a],{config:i,timeoutMs:55e3});else{const s=e.mkdtempSync(r.join(t.tmpdir(),"huicai-fin-"));try{c=await u(s,o,e=>p(n,"fin_model.py",[e],{config:i,timeoutMs:55e3}))}finally{e.rmSync(s,{recursive:!0,force:!0})}}if(!c.ok)return`测算失败:${c.error}`;const m=c.unit,d=[`【结论句(正文直接引用,数字与表一致)】\n${c.conclusion}`];c.warnings?.length&&d.push("【预警(评审会追问的地方,先处理再写)】\n"+c.warnings.map(e=>"· "+e).join("\n")),d.push("【测算口径说明(写进报告的「财务评价」开头)】\n"+c.notes.map(e=>"· "+e).join("\n"));for(const[e,t]of Object.entries(c.tables))d.push(`表:${e}(单位:${m})\n${t}`);return d.push("【可直接出图的spec(按需挑 2~3 张交给render_chart;path自定)】\n"+Object.entries(c.charts).map(([e,t])=>`${e}:${JSON.stringify(t)}`).join("\n")),d.join("\n\n")}catch(e){return`测算失败:${e.message}`}}},{readOnly:!0,timeoutMs:18e4,spec:{type:"function",function:{name:"project_facts",description:"给项目自己写报告(可研/概要设计/项目总结/结题/申报材料)前先取证:从代码库数出子工程与技术栈版本、后端模块、可部署服务与端口、HTTP接口数、前端页面、数据库表、测试用例、代码规模(同步副本去重)、提交历史与文档清单,每条带来源路径,另给一份模块树spec(交给render_diagram的tree图型)。正文里的规模数字从这里引,不要凭印象写「若干」「约十万行」。只出计数与名称,不读出任何配置值与密钥。",parameters:{type:"object",properties:{path:{type:"string",description:"要取证的目录(默认当前项目根)"}},required:[]}}},run:async t=>{try{const s=r.resolve(n,String(t?.path||"."));if(!e.existsSync(s)||!e.statSync(s).isDirectory())return`目录不存在:${t?.path||"."}`;const o=await p(n,"project_facts.py",[s],{config:i,timeoutMs:17e4});if(!o.ok)return`取证失败:${o.error}`;const a=[`【项目事实清单】(${o.root},扫描 ${o.files_scanned} 个文件)\n${o.markdown}`];return o.maven_modules?.length&&a.push("【后端模块】\n"+o.maven_modules.map(e=>`· ${e.module}:${e.description||"—"}`+(e.frameworks?.length?`(${e.frameworks.join("、")})`:"")).join("\n")),o.api?.total&&a.push("【接口分布】\n"+Object.entries(o.api.by_module).sort((e,t)=>t[1]-e[1]).map(([e,t])=>`· ${e}:${t}`).join("\n")),o.docs?.items?.length&&a.push(`【已有文档】(${o.docs.count} 篇,取需求/设计/方案类作论据)\n`+o.docs.items.slice(0,60).map(e=>`· ${e.path}:${e.title}`).join("\n")),o.diagram_tree&&a.push("【模块树spec(render_diagram用;按报告口径改名称与说明后再画)】\n"+JSON.stringify(o.diagram_tree)),o.warnings?.length&&a.push("提醒:\n"+o.warnings.map(e=>"· "+e).join("\n")),a.join("\n\n")}catch(e){return`取证失败:${e.message}`}}}]}
package/src/routing.js ADDED
@@ -0,0 +1 @@
1
+ export const SIGNALS=[["plan",t=>!!t.planMode,.6,"计划模式:用户显式要求先规划再动手,这本身就是判断密集的活"],["reshape",t=>/重构|重写|迁移|架构|设计方案|技术选型|拆分服务|升级到/.test(t.text),.55,"改造既有系统:要先读懂现状才能动手"],["diagnose",t=>/为什么.{0,12}(慢|不对|失败|报错|挂|崩|不生效|没反应)/.test(t.text)||/(慢|不对|失败|报错|挂了|崩了|不生效|没反应).{0,12}(为什么|怎么回事|咋回事)/.test(t.text)||/排查|定位|根因|复现|偶发|时好时坏|查一下.{0,8}原因/.test(t.text),.55,"排障:答案不在提问里,要靠推理找"],["concurrency",t=>/并发|死锁|竞态|线程安全|分布式事务|一致性|幂等/.test(t.text),.5,"并发与一致性:这类问题的错误答案看起来都对"],["security",t=>/安全审查|漏洞|越权|注入|渗透|加固|等保/.test(t.text),.45,"安全:漏判的代价不对称,宁可多花钱"],["build",t=>/(开发|搭建|做|写|实现|生成|制作|设计)(一个|一套|个|一款|一版)?.{0,16}(网站|网页|H5|h5|落地页|官网|小程序|App|APP|应用|系统|平台|后台|小游戏|游戏|邀请函|请柬|活动页|报名页|作品集|前端项目)/.test(t.text),.5,"从零构建完整作品:要同时做好设计、工程与验证,弱模型最容易交付半成品"],["perf",t=>/性能|优化|太慢|吞吐|延迟|内存泄漏|OOM/i.test(t.text),.3,"性能:可能是深度问题,也可能是一句话调参——只作佐证"],["long",t=>t.text.length>400,.25,"篇幅长:可能是复杂需求,也可能只是粘了一段日志——只作佐证"],["multistep",t=>/(先.{0,10}再|第一步|然后.{0,6}最后|分.{0,3}步|依次)/.test(t.text),.25,"多步骤:说明用户自己已经意识到这不是一步能完成的"],["attachments",t=>(t.attachmentCount||0)>0,.2,"带附件:要先读懂材料"]];export const DEFAULT_THRESHOLD=.5;export function scoreDifficulty(t={}){const e={text:String(t.text||""),planMode:!!t.planMode,attachmentCount:t.attachmentCount||0},o=[];let n=1;for(const[t,s,r,a]of SIGNALS){let u=!1;try{u=!!s(e)}catch{u=!1}u&&(o.push({name:t,weight:r,why:a}),n*=1-r)}return{score:Number((1-n).toFixed(4)),hits:o}}export function decideUpgrade(t={},e={}){const o=Number(e.threshold),n=Number.isFinite(o)?Math.min(1,Math.max(.01,o)):.5,{score:s,hits:r}=scoreDifficulty(t),a=s>=n,u=r.map(t=>t.name).join("+")||"无信号";return{upgrade:a,score:s,threshold:n,hits:r,reason:`${u} → ${s.toFixed(2)} ${a?"≥":"<"} ${n}`}}export const ROUTE_STATS_FILE="route-stats.json";export function emptyRouteStats(){return{total:0,upgraded:0,bySignal:{},byThreshold:{},since:0,updatedAt:0}}export function accumulateRoute(t,e,o=Date.now()){const n=t&&"object"==typeof t?{...emptyRouteStats(),...t}:emptyRouteStats();n.bySignal={...n.bySignal||{}},n.byThreshold={...n.byThreshold||{}},n.total+=1,e?.upgrade&&(n.upgraded+=1);for(const t of e?.hits||[])n.bySignal[t.name]=(n.bySignal[t.name]||0)+1;const s=String(e?.threshold??"");return s&&(n.byThreshold[s]=(n.byThreshold[s]||0)+1),n.since||(n.since=o),n.updatedAt=o,n}export function formatRouteStats(t){const e=t||emptyRouteStats();if(!e.total)return"模型升配:暂无数据(未配置autoModelStrong,或还没跑过回合)。";const o=(e.upgraded/e.total*100).toFixed(1),n=Object.entries(e.bySignal||{}).sort((t,e)=>e[1]-t[1]).slice(0,5).map(([t,e])=>`${t} ${e}`).join(" · "),s=[`模型升配:${e.upgraded}/${e.total} 回合(${o}%)`];n&&s.push(` 命中信号Top5:${n}`);const r=e.upgraded/e.total;return r>.7?s.push(" ⚠ 升配率偏高——阈值可能定得太低,等于把强模型当默认了"):r<.05&&s.push(" ⚠ 升配率偏低——阈值可能定得太高,这个功能几乎没生效"),s.join("\n")}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import o from"node:os";import r from"node:path";import t from"node:crypto";import{spawn as n}from"node:child_process";const c=new Map;export async function verifyBackend(i){if(!i||!i.kind)return{ok:!1,reason:"没有可用的沙箱后端。"};if(c.has(i.kind))return c.get(i.kind);const s=e.mkdtempSync(r.join(o.tmpdir(),"huicai-sbxprobe-")),a=r.join(o.homedir(),`.huicai-sandbox-probe-${t.randomBytes(6).toString("hex")}`),p="win32"===process.platform?[process.env.COMSPEC||"cmd.exe","/d","/s","/c",`echo probe > "${a}"`]:["/bin/sh","-c",`echo probe > "${a}"`];let d;try{const o=i.confineFor(p,{mode:"workspace-write",workspaceRoot:s}),r=await function(o,r,t){return new Promise(c=>{let i;try{i=n(o[0],o.slice(1),{cwd:t,stdio:["ignore","ignore","ignore"],env:{...process.env,HUICAI_PROBE_TARGET:r}})}catch{return c({spawned:!1,wrote:!1})}let s=!1;const a=()=>{if(s)return;s=!0,clearTimeout(p);let o=!1;try{o=e.existsSync(r)}catch{o=!1}c({spawned:!0,wrote:o})},p=setTimeout(()=>{try{i.kill("SIGKILL")}catch{}a()},15e3);i.on("error",()=>{clearTimeout(p),c({spawned:!1,wrote:!1})}),i.on("close",a)})}(o,a,s);d=r.spawned?r.wrote?{ok:!1,reason:"自检发现越界写入**没有被挡住**——沙箱在本机未真正生效,已按不可用处理。"}:{ok:!0,reason:""}:{ok:!1,reason:"沙箱运行器无法启动(自检未通过),不能声称保护已生效。"}}catch(e){d={ok:!1,reason:"沙箱自检异常:"+e.message}}finally{try{e.rmSync(a,{force:!0})}catch{}try{e.rmSync(s,{recursive:!0,force:!0})}catch{}}return c.set(i.kind,d),d}export function resetVerification(){c.clear()}export function verifiedResult(e){return c.get(e)||null}
@@ -0,0 +1,156 @@
1
+ // Windows 受限令牌启动器(P2-13)。
2
+ //
3
+ // 被 sandbox.js 当作「argv 包装器」使用,与 macOS 的 sandbox-exec、Linux 的 bwrap 同一个形状:
4
+ // node sandbox-win-launcher.mjs <真正要跑的命令...>
5
+ // 它用**写受限令牌**(write-restricted token)启动那条命令,然后把退出码原样带回。
6
+ //
7
+ // 为什么用 WRITE_RESTRICTED 而不是 deny-only SID:
8
+ // WRITE_RESTRICTED 的限制**只作用于写访问检查**,读完全不受影响。这与 macOS/Linux 侧
9
+ // 「只收紧写」的取舍一致——断掉读会让 npm/pip/git 全废,用户立刻就把沙箱关了。
10
+ // 带 deny-only SID 的方案会连读一起削,兼容性差得多。
11
+ //
12
+ // 写权限怎么给回工作区:调用方(sandbox.js)预先用 icacls 给工作区授予
13
+ // NT AUTHORITY\WRITE RESTRICTED(S-1-5-33)的修改权。写受限进程要成功写入,
14
+ // 必须同时通过「普通令牌」与「限制 SID」两道检查——于是只有被显式授权的工作区能写。
15
+ //
16
+ // stdio:直接把本进程的三个标准句柄交给子进程(STARTF_USESTDHANDLES),
17
+ // 于是不必在 FFI 里建管道——父进程(Node child_process)建的管道被一路继承下去。
18
+
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { createRequire } from 'node:module';
22
+
23
+ const require = createRequire(import.meta.url);
24
+
25
+ /** 与 CreateRestrictedToken 的 WRITE_RESTRICT 标志对应。 */
26
+ const WRITE_RESTRICTED = 0x0008;
27
+ const TOKEN_DUPLICATE = 0x0002;
28
+ const TOKEN_QUERY = 0x0008;
29
+ const TOKEN_ASSIGN_PRIMARY = 0x0001;
30
+ const TOKEN_ADJUST_DEFAULT = 0x0080;
31
+ const TOKEN_ADJUST_SESSIONID = 0x0100;
32
+ const STARTF_USESTDHANDLES = 0x00000100;
33
+ const CREATE_UNICODE_ENVIRONMENT = 0x00000400;
34
+ const INFINITE = 0xFFFFFFFF;
35
+
36
+ /**
37
+ * 把 argv 拼成 Windows 命令行(CreateProcess 只收单串;按 MSDN 的引号规则转义)。
38
+ *
39
+ * 导出是为了可测:这是 Windows 路径上最容易**静默**出错的一处——带空格的路径、
40
+ * 结尾反斜杠、内嵌引号,任何一处转义错了都不会报错,只会把命令悄悄执行成别的东西。
41
+ * 而这段逻辑是纯字符串处理,不需要真 Windows 就能验。
42
+ */
43
+ export function quoteArg(a) {
44
+ const s = String(a);
45
+ if (s !== '' && !/[\s"]/.test(s)) return s;
46
+ let out = '"';
47
+ let slashes = 0;
48
+ for (const ch of s) {
49
+ if (ch === '\\') { slashes++; out += ch; continue; }
50
+ if (ch === '"') { out += '\\'.repeat(slashes + 1) + '"'; slashes = 0; continue; }
51
+ slashes = 0;
52
+ out += ch;
53
+ }
54
+ out += '\\'.repeat(slashes) + '"';
55
+ return out;
56
+ }
57
+
58
+ function fail(msg) {
59
+ // 用固定前缀,便于 sandbox.js 的运行器失败规则识别(与 bwrap:/sandbox-exec: 同一角色)
60
+ process.stderr.write(`huicai-sandbox: ${msg}\n`);
61
+ process.exit(127);
62
+ }
63
+
64
+ // 被当模块 import 时不执行主流程(供单测验证 quoteArg 这类纯逻辑)。
65
+ // 直接被 node 拉起来跑时,process.argv[1] 就是本文件。
66
+ const invokedDirectly = (() => {
67
+ try { return process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); } catch { return false; }
68
+ })();
69
+ if (!invokedDirectly) { /* 仅导出工具函数,不启动任何进程 */ } else { main(); }
70
+
71
+ function main() {
72
+ const argv = process.argv.slice(2);
73
+ if (!argv.length) fail('缺少要执行的命令');
74
+
75
+ let koffi;
76
+ try {
77
+ koffi = require('koffi');
78
+ } catch (e) {
79
+ fail(`koffi不可用(${e.message})——Windows沙箱需要它。`);
80
+ }
81
+
82
+ try {
83
+ const advapi = koffi.load('advapi32.dll');
84
+ const kernel = koffi.load('kernel32.dll');
85
+
86
+ // 结构体:STARTUPINFOW / PROCESS_INFORMATION
87
+ const STARTUPINFOW = koffi.struct('STARTUPINFOW', {
88
+ cb: 'uint32', lpReserved: 'void *', lpDesktop: 'void *', lpTitle: 'void *',
89
+ dwX: 'uint32', dwY: 'uint32', dwXSize: 'uint32', dwYSize: 'uint32',
90
+ dwXCountChars: 'uint32', dwYCountChars: 'uint32', dwFillAttribute: 'uint32',
91
+ dwFlags: 'uint32', wShowWindow: 'uint16', cbReserved2: 'uint16', lpReserved2: 'void *',
92
+ hStdInput: 'void *', hStdOutput: 'void *', hStdError: 'void *',
93
+ });
94
+ const PROCESS_INFORMATION = koffi.struct('PROCESS_INFORMATION', {
95
+ hProcess: 'void *', hThread: 'void *', dwProcessId: 'uint32', dwThreadId: 'uint32',
96
+ });
97
+
98
+ const GetCurrentProcess = kernel.func('void *GetCurrentProcess()');
99
+ const GetStdHandle = kernel.func('void *GetStdHandle(uint32 nStdHandle)');
100
+ const CloseHandle = kernel.func('bool CloseHandle(void *h)');
101
+ const WaitForSingleObject = kernel.func('uint32 WaitForSingleObject(void *h, uint32 ms)');
102
+ const GetExitCodeProcess = kernel.func('bool GetExitCodeProcess(void *h, _Out_ uint32 *code)');
103
+ const GetLastError = kernel.func('uint32 GetLastError()');
104
+
105
+ const OpenProcessToken = advapi.func('bool OpenProcessToken(void *proc, uint32 access, _Out_ void **token)');
106
+ const CreateRestrictedToken = advapi.func(
107
+ 'bool CreateRestrictedToken(void *existing, uint32 flags, uint32 disableCount, void *disable,'
108
+ + ' uint32 deleteCount, void *deletePriv, uint32 restrictCount, void *restrict, _Out_ void **newToken)',
109
+ );
110
+ const CreateProcessAsUserW = advapi.func(
111
+ 'bool CreateProcessAsUserW(void *token, const char16_t *appName, char16_t *cmdLine, void *procAttr, void *threadAttr,'
112
+ + ' bool inherit, uint32 flags, void *env, const char16_t *cwd, STARTUPINFOW *si, _Out_ PROCESS_INFORMATION *pi)',
113
+ );
114
+
115
+ // 1) 取自身令牌
116
+ const tokenOut = [null];
117
+ const access = TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID;
118
+ if (!OpenProcessToken(GetCurrentProcess(), access, tokenOut)) fail(`OpenProcessToken失败(err=${GetLastError()})`);
119
+
120
+ // 2) 派生写受限令牌。不传自定义限制 SID:WRITE_RESTRICTED 会自动加入
121
+ // NT AUTHORITY\WRITE RESTRICTED(S-1-5-33),正是我们用 icacls 授权工作区的那个 SID。
122
+ const restrictedOut = [null];
123
+ if (!CreateRestrictedToken(tokenOut[0], WRITE_RESTRICTED, 0, null, 0, null, 0, null, restrictedOut)) {
124
+ fail(`CreateRestrictedToken失败(err=${GetLastError()})`);
125
+ }
126
+
127
+ // 3) 用受限令牌起进程,标准句柄直接传下去(父进程建的管道一路继承)
128
+ const si = {
129
+ cb: koffi.sizeof(STARTUPINFOW),
130
+ lpReserved: null, lpDesktop: null, lpTitle: null,
131
+ dwX: 0, dwY: 0, dwXSize: 0, dwYSize: 0, dwXCountChars: 0, dwYCountChars: 0, dwFillAttribute: 0,
132
+ dwFlags: STARTF_USESTDHANDLES, wShowWindow: 0, cbReserved2: 0, lpReserved2: null,
133
+ hStdInput: GetStdHandle(0xFFFFFFF6), // STD_INPUT_HANDLE (-10)
134
+ hStdOutput: GetStdHandle(0xFFFFFFF5), // STD_OUTPUT_HANDLE (-11)
135
+ hStdError: GetStdHandle(0xFFFFFFF4), // STD_ERROR_HANDLE (-12)
136
+ };
137
+ const pi = {};
138
+ const cmdLine = argv.map(quoteArg).join(' ');
139
+ const ok = CreateProcessAsUserW(
140
+ restrictedOut[0], null, cmdLine, null, null,
141
+ true, CREATE_UNICODE_ENVIRONMENT, null, process.cwd(), si, pi,
142
+ );
143
+ if (!ok) fail(`CreateProcessAsUserW失败(err=${GetLastError()})`);
144
+
145
+ WaitForSingleObject(pi.hProcess, INFINITE);
146
+ const codeOut = [0];
147
+ GetExitCodeProcess(pi.hProcess, codeOut);
148
+ CloseHandle(pi.hThread);
149
+ CloseHandle(pi.hProcess);
150
+ CloseHandle(restrictedOut[0]);
151
+ CloseHandle(tokenOut[0]);
152
+ process.exit(Number(codeOut[0]) || 0);
153
+ } catch (e) {
154
+ fail(`受限令牌启动异常:${e && e.message ? e.message : String(e)}`);
155
+ }
156
+ }
package/src/sandbox.js ADDED
@@ -0,0 +1 @@
1
+ import e from"node:fs";import n from"node:os";import r from"node:path";import{execFileSync as o}from"node:child_process";import{createRequire as t}from"node:module";import{fileURLToPath as i}from"node:url";import{verifyBackend as a,verifiedResult as s}from"./sandbox-probe.js";export const SANDBOX_MODES=["read-only","workspace-write","danger-full-access"];export const SANDBOX_UNAVAILABLE="SANDBOX_UNAVAILABLE";let l,c=null;function d(){if(void 0!==l)return l;if(l=null,"linux"!==process.platform)return null;const e=[];try{const n=t(import.meta.url)("@deepseek-ai/node-addon-landlock-run");"function"==typeof n?.launcherPath&&e.push(n.launcherPath())}catch{}u("landlock-run")&&e.push("landlock-run");for(const n of e)try{return o(n,["--probe"],{stdio:"ignore",timeout:5e3}),l=n,l}catch{}return null}function u(e){try{return o("win32"===process.platform?"where":"which",[e],{stdio:"ignore"}),!0}catch{return!1}}export function detectBackend(){return c||(c="darwin"===process.platform&&u("sandbox-exec")?{kind:"seatbelt",reason:""}:"linux"===process.platform&&d()?{kind:"landlock",reason:"",bin:d()}:"linux"===process.platform&&u("bwrap")?{kind:"bwrap",reason:""}:"win32"===process.platform?function(){try{return t(import.meta.url)("koffi"),!0}catch{return!1}}()?{kind:"win-restricted",reason:""}:{kind:null,reason:"Windows沙箱需要可选依赖koffi(未安装或加载失败)。可执行npm i koffi后重试;或在WSL2 里运行并安装bwrap;也可直接用 /sandbox权限模式——Windows上它会退回严格黑名单,比放行更保守。"}:"linux"===process.platform?{kind:null,reason:"未找到可用的内核沙箱后端。两条路任选其一:① npm i @deepseek-ai/node-addon-landlock-run(需内核 5.13+ 且启用Landlock,无需user namespace);② 安装bwrap(bubblewrap)。"}:{kind:null,reason:`当前平台 ${process.platform} 没有可用的沙箱后端。`},c)}export function resetSandboxProbe(){c=null,l=void 0}export function __setBackendForTest(e){c=e}const p=new Set;function f(e){if(p.has(e))return!0;try{return o("icacls",[e,"/grant","*S-1-5-33:(OI)(CI)(M)","/T","/C","/Q"],{stdio:"ignore"}),p.add(e),!0}catch{return!1}}export function resetGrants(){p.clear()}function m(n){try{return e.realpathSync(n)}catch{return r.resolve(String(n||"."))}}export function seatbeltProfile(e,r){const o=['(literal "/dev/null")','(literal "/dev/zero")','(literal "/dev/random")','(literal "/dev/urandom")','(literal "/dev/stdout")','(literal "/dev/stderr")','(literal "/dev/tty")','(regex #"^/dev/fd/")','(regex #"^/dev/ttys")'],t=["(version 1)","(allow default)","(deny file-write*)"];if("workspace-write"===e){const e=[m(r),m(n.tmpdir()),"/private/tmp","/private/var/tmp"].filter((e,n,r)=>e&&r.indexOf(e)===n);t.push("(allow file-write*",...e.map(e=>{return` (subpath ${n=e,'"'+String(n).replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'})`;var n}),...o.map(e=>" "+e),")")}else t.push("(allow file-write*",...o.map(e=>" "+e),")");return t.join("\n")+"\n"}export function confine(e,r){const o=String(r?.mode||"");if("danger-full-access"===o)throw new Error("danger-full-access不该走confine(调用方应直接执行原argv)");if(!SANDBOX_MODES.includes(o))throw new Error(`未知沙箱模式:${o}`);const t=detectBackend();if(!t.kind){const e=new Error(t.reason||"本机没有可用的沙箱后端。");throw e.code=SANDBOX_UNAVAILABLE,e}const a=m(r?.workspaceRoot||process.cwd());if("seatbelt"===t.kind)return{argv:["sandbox-exec","-p",seatbeltProfile(o,a),...e],backend:"seatbelt",enforcement:"full",denialSignatures:["operation not permitted","permission denied"],runnerFailureRules:[{fatalSignatures:["sandbox-exec:","failed to parse profile","sandbox_apply"],informationalLines:[],informationalPatterns:["deprecated"]}]};if("win-restricted"===t.kind){if(f(a),"workspace-write"===o){const e=m(n.tmpdir());e&&e!==a&&f(e)}return{argv:[process.execPath,i(new URL("./sandbox-win-launcher.mjs",import.meta.url)),...e],backend:"win-restricted",enforcement:"partial",denialSignatures:["access is denied","拒绝访问","permission denied"],runnerFailureRules:[{fatalSignatures:["huicai-sandbox:"],informationalLines:[],informationalPatterns:[]}]}}if("landlock"===t.kind){const r=["--ro","/"];if(r.push("--rw","/dev/null"),"workspace-write"===o){r.push("--rw",a,"--rw","/tmp");const e=m(n.tmpdir());"/tmp"!==e&&r.push("--rw",e)}return{argv:[t.bin,...r,"--",...e],backend:"landlock",enforcement:"full",denialSignatures:["permission denied","operation not permitted"],runnerFailureRules:[{fatalSignatures:["landlock-run: "],informationalLines:[],informationalPatterns:["partial enforcement"]}]}}const s=["--ro-bind","/","/","--dev","/dev","--proc","/proc"];if("workspace-write"===o){s.push("--bind",a,a,"--bind","/tmp","/tmp");const e=m(n.tmpdir());"/tmp"!==e&&s.push("--bind",e,e)}return{argv:["bwrap",...s,"--",...e],backend:"bwrap",enforcement:"full",denialSignatures:["read-only file system","permission denied"],runnerFailureRules:[{fatalSignatures:["bwrap:","setting up uid map","no permissions to create new namespace"],informationalLines:[],informationalPatterns:[]}]}}export function classifyFailure(e,n){const r=String(e||""),o=r.split("\n").map(e=>e.trim()).filter(Boolean);for(const e of n?.runnerFailureRules||[]){const n=new Set((e.informationalLines||[]).map(e=>e.toLowerCase())),r=(e.informationalPatterns||[]).map(e=>e.toLowerCase()),t=o.filter(e=>{const o=e.toLowerCase();return!n.has(o)&&!r.some(e=>o.includes(e))});if(t.some(n=>(e.fatalSignatures||[]).some(e=>n.toLowerCase().includes(String(e).toLowerCase()))))return"runner-failed"}const t=r.toLowerCase();return(n?.denialSignatures||[]).some(e=>t.includes(String(e).toLowerCase()))?"denied":"other"}export function denialNote(e,n){return`\n【沙箱拦截】当前处于沙箱模式(${e}),该命令试图写入工作区之外的位置,已被系统内核拒绝。\n可写范围:${n} 与临时目录。这是**策略拒绝,不是命令本身的bug**——换个写法重试同样会被拒。\n确实需要写到外面,请让用户关闭沙箱模式或改到工作区内操作。`}export function runnerFailureNote(e,n){return`\n【沙箱未生效】沙箱运行器(${e})启动失败,本次命令**未在沙箱中执行**。\n请把这一情况告知用户(保护没有生效)。运行器输出:${String(n||"").split("\n")[0].slice(0,200)}`}export function resolvePolicy(e,n){const r=String(e?.execSandbox||"off").trim();let o="read-only"===r?"read-only":"workspace-write"===r||"on"===r?"workspace-write":"danger-full-access";return"danger-full-access"===o&&e?.sandbox&&(o="workspace-write"),{mode:o,workspaceRoot:n,confine:"danger-full-access"!==o}}export function kernelAvailable(){const e=detectBackend();if(!e.kind)return!1;const n=s(e.kind);return n?n.ok:(ensureVerified(),!1)}export async function ensureVerified(){const e=detectBackend();return e.kind?a({kind:e.kind,confineFor:(e,n)=>confine(e,n).argv}):{ok:!1,reason:e.reason}}export function confineArgv(e,n,r){const o=resolvePolicy(n,r);if(!o.confine)return{argv:e,sandbox:null,note:""};try{const n=confine(e,o);return{argv:n.argv,sandbox:{...n,policy:o},note:""}}catch(n){if(n.code===SANDBOX_UNAVAILABLE)return{argv:e,sandbox:null,note:`\n【沙箱未生效】已配置执行沙箱,但${n.message}本次执行未受约束,请告知用户。`};throw n}}export function sandboxNote(e,n){if(!e)return"";const r=classifyFailure(n,e);return"runner-failed"===r?runnerFailureNote(e.backend,n):"denied"===r?denialNote(e.policy.mode,e.policy.workspaceRoot):""}export function sandboxStatus(e,n){const r=detectBackend(),o=resolvePolicy(e,n),t=r.kind?s(r.kind):null,i=!(!t||!t.ok),a=i?"win-restricted"===r.kind?"partial":"full":"none";return{backend:r.kind,reason:r.reason||(t&&!t.ok?t.reason:""),detected:!!r.kind,verified:t?t.ok:null,available:i,execSandbox:String(e?.execSandbox||"off"),mode:o.mode,confining:o.confine&&i,enforcement:a,workspaceRoot:n}}
package/src/scan.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import{execSync as n,execFileSync as r}from"node:child_process";import{captureCommand as a}from"./autofix.js";function o(n,r){try{return e.existsSync(t.join(n,r))}catch{return!1}}function c(e){try{return n("win32"===process.platform?`where ${e}`:`command -v ${e}`,{stdio:"ignore"}),!0}catch{return!1}}export function detectScanners(e){const t=[];return o(e,"package.json")&&c("npm")&&t.push({name:"npm audit",command:"npm audit --audit-level=high",why:"Node 依赖漏洞"}),(o(e,"requirements.txt")||o(e,"pyproject.toml"))&&c("pip-audit")&&t.push({name:"pip-audit",command:"pip-audit",why:"Python 依赖漏洞"}),o(e,"go.mod")&&c("govulncheck")&&t.push({name:"govulncheck",command:"govulncheck ./...",why:"Go 漏洞"}),o(e,"Cargo.toml")&&c("cargo-audit")&&t.push({name:"cargo audit",command:"cargo audit",why:"Rust 依赖漏洞"}),c("semgrep")&&t.push({name:"semgrep",command:"semgrep --config auto --error -q .",why:"代码静态安全(SAST)"}),c("gitleaks")&&t.push({name:"gitleaks",command:"gitleaks detect --no-banner -r /dev/stdout",why:"密钥泄漏"}),c("trivy")&&t.push({name:"trivy fs",command:"trivy fs --scanners vuln,secret -q .",why:"依赖漏洞+密钥"}),t}export async function runScanners(e,t){const n=[];for(const r of t){const{code:t,output:o}=await a(e,r.command,18e4);n.push({name:r.name,why:r.why,command:r.command,code:t,output:o})}return n}export const HEALTH_RULES=[{id:"dynamic-eval",severity:"high",why:"动态执行 eval / new Function",pattern:"\\beval\\(|new Function\\("},{id:"hardcoded-secret",severity:"high",why:"疑似硬编码密钥/口令",pattern:"(password|passwd|secret|api[_-]?key|access[_-]?key)\\s*[:=]\\s*[\"'][^\"']{6,}"},{id:"insecure-random",severity:"high",why:"安全场景疑用不安全随机源",pattern:"Math\\.random\\(\\)[^\\n]*(token|secret|key|nonce|salt|otp|passw)"},{id:"print-stack-trace",severity:"warn",why:"Java 直接打印堆栈(应走日志框架)",pattern:"printStackTrace\\("},{id:"stdout-print",severity:"warn",why:"裸 System.out/err 输出(应走日志)",pattern:"System\\.(out|err)\\.print"},{id:"empty-catch",severity:"warn",why:"空 catch 吞异常",pattern:"catch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}",multiline:!0},{id:"debugger",severity:"warn",why:"遗留 debugger 断点",pattern:"\\bdebugger\\b"},{id:"todo-fixme",severity:"info",why:"未完成标记 TODO/FIXME/HACK",pattern:"\\b(TODO|FIXME|HACK)\\b"}];const s=["!**/node_modules/**","!**/dist/**","!**/build/**","!**/target/**","!**/out/**","!**/.git/**","!**/*.min.js","!**/vendor/**"],i={high:0,warn:1,info:2};function u(e,t){if(!c("rg"))return{count:-1,sample:""};const n=s.flatMap(e=>["-g",e]),a=e.multiline?["-lU"]:["-l"];try{const o=r("rg",[...a,...n,"-e",e.pattern,"."],{cwd:t,encoding:"utf8",maxBuffer:16<<20,stdio:["ignore","pipe","ignore"]}).split("\n").filter(Boolean);return{count:o.length,sample:o[0]||""}}catch{return{count:0,sample:""}}}export function runHealthScan(e,t=u){const n=[];for(const r of HEALTH_RULES){const{count:a,sample:o}=t(r,e);a>0&&n.push({id:r.id,severity:r.severity,why:r.why,count:a,sample:o})}return n.sort((e,t)=>i[e.severity]-i[t.severity]||t.count-e.count),n}export function formatHealthScan(e){return e.length?["发现以下高信号项:",...e.map(e=>`- [${e.severity}] ${e.why}:命中 ${e.count} 文件${e.sample?`(如 ${e.sample})`:""}`)].join("\n"):"未发现高信号问题(eval/硬编码密钥/不安全随机/printStackTrace/空catch/debugger 等)。"}export function makeScanTool({cwd:e}){return{readOnly:!0,spec:{type:"function",function:{name:"security_scan",description:"运行内置代码健康自检(eval/硬编码密钥/不安全随机/printStackTrace/空catch/debugger 等高信号规则,无需外部工具)并自动检测运行本项目适用的外部扫描器(npm audit / semgrep / trivy / gitleaks,仅跑本机已装的),返回发现的问题。用于安全审查或提交前自检;发现问题后你应逐条修复再重扫确认。",parameters:{type:"object",properties:{},required:[]}}},run:async()=>{const t=[];t.push("## 代码健康自检(内置)\n"+formatHealthScan(runHealthScan(e)));const n=detectScanners(e);if(n.length){const r=await runScanners(e,n);t.push(...r.map(e=>`## ${e.name}(${0===e.code?"无高危":"有发现"})\n${e.output.slice(0,4e3)}`))}else t.push("(未检测到外部扫描器:可装 semgrep/trivy/gitleaks 获取更深的依赖漏洞/SAST/密钥扫描。)");return t.join("\n\n")}}}
1
+ import e from"node:fs";import t from"node:path";import{execSync as n,execFileSync as r}from"node:child_process";import{captureCommand as a}from"./autofix.js";function o(n,r){try{return e.existsSync(t.join(n,r))}catch{return!1}}function c(e){try{return n("win32"===process.platform?`where ${e}`:`command -v ${e}`,{stdio:"ignore"}),!0}catch{return!1}}export function detectScanners(e){const t=[];return o(e,"package.json")&&c("npm")&&t.push({name:"npm audit",command:"npm audit --audit-level=high",why:"Node依赖漏洞"}),(o(e,"requirements.txt")||o(e,"pyproject.toml"))&&c("pip-audit")&&t.push({name:"pip-audit",command:"pip-audit",why:"Python依赖漏洞"}),o(e,"go.mod")&&c("govulncheck")&&t.push({name:"govulncheck",command:"govulncheck ./...",why:"Go漏洞"}),o(e,"Cargo.toml")&&c("cargo-audit")&&t.push({name:"cargo audit",command:"cargo audit",why:"Rust依赖漏洞"}),c("semgrep")&&t.push({name:"semgrep",command:"semgrep --config auto --error -q .",why:"代码静态安全(SAST)"}),c("gitleaks")&&t.push({name:"gitleaks",command:"gitleaks detect --no-banner -r /dev/stdout",why:"密钥泄漏"}),c("trivy")&&t.push({name:"trivy fs",command:"trivy fs --scanners vuln,secret -q .",why:"依赖漏洞+密钥"}),t}export async function runScanners(e,t){const n=[];for(const r of t){const{code:t,output:o}=await a(e,r.command,18e4);n.push({name:r.name,why:r.why,command:r.command,code:t,output:o})}return n}export const HEALTH_RULES=[{id:"dynamic-eval",severity:"high",why:"动态执行eval / new Function",pattern:"\\beval\\(|new Function\\("},{id:"hardcoded-secret",severity:"high",why:"疑似硬编码密钥/口令",pattern:"(password|passwd|secret|api[_-]?key|access[_-]?key)\\s*[:=]\\s*[\"'][^\"']{6,}"},{id:"insecure-random",severity:"high",why:"安全场景疑用不安全随机源",pattern:"Math\\.random\\(\\)[^\\n]*(token|secret|key|nonce|salt|otp|passw)"},{id:"print-stack-trace",severity:"warn",why:"Java直接打印堆栈(应走日志框架)",pattern:"printStackTrace\\("},{id:"stdout-print",severity:"warn",why:"裸System.out/err输出(应走日志)",pattern:"System\\.(out|err)\\.print"},{id:"empty-catch",severity:"warn",why:"空catch吞异常",pattern:"catch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}",multiline:!0},{id:"debugger",severity:"warn",why:"遗留debugger断点",pattern:"\\bdebugger\\b"},{id:"todo-fixme",severity:"info",why:"未完成标记TODO/FIXME/HACK",pattern:"\\b(TODO|FIXME|HACK)\\b"}];const s=["!**/node_modules/**","!**/dist/**","!**/build/**","!**/target/**","!**/out/**","!**/.git/**","!**/*.min.js","!**/vendor/**"],i={high:0,warn:1,info:2};function u(e,t){if(!c("rg"))return{count:-1,sample:""};const n=s.flatMap(e=>["-g",e]),a=e.multiline?["-lU"]:["-l"];try{const o=r("rg",[...a,...n,"-e",e.pattern,"."],{cwd:t,encoding:"utf8",maxBuffer:16<<20,stdio:["ignore","pipe","ignore"]}).split("\n").filter(Boolean);return{count:o.length,sample:o[0]||""}}catch{return{count:0,sample:""}}}export function runHealthScan(e,t=u){const n=[];for(const r of HEALTH_RULES){const{count:a,sample:o}=t(r,e);a>0&&n.push({id:r.id,severity:r.severity,why:r.why,count:a,sample:o})}return n.sort((e,t)=>i[e.severity]-i[t.severity]||t.count-e.count),n}export function formatHealthScan(e){return e.length?["发现以下高信号项:",...e.map(e=>`- [${e.severity}] ${e.why}:命中 ${e.count} 文件${e.sample?`(如 ${e.sample})`:""}`)].join("\n"):"未发现高信号问题(eval/硬编码密钥/不安全随机/printStackTrace/空catch/debugger等)。"}export function makeScanTool({cwd:e}){return{readOnly:!0,spec:{type:"function",function:{name:"security_scan",description:"运行内置代码健康自检(eval/硬编码密钥/不安全随机/printStackTrace/空catch/debugger等高信号规则,无需外部工具)并自动检测运行本项目适用的外部扫描器(npm audit / semgrep / trivy / gitleaks,仅跑本机已装的),返回发现的问题。用于安全审查或提交前自检;发现问题后你应逐条修复再重扫确认。",parameters:{type:"object",properties:{},required:[]}}},run:async()=>{const t=[];t.push("## 代码健康自检(内置)\n"+formatHealthScan(runHealthScan(e)));const n=detectScanners(e);if(n.length){const r=await runScanners(e,n);t.push(...r.map(e=>`## ${e.name}(${0===e.code?"无高危":"有发现"})\n${e.output.slice(0,4e3)}`))}else t.push("(未检测到外部扫描器:可装semgrep/trivy/gitleaks获取更深的依赖漏洞/SAST/密钥扫描。)");return t.join("\n\n")}}}
package/src/schedule.js CHANGED
@@ -1 +1 @@
1
- import n from"node:fs";import e from"node:path";import t from"node:os";import{freqToCron as r}from"./schedule-freq.js";export const SCHEDULE_FILE=e.join(t.homedir(),".huicai","schedule.json");function o(){try{const e=JSON.parse(n.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(e)?e:[]}catch{return[]}}function i(t){n.mkdirSync(e.dirname(SCHEDULE_FILE),{recursive:!0}),n.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}function d(n){const e={...n};if(e.freq&&"object"==typeof e.freq){const n=r(e.freq);n&&(e.cron=n)}return void 0===e.enabled&&(e.enabled=!0),null==e.permission&&(e.permission="default"),null==e.model&&(e.model="auto"),e}export function listTasks(){return o()}export function getTask(n){return o().find(e=>e.id===n)||null}export function addTask(n){const e=o(),t=n&&n.id||Date.now().toString(36)+Math.random().toString(36).slice(2,4),r=Date.now(),u=d({...n,id:t,created:n&&n.created||r,updated:r});if(!u.cron&&!u.freq)throw new Error("缺少执行频率(freq)或 cron 表达式");return e.push(u),i(e),{id:t}}export function updateTask(n,e){const t=o(),r=t.findIndex(e=>e.id===n);return!(r<0)&&(t[r]=d({...t[r],...e,id:n,updated:Date.now()}),i(t),!0)}export function removeTask(n){const e=o(),t=e.filter(e=>e.id!==n);return i(t),t.length<e.length}export function setEnabled(n,e){return updateTask(n,{enabled:!!e})}export function touchRun(n,e=Date.now()){const t=o(),r=t.findIndex(e=>e.id===n);return!(r<0)&&(t[r].lastRunAt=e,i(t),!0)}export function crontabLine(n,r="huicai"){return`${n.cron} cd ${JSON.stringify(n.cwd)} && ${r} -p ${JSON.stringify(n.prompt)} --yolo --plain >> ${JSON.stringify(e.join(t.homedir(),".huicai","schedule-"+n.id+".log"))} 2>&1`}
1
+ import e from"node:fs";import n from"node:path";import t from"node:os";import{freqToCron as r}from"./schedule-freq.js";import{assertFeature as o,assertLimit as i}from"./plan-limits.js";export const SCHEDULE_FILE=n.join(t.homedir(),".huicai","schedule.json");function d(){try{const n=JSON.parse(e.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(n)?n:[]}catch{return[]}}function c(t){e.mkdirSync(n.dirname(SCHEDULE_FILE),{recursive:!0}),e.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}function a(e){const n={...e};if(n.freq&&"object"==typeof n.freq){const e=r(n.freq);e&&(n.cron=e)}return void 0===n.enabled&&(n.enabled=!0),null==n.permission&&(n.permission="default"),null==n.model&&(n.model="auto"),"watch"===n.kind?(n.watch={source:"deps",deliver:"dry",...n.watch||{}},["dry","notify","webhook","channel"].includes(n.watch.deliver)||(n.watch.deliver="dry"),"webhook"!==n.watch.deliver||String(n.watch.webhookUrl||"").trim()||(n.watch.deliver="dry"),"channel"!==n.watch.deliver||n.watch.channelId||(n.watch.deliver="dry")):null==n.kind&&(n.kind="action"),n}export function listTasks(){return d()}export function countActiveTasks(e=d()){return e.filter(e=>e&&!(e.freq&&"once"===e.freq.kind&&e.lastRunAt)).length}export function getTask(e){return d().find(n=>n.id===e)||null}export function addTask(e){const n=d(),t=e&&e.id||Date.now().toString(36)+Math.random().toString(36).slice(2,4),r=Date.now(),o=a({...e,id:t,created:e&&e.created||r,updated:r});if(!o.cron&&!o.freq)throw new Error("缺少执行频率(freq)或cron表达式");return i("scheduled_tasks",countActiveTasks(n)),n.push(o),c(n),{id:t}}export function updateTask(e,n){const t=d(),r=t.findIndex(n=>n.id===e);return!(r<0)&&(function(e){return!e||Object.keys(e).every(n=>"enabled"===n&&!0!==e.enabled)}(n)||o("agent_tasks"),t[r]=a({...t[r],...n,id:e,updated:Date.now()}),c(t),!0)}export function removeTask(e){const n=d(),t=n.filter(n=>n.id!==e);return c(t),t.length<n.length}export function setEnabled(e,n){return updateTask(e,{enabled:!!n})}export function touchRun(e,n=Date.now()){const t=d(),r=t.findIndex(n=>n.id===e);return!(r<0)&&(t[r].lastRunAt=n,c(t),!0)}export function crontabLine(e,r="huicai"){return`${e.cron} cd ${JSON.stringify(e.cwd)} && ${r} -p ${JSON.stringify(e.prompt)} --yolo --plain >> ${JSON.stringify(n.join(t.homedir(),".huicai","schedule-"+e.id+".log"))} 2>&1`}
@@ -0,0 +1 @@
1
+ const t=/(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|requirements\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|Dockerfile|docker-compose\.ya?ml|Makefile|\.github\/workflows\/|tsconfig[^/]*\.json|vite\.config\.[jt]s|\.env(\.|$)|nginx\.conf)/i,e=/(^|\/)(package\.json|requirements\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?)$/i;export function parseDiff(t){const e=[];let n=null,o=null;for(const i of String(t||"").split(/\r?\n/)){const t=/^diff --git a\/(.+?) b\/(.+)$/.exec(i);t?(n={path:t[2],oldPath:t[1],added:[],removed:[],hunks:[],renamedFrom:null,binary:!1},e.push(n),o=null):n&&(i.startsWith("rename from ")?n.renamedFrom=i.slice(12).trim():i.startsWith("Binary files ")||i.startsWith("GIT binary patch")?n.binary=!0:i.startsWith("@@")?(o={header:i.slice(0,120),churn:0},n.hunks.push(o)):o&&(i.startsWith("+")&&!i.startsWith("+++")?(n.added.push(i.slice(1)),o.churn++):i.startsWith("-")&&!i.startsWith("---")&&(n.removed.push(i.slice(1)),o.churn++)))}return e}export function moduleOf(t){const e=String(t||"").split("/").filter(Boolean);return e.length<=1?"(根目录)":e.length>=3?`${e[0]}/${e[1]}`:e[0]}export function tokens(t){const e=new Set,n=String(t||"").toLowerCase();for(const t of n.match(/[a-z0-9][a-z0-9_.-]{1,}/g)||[]){e.add(t);for(const n of t.split(/[_.-]/))n.length>=2&&e.add(n)}for(const t of n.match(/[一-龥]{2,}/g)||[])for(let n=0;n+2<=t.length;n++)e.add(t.slice(n,n+2));return e}function n(t){if(!t.added.length||!t.removed.length)return!1;const e=t=>t.map(t=>t.replace(/\s+/g,"")).filter(Boolean).sort().join("\n");return e(t.added)===e(t.removed)}function o(t){return e.test(t.path)&&t.added.some(t=>/["']?[\w@./-]+["']?\s*[:=><^~"]/.test(t))}export function detect(e,i={}){const s=parseDiff(e).filter(t=>!t.binary),r=i.bigHunk>0?i.bigHunk:80,l=tokens(i.intent);if(!s.length)return{ok:!0,files:0,skipped:"没有可分析的改动",findings:[],outliers:[]};if(s.length<4)return{ok:!0,files:s.length,skipped:`只改了 ${s.length} 个文件,谈不上范围蔓延`,findings:[],outliers:[]};const a=new Map;for(const t of s){const e=moduleOf(t.path);a.has(e)||a.set(e,[]),a.get(e).push(t)}const h=[...a.entries()].sort((t,e)=>e[1].length-t[1].length),[c,f]=h[0],p=[];if(f.length/s.length>=.5&&a.size>1)for(const[t,e]of h.slice(1))for(const n of e){l.size>0&&[...tokens(n.path+" "+n.added.join(" ").slice(0,2e3))].some(t=>l.has(t))||p.push({path:n.path,module:t})}const u=[];p.length&&u.push({kind:"off-main-module",level:"ask",what:`主要改动集中在 ${c}(${f.length}/${s.length} 个文件),但另有 ${p.length} 个文件落在别处:${p.slice(0,8).map(t=>t.path).join("、")}`+(p.length>8?` 等 ${p.length} 个`:""),ask:"这些是本次要做的事必须带上的吗?是就说明一句理由;不是就拆成单独一次改动——合在一起最直接的代价是:评审的人只能整体通过或整体打回。"});const d=s.filter(e=>t.test(e.path));d.length&&u.push({kind:"config-edit",level:"ask",what:`改到了配置/构建文件:${d.map(t=>t.path).join("、")}`,ask:"这类文件的影响面远超它自身,且最容易在一堆功能改动里被漏看。确认是本次必需的改动,并在提交信息里单独点出来。"});const g=s.filter(o);g.length&&u.push({kind:"new-dependency",level:"ask",what:`依赖清单有新增:${g.map(t=>t.path).join("、")}`,ask:"引入一个依赖是需要单独说明的决定(体积、许可证、维护状况、供应链风险),不该混在功能提交里悄悄进来。"});const m=[];for(const t of s)for(const e of t.hunks)e.churn>r&&m.push({path:t.path,churn:e.churn});m.length&&u.push({kind:"oversized-hunk",level:"info",what:`有 ${m.length} 处超过 ${r} 行的巨型改动块:`+m.slice(0,5).map(t=>`${t.path}(${t.churn} 行)`).join("、"),ask:"这么大的块在评审时几乎必然被囫囵放过。能拆的话拆开,拆不了就在提交信息里写清它做了什么。"});const k=s.filter(n);return k.length&&u.push({kind:"formatting-only",level:"info",what:`${k.length} 个文件只是格式调整:${k.map(t=>t.path).join("、")}`,ask:"格式调整单独提一次。混在功能改动里会让真正的改动淹没在一片重排里,是评审最难受的情形。"}),{ok:!0,files:s.length,mainModule:c,mainCount:f.length,findings:u,outliers:p}}export function formatReport(t){if(t.skipped)return`范围检查:${t.skipped}。`;if(!t.findings.length)return`范围检查:${t.files} 个文件,主要集中在 ${t.mainModule},没有发现明显跑题。`;const e=[`范围检查:${t.files} 个文件(主战场 ${t.mainModule},${t.mainCount} 个)。有 ${t.findings.length} 处需要你确认——这些是**问题**不是**结论**,很多跨模块改动本来就是必要的:`];for(const n of t.findings)e.push(""),e.push(`· ${n.what}`),e.push(` ${n.ask}`);return e.join("\n")}export function makeScopeCheckTool({cwd:t,runGit:e,goalOf:n}){return[{readOnly:!0,spec:{type:"function",function:{name:"scope_check",description:"检查当前改动有没有**跑题**:主要改动集中在哪个模块、有哪些文件落在别处、是否顺手改了配置/构建文件、是否悄悄加了依赖、有没有巨型改动块或纯格式改动。\n什么时候用:提交前自查;一个长任务跑完之后(这时最容易发现「顺手改了八个无关文件」);用户问「这次改了些什么/是不是改多了」时。\n**它只提问不下结论**——很多跨模块改动是必要的,判断该由人做。",parameters:{type:"object",properties:{intent:{type:"string",description:"(可选)本次改动要做什么。不给则取当前目标;都没有就只按模块聚类判断"},staged:{type:"boolean",description:"(可选)只看已暂存的改动(默认看工作区全部改动)"}},required:[]}}},run:async o=>{const i=e(t,o?.staged?["diff","--cached"]:["diff","HEAD"]);if(null==i)return"取不到git diff(当前目录可能不是git仓库)。";if(!i.trim())return"当前没有未提交的改动。";return formatReport(detect(i,{intent:String(o?.intent||"").trim()||("function"==typeof n?n():"")||""}))}}]}
package/src/sdk.js ADDED
@@ -0,0 +1 @@
1
+ import{spawn as e}from"node:child_process";import{createInterface as o}from"node:readline";import t from"node:path";import{fileURLToPath as r}from"node:url";const s=t.join(t.dirname(r(import.meta.url)),"..","bin","huicai.js");export function connect({input:e,output:t,onUpdate:r,onPermission:s,kill:n}={}){const i=new Map;let c=0,p=!1;const a=e=>{p||t.write(JSON.stringify(e)+"\n")},d=o({input:e,crlfDelay:1/0});d.on("line",async e=>{const o=e.trim();if(!o)return;let t;try{t=JSON.parse(o)}catch{return void r?.({sessionUpdate:"protocol_error",text:`收到非协议数据:${o.slice(0,200)}`})}if("session/request_permission"===t.method&&null!=t.id){let e="reject";try{e=String(await(s?.(t.params))||"reject")}catch{e="reject"}return void a({jsonrpc:"2.0",id:t.id,result:{outcome:{optionId:e}}})}if("session/update"!==t.method){if(null!=t.id&&i.has(t.id)){const{resolve:e,reject:o}=i.get(t.id);i.delete(t.id),t.error?o(Object.assign(new Error(t.error.message),{code:t.error.code})):e(t.result)}}else r?.(t.params?.update,t.params)});const l=(e,o)=>new Promise((t,r)=>{if(p)return void r(new Error("连接已关闭"));const s=++c;i.set(s,{resolve:t,reject:r}),a({jsonrpc:"2.0",id:s,method:e,params:o})});return{initialize:(e={})=>l("initialize",{protocolVersion:1,clientCapabilities:{permission:!!s},...e}),newSession:async e=>(await l("session/new",{cwd:e})).sessionId,prompt:(e,o)=>l("session/prompt",{sessionId:e,prompt:[{type:"text",text:String(o)}]}),cancel:e=>l("session/cancel",{sessionId:e}),close(){if(!p){p=!0;for(const{reject:e}of i.values())e(new Error("连接已关闭"));i.clear(),d.close(),n?.()}}}}export function spawn({cwd:o,yolo:t=!1,env:r,onUpdate:n,onPermission:i,onStderr:c}={}){const p=[s,"acp"];t&&p.push("--yolo");const a=e(process.execPath,p,{cwd:o||process.cwd(),env:{...process.env,...r||{}},stdio:["pipe","pipe","pipe"]});a.stderr.on("data",e=>c?.(String(e)));const d=connect({input:a.stdout,output:a.stdin,onUpdate:n,onPermission:i,kill:()=>{try{a.kill("SIGTERM")}catch{}}});return d.child=a,d}
@@ -0,0 +1 @@
1
+ export function questionSelection(e){const t="text"===e.kind||"date_range"===e.kind,n="confirm"===e.kind?[{value:!0,label:"是,确认"},{value:!1,label:"否"}]:(e.options||[]).map(e=>({...e,description:e.hint||""}));return t||n.push({value:null,label:"其他意见…",custom:!0,description:"展开输入区,补充自己的方案或要求"}),{kind:"question",title:e.question,context:e.note||"",options:n,multiple:"multi_choice"===e.kind,current:e.defaultValue,selected:Array.isArray(e.defaultValue)?n.filter(t=>!t.custom&&e.defaultValue.includes(t.value)).map(e=>e.value):[],textOnly:t,editing:t,answerText:"",cursor:0,searchable:!1}}export function approvalSelection({label:e,detail:t="",name:n,args:r,risk:l="",consent:u=!1,policyRule:i=!1}){return{kind:"approval",title:"auto_execute"===n?"方案已生成,是否开始执行?":"是否允许此操作?",current:"deny",searchable:!1,context:`${e}${t?"\n"+t:""}${l?"\n⚠ "+l:""}\n${JSON.stringify(r,null,2)}`,options:[{value:"once",label:"auto_execute"===n?"是,执行本次方案":"是,仅允许这一次",description:"本次授权,不改变后续操作的审批策略"},...i?[]:[{value:"session",label:"是,本会话同类操作不再询问",description:`仅在当前会话中允许 ${n}`},...u?[]:[{value:"project",label:"是,记住为项目规则",description:"将允许规则写入 .huicai/settings.json,后续会话也会生效"}]],{value:"deny",label:"auto_execute"===n?"暂不执行,继续调整方案":"否,拒绝本次操作",description:"保持现有权限与文件不变"}]}}export function initialSelection(e){return{...e,query:"",index:Math.max(0,e.options.findIndex(t=>!t.custom&&t.value===e.current))}}const e=new Intl.Segmenter(void 0,{granularity:"grapheme"});function t(t,n,r){const l=[...e.segment(t)].map(e=>e.index).concat(t.length);return r<0?l.filter(e=>e<n).at(-1)??0:l.find(e=>e>n)??t.length}export function insertSelectionText(e,t){const n=String(t).replace(/\r\n?/g,"\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,"");return{...e,answerText:e.answerText.slice(0,e.cursor)+n+e.answerText.slice(e.cursor),cursor:e.cursor+n.length}}export function selectionKey(e,n,r){if(r.escape)return e.editing&&!e.textOnly?{menu:{...e,editing:!1}}:{result:null};if(e.editing){if(r.return&&!r.shift&&!r.meta)return e.answerText.trim()?{result:{value:e.textOnly?e.answerText.trim():e.multiple?e.selected:null,...e.textOnly?{}:{freeText:e.answerText.trim()}}}:{menu:e};if(r.leftArrow||r.rightArrow)return{menu:{...e,cursor:t(e.answerText,e.cursor,r.leftArrow?-1:1)}};if(r.backspace||r.delete){const n=t(e.answerText,e.cursor,-1);return{menu:{...e,answerText:e.answerText.slice(0,n)+e.answerText.slice(e.cursor),cursor:n}}}return r.ctrl&&"u"===n?{menu:{...e,answerText:"",cursor:0}}:r.ctrl&&["a","e"].includes(n)?{menu:{...e,cursor:"a"===n?0:e.answerText.length}}:r.return||r.ctrl&&"j"===n?{menu:insertSelectionText(e,"\n")}:!n||r.ctrl||r.meta||r.tab?{menu:e}:{menu:insertSelectionText(e,n)}}const l=e.options[e.index];if(r.return)return l?.custom?{menu:{...e,editing:!0}}:"approval"===e.kind?{result:l?.value??null}:{result:{value:e.multiple?e.options.filter(t=>!t.custom&&e.selected.includes(t.value)).map(e=>e.value):l?.value??null}};if(" "===n&&e.multiple&&l&&!l.custom)return{menu:{...e,selected:e.selected.includes(l.value)?e.selected.filter(e=>e!==l.value):[...e.selected,l.value]}};if(r.upArrow||r.downArrow||r.tab||r.pageUp||r.pageDown||r.ctrl&&["p","n"].includes(n)){const t=r.pageUp||r.pageDown?4:1,l=r.upArrow||r.pageUp||r.tab&&r.shift||r.ctrl&&"p"===n?-1:1;return{menu:{...e,index:Math.max(0,Math.min(e.options.length-1,e.index+t*l))}}}if(/^[1-9]$/.test(n)&&!r.ctrl&&!r.meta&&Number(n)<=e.options.length)return{menu:{...e,index:Number(n)-1}};if("approval"===e.kind&&!r.ctrl&&!r.meta){const t={y:"once",a:"session",A:"project",n:"deny"}[n];if(e.options.some(e=>e.value===t))return{result:t}}return{menu:e}}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:path";import{getIndex as n}from"./sessionindex.js";import{SESSIONS_DIR as r,userKeyOf as s}from"./session.js";import{tokenize as o}from"./codeindex.js";const i=1200;function c(e){return"string"==typeof e?e:Array.isArray(e)?e.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):""}function u(e,t){const n=e.toLowerCase();let r=-1;for(const e of t){const t=n.indexOf(e);t>=0&&(r<0||t<r)&&(r=t)}r<0&&(r=0);const s=Math.max(0,r-110),o=Math.min(e.length,r+110);return(s>0?"…":"")+e.slice(s,o).replace(/\s+/g," ").trim()+(o<e.length?"…":"")}function l(e,t,n){if(null===t)return!0;const r=e.user||"";return r===t||n&&""===r}export function searchEvents(e,t={}){const r=[...new Set(o(e||""))];if(!r.length)return[];const s=t.index||n();if(!s.N)return[];const{df:i,N:c,avgLen:d}=s,a=new Map;for(const e of r){const t=i.get(e)||1;a.set(e,Math.log(1+(c-t+.5)/(t+.5)))}const p=void 0===t.userKey?null:String(t.userKey||""),f=[];let m=0;for(const e of s.entries){if(!l(e.meta,p,!!t.includeUnowned)){""===(e.meta.user||"")&&p&&m++;continue}if(t.cwd&&e.meta.cwd!==t.cwd)continue;if(t.sessionId&&e.meta.id!==t.sessionId)continue;if(t.role&&e.role!==t.role)continue;let n=0;for(const t of r){const r=e.tf.get(t);r&&(n+=a.get(t)*(2.5*r/(r+1.5*(.25+e.len/d*.75))))}n<=0||f.push({sessionId:e.meta.id,title:e.meta.title,cwd:e.meta.cwd,updatedAt:e.meta.updatedAt,i:e.i,role:e.role,score:n,snippet:u(e.text,r)})}const g=Number.isFinite(t.k)&&t.k>0?Math.min(t.k,40):10,h=f.sort((e,t)=>t.score-e.score).slice(0,g);return Object.defineProperty(h,"unownedFiltered",{value:m,enumerable:!1}),h}const d=(e,t)=>{const n=String(e??"").replace(/\r/g,"");return n.length>t?n.slice(0,t)+`…(截断,原长 ${n.length} 字符)`:n};export function traceEvent(n,s,o={}){const u=function(n){const s=String(n||"").trim();if(!s||!/^[A-Za-z0-9._-]{1,128}$/.test(s))return null;try{const n=JSON.parse(e.readFileSync(t.join(r,s+".json"),"utf8"));return n&&Array.isArray(n.messages)?n:null}catch{return null}}(n);if(!u)return{ok:!1,error:`找不到会话或无法读取:${n}`};const a=void 0===o.userKey?null:String(o.userKey||"");if(!l({user:u.user?String(u.user):""},a,!!o.includeUnowned))return{ok:!1,error:`找不到会话或无法读取:${n}`};const p=u.messages;let f=Number(s);if(!Number.isInteger(f)||f<0||f>=p.length)return{ok:!1,error:`锚点下标越界:${s}(该会话共 ${p.length} 条消息)`};let m=f;for(;m>0&&"user"!==p[m].role;)m--;"user"!==p[m].role&&(m=Math.max(1,f));let g=m+1;for(;g<p.length&&"user"!==p[g].role;)g++;let h=0;for(let e=0;e<=m;e++)"user"===p[e].role&&h++;const y=new Map;for(let e=m;e<g;e++){const t=p[e];"tool"===t.role&&t.tool_call_id&&y.set(t.tool_call_id,{i:e,content:c(t.content)})}const $=[];for(let e=m;e<g;e++){const t=p[e];if("tool"===t.role)continue;const n=c(t.content).trim();if("user"===t.role){$.push({i:e,kind:"user",text:d(n,i)});continue}if("assistant"!==t.role)continue;const r=Array.isArray(t.tool_calls)?t.tool_calls:[];if(r.length){n&&$.push({i:e,kind:"assistant",text:d(n,i)});for(const t of r){const n=y.get(t.id);$.push({i:e,kind:"tool",name:t.function?.name||"(未知工具)",args:d(t.function?.arguments||"",300),resultAt:n?n.i:null,result:n?d(n.content,700):"(无结果:该调用可能被中断或未执行)"})}}else n&&$.push({i:e,kind:"assistant",text:d(n,i)})}return{ok:!0,sessionId:u.id||n,title:u.title||"",cwd:u.cwd||"",updatedAt:u.updatedAt||0,turn:h,range:[m,g-1],anchor:f,steps:$}}const a=(e,t)=>e.i===t||e.resultAt===t;export function formatTrace(e){if(!e.ok)return e.error;const t=[`会话「${e.title||e.sessionId}」第 ${e.turn} 个用户回合(消息 ${e.range[0]}–${e.range[1]}${e.cwd?",项目 "+e.cwd:""}):`,""];for(const n of e.steps){const r=a(n,e.anchor)?" ← 锚点":"";"user"===n.kind?t.push(`[${n.i}] 用户${r}:${n.text}`):"assistant"===n.kind?t.push(`[${n.i}] 助手${r}:${n.text}`):(t.push(`[${n.i}] 调用 ${n.name}(${n.args})${r}`),t.push(` → ${String(n.result).replace(/\n/g,"\n ")}`)),t.push("")}return t.join("\n").trimEnd()}export function formatEvents(e,t){if(!e.length){const n=e.unownedFiltered||0;return`历史对话里没有匹配「${t}」的消息。`+(n?`\n(另有 ${n} 条登录之前留下的旧记录命中,因不带用户标记未列出;带include_unowned=true重试可纳入。)`:"")}const n={user:"用户",assistant:"助手",tool:"工具"},r=[`匹配「${t}」的 ${e.length} 条消息(按相关性;用session_event_trace展开某一条的前因后果):`,""];for(const t of e)r.push(`· [${t.sessionId} #${t.i}] ${n[t.role]||t.role} ${t.title||"(未命名会话)"}${t.cwd?" "+t.cwd:""}`),r.push(` ${t.snippet}`);return r.join("\n")}export function makeSessionEventTools({cwd:e,config:t}){const n=()=>("function"==typeof t?t():t)||{};return[{readOnly:!0,spec:{type:"function",function:{name:"session_event_search",description:"在历史对话里按内容检索到**具体某一条消息**(用户提问 / 助手回复 / 工具结果),返回会话id与消息下标。与search_history的分工:那个回答「哪次对话聊过」,这个回答「具体是哪一步」——找到后用session_event_trace展开它的前因后果。适合「上次改nginx配置那条命令具体是怎么写的」这类要看细节的问题。",parameters:{type:"object",properties:{query:{type:"string",description:"要找什么(自然语言或关键词,中英皆可)"},k:{type:"number",description:"可选:返回条数,默认 10,上限 40"},role:{type:"string",enum:["user","assistant","tool"],description:"可选:只看某一类消息"},session_id:{type:"string",description:"可选:限定在某个会话里找"},this_project:{type:"boolean",description:"可选:只在当前项目目录的会话里找"},include_unowned:{type:"boolean",description:"可选:连登录之前留下的无主旧会话一起检索"}},required:["query"]}}},run:async t=>{const r=String(t?.query||"").trim();if(!r)return"缺少query(要在历史对话里找什么)";return formatEvents(searchEvents(r,{k:t?.k,role:t?.role,sessionId:t?.session_id,userKey:s(n()),cwd:t?.this_project?e:void 0,includeUnowned:!!t?.include_unowned}),r)}},{readOnly:!0,spec:{type:"function",function:{name:"session_event_trace",description:"展开历史对话里某一条消息的**前因后果**:它所在那个用户回合的完整链条——用户提了什么要求、助手调了哪些工具、每个工具返回了什么、最后得出什么结论。先用session_event_search或search_history定位,再用它看细节。",parameters:{type:"object",properties:{session_id:{type:"string",description:"会话id(检索结果里方括号内的第一段)"},index:{type:"number",description:"消息下标(检索结果里 # 后面的数字)"},include_unowned:{type:"boolean",description:"可选:允许追踪登录之前留下的无主旧会话"}},required:["session_id","index"]}}},run:async e=>formatTrace(traceEvent(e?.session_id,e?.index,{userKey:s(n()),includeUnowned:!!e?.include_unowned}))}]}
package/src/session.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n}){const r=Date.now();return{id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:r,updatedAt:r,title:"",messages:n||[],compaction:{}}}export function saveSession(e){if(e&&e.id)try{r(),e.updatedAt=Date.now(),t.writeFileSync(o(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(o(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{r(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const i=[];for(const r of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,r),"utf8"));if(n&&o.cwd!==n)continue;i.push({id:o.id||r.replace(/\.json$/,""),title:o.title||"(未命名会话)",cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages),user:o.user?String(o.user):""})}catch{}return i.sort((t,e)=>e.updatedAt-t.updatedAt),i}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e){try{return t.unlinkSync(o(e)),!0}catch{return!1}}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
1
+ import t from"node:fs";import e from"node:path";import{randomUUID as n}from"node:crypto";import{CONFIG_DIR as r}from"./config.js";import{dropSessionSpill as o}from"./spill.js";import{dropSessionJobs as i}from"./jobs.js";import{dropSessionObservations as s}from"./fs-observe.js";import{dropSessionTerminals as c}from"./terminal.js";import{dropSessionLog as l}from"./sessionlog.js";export const SESSIONS_DIR=e.join(r,"sessions");function a(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function u(t){return e.join(SESSIONS_DIR,t+".json")}export function userKeyOf(t){const e=t?.user;if(!e||"object"!=typeof e)return"";const n=e.id??e.userId??e.username??"";return null==n?"":String(n)}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n,parentSession:r,origin:o,delegationDepth:i,label:s,user:c}){const l=Date.now(),a={id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:l,updatedAt:l,title:"",messages:n||[],compaction:{}};return r&&(a.parentSession=String(r)),o&&(a.origin=String(o)),Number.isFinite(i)&&(a.delegationDepth=Math.max(0,Math.floor(i))),s&&(a.label=String(s).replace(/\s+/g," ").slice(0,120)),c&&(a.user=String(c)),a}export function saveSession(e){if(!e||!e.id)return;let r;try{a(),e.updatedAt=Date.now();const o=JSON.stringify(e),i=u(e.id);r=i+"."+n()+".tmp",t.writeFileSync(r,o,{mode:384,flag:"wx"}),t.renameSync(r,i)}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}finally{if(r)try{t.unlinkSync(r)}catch{}}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(u(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let r;try{a(),r=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const o=[];for(const i of r)try{const r=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,i),"utf8"));if(n&&r.cwd!==n)continue;o.push({id:r.id||i.replace(/\.json$/,""),title:r.title||"(未命名会话)",cwd:r.cwd,model:r.model,updatedAt:r.updatedAt||0,turns:turnCount(r.messages),user:r.user?String(r.user):"",origin:r.origin||"",parentSession:r.parentSession||"",delegationDepth:Number.isFinite(r.delegationDepth)?r.delegationDepth:0,label:r.label||""})}catch{}return o.sort((t,e)=>e.updatedAt-t.updatedAt),o}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e,n){const r=n||new Set;if(r.has(e))return!1;r.add(e);for(const t of listChildSessions(e))deleteSession(t.id,r);let a=!1;try{t.unlinkSync(u(e)),a=!0}catch{a=!1}return o(e),i(e),c(e),l(e),s(e),a}export function listChildSessions(t){return t?listSessions(null).filter(e=>e.parentSession===String(t)):[]}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:path";import n from"node:zlib";import{CONFIG_DIR as o}from"./config.js";import{SESSIONS_DIR as r,userKeyOf as s}from"./session.js";import{tokenize as i}from"./codeindex.js";const c=t.join(o,"cache","sessions-index.json.gz");let l=null;function u(e){if(!e||"system"===e.role)return"";const t=(n=e.content,"string"==typeof n?n:Array.isArray(n)?n.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):"").trim();var n;return t?t.slice(0,"tool"===e.role?1e3:4e3):""}function a(e){const t=new Map;for(const n of i(e))t.set(n,(t.get(n)||0)+1);return t}function f(n){let o;try{o=JSON.parse(e.readFileSync(n,"utf8"))}catch{return null}if(!o||!Array.isArray(o.messages))return null;const r=o.messages.slice(-300),s=o.messages.length-r.length,i=[];for(let e=0;e<r.length;e++){const t=u(r[e]);if(!t)continue;const n=a(t);i.push({i:s+e,role:r[e].role,text:t,tf:[...n],len:t.length})}return i.length?{meta:{id:o.id||t.basename(n,".json"),title:o.title||"",cwd:o.cwd||"",model:o.model||"",updatedAt:o.updatedAt||0,user:o.user?String(o.user):"",origin:o.origin||""},docs:i}:null}export function buildIndex(){const o=function(){try{const t=JSON.parse(n.gunzipSync(e.readFileSync(c)).toString("utf8"));return t&&1===t.v&&t.files&&"object"==typeof t.files?t.files:null}catch{return null}}()||Object.create(null),s=Object.create(null),i=Date.now();let l=[];try{l=e.readdirSync(r).filter(e=>e.endsWith(".json")).map(n=>{const o=t.join(r,n);try{const t=e.statSync(o);return{full:o,key:n,mtimeMs:t.mtimeMs,size:t.size}}catch{return null}}).filter(Boolean).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,800)}catch{l=[]}for(const e of l){const t=o[e.key];if(t&&t.mtimeMs===e.mtimeMs&&t.size===e.size&&Array.isArray(t.docs)){s[e.key]=t;continue}if(Date.now()-i>3e3){t&&(s[e.key]=t);continue}const n=f(e.full);n&&(s[e.key]={mtimeMs:e.mtimeMs,size:e.size,meta:n.meta,docs:n.docs})}!function(o){try{e.mkdirSync(t.dirname(c),{recursive:!0});const r=c+"."+process.pid+".tmp";e.writeFileSync(r,n.gzipSync(JSON.stringify({v:1,at:Date.now(),files:o})),{mode:384}),e.renameSync(r,c)}catch{}}(s);const u=[],a=new Map;let d=0;for(const e of Object.keys(s)){const t=s[e];for(const e of t.docs){const n=new Map(e.tf);u.push({meta:t.meta,i:e.i,role:e.role,text:e.text,tf:n,len:e.len}),d+=e.len;for(const e of n.keys())a.set(e,(a.get(e)||0)+1)}}return{entries:u,df:a,N:u.length,avgLen:u.length?d/u.length:1}}export function getIndex({force:t=!1}={}){const n=function(){try{return String(e.statSync(r).mtimeMs)}catch{return"none"}}();if(!t&&l&&l.stamp===n&&Date.now()-l.builtAt<3e5)return l.index;const o=buildIndex();return l={index:o,builtAt:Date.now(),stamp:n},o}export function clearIndex(){l=null;try{e.rmSync(c,{force:!0})}catch{}}function d(e,t){const n=e.toLowerCase();let o=-1;for(const e of t){const t=n.indexOf(e);t>=0&&(o<0||t<o)&&(o=t)}o<0&&(o=0);const r=Math.max(0,o-90),s=Math.min(e.length,o+90);return(r>0?"…":"")+e.slice(r,s).replace(/\s+/g," ").trim()+(s<e.length?"…":"")}export function searchSessions(e,t={}){const n=[...new Set(i(e||""))];if(!n.length)return[];const o=t.index||getIndex();if(!o.N)return[];const{df:r,N:s,avgLen:c}=o,l=new Map;for(const e of n){const t=r.get(e)||1;l.set(e,Math.log(1+(s-t+.5)/(t+.5)))}const u=void 0===t.userKey?null:String(t.userKey||""),a=!!t.includeUnowned,f=!!t.includeSubagents,m=new Map,p=new Set;for(const e of o.entries){if(null!==u){const t=e.meta.user||"";if(t!==u&&(!a||""!==t)){""===t&&u&&p.add(e.sessionId||e.meta.id||e.id);continue}}if(!f&&"subagent"===e.meta.origin)continue;if(t.cwd&&e.meta.cwd!==t.cwd)continue;let o=0;for(const t of n){const n=e.tf.get(t);n&&(o+=l.get(t)*(2.5*n/(n+1.5*(.25+e.len/c*.75))))}if(o<=0)continue;const r=new Set(i(e.meta.title));for(const e of n)r.has(e)&&(o+=.8*l.get(e));const s=m.get(e.meta.id);(!s||o>s.score)&&m.set(e.meta.id,{id:e.meta.id,title:e.meta.title,cwd:e.meta.cwd,model:e.meta.model,updatedAt:e.meta.updatedAt,role:e.role,i:e.i,score:o,snippet:d(e.text,n)})}const h=Number.isFinite(t.k)&&t.k>0?Math.min(t.k,50):10,g=[...m.values()].sort((e,t)=>t.score-e.score).slice(0,h);return Object.defineProperty(g,"unownedFiltered",{value:p.size,enumerable:!1}),g}function m(e){if(!e)return"未知时间";const t=Math.max(0,Math.floor((Date.now()-e)/1e3));if(t<60)return t+" 秒前";const n=Math.floor(t/60);if(n<60)return n+" 分钟前";const o=Math.floor(n/60);return o<24?o+" 小时前":Math.floor(o/24)+" 天前"}export function formatResults(e,t){if(!e.length){const n=e.unownedFiltered||0;return`历史会话中没有匹配「${t}」的内容。`+(n?`\n(另有 ${n} 次**登录之前留下的**旧会话命中了,因不带用户标记而未列出。用 \`/history `+t+" --legacy` 可一并查看。)":"")}const n=[`历史会话中匹配「${t}」的 ${e.length} 次对话(按相关性):`,""];for(const t of e)n.push(`· ${t.title||"(未命名会话)"} [${t.id}] ${m(t.updatedAt)}${t.cwd?" "+t.cwd:""}`),n.push(` ${"user"===t.role?"用户":"tool"===t.role?"工具":"助手"}:${t.snippet}`);return n.push("","用 `huicai --resume <会话id>` 可继续该对话。"),n.join("\n")}export async function searchHistory(e,t,n){const o=String(t?.query||"").trim();if(!o)return"缺少query(要在历史对话里找什么)";const r=searchSessions(o,{k:Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,userKey:s(n||{}),cwd:t?.this_project?e:void 0,includeUnowned:!!t?.include_unowned}),i=formatResults(r,o);return r.length||!r.unownedFiltered||t?.include_unowned?i:i+`\n(你可以带include_unowned=true重新调用search_history把这 ${r.unownedFiltered} 次旧会话一并检索。)`}
@@ -0,0 +1 @@
1
+ import t from"node:fs";import e from"node:path";import{SESSIONS_DIR as n}from"./session.js";export const LOG_VERSION=1;export const KNOWN_TYPES=new Set(["session/created","system/prompt","user/message","assistant/message","tool/result","turn/start","turn/end","session/rewind"]);export function logPathOf(t){return e.join(n,String(t)+".events.jsonl")}function o(t){if(!t||"object"!=typeof t)return null;if("system"===t.role)return{type:"system/prompt",data:{content:t.content}};if("user"===t.role)return{type:"user/message",data:{content:t.content}};if("tool"===t.role)return{type:"tool/result",data:{tool_call_id:t.tool_call_id,content:t.content}};if("assistant"===t.role){const e={content:t.content};return t.tool_calls&&(e.tool_calls=t.tool_calls),{type:"assistant/message",data:e}}return null}export function createSessionLog(n,s={}){const c=String(n?.id||""),i=s.dir?e.join(s.dir,c+".events.jsonl"):logPathOf(c),a=r(i);let l=a.length?Number(a[a.length-1].seq)||a.length:0,u=0,g=a.length>0,f=!1;if(a.length){const t=deriveMessages(a);t.ok?u=t.messages.length:f=!0}const p=(n,o)=>{if(f||!c)return;const r=JSON.stringify({v:1,seq:++l,time:Date.now(),type:n,data:o})+"\n";try{t.mkdirSync(e.dirname(i),{recursive:!0}),t.appendFileSync(i,r,{mode:384})}catch{f=!0}};return{path:i,get seq(){return l},get cursor(){return u},get broken(){return f},sync(t){const e=Array.isArray(t)?t:[];g||(g=!0,p("session/created",{id:c,cwd:n?.cwd||"",model:n?.model||"",createdAt:n?.createdAt||Date.now(),parentSession:n?.parentSession||"",origin:n?.origin||""}));for(let t=u;t<e.length;t++){const n=o(e[t]);n&&p(n.type,n.data)}u=e.length},turnStart(t){p("turn/start",{turn:t})},turnEnd(t,e){p("turn/end",{turn:t,reason:e||"stop"})},rewind(t){const e=Math.max(0,Number(t)||0);p("session/rewind",{length:e}),u=Math.min(u,e)}}}function r(e){let n;try{n=t.readFileSync(e,"utf8")}catch{return[]}const o=[];for(const t of n.split("\n")){const e=t.trim();if(!e)continue;let n;try{n=JSON.parse(e)}catch{continue}o.push(n)}return o}export function readEvents(t,n={}){return r(n.dir?e.join(n.dir,String(t)+".events.jsonl"):logPathOf(t))}export function deriveMessages(t){const e=[];for(const n of t||[]){if(!n||"object"!=typeof n)continue;if(Number(n.v)>1)return{ok:!1,error:`事件流版本 ${n.v} 高于本版本 1(由更新的客户端写入,请升级后再打开)。`};if(!KNOWN_TYPES.has(n.type)){if(n.ignorable)continue;return{ok:!1,error:`事件流含本版本不认识的事件类型「${n.type}」,拒绝重建(静默跳过可能得到一份错误的历史)。`}}const t=n.data||{};switch(n.type){case"system/prompt":e.push({role:"system",content:t.content});break;case"user/message":e.push({role:"user",content:t.content});break;case"tool/result":e.push({role:"tool",tool_call_id:t.tool_call_id,content:t.content});break;case"assistant/message":{const n={role:"assistant",content:t.content};t.tool_calls&&(n.tool_calls=t.tool_calls),e.push(n);break}case"session/rewind":e.length=Math.min(e.length,Math.max(0,Number(t.length)||0))}}return{ok:!0,messages:e}}export function dropSessionLog(e){forgetSessionLog(e);try{return t.rmSync(logPathOf(e),{force:!0}),!0}catch{return!1}}const s=new Map;export function logOf(t){const e=String(t?.id||"");if(!e)return null;let n=s.get(e);return n||(n=createSessionLog(t),s.set(e,n)),n}export function syncSessionLog(t){try{logOf(t)?.sync(t?.messages)}catch{}}export function rewindSessionLog(t,e){try{logOf(t)?.rewind(e)}catch{}}export function forgetSessionLog(t){s.delete(String(t||""))}export function resetSessionLogs(){s.clear()}
@@ -0,0 +1 @@
1
+ import e from"node:crypto";import{loadLocalSkills as n,isModelInvocable as t,compactDesc as o}from"./skills.js";import{platformMessageTokens as l}from"./context-window.js";const s=/^<system-reminder skill-catalog="([0-9a-f]{12})">/;export function catalogEntries(e){return n(e).filter(t).map(e=>({name:e.name,description:o(e.name,e.description)})).sort((e,n)=>e.name.localeCompare(n.name))}export function catalogDigest(n){const t=n.map(e=>`${e.name}\0${e.description}`).join("");return e.createHash("sha256").update(t).digest("hex").slice(0,12)}function a(n,t){const o=catalogDigest(n);return!t||t.full?o:e.createHash("sha256").update(`${o}|names|${t.shown}`).digest("hex").slice(0,12)}export function catalogShape(e,n=1/0){const t={full:!0,shown:e.length};if(!(n<1/0&&e.length))return t;const o=n=>l({role:"user",content:renderCatalogMessage(e,"replace",n).content});if(o(t)<=n)return t;let s=e.length;for(;s>1&&o({full:!1,shown:s})>n;)s--;return{full:!1,shown:s}}export function catalogDigestOf(e){if(!e||"user"!==e.role||"string"!=typeof e.content)return null;const n=s.exec(e.content);return n?n[1]:null}export function renderCatalogMessage(e,n="initial",t=null){const o=a(e,t),l=!!t&&!t.full,s=l?e.slice(0,Math.max(0,t.shown)):e;let r=`<system-reminder skill-catalog="${o}">\n`;if(!e.length)return r+="当前会话没有可用的本地技能。此前列出过的技能名一律作废,不要再尝试载入。\n",r+="</system-reminder>",{content:r,digest:o,count:0};r+="replace"===n?"技能清单已变化,以下是**当前完整清单**,此前列出过而这里没有的名称一律作废:\n\n":"restore"===n?"(上下文压缩后重新给出)当前可用的本地技能:\n\n":"技能是一组针对特定任务的可复用操作指引。本会话可用的技能如下:\n\n",l&&(r+="(当前会员等级的上下文窗口较小,这里只列技能名,描述在载入后可见。)\n"),r+="<available_skills>\n";for(const e of s)r+=l?`- ${e.name}\n`:`- ${e.name}:${e.description}\n`;return r+="</available_skills>\n",s.length<e.length&&(r+=`另有 ${e.length-s.length} 个技能未列出;用户点名时同样可以用load_skill(name) 按原名载入。\n`),r+="\n",r+=(l?"用户点名某个技能、或任务明显与某个技能名相关时,先用load_skill(name) 按原名载入,再开始动手;":"用户点名某个技能、或任务明显落在某条描述的范围内时,先用load_skill(name) 按原名载入,再开始动手;")+"可同时载入多条。这里只有摘要,未载入之前不要臆测或执行任何一条技能的内容。\n",r+="用户也可以直接敲 /技能名 把某条技能投进对话——那时它的 <skill_content> 已经在上文了,照做即可,不要再调一次load_skill。\n",r+="</system-reminder>",{content:r,digest:o,count:e.length}}export function syncSkillCatalog(e,n,t,o={}){const l=catalogEntries(t),s=catalogShape(l,o.maxTokens),r=a(l,s);let i=null;for(let e=(n?.length||0)-1;e>=0;e--){const t=catalogDigestOf(n[e]);if(t){i=t;break}}if(i===r)return{action:"none",digest:r,count:l.length};const c=e.some(e=>catalogDigestOf(e));if(!c&&!l.length)return{action:"none",digest:r,count:0};const g=c?null===i?"restore":"replace":"initial",{content:u,count:m}=renderCatalogMessage(l,g,s),f={role:"user",content:u};return"initial"===g&&o.allowInsert&&"system"===e[0]?.role?e.splice(1,0,f):e.push(f),{action:g,digest:r,count:m}}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import r from"node:path";const n=/^[a-zA-Z0-9_-]{1,48}$/,t=new Set(["name","description","version"]),s=/时使用|时载入|先载入|何时使用|适用于|场景|需要时|当用户|当需要|use when|when the user/i,o=/\]\(([^)\s#]+)\)/g,i=/(?:^|[\s`((【])((?:scripts|references|assets|templates|examples)\/[\w.\-/]+)/g,c=/(?:scripts|references|assets)\\/,a=/(?:(?:^|[\s([])(TODO|FIXME|TBD)(?=[::(])|^\s*(TODO|FIXME|TBD)\b)/gm,l=[["(补充:这个技能做什么、何时使用)","description还是脚手架默认值"],["(描述触发场景,让模型据此判断是否 load_skill)","正文「何时使用」小节还是脚手架默认值"],["(描述触发场景,让模型据此判断是否load_skill)","正文「何时使用」小节还是脚手架默认值"]];export function unquote(e){const r=String(e).trim();return r.length>=2&&r[0]===r[r.length-1]&&('"'===r[0]||"'"===r[0])?r.slice(1,-1):r}export function parseFrontmatter(e){const r={name:"",description:"",version:"",body:e,keys:[],headLines:0,ok:!1},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(e);if(!n)return r;r.ok=!0,r.body=e.slice(n[0].length),r.headLines=n[0].split(/\r?\n/).length-1;for(const e of n[1].split(/\r?\n/)){const n=/^([a-zA-Z][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(r.keys.push(n[1]),t.has(n[1])&&(r[n[1]]=unquote(n[2])))}return r}export function proseOf(e){return e.replace(/^```[\s\S]*?^```/gm,"").replace(/`[^`\n]*`/g,"")}export function estimateTokens(e){const r=String(e||""),n=(r.match(/[぀-ヿ㐀-鿿가-힯豈-﫿＀-￯]/g)||[]).length;return Math.ceil(1.6*n+(r.length-n)/4)}export function lintSkill(p,{strict:f=!1,overrideBody:u=null}={}){const g=r.basename(p),h={name:g,dir:p,errors:[],warnings:[],infos:[],fm:null};let m;if(null!=u)m=String(u);else{const n=r.join(p,"SKILL.md");if(!e.existsSync(n))return h.errors.push("缺SKILL.md(技能目录必须在根部有SKILL.md)"),h;try{m=e.readFileSync(n,"utf8")}catch(e){return h.errors.push("SKILL.md读取失败:"+e.message),h}}const d=parseFrontmatter(m);if(h.fm=d,!d.ok)return h.errors.push("SKILL.md开头缺 --- frontmatter块(模型将读不到名称与描述)"),h;if(h.name=d.name||g,d.name?n.test(d.name)?d.name!==g&&h.errors.push(`name ${JSON.stringify(d.name)} 与目录名 ${JSON.stringify(g)} 不一致:改目录名或改name,二者必须相同(否则用户在目录里看到的与模型载入的是两个名字)`):h.errors.push(`name ${JSON.stringify(d.name)} 非法:仅限字母/数字/下划线/连字符,长度 1–48(与src/skills.js的isValidSkillName同口径,不合法会在导入时被直接拒绝)`):h.errors.push("frontmatter缺name"),d.description){d.description.length>1024&&h.errors.push(`description ${d.description.length} 字符,超上限 1024`),d.description.length<40&&h.warnings.push(`description仅 ${d.description.length} 字符,太单薄不足以稳定触发:补上「做什么」+「用户会怎么说」的关键词`),s.test(d.description)||h.warnings.push("description没有说清「何时使用」:补一句「当用户……时使用」并带上用户真正会说的词");for(const[e,r]of l)d.description.includes(e)&&h.errors.push(`${r},发布前必须换成真内容`)}else h.errors.push("frontmatter缺description(模型据此判断是否载入,缺了就等于这个技能永远不会被用到)");for(const e of new Set(d.keys))t.has(e)||h.warnings.push(`未知frontmatter键 ${JSON.stringify(e)}:慧才只解析name / description / version,其余会被静默忽略`);const S=d.body.split(/\r?\n/).length;if(S>=500){const e=`正文 ${S} 行(上限 500):把细节挪进references/ 按需载入`;(f?h.errors:h.warnings).push(e)}else S>400&&h.warnings.push(`正文 ${S} 行(告警线 400 / 上限 500):该往references/ 搬了`);const y=estimateTokens(d.body);y>5e3&&h.warnings.push(`正文约 ${y} token(预算 5000):技能一旦载入,每个token都在和用户的任务抢上下文`);const $=proseOf(d.body);c.test(d.body)&&h.errors.push("正文里出现反斜杠路径:四端跨平台,相对路径一律用正斜杠");const x=new Set;for(const e of $.matchAll(o))x.add(e[1]);for(const e of $.matchAll(i))x.add(e[1]);for(let n of x)n=n.replace(/[.,:;))`'"、。]+$/,""),!n||/^(?:https?:|mailto:|#|\/)/.test(n)||n.includes("\\")||e.existsSync(r.join(p,n))||h.errors.push(`正文引用了 ${JSON.stringify(n)},但技能目录下没有这个文件:修路径或补文件`);const w=new Set([...$.matchAll(a)].map(e=>e[1]||e[2]));for(const e of w)h.infos.push(`正文里有未完成标记 ${e}:确认是有意保留还是忘了收尾`);for(const[e,r]of l)d.body.includes(e)&&h.errors.push(`${r},发布前必须换成真内容`);return h}export function buildScorer(e,r){const n=e=>new Set(r(e||"").filter(e=>e.length>=2)),t=new Map;for(const r of e)t.set(r.name,n(r.description));const s=Math.max(1,t.size),o=new Map;for(const e of t.values())for(const r of e)o.set(r,(o.get(r)||0)+1);const i=e=>Math.log((s+1)/((o.get(e)||0)+1))+1,c=e=>{let r=0;for(const n of e)r+=i(n);return r};return{score:(e,r)=>{const n=t.get(r);if(!n||!e.size||!n.size)return 0;let s=0;for(const r of e)n.has(r)&&(s+=i(r));return s/Math.sqrt(Math.max(1e-9,c(e)*c(n)))},overlap:(e,r)=>{const n=t.get(e),s=t.get(r);if(!n||!s)return 0;let o=0;for(const e of n)s.has(e)&&(o+=i(e));return o/Math.max(1e-9,Math.min(c(n),c(s)))},sets:t,prep:n}}export function readTriggerCases(n){const t=r.join(n,"trigger-cases.json");if(!e.existsSync(t))return null;try{const r=JSON.parse(e.readFileSync(t,"utf8"));return{cases:Array.isArray(r.cases)?r.cases:[],intentionalOverlap:Array.isArray(r.intentionalOverlap)?r.intentionalOverlap:[]}}catch(e){return{parseError:e.message,cases:[],intentionalOverlap:[]}}}export function routeCheck(e,r){const{score:n,overlap:t,prep:s}=buildScorer(e,r),o=s,i=[],c=[],a=[],l=e.map(e=>e.name),p=new Map(e.map(e=>[e.name,new Set(e.trigger?.intentionalOverlap||[])])),f=[];for(let e=0;e<l.length;e++)for(let r=e+1;r<l.length;r++){const n=l[e],s=l[r],o=t(n,s);o<=.5||(p.get(n)?.has(s)||p.get(s)?.has(n)?a.push(`${n} ⟷ ${s} 描述重合 ${(100*o).toFixed(0)}%(已在trigger-cases.json声明为刻意分层)`):f.push([o,n,s]))}f.sort((e,r)=>r[0]-e[0]);for(const[e,r,n]of f)c.push(`${r} ⟷ ${n} 描述重合 ${(100*e).toFixed(0)}%(上限 50%):两者会互相抢触发。要么把描述改到各自领域的独有词汇上,要么在任一方的trigger-cases.json里写 "intentionalOverlap": ["${n}"] 声明这是刻意分层`);let u=0;for(const r of e){const e=r.trigger;if(!e)continue;if(e.parseError){i.push(`${r.name}: trigger-cases.json解析失败:${e.parseError}`);continue}if(!e.cases.length)continue;u++;const t=[],s=[];for(const c of e.cases){if(!c||"string"!=typeof c.prompt)continue;if(!1===c.lexical)continue;const e=o(c.prompt),a=n(e,r.name);if((c.shouldTrigger?t:s).push({sc:a,id:c.id||c.prompt.slice(0,20)}),c.shouldTrigger&&l.length>1){let t=r.name,s=a;for(const o of l){if(o===r.name)continue;const i=n(e,o);i>s&&(s=i,t=o)}t!==r.name&&i.push(`${r.name}: 用例 ${JSON.stringify(c.id||c.prompt.slice(0,24))} 路由到了 ${t}:把该用例里用户真正会说的词补进本技能的description`)}}if(t.length&&s.length){const e=t.reduce((e,r)=>e.sc<=r.sc?e:r),n=s.reduce((e,r)=>e.sc>=r.sc?e:r);e.sc<=1.15*n.sc&&i.push(`${r.name}: 最弱正样例 ${JSON.stringify(e.id)}(${e.sc.toFixed(2)})没有拉开最强近似负样例 ${JSON.stringify(n.id)}(${n.sc.toFixed(2)})1.15 倍:description缺少用户真正会说的词,或某个负样例与描述重合过多`)}}const g=e.length-u;return g&&a.push(`${g}/${e.length} 个技能还没有trigger-cases.json(分批补即可;新增技能应随包带上)`),{errors:i,warnings:c,infos:a}}export function discoverSkills(n){const t=[];let s;try{s=e.readdirSync(n,{withFileTypes:!0})}catch{return t}for(const o of s.sort((e,r)=>e.name.localeCompare(r.name))){if(!o.isDirectory()||o.name.startsWith("."))continue;const s=r.join(n,o.name);e.existsSync(r.join(s,"SKILL.md"))&&t.push(s)}return t}export function lintSkillLibrary(n,t,{strict:s=!1,route:o=!0}={}){const i=n.flatMap(n=>e.existsSync(r.join(n,"SKILL.md"))?[n]:discoverSkills(n)),c=i.map(e=>lintSkill(e,{strict:s}));let a={errors:[],warnings:[],infos:[]};if(o&&t){a=routeCheck(c.filter(e=>e.fm&&e.fm.name).map(e=>({name:e.name,description:e.fm.description,dir:e.dir,trigger:readTriggerCases(e.dir)})),t)}const l=c.reduce((e,r)=>e+r.errors.length,0)+a.errors.length,p=c.reduce((e,r)=>e+r.warnings.length,0)+a.warnings.length,f=c.reduce((e,r)=>e+r.infos.length,0)+a.infos.length;return{dirs:i,results:c,route:a,counts:{skills:c.length,errors:l,warnings:p,infos:f}}}
@@ -0,0 +1 @@
1
+ import{lintSkill as t}from"./skill-lint.js";export const STRATEGIES=["add_example","add_constraint","restructure","add_edge_case"];const n={add_example:"补例子",add_constraint:"加约束",restructure:"重构",add_edge_case:"补边界情况"};export function parseJson(t){const n=String(t??""),e=/```(?:json)?\s*([\s\S]*?)```/.exec(n),s=e?e[1]:n,r=s.search(/[{[]/);if(r<0)return null;const a=s[r],i="{"===a?"}":"]";let o=0,l=!1;for(let t=r;t<s.length;t++){const n=s[t];if(l)"\\"===n?t++:'"'===n&&(l=!1);else if('"'!==n){if(n===a)o++;else if(n===i&&(o--,!o))try{return JSON.parse(s.slice(r,t+1))}catch{return null}}else l=!0}return null}export function splitEvals(t,n=.3){const e=Array.isArray(t)?t.slice():[];if(e.length<4)return{optimize:e,holdout:[]};const s=Math.max(1,Math.round(e.length*n));return{optimize:e.slice(0,e.length-s),holdout:e.slice(e.length-s)}}export async function analyzeSkill(t,n){const e=parseJson(await n('你在为一个「智能体技能」设计评测集。\n\n产出两样东西:\n1. scenarios:3–5 个多样化的测试场景,每个是一句用户真实会说的话(覆盖典型用法与边界,不要互相重复);\n2. evals:4–6 条**二元判定**标准,每条只问一个「是/否」问题,能对着输出客观回答。\n\n标准要具体到可核对("是否列出了具体步骤"),不要抽象评价("是否专业")。\n\n只输出JSON,不要任何其它内容:\n{"scenarios":[{"id":1,"name":"简短名","input":"用户会说的话"}],\n "evals":[{"id":1,"name":"检查点","question":"关于输出的是否问句","pass":"什么算通过"}]}',"技能内容:\n\n"+t));return e&&Array.isArray(e.scenarios)&&Array.isArray(e.evals)?{scenarios:e.scenarios.filter(t=>t&&t.input).map((t,n)=>({id:t.id??n+1,name:t.name||"场景"+(n+1),input:String(t.input)})),evals:e.evals.filter(t=>t&&t.question).map((t,n)=>({id:t.id??n+1,name:t.name||"标准"+(n+1),question:String(t.question),pass:t.pass||""}))}:null}export async function scoreSkill(t,n,e,s){const r=[];let a=0,i=0;for(const o of n){const n=await s("你要严格按照下面这份技能的指示,处理用户的请求,直接产出该技能应当产出的内容。\n不要解释你在做什么,不要复述技能内容,不要输出任何元评论。\n\n===== 技能 =====\n"+t,o.input),l=parseJson(await s('你在按给定标准给一段输出打分。\n逐条判定,每条只回答true/false。宽严一致,不要因为输出看起来用心就放宽标准。\n\n只输出JSON:{"results":[{"id":1,"pass":true,"why":"一句话理由"}]}',`用户请求:${o.input}\n\n输出:\n${n}\n\n标准:\n${JSON.stringify(e,null,2)}`)),u=l&&Array.isArray(l.results)?l.results:[];for(const t of e){const e=u.find(n=>String(n.id)===String(t.id)),s=!(!e||!e.pass);i++,s&&a++,r.push({scenario:o.id,scenarioName:o.name,evalId:t.id,evalName:t.name,pass:s,why:e?.why||"",output:n})}}return{passed:a,total:i,rate:i?a/i:0,details:r}}export async function diagnose(t,n,e,s=5){const r=n.filter(t=>!t.pass).slice(0,s);if(!r.length)return null;const a=parseJson(await e('你在诊断一个「智能体技能」为什么在评测中失败。\n\n读失败样本,找出**一个**根因,并从四种改法里选一种:\n- add_example(补例子):模型没理解要产出什么样子\n- add_constraint(加约束):模型知道要做什么但做过头或做漏了\n- restructure(重构):技能结构混乱,关键指示被埋没\n- add_edge_case(补边界情况):某类输入没被覆盖\n\n只输出JSON:{"diagnosis":"根因,一两句","strategy":"四选一","target":"要改技能的哪一节","change":"具体改什么"}',`技能:\n${t.slice(0,4e3)}\n\n失败样本:\n`+r.map(t=>`- 场景「${t.scenarioName}」未通过「${t.evalName}」:${t.why}`).join("\n")));return a&&a.diagnosis?{diagnosis:String(a.diagnosis),strategy:STRATEGIES.includes(a.strategy)?a.strategy:"add_constraint",target:String(a.target||""),change:String(a.change||"")}:null}export async function mutate(t,e,s){const r=parseJson(await s('你在修改一份SKILL.md。\n\n硬性要求:\n1. **只做一处修改**,就是诊断指出的那处。不要顺手改别处,不要重写全文;\n2. 完整保留YAML frontmatter(--- 之间的部分)一字不改;\n3. 保留原有的整体结构与小节顺序;\n4. 输出完整的修改后SKILL.md全文。\n\n只输出JSON:{"summary":"一句话说明改了什么","reason":"为什么这么改能解决问题","skill":"修改后的SKILL.md全文"}',`原SKILL.md:\n${t}\n\n诊断:${e.diagnosis}\n改法:${n[e.strategy]}\n位置:${e.target}\n具体改什么:${e.change}`));return r&&r.skill&&String(r.skill).trim()?{summary:String(r.summary||"未说明"),reason:String(r.reason||""),skill:String(r.skill)}:null}export function frontmatterUnchanged(t,n){const e=t=>(/^---\r?\n[\s\S]*?\r?\n---/.exec(String(t??""))||[""])[0],s=e(t);return!!s&&s===e(n)}export async function optimize({skillMd:n,dir:s,scenarios:r,evals:a,ask:i,rounds:o=3,onEvent:l=()=>{}}){const{optimize:u,holdout:c}=splitEvals(a),d=[],g={};let p=n,y=await scoreSkill(p,r,u,i);const m=c.length?await scoreSkill(p,r,c,i):null;l({type:"baseline",rate:y.rate,holdout:m?.rate??null});for(let n=1;n<=o;n++){const a=await diagnose(p,y.details,i);if(!a){l({type:"stop",round:n,why:"没有可诊断的失败样本"});break}const o=await mutate(p,a,i);if(!o){l({type:"round",round:n,strategy:a.strategy,kept:!1,why:"变异未产出可用结果"});continue}const c=[];if(frontmatterUnchanged(p,o.skill)||c.push("frontmatter被改动"),s){const n=t(s,{overrideBody:o.skill});n.errors.length&&c.push("结构校验未过:"+n.errors[0])}if(c.length){d.push({round:n,strategy:a.strategy,diagnosis:a.diagnosis,summary:o.summary,before:y.rate,after:null,kept:!1,invalid:c}),e(g,a.strategy,!1),l({type:"round",round:n,strategy:a.strategy,kept:!1,why:c.join(";")});continue}const m=await scoreSkill(o.skill,r,u,i),f=m.rate>y.rate;d.push({round:n,strategy:a.strategy,diagnosis:a.diagnosis,summary:o.summary,before:y.rate,after:m.rate,kept:f}),e(g,a.strategy,f),f&&(p=o.skill,y=m),l({type:"round",round:n,strategy:a.strategy,before:d[d.length-1].before,after:m.rate,kept:f})}const f=c.length?await scoreSkill(p,r,c,i):null;return{changed:p!==n,skill:p,baseline:d.length?d[0].before:y.rate,final:y.rate,holdout:m&&f?{before:m.rate,after:f.rate,overfit:f.rate<m.rate}:null,rounds:d,stats:g,evalSplit:{optimize:u.length,holdout:c.length}}}function e(t,n,e){const s=t[n]||(t[n]={proposed:0,kept:0});s.proposed++,e&&s.kept++}export function estimateCalls(t,n,e){const s=2*t;return s+(e?s:0)+n*(2+s)+(e?s:0)}