@ohos-cpf/3rdloop 0.0.2 → 0.0.4

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 (393) hide show
  1. package/README.md +290 -244
  2. package/bin/3rdloop.mjs +46 -46
  3. package/lib/cli.js +781 -781
  4. package/lib/config-cmd.js +451 -451
  5. package/lib/config.js +318 -318
  6. package/lib/doctor.js +113 -113
  7. package/lib/exit-codes.js +30 -30
  8. package/lib/opencode.js +155 -155
  9. package/lib/orch.js +426 -426
  10. package/lib/queries.js +263 -263
  11. package/lib/step.js +325 -325
  12. package/lib/ui.js +222 -222
  13. package/lib/update.js +279 -279
  14. package/lib/workflow.js +308 -308
  15. package/lib/workflows.js +131 -131
  16. package/package.json +50 -50
  17. package/vendor/Server/Agent/SkillSelector/README.md +343 -343
  18. package/vendor/Server/Agent/SkillSelector/core/Agent.js +207 -207
  19. package/vendor/Server/Agent/SkillSelector/index.js +14 -14
  20. package/vendor/Server/Agent/SkillSelector/llm/llmClient.js +313 -313
  21. package/vendor/Server/Agent/SkillSelector/llm/prompts.js +161 -161
  22. package/vendor/Server/Agent/SkillSelector/llm/reactSteps.js +144 -144
  23. package/vendor/Server/Agent/SkillSelector/matcher/fuzzyMatcher.js +433 -433
  24. package/vendor/Server/Agent/SkillSelector/scanner/skillScanner.js +382 -382
  25. package/vendor/Server/Agent/SkillSelector/utils/actionWords.js +59 -59
  26. package/vendor/Server/Brain/AnalysisStrategies.js +422 -422
  27. package/vendor/Server/Brain/Brain.js +686 -686
  28. package/vendor/Server/Brain/DecompositionStrategies.js +621 -621
  29. package/vendor/Server/Brain/PlanPersistence.js +196 -196
  30. package/vendor/Server/Brain/SkillExecutor.js +251 -251
  31. package/vendor/Server/Brain/index.js +11 -11
  32. package/vendor/Server/CLI/cli.js +275 -275
  33. package/vendor/Server/CLI/deveco-code/index.js +71 -71
  34. package/vendor/Server/CLI/deveco-code.js +5 -5
  35. package/vendor/Server/CLI/opencode/index.js +817 -818
  36. package/vendor/Server/CLI/opencode.js +5 -5
  37. package/vendor/Server/DbUse/MessageArchive.js +61 -61
  38. package/vendor/Server/DbUse/StorageManager.js +107 -107
  39. package/vendor/Server/DbUse/adapters/BaseAdapter.js +94 -94
  40. package/vendor/Server/DbUse/adapters/FileAdapter.js +138 -138
  41. package/vendor/Server/DbUse/adapters/JsonAdapter.js +138 -138
  42. package/vendor/Server/DbUse/adapters/JsonlAdapter.js +114 -114
  43. package/vendor/Server/DbUse/adapters/SqliteAdapter.js +157 -157
  44. package/vendor/Server/DbUse/index.js +160 -160
  45. package/vendor/Server/DbUse/stores/SessionStore.js +279 -279
  46. package/vendor/Server/DbUse/stores/TaskExecutionStore.js +133 -133
  47. package/vendor/Server/DbUse/stores/TaskRegistryStore.js +262 -262
  48. package/vendor/Server/DbUse/stores/TaskStepStore.js +279 -279
  49. package/vendor/Server/FlexRunner/FlexRunner.js +1759 -1759
  50. package/vendor/Server/FlexRunner/index.js +12 -12
  51. package/vendor/Server/Knowledge/KnowledgeImporter.js +523 -523
  52. package/vendor/Server/Knowledge/KnowledgeManager.js +341 -341
  53. package/vendor/Server/Knowledge/index.js +33 -33
  54. package/vendor/Server/LoopEngine/LoopEngine.js +1133 -1133
  55. package/vendor/Server/LoopEngine/index.js +11 -11
  56. package/vendor/Server/Orchestrator/Orchestrator.js +0 -3
  57. package/vendor/Server/Orchestrator/StepNode.js +312 -312
  58. package/vendor/Server/Orchestrator/index.js +13 -13
  59. package/vendor/Server/Routes/controllers/BrainController.js +241 -241
  60. package/vendor/Server/Routes/controllers/FlexRunnerController.js +1125 -1125
  61. package/vendor/Server/Routes/controllers/HealthController.js +44 -44
  62. package/vendor/Server/Routes/controllers/KnowledgeImportController.js +355 -355
  63. package/vendor/Server/Routes/controllers/LoopEngineController.js +788 -788
  64. package/vendor/Server/Routes/controllers/StatsController.js +57 -57
  65. package/vendor/Server/Routes/controllers/StreamController.js +67 -67
  66. package/vendor/Server/Routes/library/logger.js +179 -179
  67. package/vendor/Server/Routes/library/zipArchive.js +172 -172
  68. package/vendor/Server/Routes/router/Router.js +204 -204
  69. package/vendor/Server/Routes/routes/brain.js +47 -47
  70. package/vendor/Server/Routes/routes/flexrunner.js +72 -72
  71. package/vendor/Server/Routes/routes/health.js +28 -28
  72. package/vendor/Server/Routes/routes/index.js +57 -57
  73. package/vendor/Server/Routes/routes/knowledge.js +55 -55
  74. package/vendor/Server/Routes/routes/loopengine.js +58 -58
  75. package/vendor/Server/Routes/routes/orchestrator.js +61 -61
  76. package/vendor/Server/Routes/routes/registry.js +97 -97
  77. package/vendor/Server/Routes/routes/stats.js +23 -23
  78. package/vendor/Server/Routes/routes/stream.js +30 -30
  79. package/vendor/Server/Routes/server.js +203 -203
  80. package/vendor/Server/Routes/sse/SseHub.js +308 -308
  81. package/vendor/Server/Skills/acceptance-check/SKILL.md +191 -191
  82. package/vendor/Server/Skills/acceptance-check/references/PHASE1_ARTIFACT_COLLECTION.md +52 -52
  83. package/vendor/Server/Skills/acceptance-check/references/PHASE2_CRITERIA_EVALUATION.md +77 -77
  84. package/vendor/Server/Skills/acceptance-check/references/PHASE2_EVIDENCE_VERIFICATION.md +70 -70
  85. package/vendor/Server/Skills/arkts-code-check/SKILL.md +320 -320
  86. package/vendor/Server/Skills/arkts-code-check/references/CODE_CHECK_GUIDE.md +210 -210
  87. package/vendor/Server/Skills/arkts-code-check/references/FILE_TYPE_GUIDE.md +28 -28
  88. package/vendor/Server/Skills/arkts-code-check/references/PHASE1_INFO_COLLECTION.md +233 -233
  89. package/vendor/Server/Skills/arkts-code-check/references/REPORT_TEMPLATE.md +219 -219
  90. package/vendor/Server/Skills/arkts-library-compile-validate/SKILL.md +328 -328
  91. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/build.ps1 +93 -93
  92. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/code-linter.cjs +432 -432
  93. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/migrate.cjs +851 -851
  94. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/ohpm-switch.cjs +788 -788
  95. package/vendor/Server/Skills/arkts-library-demo-code/SKILL.md +291 -291
  96. package/vendor/Server/Skills/arkts-library-demo-code/references/API_SIGNATURE_CHECKS.md +68 -68
  97. package/vendor/Server/Skills/arkts-library-demo-code/references/BUILD_VALIDATION.md +184 -184
  98. package/vendor/Server/Skills/arkts-library-demo-code/references/CODE_STANDARDS.md +337 -337
  99. package/vendor/Server/Skills/arkts-library-demo-code/references/EXAMPLES.md +400 -400
  100. package/vendor/Server/Skills/arkts-library-demo-code/references/SYSTEM_API_LOOKUP.md +98 -98
  101. package/vendor/Server/Skills/arkts-library-demo-code/references/TEMPLATE.md +544 -544
  102. package/vendor/Server/Skills/arkts-library-demo-code/scripts/build.ps1 +93 -93
  103. package/vendor/Server/Skills/arkts-library-demo-code/scripts/code-linter.cjs +432 -432
  104. package/vendor/Server/Skills/arkts-library-demo-code/scripts/migrate.cjs +851 -851
  105. package/vendor/Server/Skills/arkts-library-demo-code/scripts/ohpm-switch.cjs +788 -788
  106. package/vendor/Server/Skills/arkts-library-demo-coverage/SKILL.md +174 -174
  107. package/vendor/Server/Skills/arkts-library-demo-scenario/SKILL.md +321 -321
  108. package/vendor/Server/Skills/arkts-library-demo-scenario/references/EXAMPLES.md +100 -100
  109. package/vendor/Server/Skills/arkts-library-demo-scenario/references/TEMPLATE.md +185 -185
  110. package/vendor/Server/Skills/arkts-library-documentation-quality-check/SKILL.md +233 -233
  111. package/vendor/Server/Skills/arkts-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +102 -102
  112. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/CHECK_DIMENSIONS.md +89 -89
  113. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/COMPLIANCE_CHECKLIST.md +62 -62
  114. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/KB_QUERY_GUIDE.md +107 -107
  115. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/MCP_TOOL_DISCOVERY.md +124 -124
  116. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/REPORT_TEMPLATE.md +90 -90
  117. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/SCORING_FORMULA.md +57 -57
  118. package/vendor/Server/Skills/arkts-library-documentation-quality-check/scripts/validate-skill.cjs +259 -259
  119. package/vendor/Server/Skills/arkts-library-interface-spec/SKILL.md +97 -97
  120. package/vendor/Server/Skills/arkts-library-memory-leak-check/SKILL.md +391 -391
  121. package/vendor/Server/Skills/arkts-library-source-clone/SKILL.md +113 -113
  122. package/vendor/Server/Skills/arkts-library-stability-check/SKILL.md +312 -312
  123. package/vendor/Server/Skills/arkts-library-stability-check/assets/REPORT_TEMPLATE.md +154 -154
  124. package/vendor/Server/Skills/arkts-library-stability-check/references/CHECKLIST.md +165 -165
  125. package/vendor/Server/Skills/arkts-library-stability-check/references/CODE_EXAMPLES.md +308 -308
  126. package/vendor/Server/Skills/arkts-library-stability-check/references/SCORING_RULES.md +90 -90
  127. package/vendor/Server/Skills/arkts-library-stability-check/scripts/validate-skill.cjs +282 -282
  128. package/vendor/Server/Skills/arkts-library-test-coverage-check/SKILL.md +360 -360
  129. package/vendor/Server/Skills/arkts-library-test-coverage-check/assets/REPORT_TEMPLATE.md +145 -145
  130. package/vendor/Server/Skills/arkts-library-test-coverage-check/references/CHECKLIST.md +252 -252
  131. package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +114 -114
  132. package/vendor/Server/Skills/arkts-library-test-coverage-check/scripts/validate-skill.cjs +262 -262
  133. package/vendor/Server/Skills/arkts-library-xts-code/SKILL.md +455 -455
  134. package/vendor/Server/Skills/arkts-library-xts-code/assets/report-template.md +60 -60
  135. package/vendor/Server/Skills/arkts-library-xts-code/references/ACCESS_MODIFIER_PRECHECK.md +76 -76
  136. package/vendor/Server/Skills/arkts-library-xts-code/references/BUILD_VERIFICATION.md +200 -200
  137. package/vendor/Server/Skills/arkts-library-xts-code/references/CODE_STANDARDS.md +518 -518
  138. package/vendor/Server/Skills/arkts-library-xts-code/references/SUBAGENT_PROMPT_TEMPLATE.md +276 -276
  139. package/vendor/Server/Skills/arkts-library-xts-code/references/SYSTEM_API_LOOKUP.md +92 -92
  140. package/vendor/Server/Skills/arkts-library-xts-code/references/XTS_TEMPLATE.md +767 -767
  141. package/vendor/Server/Skills/arkts-library-xts-code/scripts/build.ps1 +93 -93
  142. package/vendor/Server/Skills/arkts-library-xts-code/scripts/code-linter.cjs +432 -432
  143. package/vendor/Server/Skills/arkts-library-xts-code/scripts/migrate.cjs +851 -851
  144. package/vendor/Server/Skills/arkts-library-xts-code/scripts/ohpm-switch.cjs +788 -788
  145. package/vendor/Server/Skills/arkts-library-xts-code/scripts/precheck-access-modifiers.cjs +221 -221
  146. package/vendor/Server/Skills/arkts-library-xts-code-fix/SKILL.md +228 -228
  147. package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +97 -97
  148. package/vendor/Server/Skills/arkts-library-xts-doc/SKILL.md +194 -194
  149. package/vendor/Server/Skills/arkts-library-xts-doc/references/EXAMPLES.md +575 -575
  150. package/vendor/Server/Skills/arkts-library-xts-doc/references/TEMPLATE.md +574 -574
  151. package/vendor/Server/Skills/arkts-library-xts-doc/references/TESTABILITY_GUIDE.md +182 -182
  152. package/vendor/Server/Skills/arkts-library-xts-execution/SKILL.md +490 -490
  153. package/vendor/Server/Skills/arkts-library-xts-execution/references/error-fixing-examples.md +217 -217
  154. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/build.ps1 +93 -93
  155. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/code-linter.cjs +432 -432
  156. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/migrate.cjs +851 -851
  157. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/ohpm-switch.cjs +788 -788
  158. package/vendor/Server/Skills/flutter-code-check/SKILL.md +299 -299
  159. package/vendor/Server/Skills/flutter-code-check/assets/OUTPUT_TEMPLATE.md +311 -311
  160. package/vendor/Server/Skills/flutter-code-check/references/CHECKLIST.md +190 -190
  161. package/vendor/Server/Skills/flutter-code-check/references/PLATFORMS.md +133 -133
  162. package/vendor/Server/Skills/flutter-code-check/scripts/validate-skill.cjs +259 -259
  163. package/vendor/Server/Skills/flutter-interface/README.md +75 -75
  164. package/vendor/Server/Skills/flutter-interface/SKILL.md +291 -291
  165. package/vendor/Server/Skills/flutter-interface/references/EXAMPLES.md +336 -336
  166. package/vendor/Server/Skills/flutter-interface/references/GIT_CLONE_GUIDE.md +190 -190
  167. package/vendor/Server/Skills/flutter-interface/references/SCRIPT_REFERENCE.md +258 -258
  168. package/vendor/Server/Skills/flutter-interface/references/TEMPLATE.md +254 -254
  169. package/vendor/Server/Skills/flutter-library-consistency-check/SKILL.md +517 -517
  170. package/vendor/Server/Skills/flutter-library-consistency-check/references/CHECKLIST.md +381 -381
  171. package/vendor/Server/Skills/flutter-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +341 -341
  172. package/vendor/Server/Skills/flutter-library-consistency-check/references/PLATFORM_LIMITATION_GUIDE.md +71 -71
  173. package/vendor/Server/Skills/flutter-library-documentation-quality-check/SKILL.md +518 -518
  174. package/vendor/Server/Skills/flutter-library-documentation-quality-check/references/CHECKLIST.md +406 -406
  175. package/vendor/Server/Skills/flutter-library-memory-leak-check/SKILL.md +652 -652
  176. package/vendor/Server/Skills/flutter-library-memory-leak-check/references/CHECKLIST.md +396 -396
  177. package/vendor/Server/Skills/flutter-library-stability-check/SKILL.md +744 -744
  178. package/vendor/Server/Skills/flutter-library-stability-check/references/CHECKLIST.md +720 -720
  179. package/vendor/Server/Skills/flutter-library-test-coverage-check/SKILL.md +414 -414
  180. package/vendor/Server/Skills/flutter-library-test-coverage-check/assets/REPORT_TEMPLATE.md +150 -150
  181. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/CHECKLIST.md +304 -304
  182. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCORING_RULES.md +265 -265
  183. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCRIPT_USAGE.md +286 -286
  184. package/vendor/Server/Skills/knowledge-import/SKILL.md +383 -383
  185. package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +165 -165
  186. package/vendor/Server/Skills/knowledge-import/references/SKILL_LOCATOR_GUIDE.md +91 -91
  187. package/vendor/Server/Skills/knowledge-import/references/SKILL_OPTIMIZATION_GUIDE.md +127 -127
  188. package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +453 -301
  189. package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_CHECKLIST.md +151 -0
  190. package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_ISSUE_TEMPLATE.md +257 -105
  191. package/vendor/Server/Skills/ohos-lib-pr-push/references/GITCODE_API.md +191 -127
  192. package/vendor/Server/Skills/ohos-lib-pr-push/scripts/gitcode-api.cjs +317 -208
  193. package/vendor/Server/Skills/rn-code-check/SKILL.md +320 -320
  194. package/vendor/Server/Skills/rn-code-check/references/API-GUIDE.md +234 -234
  195. package/vendor/Server/Skills/rn-code-check/references/CHECKLIST.md +188 -188
  196. package/vendor/Server/Skills/rn-code-check/references/REPORT-TEMPLATE.md +246 -246
  197. package/vendor/Server/Skills/rn-lib-add-example/SKILL.md +198 -198
  198. package/vendor/Server/Skills/rn-lib-add-example/references/APP_RENAME_GUIDE.md +40 -40
  199. package/vendor/Server/Skills/rn-lib-add-example/references/SHELL_ADAPTATION_CHECKLIST.md +78 -78
  200. package/vendor/Server/Skills/rn-lib-add-example/references/TEMPLATE_REPO_GUIDE.md +70 -70
  201. package/vendor/Server/Skills/rn-lib-add-example/references/VERIFICATION_GUIDE.md +47 -47
  202. package/vendor/Server/Skills/rn-lib-add-example/scripts/create-example.cjs +252 -252
  203. package/vendor/Server/Skills/rn-lib-update-doc/SKILL.md +176 -176
  204. package/vendor/Server/Skills/rn-lib-update-doc/config.json +5 -5
  205. package/vendor/Server/Skills/rn-lib-update-doc/references/changelog-template.md +35 -35
  206. package/vendor/Server/Skills/rn-lib-update-doc/references/format-diff.md +137 -137
  207. package/vendor/Server/Skills/rn-lib-update-doc/references/link-registry.json +33 -33
  208. package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2-overview.md +40 -40
  209. package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2.md +429 -429
  210. package/vendor/Server/Skills/rn-lib-update-doc/scripts/detect-readme-format.cjs +105 -105
  211. package/vendor/Server/Skills/rn-lib-update-doc/scripts/find-usage-doc.cjs +90 -90
  212. package/vendor/Server/Skills/rn-lib-update-doc/scripts/link-verify.cjs +213 -213
  213. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-changelog.cjs +73 -73
  214. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-package-meta.cjs +258 -258
  215. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-readme-links.cjs +341 -341
  216. package/vendor/Server/Skills/rn-lib-update-doc/scripts/resolve-usage-docs.cjs +67 -67
  217. package/vendor/Server/Skills/rn-library-consistency-check/SKILL.md +299 -299
  218. package/vendor/Server/Skills/rn-library-consistency-check/assets/REPORT_TEMPLATE.md +293 -293
  219. package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_COMPONENT_CHECK_GUIDE.md +857 -857
  220. package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_TURBOMODULE_CHECK_GUIDE.md +856 -856
  221. package/vendor/Server/Skills/rn-library-consistency-check/references/CHECKLIST.md +157 -157
  222. package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_COMPONENT_CHECK_GUIDE.md +801 -801
  223. package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_TURBOMODULE_CHECK_GUIDE.md +683 -683
  224. package/vendor/Server/Skills/rn-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +233 -233
  225. package/vendor/Server/Skills/rn-library-consistency-check/references/NATIVE_COMPONENT_IDENTIFICATION.md +1373 -1373
  226. package/vendor/Server/Skills/rn-library-consistency-check/references/SCORING_STANDARD.md +220 -220
  227. package/vendor/Server/Skills/rn-library-consistency-check/scripts/validate-skill.py +245 -245
  228. package/vendor/Server/Skills/rn-library-documentation-quality-check/SKILL.md +263 -263
  229. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL.md +414 -414
  230. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL_en.md +413 -413
  231. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +311 -311
  232. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CHECK_CHECKLIST.md +355 -355
  233. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CROSS_VALIDATION_GUIDE.md +115 -115
  234. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/DOC_COLLECTION_GUIDE.md +139 -139
  235. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/EXAMPLES.md +586 -586
  236. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/GOTCHAS.md +57 -57
  237. package/vendor/Server/Skills/rn-library-documentation-quality-check/scripts/validate-skill.py +259 -259
  238. package/vendor/Server/Skills/rn-library-interface-analyzer/SKILL.md +301 -301
  239. package/vendor/Server/Skills/rn-library-interface-analyzer/assets/REPORT_TEMPLATE.md +148 -148
  240. package/vendor/Server/Skills/rn-library-interface-analyzer/references/DOC_ANNOTATION_GUIDE.md +118 -118
  241. package/vendor/Server/Skills/rn-library-interface-analyzer/references/SCRIPT_USAGE_GUIDE.md +168 -168
  242. package/vendor/Server/Skills/rn-library-interface-analyzer/references/URL_PARSING_RULES.md +176 -176
  243. package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +274 -274
  244. package/vendor/Server/Skills/rn-library-issue-generalizer/assets/case-template.md +45 -45
  245. package/vendor/Server/Skills/rn-library-issue-generalizer/assets/rule-template.md +25 -25
  246. package/vendor/Server/Skills/rn-library-issue-generalizer/references/PR_FETCH_GUIDE.md +61 -61
  247. package/vendor/Server/Skills/rn-library-issue-generalizer/references/RULE_TEMPLATE_GUIDE.md +165 -165
  248. package/vendor/Server/Skills/rn-library-issue-generalizer/scripts/fetch-pr-info.py +239 -239
  249. package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +263 -263
  250. package/vendor/Server/Skills/rn-library-known-issue-check/assets/REPORT_TEMPLATE.md +96 -96
  251. package/vendor/Server/Skills/rn-library-known-issue-check/references/COMPLIANCE_CHECKLIST.md +57 -57
  252. package/vendor/Server/Skills/rn-library-known-issue-check/references/INSPECTION_GUIDE.md +99 -99
  253. package/vendor/Server/Skills/rn-library-known-issue-check/references/PRESCREEN_GUIDE.md +64 -64
  254. package/vendor/Server/Skills/rn-library-known-issue-check/references/SOURCE_PREP_GUIDE.md +74 -74
  255. package/vendor/Server/Skills/rn-library-known-issue-check/scripts/validate-known-issue-report.py +164 -164
  256. package/vendor/Server/Skills/rn-library-memory-leak-check/SKILL.md +356 -339
  257. package/vendor/Server/Skills/rn-library-memory-leak-check/assets/REPORT_TEMPLATE.md +85 -83
  258. package/vendor/Server/Skills/rn-library-memory-leak-check/references/CHECKLIST.md +905 -859
  259. package/vendor/Server/Skills/rn-library-memory-leak-check/references/HARMONY_CODE_PATTERNS.md +124 -122
  260. package/vendor/Server/Skills/rn-library-memory-leak-check/scripts/scan_harmony_code.py +287 -279
  261. package/vendor/Server/Skills/rn-library-stability-check/SKILL.md +273 -273
  262. package/vendor/Server/Skills/rn-library-stability-check/assets/REPORT_TEMPLATE.md +129 -129
  263. package/vendor/Server/Skills/rn-library-stability-check/references/CHECKLIST.md +581 -581
  264. package/vendor/Server/Skills/rn-library-stability-check/references/SCORING_RULES.md +138 -138
  265. package/vendor/Server/Skills/rn-library-stability-check/scripts/scan-harmony-code.py +168 -168
  266. package/vendor/Server/Skills/rn-library-test-coverage-check/SKILL.md +526 -506
  267. package/vendor/Server/Skills/rn-library-test-coverage-check/references/CHECKLIST.md +321 -280
  268. package/vendor/Server/Skills/rn-library-test-coverage-check/references/DEMO_LOCATION.md +221 -221
  269. package/vendor/Server/Skills/rn-library-test-coverage-check/references/REPORT_TEMPLATE.md +220 -209
  270. package/vendor/Server/Skills/rn-library-test-coverage-check/references/SCRIPT_USAGE.md +325 -325
  271. package/vendor/Server/Skills/rn-ohos-xts-generator/SKILL.md +279 -279
  272. package/vendor/Server/Skills/rn-ohos-xts-generator/assets/TEST_FILE_TEMPLATES.md +404 -404
  273. package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_TROUBLESHOOTING.md +185 -185
  274. package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_WORKFLOW.md +265 -265
  275. package/vendor/Server/Skills/rn-ohos-xts-generator/references/COMMON_CONFIG.md +189 -189
  276. package/vendor/Server/Skills/rn-ohos-xts-generator/references/CONFIG_GUIDE.md +203 -203
  277. package/vendor/Server/Skills/rn-ohos-xts-generator/references/COVERAGE_ANALYZER.md +147 -147
  278. package/vendor/Server/Skills/rn-ohos-xts-generator/references/DESIGN_DOC_GUIDE.md +148 -148
  279. package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_FLOW.md +165 -165
  280. package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_STRATEGY.md +195 -195
  281. package/vendor/Server/Skills/rn-ohos-xts-generator/references/HARMONY_SCANNER.md +249 -249
  282. package/vendor/Server/Skills/rn-ohos-xts-generator/references/JEST_FRAMEWORK.md +235 -235
  283. package/vendor/Server/Skills/rn-ohos-xts-generator/references/PLATFORM_COMPAT.md +161 -161
  284. package/vendor/Server/Skills/rn-ohos-xts-generator/references/RN_TEST_PATTERNS.md +413 -413
  285. package/vendor/Server/Skills/rn-ohos-xts-generator/references/SOURCE_PARSER.md +235 -235
  286. package/vendor/Server/Skills/rn-ohos-xts-generator/references/TEST_CONVENTIONS.md +245 -245
  287. package/vendor/Server/Skills/rn-ohos-xts-generator/references/TROUBLESHOOTING.md +282 -282
  288. package/vendor/Server/Skills/rn-ohos-xts-generator/references/USAGE_GUIDE.md +251 -251
  289. package/vendor/Server/Skills/rnoh-cptf-api-test/SKILL.md +469 -469
  290. package/vendor/Server/Skills/rnoh-cptf-api-test/assets/CPTF_TEST_TEMPLATE.md +310 -310
  291. package/vendor/Server/Skills/rnoh-cptf-api-test/assets/DEMO_PAGE_TEMPLATE.md +303 -303
  292. package/vendor/Server/Skills/rnoh-cptf-api-test/references/CPTF_TEST_GENERATION_GUIDE.md +421 -421
  293. package/vendor/Server/Skills/rnoh-cptf-api-test/references/DEMO_GENERATION_GUIDE.md +358 -358
  294. package/vendor/Server/Skills/rnoh-cptf-api-test/references/QUALITY_CHECKLIST.md +114 -114
  295. package/vendor/Server/Skills/rnoh-cptf-api-test/references/SPEC_PARSING_GUIDE.md +274 -274
  296. package/vendor/Server/Skills/rnoh-cptf-api-test/scripts/validate-rnoh-cptf-api-test.sh +185 -185
  297. package/vendor/Server/Skills/rnoh-cptf-demo-test/SKILL.md +191 -191
  298. package/vendor/Server/Skills/rnoh-cptf-demo-test/assets/TEST_TEMPLATE.md +216 -216
  299. package/vendor/Server/Skills/rnoh-cptf-demo-test/references/CPTF_SETUP.md +107 -107
  300. package/vendor/Server/Skills/rnoh-cptf-demo-test/references/DEMO_ANALYSIS.md +120 -120
  301. package/vendor/Server/Skills/rnoh-cptf-demo-test/scripts/validate-rnoh-cptf-test.sh +108 -108
  302. package/vendor/Server/Skills/rnoh-cptf-test-run/SKILL.md +237 -237
  303. package/vendor/Server/Skills/rnoh-cptf-test-run/references/DEBUG_PLAYBOOK.md +261 -261
  304. package/vendor/Server/Skills/rnoh-cptf-test-run/scripts/validate-run-env.sh +112 -112
  305. package/vendor/Server/Skills/rnoh-lib-code-adapt/SKILL.md +497 -497
  306. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/adapt-checklist.md +270 -270
  307. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/api-migration-checklist.md +164 -164
  308. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/common-compile-errors.md +556 -556
  309. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/deveco-cli-tools.md +459 -459
  310. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/edge-cases.md +65 -65
  311. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/har-tgz-build.md +512 -512
  312. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/version-compat-matrix.md +132 -132
  313. package/vendor/Server/Skills/rnoh-lib-code-adapt/scripts/auto-integrate.sh +424 -424
  314. package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +137 -137
  315. package/vendor/Server/Skills/rnoh-lib-demo-create/SKILL.md +306 -306
  316. package/vendor/Server/Skills/rnoh-lib-demo-create/assets/DEMO_PAGE_TEMPLATE.md +485 -485
  317. package/vendor/Server/Skills/rnoh-lib-demo-create/assets/INDEX_TEMPLATE.md +172 -172
  318. package/vendor/Server/Skills/rnoh-lib-demo-create/references/DEMO_CODE_RULES.md +422 -422
  319. package/vendor/Server/Skills/rnoh-lib-demo-create/references/QUALITY_CHECKLIST.md +95 -95
  320. package/vendor/Server/Skills/rnoh-lib-demo-create/references/SOURCE_EXPLORATION.md +169 -169
  321. package/vendor/Server/Skills/rnoh-lib-demo-doc/SKILL.md +554 -554
  322. package/vendor/Server/Skills/rnoh-lib-demo-doc/assets/DEMO_DOC_TEMPLATE.md +106 -106
  323. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/COVERAGE_INCREMENTAL.md +216 -216
  324. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/DEMO_DESIGN_RULES.md +413 -413
  325. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/OUTPUT_FORMAT.md +269 -269
  326. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/QUALITY_CHECKLIST.md +98 -98
  327. package/vendor/Server/Skills/rnoh-lib-func-test/SKILL.md +337 -337
  328. package/vendor/Server/Skills/rnoh-lib-rnt-search/SKILL.md +722 -722
  329. package/vendor/Server/Skills/rnoh-lib-rnt-search/assets/PORT_REPORT_TEMPLATE.md +219 -219
  330. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/DEMO_PORTING_GUIDE.md +345 -345
  331. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/PORT_VERIFICATION.md +269 -269
  332. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/RNT_PROJECT_STRUCTURE.md +173 -173
  333. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/analyze-demo-deps.cjs +267 -267
  334. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/detect-rnoh-version.cjs +174 -174
  335. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/find-rnt-demo.sh +111 -111
  336. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/port-demo.sh +186 -186
  337. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/read-lib-meta.cjs +150 -150
  338. package/vendor/Server/Skills/rnoh-lib-test-align/SKILL.md +296 -296
  339. package/vendor/Server/Skills/rnoh-lib-test-align/references/jest-troubleshooting.md +371 -371
  340. package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +212 -212
  341. package/vendor/Server/Skills/rnoh-lib-test-align/references/runtime-troubleshooting.md +345 -345
  342. package/vendor/Server/Skills/rnoh-lib-test-align/references/unit-test-template.md +223 -223
  343. package/vendor/Server/Skills/rnoh-lib-test-align/scripts/port-test-demo.sh +1053 -1053
  344. package/vendor/Server/Skills/rnoh-lib-version-diff/SKILL.md +295 -255
  345. package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_ADAPTATION.md +82 -77
  346. package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_API_LOOKUP.md +224 -224
  347. package/vendor/Server/Skills/rnoh-lib-version-diff/references/script-reference.md +175 -174
  348. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/detect-upstream-version.js +967 -967
  349. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/generate-diff.js +1798 -1798
  350. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/package.json +5 -5
  351. package/vendor/Server/Skills/rnoh-lib-version-diff/templates/report-template.md +180 -137
  352. package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +145 -145
  353. package/vendor/Server/Skills/skill-generator/SKILL.md +365 -365
  354. package/vendor/Server/Skills/skill-generator/assets/SKILL_TEMPLATE.md +183 -183
  355. package/vendor/Server/Skills/skill-generator/references/COMPLIANCE_CHECKLIST.md +82 -82
  356. package/vendor/Server/Skills/skill-generator/references/KB_QUERY_GUIDE.md +110 -110
  357. package/vendor/Server/Skills/skill-generator/references/MCP_TOOL_DISCOVERY.md +122 -122
  358. package/vendor/Server/Skills/skill-generator/references/SKILL_SPEC.md +121 -121
  359. package/vendor/Server/Skills/skill-generator/scripts/validate-skill.cjs +259 -259
  360. package/vendor/Server/Skills/task-analysis/SKILL.md +234 -234
  361. package/vendor/Server/Skills/task-analysis/assets/OUTPUT_EXAMPLES.md +160 -160
  362. package/vendor/Server/Skills/task-analysis/references/ACCEPTANCE_CRITERIA_GUIDE.md +190 -190
  363. package/vendor/Server/Skills/task-analysis/references/ANALYSIS_SCHEMA.md +224 -224
  364. package/vendor/Server/Skills/task-analysis/references/INTEGRATION_CONTEXT.md +162 -162
  365. package/vendor/Server/Skills/task-analysis/references/LESSONS_LEARNED.md +55 -55
  366. package/vendor/Server/Skills/task-analysis/references/REPLAN_CONTEXT_GUIDE.md +126 -126
  367. package/vendor/Server/Skills/task-analysis/scripts/validate-analysis-output.sh +256 -256
  368. package/vendor/Server/Skills/task-decomposition/SKILL.md +528 -528
  369. package/vendor/Server/Skills/task-decomposition/assets/OUTPUT_EXAMPLES.md +451 -451
  370. package/vendor/Server/Skills/task-decomposition/references/ACCEPTANCE_CRITERIA_GUIDE.md +179 -179
  371. package/vendor/Server/Skills/task-decomposition/references/DEPENDENCY_GRAPH_GUIDE.md +152 -152
  372. package/vendor/Server/Skills/task-decomposition/references/INTEGRATION_CONTEXT.md +175 -175
  373. package/vendor/Server/Skills/task-decomposition/references/REPLAN_CONTEXT_GUIDE.md +195 -195
  374. package/vendor/Server/Skills/task-decomposition/references/TASK_DECOMPOSITION_TEMPLATES.md +226 -226
  375. package/vendor/Server/Skills/task-decomposition/scripts/list-available-skills.cjs +263 -263
  376. package/vendor/Server/Skills/task-decomposition/scripts/skill-blacklist.json +29 -29
  377. package/vendor/Server/Skills/task-decomposition/scripts/validate-decomposition-output.cjs +376 -376
  378. package/vendor/Server/Skills/task-decomposition/scripts/validate-param-consistency.cjs +259 -259
  379. package/vendor/Server/Skills/task-decomposition/scripts/validate-specified-skills.cjs +216 -216
  380. package/vendor/Server/Skills/task-decomposition/scripts/validate-step-graph.cjs +424 -424
  381. package/vendor/Server/Skills/tpc-cpp-check/SKILL.md +427 -427
  382. package/vendor/Server/Skills/tpc-cpp-check/references/CHECKLIST.md +214 -214
  383. package/vendor/Server/Skills/tpc-cpp-check/references/HPKBUILD_GUIDE.md +235 -235
  384. package/vendor/Server/Skills/tpc-cpp-check/references/REPORT_TEMPLATE.md +250 -250
  385. package/vendor/Server/TestCheck/TestCheck.js +775 -775
  386. package/vendor/Server/TestCheck/index.js +11 -11
  387. package/vendor/Server/library/McpToolGuide.js +280 -280
  388. package/vendor/Server/library/StatsService.js +126 -126
  389. package/vendor/Server/library/cstTime.js +16 -16
  390. package/vendor/VERSION +3 -3
  391. package/workflows/pr-check.mjs +105 -105
  392. package/workflows/pr-push.mjs +192 -192
  393. package/vendor/Server/Agent/SkillSelector/types.js +0 -63
@@ -1,818 +1,817 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import MessageArchive from '../../DbUse/MessageArchive.js';
5
- import { nowCST } from '../../library/cstTime.js';
6
- import { createLogger } from '../../Routes/library/logger.js';
7
-
8
- /**
9
- * OpenCode CLI 适配器
10
- * 通过 @opencode-ai/sdk 与运行中的 opencode 服务通信,提供统一的 Session 管理接口。
11
- *
12
- * 依赖环境变量(可由 options 覆盖):
13
- * OPENCODE_HOST - opencode 服务主机(默认 localhost)
14
- * OPENCODE_PORT - opencode 服务端口(默认 4096)
15
- */
16
- class OpenCodeCLI {
17
- /**
18
- * CLI 类型标识(写入存档 meta,供导出/审计区分底层 CLI)。
19
- * 子类通过 static 字段覆盖,父类构造期间即可生效。
20
- */
21
- static _cliType = 'opencode';
22
-
23
- /**
24
- * 日志器名称。子类通过 static 字段覆盖,父类构造期间即可生效。
25
- */
26
- static _loggerName = 'OpenCodeCLI';
27
-
28
- /**
29
- * @param {object} options
30
- * @param {string} [options.host] - opencode 服务主机
31
- * @param {string} [options.port] - opencode 服务端口
32
- * @param {string} [options.baseUrl] - 完整 baseUrl,优先级高于 host/port
33
- * @param {import('../../DbUse/MessageArchive.js').default} [options.archive] - 消息存档服务
34
- */
35
- constructor(options = {}) {
36
- this.host = options.host || process.env.OPENCODE_HOST || 'localhost';
37
- this.port = options.port || process.env.OPENCODE_PORT || '4096';
38
- this.baseUrl = options.baseUrl || `http://${this.host}:${this.port}`;
39
- this.client = null;
40
- // CLI 类型标识 / 日志器名称(经 static 覆盖,子类构造期间即正确)
41
- this.cliType = this.constructor._cliType;
42
- this._loggerName = this.constructor._loggerName;
43
- // 消息存档:用于保留对话消息、判断 opencode 是否长时间无新响应(防卡死)
44
- this.archive = options.archive ?? new MessageArchive();
45
- // 记录各 session 首次进入 busy/retry 的时间戳(ms),用于"busy 停滞兜底"判定
46
- this._busySince = {};
47
- this._init();
48
- }
49
-
50
- // ─── 初始化 ────────────────────────────────────────────────────────────────
51
-
52
- _init() {
53
- try {
54
- this.client = createOpencodeClient({ baseUrl: this.baseUrl });
55
- this._log('info', `已连接至 ${this.baseUrl}`);
56
- } catch (error) {
57
- this._log('error', `初始化失败: ${error.message}`);
58
- throw error;
59
- }
60
- }
61
-
62
- // ─── 运行状态检查 ──────────────────────────────────────────────────────────
63
-
64
- async checkHealth() {
65
- try {
66
- // 轻量健康检查:只调用 session.list() 验证连通性
67
- // 不再为每个 session 拉取全量消息(原 listSessions 会拉所有 session 的所有消息)
68
- await this.client.session.list();
69
- return { status: 'ok', message: 'OpenCode CLI is healthy', target: this.baseUrl };
70
- } catch (error) {
71
- this._log('warn', `checkHealth: 连接失败 — ${error.message}`);
72
- return {
73
- status: 'error',
74
- message: `Health check failed: ${error.message}`,
75
- error: error.message
76
- };
77
- }
78
- }
79
-
80
- // ─── Session 管理 ──────────────────────────────────────────────────────────
81
-
82
- /**
83
- * 获取所有 Session(附带状态)
84
- *
85
- * 优化:不再为每个 session 拉取全量消息来判断状态。
86
- * 仅返回 session 元信息,状态通过 checkSessionStatus 按需查询。
87
- * 原实现对 N 个 session 发起 N 个并发 HTTP 请求拉全量消息,
88
- * session 数量多时会压垮 OpenCode 服务。
89
- */
90
- async listSessions() {
91
- this._log('info', 'listSessions 开始');
92
- try {
93
- const sessions = await this.client.session.list();
94
- // 只返回 session 元信息,不拉取消息
95
- const result = sessions.data.map(s => ({ ...s, status: 'unknown' }));
96
- this._log('info', `listSessions 完成,共 ${result.length} 个 Session`);
97
- return result;
98
- } catch (error) {
99
- this._log('error', `listSessions 失败: ${error.message}`);
100
- throw new Error(`Failed to list sessions: ${error.message}`);
101
- }
102
- }
103
-
104
- /**
105
- * 获取指定 Session(含状态)
106
- *
107
- * 使用 session.get() 获取元信息 + session.status() 获取运行状态,
108
- * 不再拉取全量消息。
109
- *
110
- * @param {string} sessionId
111
- * @returns {Promise<object>} Session 元信息(含 status 字段)
112
- */
113
- async getSession(sessionId) {
114
- this._log('info', `getSession id=${sessionId}`);
115
- try {
116
- const [sessionRes, statusMap] = await Promise.all([
117
- this.client.session.get({ path: { id: sessionId } }),
118
- this.client.session.status({ query: {} }).catch(() => ({ data: {} }))
119
- ]);
120
- const session = sessionRes.data;
121
- const rawStatus = statusMap.data?.[sessionId];
122
- const status = this._mapSessionStatus(rawStatus);
123
- const result = { ...session, status };
124
- this._log('info', `getSession 完成,status=${status}`);
125
- return result;
126
- } catch (error) {
127
- this._log('error', `getSession 失败 id=${sessionId}: ${error.message}`);
128
- throw new Error(`Failed to get session: ${error.message}`);
129
- }
130
- }
131
-
132
- /**
133
- * opencode SessionStatus 映射为统一状态
134
- * busy → running, retry → running, idle → idle, 其他 → unknown
135
- */
136
- _mapSessionStatus(rawStatus) {
137
- if (!rawStatus || !rawStatus.type) return 'unknown';
138
- switch (rawStatus.type) {
139
- case 'busy':
140
- case 'retry':
141
- return 'running';
142
- case 'idle':
143
- return 'idle';
144
- default:
145
- return 'unknown';
146
- }
147
- }
148
-
149
- /**
150
- * 删除 Session
151
- * @param {string} sessionId
152
- */
153
- async deleteSession(sessionId) {
154
- this._log('info', `deleteSession id=${sessionId}`);
155
- try {
156
- const result = await this.client.session.delete({ path: { id: sessionId } });
157
- this._log('info', `deleteSession 成功 id=${sessionId}`);
158
- return result.data;
159
- } catch (error) {
160
- this._log('error', `deleteSession 失败 id=${sessionId}: ${error.message}`);
161
- throw new Error(`Failed to delete session: ${error.message}`);
162
- }
163
- // 保留本地存档文件,不删除(用于事后审计/对比)
164
- }
165
-
166
- /**
167
- * 终止 Session(中断正在运行的任务)
168
- * @param {string} sessionId
169
- */
170
- async abortSession(sessionId) {
171
- this._log('info', `abortSession id=${sessionId}`);
172
- try {
173
- const result = await this.client.session.abort({ path: { id: sessionId } });
174
- this._log('info', `abortSession 成功 id=${sessionId}`);
175
- return result.data;
176
- } catch (error) {
177
- this._log('error', `abortSession 失败 id=${sessionId}: ${error.message}`);
178
- throw new Error(`Failed to abort session: ${error.message}`);
179
- }
180
- }
181
-
182
- /**
183
- * 向指定 Session 发送消息
184
- * @param {string} sessionId
185
- * @param {string} message
186
- */
187
- async sendMessage(sessionId, message) {
188
- try {
189
- await this.client.session.promptAsync({
190
- path: { id: sessionId },
191
- body: {
192
- parts: [{ type: 'text', text: message }],
193
- noReply: false
194
- },
195
- url: `/session/${sessionId}/prompt_async`
196
- });
197
- // 用户消息不由这里归档,统一由 checkSessionStatus / getSessionMessages
198
- // 从远端拉取归档,确保所有消息用 opencode id 去重,避免重复
199
- this._log('info', `sendMessage 成功 id=${sessionId}`);
200
- return { success: true };
201
- } catch (error) {
202
- this._log('error', `sendMessage 失败 id=${sessionId}: ${error.message}`);
203
- throw new Error(`Failed to send message: ${error.message}`);
204
- }
205
- }
206
-
207
- /**
208
- * 向指定 Session 发送消息并同步等待 AI 回复完成
209
- *
210
- * 使用 opencode SDK session.prompt()(同步版本),发送消息后阻塞等待
211
- * AI 生成完整回复,然后直接返回 AssistantMessage 及其 parts。
212
- *
213
- * sendMessage 的区别:
214
- * sendMessage promptAsync,发送后立即返回,不等 AI 回复(fire-and-forget)
215
- * sendAndWaitMessage → prompt,发送后等待 AI 回复完成再返回
216
- *
217
- * 适用场景:
218
- * - 需要立即获取 AI 回复内容,不适合轮询等待
219
- * - 简单问答、单轮对话
220
- * - 调用方需要同步拿到结果继续处理
221
- *
222
- * ⚠️ 注意:AI 回复可能耗时较长(几十秒到几分钟),调用方需设置合理的超时。
223
- *
224
- * @param {string} sessionId
225
- * @param {string} message - 发送的消息
226
- * @returns {Promise<{ success: boolean, response: object, content: string }>}
227
- * response: opencode 返回的完整 AssistantMessage(含 info + parts)
228
- * content: AI 回复的文本内容(从 parts 中提取拼接)
229
- */
230
- async sendAndWaitMessage(sessionId, message) {
231
- try {
232
- const result = await this.client.session.prompt({
233
- path: { id: sessionId },
234
- body: {
235
- parts: [{ type: 'text', text: message }],
236
- noReply: false
237
- },
238
- url: `/session/${sessionId}/message`
239
- });
240
- const assistantMsg = result.data;
241
- // parts 中提取文本内容
242
- const textParts = (assistantMsg.parts || []).filter(p => p.type === 'text');
243
- const content = textParts.map(p => p.text).join('\n');
244
- this._log('info',
245
- `sendAndWaitMessage 成功 id=${sessionId}, ` +
246
- `回复长度=${content.length}字, finish=${assistantMsg.info?.finish ?? 'N/A'}`);
247
- // 用户消息 + AI 回复均不由这里归档,统一由 getSessionMessages
248
- // 从远端拉取归档,确保用 opencode id 去重
249
- return {
250
- success: true,
251
- response: assistantMsg,
252
- content
253
- };
254
- } catch (error) {
255
- this._log('error', `sendAndWaitMessage 失败 id=${sessionId}: ${error.message}`);
256
- throw new Error(`Failed to send and wait message: ${error.message}`);
257
- }
258
- }
259
-
260
- /**
261
- * 向指定 Session 注入提示词/上下文(不触发 AI 响应)
262
- *
263
- * 利用 opencode SDK 的 noReply: true 能力,将文本作为 UserMessage
264
- * 注入对话历史,但不会触发 AI 生成回复。
265
- *
266
- * 适用场景:
267
- * - 对话过程中动态补充上下文、规则、指令
268
- * - 插件向会话注入参考信息,不打断当前 AI 任务
269
- * - 追加系统级约束(如代码规范、输出格式要求)
270
- *
271
- * sendMessage 的区别:
272
- * sendMessage → noReply: false,发送后 AI 会生成回复
273
- * injectPrompt → noReply: true,仅注入上下文,AI 不回复
274
- *
275
- * @param {string} sessionId
276
- * @param {string} message - 要注入的提示词/上下文
277
- * @returns {Promise<{ success: boolean }>}
278
- */
279
- async injectPrompt(sessionId, message) {
280
- this._log('debug', `injectPrompt id=${sessionId}`);
281
- this._log('debug', ` message (前100字): ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`);
282
- try {
283
- await this.client.session.promptAsync({
284
- path: { id: sessionId },
285
- body: {
286
- parts: [{ type: 'text', text: message }],
287
- noReply: true
288
- },
289
- url: `/session/${sessionId}/prompt_async`
290
- });
291
- // 注入的上下文消息不由这里归档,统一由 getSessionMessages
292
- // 从远端拉取归档,确保用 opencode id 去重
293
- this._log('info', `injectPrompt 成功 id=${sessionId}(已注入上下文,不触发 AI 响应)`);
294
- return { success: true };
295
- } catch (error) {
296
- this._log('error', `injectPrompt 失败 id=${sessionId}: ${error.message}`);
297
- throw new Error(`Failed to inject prompt: ${error.message}`);
298
- }
299
- }
300
-
301
- /**
302
- * 创建新 Session,可选附带初始提示词
303
- * @param {string} title - Session 标题
304
- * @param {string} [prompt] - 初始提示词(可选)
305
- * @param {object} [options]
306
- * @param {string} [options.cwd] - 工作目录(通过 opencode session.create 的 query.directory 设定)
307
- */
308
- async startSession(title, prompt, options = {}) {
309
- const cwd = options.cwd;
310
- this._log('info', `startSession title="${title}"${cwd ? ` cwd="${cwd}"` : ''}`);
311
-
312
- // 前置检查:如果指定了 cwd,确保目录存在
313
- // opencode session.create 不会检查目录是否存在,但 promptAsync 时会
314
- // realPath 检查,目录不存在时静默失败(返回 204,tokens=0+0)。
315
- if (cwd) {
316
- try {
317
- const stats = fs.statSync(cwd);
318
- if (!stats.isDirectory()) {
319
- throw new Error(`cwd 不是目录: ${cwd}`);
320
- }
321
- } catch (statErr) {
322
- if (statErr.code === 'ENOENT') {
323
- this._log('info', `startSession: cwd 不存在,自动创建 ${cwd}`);
324
- fs.mkdirSync(cwd, { recursive: true });
325
- } else {
326
- throw new Error(`startSession: cwd 检查失败: ${statErr.message}`);
327
- }
328
- }
329
- }
330
-
331
- try {
332
- // opencode SDK session.create 支持 query.directory 参数,
333
- // 用于指定该 Session 的工作目录(git clone、文件读写等操作均在此目录下执行)
334
- const createParams = { body: { title } };
335
- if (cwd) {
336
- createParams.query = { directory: cwd };
337
- }
338
- const session = await this.client.session.create(createParams);
339
- const sessionId = session.data.id;
340
- this._log('info', `Session 已创建 id=${sessionId}${cwd ? ` directory="${cwd}"` : ''}`);
341
-
342
- // 初始化存档元信息
343
- await this.archive.patchMeta(sessionId, {
344
- sessionId,
345
- title,
346
- cliType: this.cliType,
347
- createdAt: nowCST(),
348
- status: 'idle'
349
- }).catch(e => {
350
- this._log('warn', `startSession: 初始化存档 meta 失败 id=${sessionId}: ${e.message}`);
351
- });
352
-
353
- if (prompt) {
354
- await this.client.session.promptAsync({
355
- path: { id: sessionId },
356
- body: {
357
- parts: [{ type: 'text', text: prompt }],
358
- noReply: false
359
- },
360
- url: `/session/${sessionId}/prompt_async`
361
- });
362
- // 初始 prompt 的用户消息不由这里归档,统一由 checkSessionStatus /
363
- // getSessionMessages 从远端拉取归档,确保用 opencode id 去重
364
- await this.archive.patchMeta(sessionId, { status: 'running' }).catch(() => {});
365
- }
366
-
367
- return {
368
- sessionId,
369
- action: 'create',
370
- message: prompt ? '已创建新 Session 并开始执行' : '已创建新 Session'
371
- };
372
- } catch (error) {
373
- this._log('error', `startSession 失败: ${error.message}`);
374
- throw new Error(`Failed to start session: ${error.message}`);
375
- }
376
- }
377
-
378
- /**
379
- * 查询 Session 当前状态
380
- * @param {string} sessionId
381
- * @returns {{ status: 'completed'|'running'|'unknown', lastMessage?: object }}
382
- */
383
- async checkSessionStatus(sessionId) {
384
- try {
385
- // ── 快路径:先查 session.status(),busy/retry 时直接返回 running,不拉消息 ──
386
- // session.status() 返回 {[id]: {type: "busy"|"idle"|"retry"}},极轻量
387
- // 只有 idle 时才需要拉最后一条消息检查 finish/error 判断 completed/error
388
- let statusMap;
389
- try {
390
- statusMap = await this.client.session.status({ query: {} });
391
- } catch (e) {
392
- this._log('warn', `checkSessionStatus: session.status() 失败,降级为拉消息: ${e.message}`);
393
- }
394
- const rawStatus = statusMap?.data?.[sessionId];
395
- if (rawStatus && (rawStatus.type === 'busy' || rawStatus.type === 'retry')) {
396
- // AI 正在生成,无需拉消息(快路径)
397
- // 但若 busy 持续过久(超过停滞阈值),则降级拉最后一条消息兜底:
398
- // opencode 的 session.status() 在任务实际完成后仍可能长时间保持 busy,
399
- // 仅依赖 status 会永远无法识别已完成的 finish:"stop" 消息,导致上层轮询超时。
400
- const now = Date.now();
401
- if (!this._busySince[sessionId]) {
402
- this._busySince[sessionId] = now;
403
- }
404
- // 复用存档的卡死阈值(默认 5 分钟),作为 busy 停滞兜底的超时判定
405
- const stallThresholdMs = this.archive?._store?.stallThresholdMs ?? 5 * 60 * 1000;
406
- const busyDurationMs = now - this._busySince[sessionId];
407
-
408
- if (busyDurationMs >= stallThresholdMs) {
409
- // busy 停滞超阈值 → 检查消息层,识别是否实际已完成
410
- this._log('warn',
411
- `checkSessionStatus: session ${sessionId} busy 持续 ${Math.round(busyDurationMs / 1000)}s,降级检查消息层`);
412
- const fallback = await this._checkMessageLayer(sessionId);
413
- if (fallback) {
414
- return fallback;
415
- }
416
- }
417
-
418
- const checkRes = await this.archive.recordCheck(sessionId, {
419
- lastMessageId: null,
420
- status: 'running'
421
- }).catch(() => ({ stalled: false, stallDurationMs: null, hasNewMessage: false }));
422
- return {
423
- status: checkRes.stalled ? 'stalled' : 'running',
424
- stallDurationMs: checkRes.stallDurationMs,
425
- stalled: !!checkRes.stalled,
426
- hasNewMessage: false
427
- };
428
- }
429
- // 非 busy/retry:清除停滞计时,避免脏状态
430
- delete this._busySince[sessionId];
431
-
432
- // ── idle status 查询失败:拉最后 1 条消息判断 completed/error/running ──
433
- const messages = await this.client.session.messages({
434
- path: { id: sessionId },
435
- query: { limit: 1 },
436
- });
437
- const data = messages.data;
438
-
439
- if (!data || data.length === 0) {
440
- return { status: 'unknown' };
441
- }
442
-
443
- const lastMessage = data[data.length - 1];
444
-
445
- // 出错 / 被中断
446
- if (lastMessage.info?.error) {
447
- const errName = lastMessage.info.error.name ?? 'UnknownError';
448
- this._log('warn', `checkSessionStatus: 检测到错误 ${errName},status=error`);
449
- await this.archive.recordCheck(sessionId, {
450
- lastMessageId: this._msgDedupeKey(lastMessage),
451
- status: 'error'
452
- }).catch(() => {});
453
- // 错误结束时也自动归档全量消息
454
- await this._autoArchive(sessionId, 'error');
455
- return { status: 'error', error: lastMessage.info.error, lastMessage };
456
- }
457
-
458
- // 正常完成
459
- const isCompleted = this._isCompletedMessage(lastMessage);
460
-
461
- const status = isCompleted ? 'completed' : 'running';
462
-
463
- // 状态检查 + 卡死检测
464
- const dedupeKey = this._msgDedupeKey(lastMessage);
465
- const checkRes = await this.archive.recordCheck(sessionId, {
466
- lastMessageId: dedupeKey,
467
- status
468
- }).catch(() => ({ stalled: false, stallDurationMs: null, hasNewMessage: false }));
469
-
470
- // 若检测到停滞(running 状态下长时间无新 assistant 消息)
471
- const finalStatus = checkRes.stalled ? 'stalled' : status;
472
- if (checkRes.stalled) {
473
- this._log('warn',
474
- `checkSessionStatus: 检测到停滞 id=${sessionId}, 已 ${Math.round((checkRes.stallDurationMs || 0) / 1000)}s 无新响应`);
475
- }
476
-
477
- // session 完成时自动归档全量消息(写入 .jsonl)
478
- if (isCompleted) {
479
- await this._autoArchive(sessionId, 'completed');
480
- }
481
-
482
- return {
483
- status: finalStatus,
484
- lastMessage,
485
- // 卡死检测相关字段
486
- stallDurationMs: checkRes.stallDurationMs,
487
- stalled: !!checkRes.stalled,
488
- hasNewMessage: !!checkRes.hasNewMessage
489
- };
490
- } catch (error) {
491
- this._log('warn', `checkSessionStatus 异常 id=${sessionId}: ${error.message}`);
492
- return { status: 'unknown' };
493
- }
494
- }
495
-
496
- /**
497
- * 判断 opencode 消息是否为正常完成(finish='stop' 且含 text part)。
498
- *
499
- * 供 checkSessionStatus 的 idle 分支与 busy 停滞兜底分支共用,避免判定逻辑漂移。
500
- *
501
- * @param {object} lastMessage - opencode 原始消息
502
- * @returns {boolean}
503
- * @private
504
- */
505
- _isCompletedMessage(lastMessage) {
506
- return Boolean(
507
- lastMessage &&
508
- lastMessage.info?.finish === 'stop' &&
509
- Array.isArray(lastMessage.parts) &&
510
- lastMessage.parts.some(p => p.type === 'text')
511
- );
512
- }
513
-
514
- /**
515
- * 从消息层检查 session 是否实际已完成/出错。
516
- *
517
- * 用于 busy/retry 停滞兜底:拉取最后 1 条消息,若已 finish='stop' 或 info.error,
518
- * 则返回对应的 completed/error 状态;否则返回 null(表示任务确实仍在运行)。
519
- *
520
- * 注意:本方法不更新 _busySince——由调用方在返回非 completed/error 时继续累积。
521
- *
522
- * @param {string} sessionId
523
- * @returns {Promise<object|null>} 已终态时返回状态对象;仍在运行返回 null
524
- * @private
525
- */
526
- async _checkMessageLayer(sessionId) {
527
- try {
528
- const messages = await this.client.session.messages({
529
- path: { id: sessionId },
530
- query: { limit: 1 },
531
- });
532
- const data = messages.data;
533
- if (!data || data.length === 0) {
534
- return null;
535
- }
536
-
537
- const lastMessage = data[data.length - 1];
538
-
539
- // 出错 / 被中断
540
- if (lastMessage.info?.error) {
541
- const errName = lastMessage.info.error.name ?? 'UnknownError';
542
- this._log('warn', `checkSessionStatus: busy 兜底检测到错误 ${errName},status=error`);
543
- await this.archive.recordCheck(sessionId, {
544
- lastMessageId: this._msgDedupeKey(lastMessage),
545
- status: 'error'
546
- }).catch(() => {});
547
- await this._autoArchive(sessionId, 'error');
548
- delete this._busySince[sessionId];
549
- return { status: 'error', error: lastMessage.info.error, lastMessage };
550
- }
551
-
552
- // 正常完成(finish='stop' 且含 text)
553
- if (this._isCompletedMessage(lastMessage)) {
554
- this._log('info', `checkSessionStatus: busy 兜底确认 session ${sessionId} 已完成`);
555
- const dedupeKey = this._msgDedupeKey(lastMessage);
556
- await this.archive.recordCheck(sessionId, {
557
- lastMessageId: dedupeKey,
558
- status: 'completed'
559
- }).catch(() => {});
560
- await this._autoArchive(sessionId, 'completed');
561
- delete this._busySince[sessionId];
562
- return {
563
- status: 'completed',
564
- lastMessage,
565
- stallDurationMs: null,
566
- stalled: false,
567
- hasNewMessage: true
568
- };
569
- }
570
-
571
- // 仍在运行:刷新 busySince(避免一次长消息生成被误判),返回 null 表示继续等待
572
- this._busySince[sessionId] = Date.now();
573
- return null;
574
- } catch (err) {
575
- this._log('warn', `checkSessionStatus: busy 兜底检查消息失败 id=${sessionId}: ${err.message}`);
576
- return null;
577
- }
578
- }
579
-
580
- /**
581
- * 生成 opencode 消息的去重键
582
- * 优先用消息 id(全局唯一);无 id 时用 role+finish+时间+内容文本长度
583
- * 注意:不使用 parts 长度,因为 checkSessionStatus(limit=1) 与 getSessionMessages(全量)
584
- * 返回的 parts 结构可能不同,导致同一条消息算出不同去重键而重复归档。
585
- */
586
- _msgDedupeKey(msg) {
587
- const id = msg.id ?? msg.info?.id;
588
- if (id) return String(id);
589
- const role = msg.info?.role ?? 'unknown';
590
- const finish = msg.info?.finish ?? '';
591
- const ts = msg.info?.time?.created ?? msg.timestamp ?? '';
592
- const textParts = (msg.parts || []).filter(p => p.type === 'text');
593
- const content = textParts.map(p => p.text).join('\n');
594
- return `${role}:${finish}:${ts}:${content.length}`;
595
- }
596
-
597
- /**
598
- * 将 opencode 原始消息转换为精简归档格式
599
- *
600
- * 只保留核心对话字段(role / content / finish / timestamp / id),
601
- * 去掉 raw 原始结构(含 parts / tokens / cost / reasoning 等),
602
- * 使归档文件轻量可读,关注实际发生了哪些对话。
603
- */
604
- _toArchiveMessage(msg) {
605
- const role = msg.info?.role ?? 'unknown';
606
- const textParts = (msg.parts || []).filter(p => p.type === 'text');
607
- const content = textParts.map(p => p.text).join('\n');
608
- // opencode 时间在 info.time.created(epoch 毫秒),非 ISO 字符串
609
- const createdMs = msg.info?.time?.created;
610
- const timestamp = createdMs != null
611
- ? nowCST(createdMs)
612
- : (msg.timestamp ?? nowCST());
613
- return {
614
- id: msg.id ?? msg.info?.id,
615
- role,
616
- content,
617
- finish: msg.info?.finish ?? null,
618
- timestamp
619
- };
620
- }
621
-
622
- /**
623
- * 自动归档 — session 完成或出错时全量拉取消息并写入 .jsonl
624
- *
625
- * checkSessionStatus 平时只用 limit=1 查状态(不归档),
626
- * 但当检测到 session 已结束(completed / error)时,
627
- * 应自动全量拉取消息归档,确保 .jsonl 文件生成。
628
- *
629
- * 归档采用 appendMessages 增量去重,重复调用安全无副作用。
630
- *
631
- * @param {string} sessionId
632
- * @param {'completed'|'error'} reason - 触发归档的原因
633
- * @private
634
- */
635
- async _autoArchive(sessionId, reason) {
636
- try {
637
- const messages = await this.client.session.messages({ path: { id: sessionId } });
638
- const data = messages.data || [];
639
- const archRes = await this.archive.appendMessages(
640
- sessionId,
641
- data.map(m => this._toArchiveMessage(m))
642
- );
643
- this._log('info',
644
- `自动归档 id=${sessionId} reason=${reason} appended=${archRes.appended} skipped=${archRes.skipped}`);
645
- } catch (e) {
646
- this._log('warn', `自动归档失败 id=${sessionId} reason=${reason}: ${e.message}`);
647
- }
648
- }
649
-
650
- /**
651
- * 获取 Session 消息历史
652
- * @param {string} sessionId
653
- */
654
- async getSessionMessages(sessionId) {
655
- this._log('info', `getSessionMessages id=${sessionId}`);
656
- try {
657
- const messages = await this.client.session.messages({ path: { id: sessionId } });
658
- const data = messages.data || [];
659
- // 增量归档全量消息(首次拉取或补档),await 确保调用方能立即读取归档结果
660
- const archRes = await this.archive.appendMessages(
661
- sessionId,
662
- data.map(m => this._toArchiveMessage(m))
663
- ).catch(e => {
664
- this._log('warn', `getSessionMessages: 归档失败 id=${sessionId}: ${e.message}`);
665
- return { appended: 0, skipped: 0 };
666
- });
667
- this._log('debug', `getSessionMessages: 归档 appended=${archRes.appended} skipped=${archRes.skipped}`);
668
- return data;
669
- } catch (error) {
670
- this._log('error', `getSessionMessages 失败 id=${sessionId}: ${error.message}`);
671
- throw new Error(`Failed to get session messages: ${error.message}`);
672
- }
673
- }
674
-
675
- /**
676
- * 实时查看当前对话消息(运行中安全调用)
677
- *
678
- * 从远端拉取当前已有消息并转为精简格式返回,**不写入本地存档**。
679
- * 适用于对话进行中查看 "目前聊了什么",不影响后续归档去重。
680
- *
681
- * @param {string} sessionId
682
- * @returns {Promise<Array<{id,role,content,finish,timestamp}>>}
683
- */
684
- async getLiveMessages(sessionId) {
685
- this._log('info', `getLiveMessages id=${sessionId}`);
686
- try {
687
- const messages = await this.client.session.messages({ path: { id: sessionId } });
688
- const data = messages.data || [];
689
- return data.map(m => this._toArchiveMessage(m));
690
- } catch (error) {
691
- this._log('error', `getLiveMessages 失败 id=${sessionId}: ${error.message}`);
692
- throw new Error(`Failed to get live messages: ${error.message}`);
693
- }
694
- }
695
-
696
- /**
697
- * 导出完整对话(合并远端 + 本地,支持运行中 / 已结束)
698
- *
699
- * 流程:
700
- * 1. 从远端拉取全量消息(运行中也能拿到已生成的部分)
701
- * 2. 增量归档到本地 jsonl(去重,补全运行中缺失的消息)
702
- * 3. 合并本地已归档消息,按 seq 排序
703
- * 4. 按 format 返回结构化结果
704
- *
705
- * @param {string} sessionId
706
- * @param {object} [opts]
707
- * @param {'json'|'markdown'} [opts.format='json'] - 导出格式
708
- * @returns {Promise<object|string>}
709
- * format='json' → { sessionId, meta, messages: [...] }
710
- * format='markdown' → Markdown 字符串
711
- */
712
- async exportSession(sessionId, opts = {}) {
713
- const format = opts.format || 'json';
714
- this._log('info', `exportSession id=${sessionId} format=${format}`);
715
- try {
716
- // 1. 远端拉取全量消息
717
- const messages = await this.client.session.messages({ path: { id: sessionId } });
718
- const data = messages.data || [];
719
-
720
- // 2. 增量归档(去重补档,运行中也能持久化已有消息)
721
- const archRes = await this.archive.appendMessages(
722
- sessionId,
723
- data.map(m => this._toArchiveMessage(m))
724
- ).catch(e => {
725
- this._log('warn', `exportSession: 归档失败 id=${sessionId}: ${e.message}`);
726
- return { appended: 0, skipped: 0 };
727
- });
728
- this._log('debug', `exportSession: 归档 appended=${archRes.appended} skipped=${archRes.skipped}`);
729
-
730
- // 3. 读本地归档(合并后按 seq 排序,确保完整且有序)
731
- const archivedMessages = await this.archive.readMessages(sessionId);
732
- const meta = await this.archive.readMeta(sessionId);
733
- const messages_sorted = archivedMessages.sort((a, b) => (a.seq || 0) - (b.seq || 0));
734
-
735
- // 4. 按格式输出
736
- if (format === 'markdown') {
737
- return this._toMarkdown(meta, messages_sorted);
738
- }
739
- return {
740
- sessionId,
741
- meta,
742
- messages: messages_sorted
743
- };
744
- } catch (error) {
745
- this._log('error', `exportSession 失败 id=${sessionId}: ${error.message}`);
746
- throw new Error(`Failed to export session: ${error.message}`);
747
- }
748
- }
749
-
750
- /**
751
- * 将对话消息渲染为 Markdown 格式
752
- * @param {object} meta - Session 元信息
753
- * @param {Array} messages - 已归档消息列表
754
- * @returns {string}
755
- */
756
- _toMarkdown(meta, messages) {
757
- const lines = [];
758
- lines.push(`# ${meta.title || meta.sessionId}`);
759
- lines.push('');
760
- lines.push(`> **Session ID**: ${meta.sessionId} `);
761
- lines.push(`> **CLI 类型**: ${meta.cliType || 'N/A'} `);
762
- lines.push(`> **创建时间**: ${meta.createdAt || 'N/A'} `);
763
- lines.push(`> **状态**: ${meta.status || 'N/A'} `);
764
- lines.push(`> **消息数**: ${messages.length} `);
765
- lines.push('');
766
- lines.push('---');
767
- lines.push('');
768
- for (const msg of messages) {
769
- const roleLabel = msg.role === 'user' ? '🧑 User' : (msg.role === 'assistant' ? '🤖 Assistant' : msg.role);
770
- const ts = msg.timestamp || '';
771
- lines.push(`### ${roleLabel}`);
772
- lines.push(`*${ts}*`);
773
- lines.push('');
774
- lines.push(msg.content || '(empty)');
775
- lines.push('');
776
- }
777
- return lines.join('\n');
778
- }
779
-
780
- /**
781
- * 读取本地已归档的消息(不请求远端,用于事后审计/对比)
782
- * @param {string} sessionId
783
- * @returns {Promise<Array<object>>}
784
- */
785
- async getArchivedMessages(sessionId) {
786
- return this.archive.readMessages(sessionId);
787
- }
788
-
789
- /**
790
- * 读取本地存档元信息(含 lastResponseAt / archivedCount 等)
791
- * @param {string} sessionId
792
- * @returns {Promise<object>}
793
- */
794
- async getArchiveMeta(sessionId) {
795
- return this.archive.readMeta(sessionId);
796
- }
797
-
798
- // ─── 内部方法 ──────────────────────────────────────────────────────────────
799
-
800
- _log(level, msg) {
801
- const logger = createLogger(this._loggerName);
802
- switch (level) {
803
- case 'error':
804
- logger.error(`❌ ${msg}`);
805
- break;
806
- case 'warn':
807
- logger.warn(`⚠️ ${msg}`);
808
- break;
809
- case 'debug':
810
- logger.debug(`🔍 ${msg}`);
811
- break;
812
- default:
813
- logger.info(`ℹ️ ${msg}`);
814
- }
815
- }
816
- }
817
-
818
- export default OpenCodeCLI;
1
+ import { createOpencodeClient } from '@opencode-ai/sdk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import MessageArchive from '../../DbUse/MessageArchive.js';
5
+ import { nowCST } from '../../library/cstTime.js';
6
+ import { createLogger } from '../../Routes/library/logger.js';
7
+
8
+ /**
9
+ * OpenCode CLI 适配器
10
+ * 通过 @opencode-ai/sdk 与运行中的 opencode 服务通信,提供统一的 Session 管理接口。
11
+ *
12
+ * 依赖环境变量(可由 options 覆盖):
13
+ * OPENCODE_HOST - opencode 服务主机(默认 localhost)
14
+ * OPENCODE_PORT - opencode 服务端口(默认 4096)
15
+ */
16
+ class OpenCodeCLI {
17
+ /**
18
+ * CLI 类型标识(写入存档 meta,供导出/审计区分底层 CLI)。
19
+ * 子类通过 static 字段覆盖,父类构造期间即可生效。
20
+ */
21
+ static _cliType = 'opencode';
22
+
23
+ /**
24
+ * 日志器名称。子类通过 static 字段覆盖,父类构造期间即可生效。
25
+ */
26
+ static _loggerName = 'OpenCodeCLI';
27
+
28
+ /**
29
+ * @param {object} options
30
+ * @param {string} [options.host] - opencode 服务主机
31
+ * @param {string} [options.port] - opencode 服务端口
32
+ * @param {string} [options.baseUrl] - 完整 baseUrl,优先级高于 host/port
33
+ * @param {import('../../DbUse/MessageArchive.js').default} [options.archive] - 消息存档服务
34
+ */
35
+ constructor(options = {}) {
36
+ this.host = options.host || process.env.OPENCODE_HOST || 'localhost';
37
+ this.port = options.port || process.env.OPENCODE_PORT || '4096';
38
+ this.baseUrl = options.baseUrl || `http://${this.host}:${this.port}`;
39
+ this.client = null;
40
+ // CLI 类型标识 / 日志器名称(经 static 覆盖,子类构造期间即正确)
41
+ this.cliType = this.constructor._cliType;
42
+ this._loggerName = this.constructor._loggerName;
43
+ // 消息存档:用于保留对话消息、判断 opencode 是否长时间无新响应(防卡死)
44
+ this.archive = options.archive ?? new MessageArchive();
45
+ // 记录各 session 首次进入 busy/retry 的时间戳(ms),用于"busy 停滞兜底"判定
46
+ this._busySince = {};
47
+ this._init();
48
+ }
49
+
50
+ // ─── 初始化 ────────────────────────────────────────────────────────────────
51
+
52
+ _init() {
53
+ try {
54
+ this.client = createOpencodeClient({ baseUrl: this.baseUrl });
55
+ } catch (error) {
56
+ this._log('error', `初始化失败: ${error.message}`);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ // ─── 运行状态检查 ──────────────────────────────────────────────────────────
62
+
63
+ async checkHealth() {
64
+ try {
65
+ // 轻量健康检查:只调用 session.list() 验证连通性
66
+ // 不再为每个 session 拉取全量消息(原 listSessions 会拉所有 session 的所有消息)
67
+ await this.client.session.list();
68
+ return { status: 'ok', message: 'OpenCode CLI is healthy', target: this.baseUrl };
69
+ } catch (error) {
70
+ this._log('warn', `checkHealth: 连接失败 — ${error.message}`);
71
+ return {
72
+ status: 'error',
73
+ message: `Health check failed: ${error.message}`,
74
+ error: error.message
75
+ };
76
+ }
77
+ }
78
+
79
+ // ─── Session 管理 ──────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * 获取所有 Session(附带状态)
83
+ *
84
+ * 优化:不再为每个 session 拉取全量消息来判断状态。
85
+ * 仅返回 session 元信息,状态通过 checkSessionStatus 按需查询。
86
+ * 原实现对 N 个 session 发起 N 个并发 HTTP 请求拉全量消息,
87
+ * session 数量多时会压垮 OpenCode 服务。
88
+ */
89
+ async listSessions() {
90
+ this._log('info', 'listSessions 开始');
91
+ try {
92
+ const sessions = await this.client.session.list();
93
+ // 只返回 session 元信息,不拉取消息
94
+ const result = sessions.data.map(s => ({ ...s, status: 'unknown' }));
95
+ this._log('info', `listSessions 完成,共 ${result.length} Session`);
96
+ return result;
97
+ } catch (error) {
98
+ this._log('error', `listSessions 失败: ${error.message}`);
99
+ throw new Error(`Failed to list sessions: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 获取指定 Session(含状态)
105
+ *
106
+ * 使用 session.get() 获取元信息 + session.status() 获取运行状态,
107
+ * 不再拉取全量消息。
108
+ *
109
+ * @param {string} sessionId
110
+ * @returns {Promise<object>} Session 元信息(含 status 字段)
111
+ */
112
+ async getSession(sessionId) {
113
+ this._log('info', `getSession id=${sessionId}`);
114
+ try {
115
+ const [sessionRes, statusMap] = await Promise.all([
116
+ this.client.session.get({ path: { id: sessionId } }),
117
+ this.client.session.status({ query: {} }).catch(() => ({ data: {} }))
118
+ ]);
119
+ const session = sessionRes.data;
120
+ const rawStatus = statusMap.data?.[sessionId];
121
+ const status = this._mapSessionStatus(rawStatus);
122
+ const result = { ...session, status };
123
+ this._log('info', `getSession 完成,status=${status}`);
124
+ return result;
125
+ } catch (error) {
126
+ this._log('error', `getSession 失败 id=${sessionId}: ${error.message}`);
127
+ throw new Error(`Failed to get session: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 将 opencode SessionStatus 映射为统一状态
133
+ * busy running, retry → running, idle → idle, 其他 → unknown
134
+ */
135
+ _mapSessionStatus(rawStatus) {
136
+ if (!rawStatus || !rawStatus.type) return 'unknown';
137
+ switch (rawStatus.type) {
138
+ case 'busy':
139
+ case 'retry':
140
+ return 'running';
141
+ case 'idle':
142
+ return 'idle';
143
+ default:
144
+ return 'unknown';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 删除 Session
150
+ * @param {string} sessionId
151
+ */
152
+ async deleteSession(sessionId) {
153
+ this._log('info', `deleteSession id=${sessionId}`);
154
+ try {
155
+ const result = await this.client.session.delete({ path: { id: sessionId } });
156
+ this._log('info', `deleteSession 成功 id=${sessionId}`);
157
+ return result.data;
158
+ } catch (error) {
159
+ this._log('error', `deleteSession 失败 id=${sessionId}: ${error.message}`);
160
+ throw new Error(`Failed to delete session: ${error.message}`);
161
+ }
162
+ // 保留本地存档文件,不删除(用于事后审计/对比)
163
+ }
164
+
165
+ /**
166
+ * 终止 Session(中断正在运行的任务)
167
+ * @param {string} sessionId
168
+ */
169
+ async abortSession(sessionId) {
170
+ this._log('info', `abortSession id=${sessionId}`);
171
+ try {
172
+ const result = await this.client.session.abort({ path: { id: sessionId } });
173
+ this._log('info', `abortSession 成功 id=${sessionId}`);
174
+ return result.data;
175
+ } catch (error) {
176
+ this._log('error', `abortSession 失败 id=${sessionId}: ${error.message}`);
177
+ throw new Error(`Failed to abort session: ${error.message}`);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 向指定 Session 发送消息
183
+ * @param {string} sessionId
184
+ * @param {string} message
185
+ */
186
+ async sendMessage(sessionId, message) {
187
+ try {
188
+ await this.client.session.promptAsync({
189
+ path: { id: sessionId },
190
+ body: {
191
+ parts: [{ type: 'text', text: message }],
192
+ noReply: false
193
+ },
194
+ url: `/session/${sessionId}/prompt_async`
195
+ });
196
+ // 用户消息不由这里归档,统一由 checkSessionStatus / getSessionMessages
197
+ // 从远端拉取归档,确保所有消息用 opencode id 去重,避免重复
198
+ this._log('info', `sendMessage 成功 id=${sessionId}`);
199
+ return { success: true };
200
+ } catch (error) {
201
+ this._log('error', `sendMessage 失败 id=${sessionId}: ${error.message}`);
202
+ throw new Error(`Failed to send message: ${error.message}`);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 向指定 Session 发送消息并同步等待 AI 回复完成
208
+ *
209
+ * 使用 opencode SDK 的 session.prompt()(同步版本),发送消息后阻塞等待
210
+ * AI 生成完整回复,然后直接返回 AssistantMessage 及其 parts。
211
+ *
212
+ * 与 sendMessage 的区别:
213
+ * sendMessage → promptAsync,发送后立即返回,不等 AI 回复(fire-and-forget)
214
+ * sendAndWaitMessage prompt,发送后等待 AI 回复完成再返回
215
+ *
216
+ * 适用场景:
217
+ * - 需要立即获取 AI 回复内容,不适合轮询等待
218
+ * - 简单问答、单轮对话
219
+ * - 调用方需要同步拿到结果继续处理
220
+ *
221
+ * ⚠️ 注意:AI 回复可能耗时较长(几十秒到几分钟),调用方需设置合理的超时。
222
+ *
223
+ * @param {string} sessionId
224
+ * @param {string} message - 发送的消息
225
+ * @returns {Promise<{ success: boolean, response: object, content: string }>}
226
+ * response: opencode 返回的完整 AssistantMessage(含 info + parts)
227
+ * content: AI 回复的文本内容(从 parts 中提取拼接)
228
+ */
229
+ async sendAndWaitMessage(sessionId, message) {
230
+ try {
231
+ const result = await this.client.session.prompt({
232
+ path: { id: sessionId },
233
+ body: {
234
+ parts: [{ type: 'text', text: message }],
235
+ noReply: false
236
+ },
237
+ url: `/session/${sessionId}/message`
238
+ });
239
+ const assistantMsg = result.data;
240
+ // parts 中提取文本内容
241
+ const textParts = (assistantMsg.parts || []).filter(p => p.type === 'text');
242
+ const content = textParts.map(p => p.text).join('\n');
243
+ this._log('info',
244
+ `sendAndWaitMessage 成功 id=${sessionId}, ` +
245
+ `回复长度=${content.length}字, finish=${assistantMsg.info?.finish ?? 'N/A'}`);
246
+ // 用户消息 + AI 回复均不由这里归档,统一由 getSessionMessages
247
+ // 从远端拉取归档,确保用 opencode id 去重
248
+ return {
249
+ success: true,
250
+ response: assistantMsg,
251
+ content
252
+ };
253
+ } catch (error) {
254
+ this._log('error', `sendAndWaitMessage 失败 id=${sessionId}: ${error.message}`);
255
+ throw new Error(`Failed to send and wait message: ${error.message}`);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * 向指定 Session 注入提示词/上下文(不触发 AI 响应)
261
+ *
262
+ * 利用 opencode SDK 的 noReply: true 能力,将文本作为 UserMessage
263
+ * 注入对话历史,但不会触发 AI 生成回复。
264
+ *
265
+ * 适用场景:
266
+ * - 对话过程中动态补充上下文、规则、指令
267
+ * - 插件向会话注入参考信息,不打断当前 AI 任务
268
+ * - 追加系统级约束(如代码规范、输出格式要求)
269
+ *
270
+ * 与 sendMessage 的区别:
271
+ * sendMessage → noReply: false,发送后 AI 会生成回复
272
+ * injectPrompt → noReply: true,仅注入上下文,AI 不回复
273
+ *
274
+ * @param {string} sessionId
275
+ * @param {string} message - 要注入的提示词/上下文
276
+ * @returns {Promise<{ success: boolean }>}
277
+ */
278
+ async injectPrompt(sessionId, message) {
279
+ this._log('debug', `injectPrompt id=${sessionId}`);
280
+ this._log('debug', ` message (前100字): ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`);
281
+ try {
282
+ await this.client.session.promptAsync({
283
+ path: { id: sessionId },
284
+ body: {
285
+ parts: [{ type: 'text', text: message }],
286
+ noReply: true
287
+ },
288
+ url: `/session/${sessionId}/prompt_async`
289
+ });
290
+ // 注入的上下文消息不由这里归档,统一由 getSessionMessages
291
+ // 从远端拉取归档,确保用 opencode id 去重
292
+ this._log('info', `injectPrompt 成功 id=${sessionId}(已注入上下文,不触发 AI 响应)`);
293
+ return { success: true };
294
+ } catch (error) {
295
+ this._log('error', `injectPrompt 失败 id=${sessionId}: ${error.message}`);
296
+ throw new Error(`Failed to inject prompt: ${error.message}`);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 创建新 Session,可选附带初始提示词
302
+ * @param {string} title - Session 标题
303
+ * @param {string} [prompt] - 初始提示词(可选)
304
+ * @param {object} [options]
305
+ * @param {string} [options.cwd] - 工作目录(通过 opencode session.create 的 query.directory 设定)
306
+ */
307
+ async startSession(title, prompt, options = {}) {
308
+ const cwd = options.cwd;
309
+ this._log('debug', `startSession title="${title}"${cwd ? ` cwd="${cwd}"` : ''}`);
310
+
311
+ // 前置检查:如果指定了 cwd,确保目录存在
312
+ // opencode session.create 不会检查目录是否存在,但 promptAsync 时会
313
+ // realPath 检查,目录不存在时静默失败(返回 204,tokens=0+0)。
314
+ if (cwd) {
315
+ try {
316
+ const stats = fs.statSync(cwd);
317
+ if (!stats.isDirectory()) {
318
+ throw new Error(`cwd 不是目录: ${cwd}`);
319
+ }
320
+ } catch (statErr) {
321
+ if (statErr.code === 'ENOENT') {
322
+ this._log('info', `startSession: cwd 不存在,自动创建 ${cwd}`);
323
+ fs.mkdirSync(cwd, { recursive: true });
324
+ } else {
325
+ throw new Error(`startSession: cwd 检查失败: ${statErr.message}`);
326
+ }
327
+ }
328
+ }
329
+
330
+ try {
331
+ // opencode SDK session.create 支持 query.directory 参数,
332
+ // 用于指定该 Session 的工作目录(git clone、文件读写等操作均在此目录下执行)
333
+ const createParams = { body: { title } };
334
+ if (cwd) {
335
+ createParams.query = { directory: cwd };
336
+ }
337
+ const session = await this.client.session.create(createParams);
338
+ const sessionId = session.data.id;
339
+ this._log('info', `Session 已创建 id=${sessionId}${cwd ? ` directory="${cwd}"` : ''}`);
340
+
341
+ // 初始化存档元信息
342
+ await this.archive.patchMeta(sessionId, {
343
+ sessionId,
344
+ title,
345
+ cliType: this.cliType,
346
+ createdAt: nowCST(),
347
+ status: 'idle'
348
+ }).catch(e => {
349
+ this._log('warn', `startSession: 初始化存档 meta 失败 id=${sessionId}: ${e.message}`);
350
+ });
351
+
352
+ if (prompt) {
353
+ await this.client.session.promptAsync({
354
+ path: { id: sessionId },
355
+ body: {
356
+ parts: [{ type: 'text', text: prompt }],
357
+ noReply: false
358
+ },
359
+ url: `/session/${sessionId}/prompt_async`
360
+ });
361
+ // 初始 prompt 的用户消息不由这里归档,统一由 checkSessionStatus /
362
+ // getSessionMessages 从远端拉取归档,确保用 opencode id 去重
363
+ await this.archive.patchMeta(sessionId, { status: 'running' }).catch(() => {});
364
+ }
365
+
366
+ return {
367
+ sessionId,
368
+ action: 'create',
369
+ message: prompt ? '已创建新 Session 并开始执行' : '已创建新 Session'
370
+ };
371
+ } catch (error) {
372
+ this._log('error', `startSession 失败: ${error.message}`);
373
+ throw new Error(`Failed to start session: ${error.message}`);
374
+ }
375
+ }
376
+
377
+ /**
378
+ * 查询 Session 当前状态
379
+ * @param {string} sessionId
380
+ * @returns {{ status: 'completed'|'running'|'unknown', lastMessage?: object }}
381
+ */
382
+ async checkSessionStatus(sessionId) {
383
+ try {
384
+ // ── 快路径:先查 session.status(),busy/retry 时直接返回 running,不拉消息 ──
385
+ // session.status() 返回 {[id]: {type: "busy"|"idle"|"retry"}},极轻量
386
+ // 只有 idle 时才需要拉最后一条消息检查 finish/error 判断 completed/error
387
+ let statusMap;
388
+ try {
389
+ statusMap = await this.client.session.status({ query: {} });
390
+ } catch (e) {
391
+ this._log('warn', `checkSessionStatus: session.status() 失败,降级为拉消息: ${e.message}`);
392
+ }
393
+ const rawStatus = statusMap?.data?.[sessionId];
394
+ if (rawStatus && (rawStatus.type === 'busy' || rawStatus.type === 'retry')) {
395
+ // AI 正在生成,无需拉消息(快路径)
396
+ // 但若 busy 持续过久(超过停滞阈值),则降级拉最后一条消息兜底:
397
+ // opencode 的 session.status() 在任务实际完成后仍可能长时间保持 busy
398
+ // 仅依赖 status 会永远无法识别已完成的 finish:"stop" 消息,导致上层轮询超时。
399
+ const now = Date.now();
400
+ if (!this._busySince[sessionId]) {
401
+ this._busySince[sessionId] = now;
402
+ }
403
+ // 复用存档的卡死阈值(默认 5 分钟),作为 busy 停滞兜底的超时判定
404
+ const stallThresholdMs = this.archive?._store?.stallThresholdMs ?? 5 * 60 * 1000;
405
+ const busyDurationMs = now - this._busySince[sessionId];
406
+
407
+ if (busyDurationMs >= stallThresholdMs) {
408
+ // busy 停滞超阈值 检查消息层,识别是否实际已完成
409
+ this._log('debug',
410
+ `checkSessionStatus: session ${sessionId} busy 持续 ${Math.round(busyDurationMs / 1000)}s,降级检查消息层`);
411
+ const fallback = await this._checkMessageLayer(sessionId);
412
+ if (fallback) {
413
+ return fallback;
414
+ }
415
+ }
416
+
417
+ const checkRes = await this.archive.recordCheck(sessionId, {
418
+ lastMessageId: null,
419
+ status: 'running'
420
+ }).catch(() => ({ stalled: false, stallDurationMs: null, hasNewMessage: false }));
421
+ return {
422
+ status: checkRes.stalled ? 'stalled' : 'running',
423
+ stallDurationMs: checkRes.stallDurationMs,
424
+ stalled: !!checkRes.stalled,
425
+ hasNewMessage: false
426
+ };
427
+ }
428
+ // 非 busy/retry:清除停滞计时,避免脏状态
429
+ delete this._busySince[sessionId];
430
+
431
+ // ── idle 或 status 查询失败:拉最后 1 条消息判断 completed/error/running ──
432
+ const messages = await this.client.session.messages({
433
+ path: { id: sessionId },
434
+ query: { limit: 1 },
435
+ });
436
+ const data = messages.data;
437
+
438
+ if (!data || data.length === 0) {
439
+ return { status: 'unknown' };
440
+ }
441
+
442
+ const lastMessage = data[data.length - 1];
443
+
444
+ // 出错 / 被中断
445
+ if (lastMessage.info?.error) {
446
+ const errName = lastMessage.info.error.name ?? 'UnknownError';
447
+ this._log('warn', `checkSessionStatus: 检测到错误 ${errName},status=error`);
448
+ await this.archive.recordCheck(sessionId, {
449
+ lastMessageId: this._msgDedupeKey(lastMessage),
450
+ status: 'error'
451
+ }).catch(() => {});
452
+ // 错误结束时也自动归档全量消息
453
+ await this._autoArchive(sessionId, 'error');
454
+ return { status: 'error', error: lastMessage.info.error, lastMessage };
455
+ }
456
+
457
+ // 正常完成
458
+ const isCompleted = this._isCompletedMessage(lastMessage);
459
+
460
+ const status = isCompleted ? 'completed' : 'running';
461
+
462
+ // 状态检查 + 卡死检测
463
+ const dedupeKey = this._msgDedupeKey(lastMessage);
464
+ const checkRes = await this.archive.recordCheck(sessionId, {
465
+ lastMessageId: dedupeKey,
466
+ status
467
+ }).catch(() => ({ stalled: false, stallDurationMs: null, hasNewMessage: false }));
468
+
469
+ // 若检测到停滞(running 状态下长时间无新 assistant 消息)
470
+ const finalStatus = checkRes.stalled ? 'stalled' : status;
471
+ if (checkRes.stalled) {
472
+ this._log('debug',
473
+ `checkSessionStatus: 检测到停滞 id=${sessionId}, 已 ${Math.round((checkRes.stallDurationMs || 0) / 1000)}s 无新响应`);
474
+ }
475
+
476
+ // session 完成时自动归档全量消息(写入 .jsonl)
477
+ if (isCompleted) {
478
+ await this._autoArchive(sessionId, 'completed');
479
+ }
480
+
481
+ return {
482
+ status: finalStatus,
483
+ lastMessage,
484
+ // 卡死检测相关字段
485
+ stallDurationMs: checkRes.stallDurationMs,
486
+ stalled: !!checkRes.stalled,
487
+ hasNewMessage: !!checkRes.hasNewMessage
488
+ };
489
+ } catch (error) {
490
+ this._log('warn', `checkSessionStatus 异常 id=${sessionId}: ${error.message}`);
491
+ return { status: 'unknown' };
492
+ }
493
+ }
494
+
495
+ /**
496
+ * 判断 opencode 消息是否为正常完成(finish='stop' 且含 text part)。
497
+ *
498
+ * 供 checkSessionStatus 的 idle 分支与 busy 停滞兜底分支共用,避免判定逻辑漂移。
499
+ *
500
+ * @param {object} lastMessage - opencode 原始消息
501
+ * @returns {boolean}
502
+ * @private
503
+ */
504
+ _isCompletedMessage(lastMessage) {
505
+ return Boolean(
506
+ lastMessage &&
507
+ lastMessage.info?.finish === 'stop' &&
508
+ Array.isArray(lastMessage.parts) &&
509
+ lastMessage.parts.some(p => p.type === 'text')
510
+ );
511
+ }
512
+
513
+ /**
514
+ * 从消息层检查 session 是否实际已完成/出错。
515
+ *
516
+ * 用于 busy/retry 停滞兜底:拉取最后 1 条消息,若已 finish='stop' 或 info.error,
517
+ * 则返回对应的 completed/error 状态;否则返回 null(表示任务确实仍在运行)。
518
+ *
519
+ * 注意:本方法不更新 _busySince——由调用方在返回非 completed/error 时继续累积。
520
+ *
521
+ * @param {string} sessionId
522
+ * @returns {Promise<object|null>} 已终态时返回状态对象;仍在运行返回 null
523
+ * @private
524
+ */
525
+ async _checkMessageLayer(sessionId) {
526
+ try {
527
+ const messages = await this.client.session.messages({
528
+ path: { id: sessionId },
529
+ query: { limit: 1 },
530
+ });
531
+ const data = messages.data;
532
+ if (!data || data.length === 0) {
533
+ return null;
534
+ }
535
+
536
+ const lastMessage = data[data.length - 1];
537
+
538
+ // 出错 / 被中断
539
+ if (lastMessage.info?.error) {
540
+ const errName = lastMessage.info.error.name ?? 'UnknownError';
541
+ this._log('warn', `checkSessionStatus: busy 兜底检测到错误 ${errName},status=error`);
542
+ await this.archive.recordCheck(sessionId, {
543
+ lastMessageId: this._msgDedupeKey(lastMessage),
544
+ status: 'error'
545
+ }).catch(() => {});
546
+ await this._autoArchive(sessionId, 'error');
547
+ delete this._busySince[sessionId];
548
+ return { status: 'error', error: lastMessage.info.error, lastMessage };
549
+ }
550
+
551
+ // 正常完成(finish='stop' 且含 text)
552
+ if (this._isCompletedMessage(lastMessage)) {
553
+ this._log('info', `checkSessionStatus: busy 兜底确认 session ${sessionId} 已完成`);
554
+ const dedupeKey = this._msgDedupeKey(lastMessage);
555
+ await this.archive.recordCheck(sessionId, {
556
+ lastMessageId: dedupeKey,
557
+ status: 'completed'
558
+ }).catch(() => {});
559
+ await this._autoArchive(sessionId, 'completed');
560
+ delete this._busySince[sessionId];
561
+ return {
562
+ status: 'completed',
563
+ lastMessage,
564
+ stallDurationMs: null,
565
+ stalled: false,
566
+ hasNewMessage: true
567
+ };
568
+ }
569
+
570
+ // 仍在运行:刷新 busySince(避免一次长消息生成被误判),返回 null 表示继续等待
571
+ this._busySince[sessionId] = Date.now();
572
+ return null;
573
+ } catch (err) {
574
+ this._log('warn', `checkSessionStatus: busy 兜底检查消息失败 id=${sessionId}: ${err.message}`);
575
+ return null;
576
+ }
577
+ }
578
+
579
+ /**
580
+ * 生成 opencode 消息的去重键
581
+ * 优先用消息 id(全局唯一);无 id 时用 role+finish+时间+内容文本长度
582
+ * 注意:不使用 parts 长度,因为 checkSessionStatus(limit=1) 与 getSessionMessages(全量)
583
+ * 返回的 parts 结构可能不同,导致同一条消息算出不同去重键而重复归档。
584
+ */
585
+ _msgDedupeKey(msg) {
586
+ const id = msg.id ?? msg.info?.id;
587
+ if (id) return String(id);
588
+ const role = msg.info?.role ?? 'unknown';
589
+ const finish = msg.info?.finish ?? '';
590
+ const ts = msg.info?.time?.created ?? msg.timestamp ?? '';
591
+ const textParts = (msg.parts || []).filter(p => p.type === 'text');
592
+ const content = textParts.map(p => p.text).join('\n');
593
+ return `${role}:${finish}:${ts}:${content.length}`;
594
+ }
595
+
596
+ /**
597
+ * 将 opencode 原始消息转换为精简归档格式
598
+ *
599
+ * 只保留核心对话字段(role / content / finish / timestamp / id),
600
+ * 去掉 raw 原始结构(含 parts / tokens / cost / reasoning 等),
601
+ * 使归档文件轻量可读,关注实际发生了哪些对话。
602
+ */
603
+ _toArchiveMessage(msg) {
604
+ const role = msg.info?.role ?? 'unknown';
605
+ const textParts = (msg.parts || []).filter(p => p.type === 'text');
606
+ const content = textParts.map(p => p.text).join('\n');
607
+ // opencode 时间在 info.time.created(epoch 毫秒),非 ISO 字符串
608
+ const createdMs = msg.info?.time?.created;
609
+ const timestamp = createdMs != null
610
+ ? nowCST(createdMs)
611
+ : (msg.timestamp ?? nowCST());
612
+ return {
613
+ id: msg.id ?? msg.info?.id,
614
+ role,
615
+ content,
616
+ finish: msg.info?.finish ?? null,
617
+ timestamp
618
+ };
619
+ }
620
+
621
+ /**
622
+ * 自动归档 — session 完成或出错时全量拉取消息并写入 .jsonl
623
+ *
624
+ * checkSessionStatus 平时只用 limit=1 查状态(不归档),
625
+ * 但当检测到 session 已结束(completed / error)时,
626
+ * 应自动全量拉取消息归档,确保 .jsonl 文件生成。
627
+ *
628
+ * 归档采用 appendMessages 增量去重,重复调用安全无副作用。
629
+ *
630
+ * @param {string} sessionId
631
+ * @param {'completed'|'error'} reason - 触发归档的原因
632
+ * @private
633
+ */
634
+ async _autoArchive(sessionId, reason) {
635
+ try {
636
+ const messages = await this.client.session.messages({ path: { id: sessionId } });
637
+ const data = messages.data || [];
638
+ const archRes = await this.archive.appendMessages(
639
+ sessionId,
640
+ data.map(m => this._toArchiveMessage(m))
641
+ );
642
+ this._log('info',
643
+ `自动归档 id=${sessionId} reason=${reason} — appended=${archRes.appended} skipped=${archRes.skipped}`);
644
+ } catch (e) {
645
+ this._log('warn', `自动归档失败 id=${sessionId} reason=${reason}: ${e.message}`);
646
+ }
647
+ }
648
+
649
+ /**
650
+ * 获取 Session 消息历史
651
+ * @param {string} sessionId
652
+ */
653
+ async getSessionMessages(sessionId) {
654
+ this._log('info', `getSessionMessages id=${sessionId}`);
655
+ try {
656
+ const messages = await this.client.session.messages({ path: { id: sessionId } });
657
+ const data = messages.data || [];
658
+ // 增量归档全量消息(首次拉取或补档),await 确保调用方能立即读取归档结果
659
+ const archRes = await this.archive.appendMessages(
660
+ sessionId,
661
+ data.map(m => this._toArchiveMessage(m))
662
+ ).catch(e => {
663
+ this._log('warn', `getSessionMessages: 归档失败 id=${sessionId}: ${e.message}`);
664
+ return { appended: 0, skipped: 0 };
665
+ });
666
+ this._log('debug', `getSessionMessages: 归档 appended=${archRes.appended} skipped=${archRes.skipped}`);
667
+ return data;
668
+ } catch (error) {
669
+ this._log('error', `getSessionMessages 失败 id=${sessionId}: ${error.message}`);
670
+ throw new Error(`Failed to get session messages: ${error.message}`);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * 实时查看当前对话消息(运行中安全调用)
676
+ *
677
+ * 从远端拉取当前已有消息并转为精简格式返回,**不写入本地存档**。
678
+ * 适用于对话进行中查看 "目前聊了什么",不影响后续归档去重。
679
+ *
680
+ * @param {string} sessionId
681
+ * @returns {Promise<Array<{id,role,content,finish,timestamp}>>}
682
+ */
683
+ async getLiveMessages(sessionId) {
684
+ this._log('info', `getLiveMessages id=${sessionId}`);
685
+ try {
686
+ const messages = await this.client.session.messages({ path: { id: sessionId } });
687
+ const data = messages.data || [];
688
+ return data.map(m => this._toArchiveMessage(m));
689
+ } catch (error) {
690
+ this._log('error', `getLiveMessages 失败 id=${sessionId}: ${error.message}`);
691
+ throw new Error(`Failed to get live messages: ${error.message}`);
692
+ }
693
+ }
694
+
695
+ /**
696
+ * 导出完整对话(合并远端 + 本地,支持运行中 / 已结束)
697
+ *
698
+ * 流程:
699
+ * 1. 从远端拉取全量消息(运行中也能拿到已生成的部分)
700
+ * 2. 增量归档到本地 jsonl(去重,补全运行中缺失的消息)
701
+ * 3. 合并本地已归档消息,按 seq 排序
702
+ * 4. format 返回结构化结果
703
+ *
704
+ * @param {string} sessionId
705
+ * @param {object} [opts]
706
+ * @param {'json'|'markdown'} [opts.format='json'] - 导出格式
707
+ * @returns {Promise<object|string>}
708
+ * format='json' { sessionId, meta, messages: [...] }
709
+ * format='markdown' → Markdown 字符串
710
+ */
711
+ async exportSession(sessionId, opts = {}) {
712
+ const format = opts.format || 'json';
713
+ this._log('info', `exportSession id=${sessionId} format=${format}`);
714
+ try {
715
+ // 1. 远端拉取全量消息
716
+ const messages = await this.client.session.messages({ path: { id: sessionId } });
717
+ const data = messages.data || [];
718
+
719
+ // 2. 增量归档(去重补档,运行中也能持久化已有消息)
720
+ const archRes = await this.archive.appendMessages(
721
+ sessionId,
722
+ data.map(m => this._toArchiveMessage(m))
723
+ ).catch(e => {
724
+ this._log('warn', `exportSession: 归档失败 id=${sessionId}: ${e.message}`);
725
+ return { appended: 0, skipped: 0 };
726
+ });
727
+ this._log('debug', `exportSession: 归档 appended=${archRes.appended} skipped=${archRes.skipped}`);
728
+
729
+ // 3. 读本地归档(合并后按 seq 排序,确保完整且有序)
730
+ const archivedMessages = await this.archive.readMessages(sessionId);
731
+ const meta = await this.archive.readMeta(sessionId);
732
+ const messages_sorted = archivedMessages.sort((a, b) => (a.seq || 0) - (b.seq || 0));
733
+
734
+ // 4. 按格式输出
735
+ if (format === 'markdown') {
736
+ return this._toMarkdown(meta, messages_sorted);
737
+ }
738
+ return {
739
+ sessionId,
740
+ meta,
741
+ messages: messages_sorted
742
+ };
743
+ } catch (error) {
744
+ this._log('error', `exportSession 失败 id=${sessionId}: ${error.message}`);
745
+ throw new Error(`Failed to export session: ${error.message}`);
746
+ }
747
+ }
748
+
749
+ /**
750
+ * 将对话消息渲染为 Markdown 格式
751
+ * @param {object} meta - Session 元信息
752
+ * @param {Array} messages - 已归档消息列表
753
+ * @returns {string}
754
+ */
755
+ _toMarkdown(meta, messages) {
756
+ const lines = [];
757
+ lines.push(`# ${meta.title || meta.sessionId}`);
758
+ lines.push('');
759
+ lines.push(`> **Session ID**: ${meta.sessionId} `);
760
+ lines.push(`> **CLI 类型**: ${meta.cliType || 'N/A'} `);
761
+ lines.push(`> **创建时间**: ${meta.createdAt || 'N/A'} `);
762
+ lines.push(`> **状态**: ${meta.status || 'N/A'} `);
763
+ lines.push(`> **消息数**: ${messages.length} `);
764
+ lines.push('');
765
+ lines.push('---');
766
+ lines.push('');
767
+ for (const msg of messages) {
768
+ const roleLabel = msg.role === 'user' ? '🧑 User' : (msg.role === 'assistant' ? '🤖 Assistant' : msg.role);
769
+ const ts = msg.timestamp || '';
770
+ lines.push(`### ${roleLabel}`);
771
+ lines.push(`*${ts}*`);
772
+ lines.push('');
773
+ lines.push(msg.content || '(empty)');
774
+ lines.push('');
775
+ }
776
+ return lines.join('\n');
777
+ }
778
+
779
+ /**
780
+ * 读取本地已归档的消息(不请求远端,用于事后审计/对比)
781
+ * @param {string} sessionId
782
+ * @returns {Promise<Array<object>>}
783
+ */
784
+ async getArchivedMessages(sessionId) {
785
+ return this.archive.readMessages(sessionId);
786
+ }
787
+
788
+ /**
789
+ * 读取本地存档元信息(含 lastResponseAt / archivedCount 等)
790
+ * @param {string} sessionId
791
+ * @returns {Promise<object>}
792
+ */
793
+ async getArchiveMeta(sessionId) {
794
+ return this.archive.readMeta(sessionId);
795
+ }
796
+
797
+ // ─── 内部方法 ──────────────────────────────────────────────────────────────
798
+
799
+ _log(level, msg) {
800
+ const logger = createLogger(this._loggerName);
801
+ switch (level) {
802
+ case 'error':
803
+ logger.error(`❌ ${msg}`);
804
+ break;
805
+ case 'warn':
806
+ logger.warn(`⚠️ ${msg}`);
807
+ break;
808
+ case 'debug':
809
+ logger.debug(`🔍 ${msg}`);
810
+ break;
811
+ default:
812
+ logger.info(`ℹ️ ${msg}`);
813
+ }
814
+ }
815
+ }
816
+
817
+ export default OpenCodeCLI;