@ohos-cpf/3rdloop 0.0.1 → 0.0.3
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.
- package/README.md +245 -425
- package/bin/3rdloop.mjs +46 -46
- package/lib/cli.js +781 -740
- package/lib/config-cmd.js +452 -0
- package/lib/config.js +318 -317
- package/lib/doctor.js +113 -113
- package/lib/exit-codes.js +30 -30
- package/lib/opencode.js +155 -155
- package/lib/orch.js +426 -426
- package/lib/queries.js +263 -263
- package/lib/step.js +325 -325
- package/lib/ui.js +222 -222
- package/lib/update.js +280 -0
- package/lib/workflow.js +308 -290
- package/lib/workflows.js +131 -131
- package/package.json +50 -50
- package/vendor/Server/Agent/SkillSelector/README.md +343 -343
- package/vendor/Server/Agent/SkillSelector/core/Agent.js +207 -207
- package/vendor/Server/Agent/SkillSelector/index.js +14 -14
- package/vendor/Server/Agent/SkillSelector/llm/llmClient.js +313 -313
- package/vendor/Server/Agent/SkillSelector/llm/prompts.js +161 -161
- package/vendor/Server/Agent/SkillSelector/llm/reactSteps.js +144 -144
- package/vendor/Server/Agent/SkillSelector/matcher/fuzzyMatcher.js +433 -433
- package/vendor/Server/Agent/SkillSelector/scanner/skillScanner.js +382 -382
- package/vendor/Server/Agent/SkillSelector/utils/actionWords.js +59 -59
- package/vendor/Server/Brain/AnalysisStrategies.js +422 -422
- package/vendor/Server/Brain/Brain.js +686 -686
- package/vendor/Server/Brain/DecompositionStrategies.js +621 -621
- package/vendor/Server/Brain/PlanPersistence.js +196 -196
- package/vendor/Server/Brain/SkillExecutor.js +251 -251
- package/vendor/Server/Brain/index.js +11 -11
- package/vendor/Server/CLI/cli.js +275 -243
- package/vendor/Server/CLI/deveco-code/index.js +71 -0
- package/vendor/Server/CLI/deveco-code.js +5 -0
- package/vendor/Server/CLI/opencode/index.js +817 -804
- package/vendor/Server/CLI/opencode.js +5 -5
- package/vendor/Server/DbUse/MessageArchive.js +61 -61
- package/vendor/Server/DbUse/StorageManager.js +107 -107
- package/vendor/Server/DbUse/adapters/BaseAdapter.js +94 -94
- package/vendor/Server/DbUse/adapters/FileAdapter.js +138 -138
- package/vendor/Server/DbUse/adapters/JsonAdapter.js +138 -138
- package/vendor/Server/DbUse/adapters/JsonlAdapter.js +114 -114
- package/vendor/Server/DbUse/adapters/SqliteAdapter.js +157 -157
- package/vendor/Server/DbUse/index.js +160 -160
- package/vendor/Server/DbUse/stores/SessionStore.js +279 -279
- package/vendor/Server/DbUse/stores/TaskExecutionStore.js +133 -133
- package/vendor/Server/DbUse/stores/TaskRegistryStore.js +262 -262
- package/vendor/Server/DbUse/stores/TaskStepStore.js +279 -279
- package/vendor/Server/FlexRunner/FlexRunner.js +1759 -1759
- package/vendor/Server/FlexRunner/index.js +12 -12
- package/vendor/Server/Knowledge/KnowledgeImporter.js +523 -523
- package/vendor/Server/Knowledge/KnowledgeManager.js +341 -341
- package/vendor/Server/Knowledge/index.js +33 -33
- package/vendor/Server/LoopEngine/LoopEngine.js +1133 -1133
- package/vendor/Server/LoopEngine/index.js +11 -11
- package/vendor/Server/Orchestrator/Orchestrator.js +0 -3
- package/vendor/Server/Orchestrator/StepNode.js +312 -312
- package/vendor/Server/Orchestrator/index.js +13 -13
- package/vendor/Server/Routes/controllers/BrainController.js +241 -241
- package/vendor/Server/Routes/controllers/FlexRunnerController.js +1125 -1125
- package/vendor/Server/Routes/controllers/HealthController.js +44 -44
- package/vendor/Server/Routes/controllers/KnowledgeImportController.js +355 -355
- package/vendor/Server/Routes/controllers/LoopEngineController.js +788 -788
- package/vendor/Server/Routes/controllers/StatsController.js +57 -57
- package/vendor/Server/Routes/controllers/StreamController.js +67 -67
- package/vendor/Server/Routes/library/logger.js +179 -179
- package/vendor/Server/Routes/library/zipArchive.js +172 -172
- package/vendor/Server/Routes/router/Router.js +204 -204
- package/vendor/Server/Routes/routes/brain.js +47 -47
- package/vendor/Server/Routes/routes/flexrunner.js +72 -72
- package/vendor/Server/Routes/routes/health.js +28 -28
- package/vendor/Server/Routes/routes/index.js +57 -57
- package/vendor/Server/Routes/routes/knowledge.js +55 -55
- package/vendor/Server/Routes/routes/loopengine.js +58 -58
- package/vendor/Server/Routes/routes/orchestrator.js +61 -61
- package/vendor/Server/Routes/routes/registry.js +97 -97
- package/vendor/Server/Routes/routes/stats.js +23 -23
- package/vendor/Server/Routes/routes/stream.js +30 -30
- package/vendor/Server/Routes/server.js +203 -202
- package/vendor/Server/Routes/sse/SseHub.js +308 -308
- package/vendor/Server/Skills/acceptance-check/SKILL.md +191 -174
- package/vendor/Server/Skills/acceptance-check/references/PHASE1_ARTIFACT_COLLECTION.md +52 -52
- package/vendor/Server/Skills/acceptance-check/references/PHASE2_CRITERIA_EVALUATION.md +77 -77
- package/vendor/Server/Skills/acceptance-check/references/PHASE2_EVIDENCE_VERIFICATION.md +70 -0
- package/vendor/Server/Skills/arkts-code-check/SKILL.md +320 -313
- package/vendor/Server/Skills/arkts-code-check/references/CODE_CHECK_GUIDE.md +210 -159
- package/vendor/Server/Skills/arkts-code-check/references/FILE_TYPE_GUIDE.md +28 -28
- package/vendor/Server/Skills/arkts-code-check/references/PHASE1_INFO_COLLECTION.md +233 -222
- package/vendor/Server/Skills/arkts-code-check/references/REPORT_TEMPLATE.md +219 -219
- package/vendor/Server/Skills/arkts-library-compile-validate/SKILL.md +328 -328
- package/vendor/Server/Skills/arkts-library-compile-validate/scripts/build.ps1 +93 -93
- package/vendor/Server/Skills/arkts-library-compile-validate/scripts/code-linter.cjs +432 -432
- package/vendor/Server/Skills/arkts-library-compile-validate/scripts/migrate.cjs +851 -851
- package/vendor/Server/Skills/arkts-library-compile-validate/scripts/ohpm-switch.cjs +788 -788
- package/vendor/Server/Skills/arkts-library-demo-code/SKILL.md +291 -291
- package/vendor/Server/Skills/arkts-library-demo-code/references/API_SIGNATURE_CHECKS.md +68 -68
- package/vendor/Server/Skills/arkts-library-demo-code/references/BUILD_VALIDATION.md +184 -184
- package/vendor/Server/Skills/arkts-library-demo-code/references/CODE_STANDARDS.md +337 -337
- package/vendor/Server/Skills/arkts-library-demo-code/references/EXAMPLES.md +400 -400
- package/vendor/Server/Skills/arkts-library-demo-code/references/SYSTEM_API_LOOKUP.md +98 -98
- package/vendor/Server/Skills/arkts-library-demo-code/references/TEMPLATE.md +544 -544
- package/vendor/Server/Skills/arkts-library-demo-code/scripts/build.ps1 +93 -93
- package/vendor/Server/Skills/arkts-library-demo-code/scripts/code-linter.cjs +432 -432
- package/vendor/Server/Skills/arkts-library-demo-code/scripts/migrate.cjs +851 -851
- package/vendor/Server/Skills/arkts-library-demo-code/scripts/ohpm-switch.cjs +788 -788
- package/vendor/Server/Skills/arkts-library-demo-coverage/SKILL.md +174 -174
- package/vendor/Server/Skills/arkts-library-demo-scenario/SKILL.md +321 -321
- package/vendor/Server/Skills/arkts-library-demo-scenario/references/EXAMPLES.md +100 -100
- package/vendor/Server/Skills/arkts-library-demo-scenario/references/TEMPLATE.md +185 -185
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/SKILL.md +233 -233
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +102 -102
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/CHECK_DIMENSIONS.md +89 -89
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/COMPLIANCE_CHECKLIST.md +62 -62
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/KB_QUERY_GUIDE.md +107 -107
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/MCP_TOOL_DISCOVERY.md +124 -124
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/REPORT_TEMPLATE.md +90 -90
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/SCORING_FORMULA.md +57 -57
- package/vendor/Server/Skills/arkts-library-documentation-quality-check/scripts/validate-skill.cjs +259 -259
- package/vendor/Server/Skills/arkts-library-interface-spec/SKILL.md +97 -97
- package/vendor/Server/Skills/arkts-library-memory-leak-check/SKILL.md +391 -391
- package/vendor/Server/Skills/arkts-library-source-clone/SKILL.md +113 -113
- package/vendor/Server/Skills/arkts-library-stability-check/SKILL.md +312 -312
- package/vendor/Server/Skills/arkts-library-stability-check/assets/REPORT_TEMPLATE.md +154 -154
- package/vendor/Server/Skills/arkts-library-stability-check/references/CHECKLIST.md +165 -165
- package/vendor/Server/Skills/arkts-library-stability-check/references/CODE_EXAMPLES.md +308 -308
- package/vendor/Server/Skills/arkts-library-stability-check/references/SCORING_RULES.md +90 -90
- package/vendor/Server/Skills/arkts-library-stability-check/scripts/validate-skill.cjs +282 -282
- package/vendor/Server/Skills/arkts-library-test-coverage-check/SKILL.md +360 -360
- package/vendor/Server/Skills/arkts-library-test-coverage-check/assets/REPORT_TEMPLATE.md +145 -145
- package/vendor/Server/Skills/arkts-library-test-coverage-check/references/CHECKLIST.md +252 -252
- package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +114 -114
- package/vendor/Server/Skills/arkts-library-test-coverage-check/scripts/validate-skill.cjs +262 -262
- package/vendor/Server/Skills/arkts-library-xts-code/SKILL.md +455 -455
- package/vendor/Server/Skills/arkts-library-xts-code/assets/report-template.md +60 -60
- package/vendor/Server/Skills/arkts-library-xts-code/references/ACCESS_MODIFIER_PRECHECK.md +76 -76
- package/vendor/Server/Skills/arkts-library-xts-code/references/BUILD_VERIFICATION.md +200 -200
- package/vendor/Server/Skills/arkts-library-xts-code/references/CODE_STANDARDS.md +518 -518
- package/vendor/Server/Skills/arkts-library-xts-code/references/SUBAGENT_PROMPT_TEMPLATE.md +276 -276
- package/vendor/Server/Skills/arkts-library-xts-code/references/SYSTEM_API_LOOKUP.md +92 -92
- package/vendor/Server/Skills/arkts-library-xts-code/references/XTS_TEMPLATE.md +767 -767
- package/vendor/Server/Skills/arkts-library-xts-code/scripts/build.ps1 +93 -93
- package/vendor/Server/Skills/arkts-library-xts-code/scripts/code-linter.cjs +432 -432
- package/vendor/Server/Skills/arkts-library-xts-code/scripts/migrate.cjs +851 -851
- package/vendor/Server/Skills/arkts-library-xts-code/scripts/ohpm-switch.cjs +788 -788
- package/vendor/Server/Skills/arkts-library-xts-code/scripts/precheck-access-modifiers.cjs +221 -221
- package/vendor/Server/Skills/arkts-library-xts-code-fix/SKILL.md +228 -228
- package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +97 -97
- package/vendor/Server/Skills/arkts-library-xts-doc/SKILL.md +194 -194
- package/vendor/Server/Skills/arkts-library-xts-doc/references/EXAMPLES.md +575 -575
- package/vendor/Server/Skills/arkts-library-xts-doc/references/TEMPLATE.md +574 -574
- package/vendor/Server/Skills/arkts-library-xts-doc/references/TESTABILITY_GUIDE.md +182 -182
- package/vendor/Server/Skills/arkts-library-xts-execution/SKILL.md +490 -490
- package/vendor/Server/Skills/arkts-library-xts-execution/references/error-fixing-examples.md +217 -217
- package/vendor/Server/Skills/arkts-library-xts-execution/scripts/build.ps1 +93 -93
- package/vendor/Server/Skills/arkts-library-xts-execution/scripts/code-linter.cjs +432 -432
- package/vendor/Server/Skills/arkts-library-xts-execution/scripts/migrate.cjs +851 -851
- package/vendor/Server/Skills/arkts-library-xts-execution/scripts/ohpm-switch.cjs +788 -788
- package/vendor/Server/Skills/flutter-code-check/SKILL.md +299 -299
- package/vendor/Server/Skills/flutter-code-check/assets/OUTPUT_TEMPLATE.md +311 -311
- package/vendor/Server/Skills/flutter-code-check/references/CHECKLIST.md +190 -190
- package/vendor/Server/Skills/flutter-code-check/references/PLATFORMS.md +133 -133
- package/vendor/Server/Skills/flutter-code-check/scripts/validate-skill.cjs +259 -259
- package/vendor/Server/Skills/flutter-interface/README.md +75 -75
- package/vendor/Server/Skills/flutter-interface/SKILL.md +291 -291
- package/vendor/Server/Skills/flutter-interface/references/EXAMPLES.md +336 -336
- package/vendor/Server/Skills/flutter-interface/references/GIT_CLONE_GUIDE.md +190 -190
- package/vendor/Server/Skills/flutter-interface/references/SCRIPT_REFERENCE.md +258 -258
- package/vendor/Server/Skills/flutter-interface/references/TEMPLATE.md +254 -254
- package/vendor/Server/Skills/flutter-library-consistency-check/SKILL.md +517 -517
- package/vendor/Server/Skills/flutter-library-consistency-check/references/CHECKLIST.md +381 -381
- package/vendor/Server/Skills/flutter-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +341 -341
- package/vendor/Server/Skills/flutter-library-consistency-check/references/PLATFORM_LIMITATION_GUIDE.md +71 -71
- package/vendor/Server/Skills/flutter-library-documentation-quality-check/SKILL.md +518 -518
- package/vendor/Server/Skills/flutter-library-documentation-quality-check/references/CHECKLIST.md +406 -406
- package/vendor/Server/Skills/flutter-library-memory-leak-check/SKILL.md +652 -652
- package/vendor/Server/Skills/flutter-library-memory-leak-check/references/CHECKLIST.md +396 -396
- package/vendor/Server/Skills/flutter-library-stability-check/SKILL.md +744 -744
- package/vendor/Server/Skills/flutter-library-stability-check/references/CHECKLIST.md +720 -720
- package/vendor/Server/Skills/flutter-library-test-coverage-check/SKILL.md +414 -414
- package/vendor/Server/Skills/flutter-library-test-coverage-check/assets/REPORT_TEMPLATE.md +150 -150
- package/vendor/Server/Skills/flutter-library-test-coverage-check/references/CHECKLIST.md +304 -304
- package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCORING_RULES.md +265 -265
- package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCRIPT_USAGE.md +286 -286
- package/vendor/Server/Skills/knowledge-import/SKILL.md +383 -383
- package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +165 -165
- package/vendor/Server/Skills/knowledge-import/references/SKILL_LOCATOR_GUIDE.md +91 -91
- package/vendor/Server/Skills/knowledge-import/references/SKILL_OPTIMIZATION_GUIDE.md +127 -127
- package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +453 -301
- package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_CHECKLIST.md +151 -0
- package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_ISSUE_TEMPLATE.md +257 -105
- package/vendor/Server/Skills/ohos-lib-pr-push/references/GITCODE_API.md +191 -127
- package/vendor/Server/Skills/ohos-lib-pr-push/scripts/gitcode-api.cjs +317 -208
- package/vendor/Server/Skills/rn-code-check/SKILL.md +320 -320
- package/vendor/Server/Skills/rn-code-check/references/API-GUIDE.md +234 -234
- package/vendor/Server/Skills/rn-code-check/references/CHECKLIST.md +188 -188
- package/vendor/Server/Skills/rn-code-check/references/REPORT-TEMPLATE.md +246 -246
- package/vendor/Server/Skills/rn-lib-add-example/SKILL.md +198 -198
- package/vendor/Server/Skills/rn-lib-add-example/references/APP_RENAME_GUIDE.md +40 -40
- package/vendor/Server/Skills/rn-lib-add-example/references/SHELL_ADAPTATION_CHECKLIST.md +78 -78
- package/vendor/Server/Skills/rn-lib-add-example/references/TEMPLATE_REPO_GUIDE.md +70 -70
- package/vendor/Server/Skills/rn-lib-add-example/references/VERIFICATION_GUIDE.md +47 -47
- package/vendor/Server/Skills/rn-lib-add-example/scripts/create-example.cjs +252 -252
- package/vendor/Server/Skills/rn-lib-update-doc/SKILL.md +176 -176
- package/vendor/Server/Skills/rn-lib-update-doc/config.json +5 -5
- package/vendor/Server/Skills/rn-lib-update-doc/references/changelog-template.md +35 -35
- package/vendor/Server/Skills/rn-lib-update-doc/references/format-diff.md +137 -137
- package/vendor/Server/Skills/rn-lib-update-doc/references/link-registry.json +33 -33
- package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2-overview.md +40 -40
- package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2.md +429 -429
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/detect-readme-format.cjs +105 -105
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/find-usage-doc.cjs +90 -90
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/link-verify.cjs +213 -213
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-changelog.cjs +73 -73
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-package-meta.cjs +258 -258
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-readme-links.cjs +341 -341
- package/vendor/Server/Skills/rn-lib-update-doc/scripts/resolve-usage-docs.cjs +67 -67
- package/vendor/Server/Skills/rn-library-consistency-check/SKILL.md +299 -299
- package/vendor/Server/Skills/rn-library-consistency-check/assets/REPORT_TEMPLATE.md +293 -293
- package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_COMPONENT_CHECK_GUIDE.md +857 -857
- package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_TURBOMODULE_CHECK_GUIDE.md +856 -856
- package/vendor/Server/Skills/rn-library-consistency-check/references/CHECKLIST.md +157 -157
- package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_COMPONENT_CHECK_GUIDE.md +801 -801
- package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_TURBOMODULE_CHECK_GUIDE.md +683 -683
- package/vendor/Server/Skills/rn-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +233 -233
- package/vendor/Server/Skills/rn-library-consistency-check/references/NATIVE_COMPONENT_IDENTIFICATION.md +1373 -1373
- package/vendor/Server/Skills/rn-library-consistency-check/references/SCORING_STANDARD.md +220 -220
- package/vendor/Server/Skills/rn-library-consistency-check/scripts/validate-skill.py +245 -245
- package/vendor/Server/Skills/rn-library-documentation-quality-check/SKILL.md +263 -263
- package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL.md +414 -414
- package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL_en.md +413 -413
- package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +311 -311
- package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CHECK_CHECKLIST.md +355 -355
- package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CROSS_VALIDATION_GUIDE.md +115 -115
- package/vendor/Server/Skills/rn-library-documentation-quality-check/references/DOC_COLLECTION_GUIDE.md +139 -139
- package/vendor/Server/Skills/rn-library-documentation-quality-check/references/EXAMPLES.md +586 -586
- package/vendor/Server/Skills/rn-library-documentation-quality-check/references/GOTCHAS.md +57 -57
- package/vendor/Server/Skills/rn-library-documentation-quality-check/scripts/validate-skill.py +259 -259
- package/vendor/Server/Skills/rn-library-interface-analyzer/SKILL.md +301 -301
- package/vendor/Server/Skills/rn-library-interface-analyzer/assets/REPORT_TEMPLATE.md +148 -148
- package/vendor/Server/Skills/rn-library-interface-analyzer/references/DOC_ANNOTATION_GUIDE.md +118 -118
- package/vendor/Server/Skills/rn-library-interface-analyzer/references/SCRIPT_USAGE_GUIDE.md +168 -168
- package/vendor/Server/Skills/rn-library-interface-analyzer/references/URL_PARSING_RULES.md +176 -176
- package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +274 -274
- package/vendor/Server/Skills/rn-library-issue-generalizer/assets/case-template.md +45 -45
- package/vendor/Server/Skills/rn-library-issue-generalizer/assets/rule-template.md +25 -25
- package/vendor/Server/Skills/rn-library-issue-generalizer/references/PR_FETCH_GUIDE.md +61 -61
- package/vendor/Server/Skills/rn-library-issue-generalizer/references/RULE_TEMPLATE_GUIDE.md +165 -165
- package/vendor/Server/Skills/rn-library-issue-generalizer/scripts/fetch-pr-info.py +239 -239
- package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +263 -263
- package/vendor/Server/Skills/rn-library-known-issue-check/assets/REPORT_TEMPLATE.md +96 -96
- package/vendor/Server/Skills/rn-library-known-issue-check/references/COMPLIANCE_CHECKLIST.md +57 -57
- package/vendor/Server/Skills/rn-library-known-issue-check/references/INSPECTION_GUIDE.md +99 -99
- package/vendor/Server/Skills/rn-library-known-issue-check/references/PRESCREEN_GUIDE.md +64 -64
- package/vendor/Server/Skills/rn-library-known-issue-check/references/SOURCE_PREP_GUIDE.md +74 -74
- package/vendor/Server/Skills/rn-library-known-issue-check/scripts/validate-known-issue-report.py +164 -164
- package/vendor/Server/Skills/rn-library-memory-leak-check/SKILL.md +356 -339
- package/vendor/Server/Skills/rn-library-memory-leak-check/assets/REPORT_TEMPLATE.md +85 -83
- package/vendor/Server/Skills/rn-library-memory-leak-check/references/CHECKLIST.md +905 -859
- package/vendor/Server/Skills/rn-library-memory-leak-check/references/HARMONY_CODE_PATTERNS.md +124 -122
- package/vendor/Server/Skills/rn-library-memory-leak-check/scripts/scan_harmony_code.py +287 -279
- package/vendor/Server/Skills/rn-library-stability-check/SKILL.md +273 -273
- package/vendor/Server/Skills/rn-library-stability-check/assets/REPORT_TEMPLATE.md +129 -129
- package/vendor/Server/Skills/rn-library-stability-check/references/CHECKLIST.md +581 -581
- package/vendor/Server/Skills/rn-library-stability-check/references/SCORING_RULES.md +138 -138
- package/vendor/Server/Skills/rn-library-stability-check/scripts/scan-harmony-code.py +168 -168
- package/vendor/Server/Skills/rn-library-test-coverage-check/SKILL.md +526 -506
- package/vendor/Server/Skills/rn-library-test-coverage-check/references/CHECKLIST.md +321 -280
- package/vendor/Server/Skills/rn-library-test-coverage-check/references/DEMO_LOCATION.md +221 -221
- package/vendor/Server/Skills/rn-library-test-coverage-check/references/REPORT_TEMPLATE.md +220 -209
- package/vendor/Server/Skills/rn-library-test-coverage-check/references/SCRIPT_USAGE.md +325 -325
- package/vendor/Server/Skills/rn-ohos-xts-generator/SKILL.md +279 -279
- package/vendor/Server/Skills/rn-ohos-xts-generator/assets/TEST_FILE_TEMPLATES.md +404 -404
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_TROUBLESHOOTING.md +185 -185
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_WORKFLOW.md +265 -265
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/COMMON_CONFIG.md +189 -189
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/CONFIG_GUIDE.md +203 -203
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/COVERAGE_ANALYZER.md +147 -147
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/DESIGN_DOC_GUIDE.md +148 -148
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_FLOW.md +165 -165
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_STRATEGY.md +195 -195
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/HARMONY_SCANNER.md +249 -249
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/JEST_FRAMEWORK.md +235 -235
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/PLATFORM_COMPAT.md +161 -161
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/RN_TEST_PATTERNS.md +413 -413
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/SOURCE_PARSER.md +235 -235
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/TEST_CONVENTIONS.md +245 -245
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/TROUBLESHOOTING.md +282 -282
- package/vendor/Server/Skills/rn-ohos-xts-generator/references/USAGE_GUIDE.md +251 -251
- package/vendor/Server/Skills/rnoh-cptf-api-test/SKILL.md +469 -469
- package/vendor/Server/Skills/rnoh-cptf-api-test/assets/CPTF_TEST_TEMPLATE.md +310 -310
- package/vendor/Server/Skills/rnoh-cptf-api-test/assets/DEMO_PAGE_TEMPLATE.md +303 -303
- package/vendor/Server/Skills/rnoh-cptf-api-test/references/CPTF_TEST_GENERATION_GUIDE.md +421 -421
- package/vendor/Server/Skills/rnoh-cptf-api-test/references/DEMO_GENERATION_GUIDE.md +358 -358
- package/vendor/Server/Skills/rnoh-cptf-api-test/references/QUALITY_CHECKLIST.md +114 -114
- package/vendor/Server/Skills/rnoh-cptf-api-test/references/SPEC_PARSING_GUIDE.md +274 -274
- package/vendor/Server/Skills/rnoh-cptf-api-test/scripts/validate-rnoh-cptf-api-test.sh +185 -185
- package/vendor/Server/Skills/rnoh-cptf-demo-test/SKILL.md +191 -191
- package/vendor/Server/Skills/rnoh-cptf-demo-test/assets/TEST_TEMPLATE.md +216 -216
- package/vendor/Server/Skills/rnoh-cptf-demo-test/references/CPTF_SETUP.md +107 -107
- package/vendor/Server/Skills/rnoh-cptf-demo-test/references/DEMO_ANALYSIS.md +120 -120
- package/vendor/Server/Skills/rnoh-cptf-demo-test/scripts/validate-rnoh-cptf-test.sh +108 -108
- package/vendor/Server/Skills/rnoh-cptf-test-run/SKILL.md +237 -237
- package/vendor/Server/Skills/rnoh-cptf-test-run/references/DEBUG_PLAYBOOK.md +261 -261
- package/vendor/Server/Skills/rnoh-cptf-test-run/scripts/validate-run-env.sh +112 -112
- package/vendor/Server/Skills/rnoh-lib-code-adapt/SKILL.md +497 -497
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/adapt-checklist.md +270 -270
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/api-migration-checklist.md +164 -164
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/common-compile-errors.md +556 -556
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/deveco-cli-tools.md +459 -459
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/edge-cases.md +65 -65
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/har-tgz-build.md +512 -512
- package/vendor/Server/Skills/rnoh-lib-code-adapt/references/version-compat-matrix.md +132 -132
- package/vendor/Server/Skills/rnoh-lib-code-adapt/scripts/auto-integrate.sh +424 -424
- package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +137 -137
- package/vendor/Server/Skills/rnoh-lib-demo-create/SKILL.md +306 -306
- package/vendor/Server/Skills/rnoh-lib-demo-create/assets/DEMO_PAGE_TEMPLATE.md +485 -485
- package/vendor/Server/Skills/rnoh-lib-demo-create/assets/INDEX_TEMPLATE.md +172 -172
- package/vendor/Server/Skills/rnoh-lib-demo-create/references/DEMO_CODE_RULES.md +422 -422
- package/vendor/Server/Skills/rnoh-lib-demo-create/references/QUALITY_CHECKLIST.md +95 -95
- package/vendor/Server/Skills/rnoh-lib-demo-create/references/SOURCE_EXPLORATION.md +169 -169
- package/vendor/Server/Skills/rnoh-lib-demo-doc/SKILL.md +554 -554
- package/vendor/Server/Skills/rnoh-lib-demo-doc/assets/DEMO_DOC_TEMPLATE.md +106 -106
- package/vendor/Server/Skills/rnoh-lib-demo-doc/references/COVERAGE_INCREMENTAL.md +216 -216
- package/vendor/Server/Skills/rnoh-lib-demo-doc/references/DEMO_DESIGN_RULES.md +413 -413
- package/vendor/Server/Skills/rnoh-lib-demo-doc/references/OUTPUT_FORMAT.md +269 -269
- package/vendor/Server/Skills/rnoh-lib-demo-doc/references/QUALITY_CHECKLIST.md +98 -98
- package/vendor/Server/Skills/rnoh-lib-func-test/SKILL.md +337 -337
- package/vendor/Server/Skills/rnoh-lib-rnt-search/SKILL.md +722 -722
- package/vendor/Server/Skills/rnoh-lib-rnt-search/assets/PORT_REPORT_TEMPLATE.md +219 -219
- package/vendor/Server/Skills/rnoh-lib-rnt-search/references/DEMO_PORTING_GUIDE.md +345 -345
- package/vendor/Server/Skills/rnoh-lib-rnt-search/references/PORT_VERIFICATION.md +269 -269
- package/vendor/Server/Skills/rnoh-lib-rnt-search/references/RNT_PROJECT_STRUCTURE.md +173 -173
- package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/analyze-demo-deps.cjs +267 -267
- package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/detect-rnoh-version.cjs +174 -174
- package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/find-rnt-demo.sh +111 -111
- package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/port-demo.sh +186 -186
- package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/read-lib-meta.cjs +150 -150
- package/vendor/Server/Skills/rnoh-lib-test-align/SKILL.md +296 -296
- package/vendor/Server/Skills/rnoh-lib-test-align/references/jest-troubleshooting.md +371 -371
- package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +212 -212
- package/vendor/Server/Skills/rnoh-lib-test-align/references/runtime-troubleshooting.md +345 -345
- package/vendor/Server/Skills/rnoh-lib-test-align/references/unit-test-template.md +223 -223
- package/vendor/Server/Skills/rnoh-lib-test-align/scripts/port-test-demo.sh +1053 -1053
- package/vendor/Server/Skills/rnoh-lib-version-diff/SKILL.md +295 -255
- package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_ADAPTATION.md +82 -77
- package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_API_LOOKUP.md +224 -224
- package/vendor/Server/Skills/rnoh-lib-version-diff/references/script-reference.md +175 -174
- package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/detect-upstream-version.js +967 -967
- package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/generate-diff.js +1798 -1798
- package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/package.json +5 -5
- package/vendor/Server/Skills/rnoh-lib-version-diff/templates/report-template.md +180 -137
- package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +145 -145
- package/vendor/Server/Skills/skill-generator/SKILL.md +365 -365
- package/vendor/Server/Skills/skill-generator/assets/SKILL_TEMPLATE.md +183 -183
- package/vendor/Server/Skills/skill-generator/references/COMPLIANCE_CHECKLIST.md +82 -82
- package/vendor/Server/Skills/skill-generator/references/KB_QUERY_GUIDE.md +110 -110
- package/vendor/Server/Skills/skill-generator/references/MCP_TOOL_DISCOVERY.md +122 -122
- package/vendor/Server/Skills/skill-generator/references/SKILL_SPEC.md +121 -121
- package/vendor/Server/Skills/skill-generator/scripts/validate-skill.cjs +259 -259
- package/vendor/Server/Skills/task-analysis/SKILL.md +234 -234
- package/vendor/Server/Skills/task-analysis/assets/OUTPUT_EXAMPLES.md +160 -160
- package/vendor/Server/Skills/task-analysis/references/ACCEPTANCE_CRITERIA_GUIDE.md +190 -190
- package/vendor/Server/Skills/task-analysis/references/ANALYSIS_SCHEMA.md +224 -224
- package/vendor/Server/Skills/task-analysis/references/INTEGRATION_CONTEXT.md +162 -162
- package/vendor/Server/Skills/task-analysis/references/LESSONS_LEARNED.md +55 -55
- package/vendor/Server/Skills/task-analysis/references/REPLAN_CONTEXT_GUIDE.md +126 -126
- package/vendor/Server/Skills/task-analysis/scripts/validate-analysis-output.sh +256 -256
- package/vendor/Server/Skills/task-decomposition/SKILL.md +528 -528
- package/vendor/Server/Skills/task-decomposition/assets/OUTPUT_EXAMPLES.md +451 -451
- package/vendor/Server/Skills/task-decomposition/references/ACCEPTANCE_CRITERIA_GUIDE.md +179 -179
- package/vendor/Server/Skills/task-decomposition/references/DEPENDENCY_GRAPH_GUIDE.md +152 -152
- package/vendor/Server/Skills/task-decomposition/references/INTEGRATION_CONTEXT.md +175 -175
- package/vendor/Server/Skills/task-decomposition/references/REPLAN_CONTEXT_GUIDE.md +195 -195
- package/vendor/Server/Skills/task-decomposition/references/TASK_DECOMPOSITION_TEMPLATES.md +226 -226
- package/vendor/Server/Skills/task-decomposition/scripts/list-available-skills.cjs +263 -263
- package/vendor/Server/Skills/task-decomposition/scripts/skill-blacklist.json +29 -29
- package/vendor/Server/Skills/task-decomposition/scripts/validate-decomposition-output.cjs +376 -376
- package/vendor/Server/Skills/task-decomposition/scripts/validate-param-consistency.cjs +259 -259
- package/vendor/Server/Skills/task-decomposition/scripts/validate-specified-skills.cjs +216 -216
- package/vendor/Server/Skills/task-decomposition/scripts/validate-step-graph.cjs +424 -424
- package/vendor/Server/Skills/tpc-cpp-check/SKILL.md +427 -427
- package/vendor/Server/Skills/tpc-cpp-check/references/CHECKLIST.md +214 -214
- package/vendor/Server/Skills/tpc-cpp-check/references/HPKBUILD_GUIDE.md +235 -235
- package/vendor/Server/Skills/tpc-cpp-check/references/REPORT_TEMPLATE.md +250 -250
- package/vendor/Server/TestCheck/TestCheck.js +775 -775
- package/vendor/Server/TestCheck/index.js +11 -11
- package/vendor/Server/library/McpToolGuide.js +280 -280
- package/vendor/Server/library/StatsService.js +126 -126
- package/vendor/Server/library/cstTime.js +16 -16
- package/vendor/VERSION +3 -3
- package/workflows/pr-check.mjs +105 -105
- package/workflows/pr-push.mjs +193 -0
- package/vendor/Server/Agent/SkillSelector/types.js +0 -63
|
@@ -1,1125 +1,1125 @@
|
|
|
1
|
-
import { FlexRunner, ControlType } from '../../FlexRunner/FlexRunner.js';
|
|
2
|
-
import { OrchestratorStatus } from '../../Orchestrator/Orchestrator.js';
|
|
3
|
-
import { nowCST } from '../../library/cstTime.js';
|
|
4
|
-
import { createZipBuffer } from '../library/zipArchive.js';
|
|
5
|
-
import fs from 'node:fs/promises';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* FlexRunnerController —— FlexRunner 执行控制器
|
|
10
|
-
*
|
|
11
|
-
* 职责:封装 FlexRunner 的 HTTP 接口,供前端测试和操作。
|
|
12
|
-
* 不感知 HTTP 层细节,只接收 RequestContext / ResponseContext。
|
|
13
|
-
*
|
|
14
|
-
* 管理多个 FlexRunner 实例(按 taskId + stepId 区分),支持:
|
|
15
|
-
* - 初始化任务
|
|
16
|
-
* - 执行单步(executeStart)
|
|
17
|
-
* - 查询状态
|
|
18
|
-
* - 控制执行(暂停/继续/终止)
|
|
19
|
-
* - 获取实时消息
|
|
20
|
-
* - 获取会话内容
|
|
21
|
-
* - 检查结果文件
|
|
22
|
-
* - 列出任务步骤记录
|
|
23
|
-
*/
|
|
24
|
-
class FlexRunnerController {
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} [deps] - 共享依赖
|
|
27
|
-
* @param {object} [deps.orchestratorController] - OrchestratorController 实例(用于从编排器中查找 FlexRunner)
|
|
28
|
-
* @param {object} [deps.storage] - Storage 实例(用于在没有活跃 runner 时直接读取 session 归档)
|
|
29
|
-
*/
|
|
30
|
-
constructor(deps = {}) {
|
|
31
|
-
/** @type {Map<string, FlexRunner>} key = `${taskId}::${stepId}` */
|
|
32
|
-
this._runners = new Map();
|
|
33
|
-
/** @type {Map<string, {logs:string[], status:string}>} key = taskId */
|
|
34
|
-
this._taskLogs = new Map();
|
|
35
|
-
/** @type {object|null} OrchestratorController 实例 */
|
|
36
|
-
this._orchestratorController = deps.orchestratorController || null;
|
|
37
|
-
/** @type {object|null} Storage 实例 */
|
|
38
|
-
this._storage = deps.storage || null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── 辅助方法 ──────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 生成实例缓存 key。
|
|
45
|
-
*/
|
|
46
|
-
_key(taskId, stepId) {
|
|
47
|
-
return `${taskId}::${stepId}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 获取已有实例,不存在则报错。
|
|
52
|
-
*
|
|
53
|
-
* 查找顺序:
|
|
54
|
-
* 1. 本地 _runners Map(通过 /init 注册的实例)
|
|
55
|
-
* 2. OrchestratorController 中编排器步骤节点的 _runner(编排执行时创建的实例)
|
|
56
|
-
*/
|
|
57
|
-
_getRunner(taskId, stepId) {
|
|
58
|
-
// 1. 本地 Map
|
|
59
|
-
const key = this._key(taskId, stepId);
|
|
60
|
-
const localRunner = this._runners.get(key);
|
|
61
|
-
if (localRunner) return localRunner;
|
|
62
|
-
|
|
63
|
-
// 2. 从 OrchestratorController 的编排器实例中查找
|
|
64
|
-
if (this._orchestratorController) {
|
|
65
|
-
const runner = this._orchestratorController.getStepRunner(taskId, stepId);
|
|
66
|
-
if (runner) return runner;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw new Error(`未找到任务实例: taskId=${taskId}, stepId=${stepId},请先初始化或执行编排`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 在没有活跃 FlexRunner 实例时,从编排器步骤元信息中提取 sessionId,
|
|
74
|
-
* 然后直接读取本地 session 归档返回消息列表。
|
|
75
|
-
*
|
|
76
|
-
* 用于服务重启后(runner 不在内存中)仍能查看已完成步骤的对话记录。
|
|
77
|
-
*
|
|
78
|
-
* @param {string} taskId
|
|
79
|
-
* @param {number} stepId
|
|
80
|
-
* @param {number} [loop] - 大循环轮次(0-based)。指定时从磁盘 orchestration_loop{N}.json 读取历史轮次步骤信息
|
|
81
|
-
* @returns {Promise<{sessionId:string, messages:Array, meta:object|null}|null>}
|
|
82
|
-
*/
|
|
83
|
-
async _readSessionArchiveFromStep(taskId, stepId, loop) {
|
|
84
|
-
if (!this._orchestratorController || !this._storage) return null;
|
|
85
|
-
|
|
86
|
-
// 指定轮次:从磁盘读取历史轮次的步骤信息
|
|
87
|
-
let stepInfo;
|
|
88
|
-
if (loop != null) {
|
|
89
|
-
stepInfo = await this._orchestratorController.getStepInfoByLoop(taskId, stepId, loop);
|
|
90
|
-
} else {
|
|
91
|
-
stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
92
|
-
}
|
|
93
|
-
if (!stepInfo || !stepInfo.sessionId) return null;
|
|
94
|
-
|
|
95
|
-
const sessionId = stepInfo.sessionId;
|
|
96
|
-
const sessionStore = this._storage.sessionStore();
|
|
97
|
-
|
|
98
|
-
const [meta, messages] = await Promise.all([
|
|
99
|
-
sessionStore.readMeta(sessionId).catch(() => null),
|
|
100
|
-
sessionStore.readMessages(sessionId).catch(() => [])
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
return { sessionId, messages, meta };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 将 FlexRunner 实例的状态序列化为 JSON。
|
|
108
|
-
*/
|
|
109
|
-
_runnerToJSON(runner) {
|
|
110
|
-
return {
|
|
111
|
-
taskId: runner.taskId,
|
|
112
|
-
stepId: runner.stepId,
|
|
113
|
-
dependId: runner.dependId,
|
|
114
|
-
nextStepId: runner.nextStepId,
|
|
115
|
-
taskDescription: runner.taskDescription,
|
|
116
|
-
taskDir: runner.taskDir,
|
|
117
|
-
skillDirectory: runner.skillDirectory,
|
|
118
|
-
inputReport: runner.inputReport,
|
|
119
|
-
maxRetries: runner.maxRetries,
|
|
120
|
-
executeStep: runner.executeStep,
|
|
121
|
-
retryTimes: runner.retryTimes,
|
|
122
|
-
sessionId: runner.sessionId,
|
|
123
|
-
startTime: runner.startTime,
|
|
124
|
-
endTime: runner.endTime,
|
|
125
|
-
resultPath: runner.resultPath,
|
|
126
|
-
initialized: runner.initialized,
|
|
127
|
-
canRetry: runner.canRetry(),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ─── HTTP Handlers ─────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* POST /api/flexrunner/init
|
|
135
|
-
* 初始化一个 FlexRunner 实例
|
|
136
|
-
*
|
|
137
|
-
* Body: {
|
|
138
|
-
* taskId, stepId, dependId, nextStepId, failStepId, taskDescription,
|
|
139
|
-
* skillDirectory, taskDir, inputReport, maxRetries
|
|
140
|
-
* }
|
|
141
|
-
*/
|
|
142
|
-
async init(req, res) {
|
|
143
|
-
const body = req.body || {};
|
|
144
|
-
const { taskId } = body;
|
|
145
|
-
|
|
146
|
-
if (!taskId) {
|
|
147
|
-
return res.error(400, 'taskId 为必填项');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const stepId = Number(body.stepId);
|
|
151
|
-
if (!Number.isInteger(stepId) || stepId <= 0) {
|
|
152
|
-
return res.error(400, 'stepId 必须为正整数');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 如果同 key 实例已存在,覆盖(重新初始化)
|
|
156
|
-
const runner = new FlexRunner();
|
|
157
|
-
await runner.initContext({
|
|
158
|
-
taskId,
|
|
159
|
-
stepId,
|
|
160
|
-
dependId: Number(body.dependId) || 0,
|
|
161
|
-
nextStepId: Number(body.nextStepId) || 0,
|
|
162
|
-
failStepId: Number(body.failStepId) || 0,
|
|
163
|
-
taskDescription: body.taskDescription || '',
|
|
164
|
-
skillDirectory: body.skillDirectory || '',
|
|
165
|
-
taskDir: body.taskDir || '',
|
|
166
|
-
inputReport: body.inputReport || '',
|
|
167
|
-
maxRetries: typeof body.maxRetries === 'number' ? body.maxRetries : undefined,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const key = this._key(taskId, stepId);
|
|
171
|
-
this._runners.set(key, runner);
|
|
172
|
-
|
|
173
|
-
res.json({
|
|
174
|
-
status: 'ok',
|
|
175
|
-
message: `FlexRunner 已初始化: taskId=${taskId}, stepId=${stepId}`,
|
|
176
|
-
runner: this._runnerToJSON(runner),
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* POST /api/flexrunner/execute
|
|
182
|
-
* 执行单次 executeStart(不内部循环,由调用方决定是否重试)
|
|
183
|
-
*
|
|
184
|
-
* Body: { taskId, stepId }
|
|
185
|
-
*/
|
|
186
|
-
async execute(req, res) {
|
|
187
|
-
const body = req.body || {};
|
|
188
|
-
const taskId = body.taskId;
|
|
189
|
-
const stepId = Number(body.stepId);
|
|
190
|
-
|
|
191
|
-
let runner;
|
|
192
|
-
try {
|
|
193
|
-
runner = this._getRunner(taskId, stepId);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
return res.error(404, err.message);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!runner.canRetry()) {
|
|
199
|
-
return res.error(409, `已达最大重试次数或已终止,无法继续执行`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const result = await runner.executeStart();
|
|
203
|
-
|
|
204
|
-
res.json({
|
|
205
|
-
status: 'ok',
|
|
206
|
-
result,
|
|
207
|
-
runner: this._runnerToJSON(runner),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* POST /api/flexrunner/execute-loop
|
|
213
|
-
* 自动循环执行直到成功或不可重试
|
|
214
|
-
*
|
|
215
|
-
* Body: { taskId, stepId }
|
|
216
|
-
*/
|
|
217
|
-
async executeLoop(req, res) {
|
|
218
|
-
const body = req.body || {};
|
|
219
|
-
const taskId = body.taskId;
|
|
220
|
-
const stepId = Number(body.stepId);
|
|
221
|
-
|
|
222
|
-
let runner;
|
|
223
|
-
try {
|
|
224
|
-
runner = this._getRunner(taskId, stepId);
|
|
225
|
-
} catch (err) {
|
|
226
|
-
return res.error(404, err.message);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const results = [];
|
|
230
|
-
let lastResult;
|
|
231
|
-
|
|
232
|
-
while (runner.canRetry()) {
|
|
233
|
-
lastResult = await runner.executeStart();
|
|
234
|
-
results.push({
|
|
235
|
-
attempt: runner.executeStep,
|
|
236
|
-
success: lastResult.success,
|
|
237
|
-
nextStepId: lastResult.nextStepId,
|
|
238
|
-
resultPath: lastResult.resultPath,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (lastResult.success) break;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
res.json({
|
|
245
|
-
status: lastResult?.success ? 'ok' : 'error',
|
|
246
|
-
totalAttempts: results.length,
|
|
247
|
-
results,
|
|
248
|
-
finalResult: lastResult,
|
|
249
|
-
runner: this._runnerToJSON(runner),
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* GET /api/flexrunner/status?taskId=xxx&stepId=yyy
|
|
255
|
-
* 查询 FlexRunner 实例状态
|
|
256
|
-
*
|
|
257
|
-
* 如果本地无实例,尝试从编排器恢复步骤状态后返回。
|
|
258
|
-
*/
|
|
259
|
-
async status(req, res) {
|
|
260
|
-
const taskId = req.query.taskId;
|
|
261
|
-
const stepId = Number(req.query.stepId);
|
|
262
|
-
|
|
263
|
-
let runner;
|
|
264
|
-
try {
|
|
265
|
-
runner = this._getRunner(taskId, stepId);
|
|
266
|
-
res.json({
|
|
267
|
-
status: 'ok',
|
|
268
|
-
runner: this._runnerToJSON(runner),
|
|
269
|
-
});
|
|
270
|
-
} catch (err) {
|
|
271
|
-
// 编排器中存在该任务但步骤已结束(runner 未存活),返回步骤元信息
|
|
272
|
-
if (this._orchestratorController) {
|
|
273
|
-
const stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
274
|
-
if (stepInfo) {
|
|
275
|
-
return res.json({
|
|
276
|
-
status: 'ok',
|
|
277
|
-
runner: { ...stepInfo, initialized: false },
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return res.error(404, err.message);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* GET /api/flexrunner/live-messages?taskId=xxx&stepId=yyy[&loop=N]
|
|
287
|
-
* 获取当前会话的实时对话消息
|
|
288
|
-
*
|
|
289
|
-
* - 不传 loop:从内存编排器实例查找(最新轮次)
|
|
290
|
-
* - 传 loop=N:从磁盘 orchestration_loop{N}.json 读取历史轮次步骤的 sessionId,再读取本地归档
|
|
291
|
-
*/
|
|
292
|
-
async liveMessages(req, res) {
|
|
293
|
-
const taskId = req.query.taskId;
|
|
294
|
-
const stepId = Number(req.query.stepId);
|
|
295
|
-
const loop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : undefined;
|
|
296
|
-
|
|
297
|
-
// 指定历史轮次:直接从磁盘读取该轮次的步骤归档(不查内存最新实例)
|
|
298
|
-
if (loop != null) {
|
|
299
|
-
const archive = await this._readSessionArchiveFromStep(taskId, stepId, loop);
|
|
300
|
-
if (archive) {
|
|
301
|
-
return res.json({
|
|
302
|
-
status: 'ok',
|
|
303
|
-
sessionId: archive.sessionId,
|
|
304
|
-
count: archive.messages.length,
|
|
305
|
-
messages: archive.messages,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
return res.error(404, `未找到第 ${loop} 轮步骤归档: taskId=${taskId}, stepId=${stepId}`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let runner;
|
|
312
|
-
try {
|
|
313
|
-
runner = this._getRunner(taskId, stepId);
|
|
314
|
-
} catch (err) {
|
|
315
|
-
// 没有活跃 runner(如服务重启后从磁盘恢复的任务),
|
|
316
|
-
// 尝试从编排器步骤元信息中获取 sessionId 并读取本地归档
|
|
317
|
-
const archive = await this._readSessionArchiveFromStep(taskId, stepId);
|
|
318
|
-
if (archive) {
|
|
319
|
-
return res.json({
|
|
320
|
-
status: 'ok',
|
|
321
|
-
sessionId: archive.sessionId,
|
|
322
|
-
count: archive.messages.length,
|
|
323
|
-
messages: archive.messages,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
return res.error(404, err.message);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 有活跃 runner,优先从远端拉取实时消息
|
|
330
|
-
try {
|
|
331
|
-
const messages = await runner.getLiveMessages();
|
|
332
|
-
return res.json({
|
|
333
|
-
status: 'ok',
|
|
334
|
-
sessionId: runner.sessionId,
|
|
335
|
-
count: messages.length,
|
|
336
|
-
messages,
|
|
337
|
-
});
|
|
338
|
-
} catch (liveErr) {
|
|
339
|
-
// 远端不可用(opencode 服务关闭/session 过期),回退到本地归档
|
|
340
|
-
const sessionId = runner.sessionId;
|
|
341
|
-
if (sessionId && this._storage) {
|
|
342
|
-
const sessionStore = this._storage.sessionStore();
|
|
343
|
-
const messages = await sessionStore.readMessages(sessionId).catch(() => []);
|
|
344
|
-
return res.json({
|
|
345
|
-
status: 'ok',
|
|
346
|
-
sessionId,
|
|
347
|
-
count: messages.length,
|
|
348
|
-
messages,
|
|
349
|
-
warning: '远端不可用,已回退到本地归档',
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return res.error(500, `获取实时消息失败: ${liveErr.message}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* POST /api/flexrunner/inject-prompt
|
|
358
|
-
* 向当前会话注入提示词/上下文(不触发 AI 回复)
|
|
359
|
-
*
|
|
360
|
-
* Body: { taskId, stepId, message, [loop] }
|
|
361
|
-
*/
|
|
362
|
-
async injectPrompt(req, res) {
|
|
363
|
-
const body = req.body || {};
|
|
364
|
-
const taskId = body.taskId;
|
|
365
|
-
const stepId = Number(body.stepId);
|
|
366
|
-
const { message } = body;
|
|
367
|
-
const loop = body.loop != null ? Number(body.loop) : undefined;
|
|
368
|
-
|
|
369
|
-
if (!message || typeof message !== 'string') {
|
|
370
|
-
return res.error(400, 'message 为必填项且必须为字符串');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 指定历史轮次:历史会话已结束,无法注入(仅支持查看归档)
|
|
374
|
-
if (loop != null) {
|
|
375
|
-
return res.error(409, `历史轮次(${loop})的会话已结束,不支持注入提示词`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
let runner;
|
|
379
|
-
try {
|
|
380
|
-
runner = this._getRunner(taskId, stepId);
|
|
381
|
-
} catch (err) {
|
|
382
|
-
return res.error(404, err.message);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const result = await runner.injectPrompt(message);
|
|
386
|
-
|
|
387
|
-
res.json({
|
|
388
|
-
status: result.success ? 'ok' : 'error',
|
|
389
|
-
sessionId: runner.sessionId,
|
|
390
|
-
success: result.success,
|
|
391
|
-
message: result.success
|
|
392
|
-
? '提示词已注入'
|
|
393
|
-
: '注入失败(可能无活跃会话或 CLI 不支持)',
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* POST /api/flexrunner/send-and-wait
|
|
399
|
-
* 向当前会话发送消息并同步等待 AI 回复完成
|
|
400
|
-
*
|
|
401
|
-
* Body: { taskId, stepId, message, [loop] }
|
|
402
|
-
*/
|
|
403
|
-
async sendAndWaitMessage(req, res) {
|
|
404
|
-
const body = req.body || {};
|
|
405
|
-
const taskId = body.taskId;
|
|
406
|
-
const stepId = Number(body.stepId);
|
|
407
|
-
const { message } = body;
|
|
408
|
-
const loop = body.loop != null ? Number(body.loop) : undefined;
|
|
409
|
-
|
|
410
|
-
if (!message || typeof message !== 'string') {
|
|
411
|
-
return res.error(400, 'message 为必填项且必须为字符串');
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 指定历史轮次:历史会话已结束,无法发送消息
|
|
415
|
-
if (loop != null) {
|
|
416
|
-
return res.error(409, `历史轮次(${loop})的会话已结束,不支持发送消息`);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
let runner;
|
|
420
|
-
try {
|
|
421
|
-
runner = this._getRunner(taskId, stepId);
|
|
422
|
-
} catch (err) {
|
|
423
|
-
return res.error(404, err.message);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const result = await runner.sendAndWaitMessage(message);
|
|
427
|
-
|
|
428
|
-
res.json({
|
|
429
|
-
status: result.success ? 'ok' : 'error',
|
|
430
|
-
sessionId: runner.sessionId,
|
|
431
|
-
success: result.success,
|
|
432
|
-
response: result.response,
|
|
433
|
-
content: result.content,
|
|
434
|
-
message: result.success
|
|
435
|
-
? '已收到 AI 回复'
|
|
436
|
-
: '发送失败(可能无活跃会话或 CLI 不支持)',
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* GET /api/flexrunner/session?taskId=xxx&stepId=yyy[&loop=N]
|
|
442
|
-
* 获取当前会话的完整归档内容
|
|
443
|
-
*
|
|
444
|
-
* - 不传 loop:从内存编排器实例查找(最新轮次)
|
|
445
|
-
* - 传 loop=N:从磁盘 orchestration_loop{N}.json 读取历史轮次步骤的 sessionId,再读取本地归档
|
|
446
|
-
*/
|
|
447
|
-
async session(req, res) {
|
|
448
|
-
const taskId = req.query.taskId;
|
|
449
|
-
const stepId = Number(req.query.stepId);
|
|
450
|
-
const loop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : undefined;
|
|
451
|
-
|
|
452
|
-
// 指定历史轮次:直接从磁盘读取该轮次的步骤归档
|
|
453
|
-
if (loop != null) {
|
|
454
|
-
const archive = await this._readSessionArchiveFromStep(taskId, stepId, loop);
|
|
455
|
-
if (archive) {
|
|
456
|
-
return res.json({
|
|
457
|
-
status: 'ok',
|
|
458
|
-
sessionId: archive.sessionId,
|
|
459
|
-
meta: archive.meta,
|
|
460
|
-
messages: archive.messages,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
return res.error(404, `未找到第 ${loop} 轮步骤归档: taskId=${taskId}, stepId=${stepId}`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let runner;
|
|
467
|
-
try {
|
|
468
|
-
runner = this._getRunner(taskId, stepId);
|
|
469
|
-
} catch (err) {
|
|
470
|
-
// 没有活跃 runner,从编排器步骤元信息获取 sessionId 并读取本地归档
|
|
471
|
-
const archive = await this._readSessionArchiveFromStep(taskId, stepId);
|
|
472
|
-
if (archive) {
|
|
473
|
-
return res.json({
|
|
474
|
-
status: 'ok',
|
|
475
|
-
sessionId: archive.sessionId,
|
|
476
|
-
meta: archive.meta,
|
|
477
|
-
messages: archive.messages,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
return res.error(404, err.message);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
const jsonStr = await runner.getSessionContent();
|
|
485
|
-
const data = JSON.parse(jsonStr);
|
|
486
|
-
|
|
487
|
-
res.json({
|
|
488
|
-
status: 'ok',
|
|
489
|
-
sessionId: runner.sessionId,
|
|
490
|
-
...data,
|
|
491
|
-
});
|
|
492
|
-
} catch (err) {
|
|
493
|
-
// runner 存在但读取失败,尝试直接从本地归档恢复
|
|
494
|
-
const sessionId = runner.sessionId;
|
|
495
|
-
if (sessionId && this._storage) {
|
|
496
|
-
const sessionStore = this._storage.sessionStore();
|
|
497
|
-
const [meta, messages] = await Promise.all([
|
|
498
|
-
sessionStore.readMeta(sessionId).catch(() => null),
|
|
499
|
-
sessionStore.readMessages(sessionId).catch(() => [])
|
|
500
|
-
]);
|
|
501
|
-
return res.json({
|
|
502
|
-
status: 'ok',
|
|
503
|
-
sessionId,
|
|
504
|
-
meta,
|
|
505
|
-
messages,
|
|
506
|
-
warning: '已回退到本地归档',
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
return res.error(500, `获取会话内容失败: ${err.message}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* POST /api/flexrunner/control
|
|
515
|
-
* 控制执行(暂停/继续/终止)
|
|
516
|
-
*
|
|
517
|
-
* Body: { taskId, stepId, type: 'pause'|'resume'|'abort' }
|
|
518
|
-
*/
|
|
519
|
-
async control(req, res) {
|
|
520
|
-
const body = req.body || {};
|
|
521
|
-
const taskId = body.taskId;
|
|
522
|
-
const stepId = Number(body.stepId);
|
|
523
|
-
const { type } = body;
|
|
524
|
-
|
|
525
|
-
let runner;
|
|
526
|
-
try {
|
|
527
|
-
runner = this._getRunner(taskId, stepId);
|
|
528
|
-
} catch (err) {
|
|
529
|
-
return res.error(404, err.message);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const validTypes = ['pause', 'resume', 'abort'];
|
|
533
|
-
if (!validTypes.includes(type)) {
|
|
534
|
-
return res.error(400, `type 必须为: ${validTypes.join(', ')}`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const ok = await runner.controlExecution(type);
|
|
538
|
-
|
|
539
|
-
res.json({
|
|
540
|
-
status: ok ? 'ok' : 'error',
|
|
541
|
-
message: `控制指令 "${type}" 已${ok ? '执行' : '失败'}`,
|
|
542
|
-
runner: this._runnerToJSON(runner),
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* POST /api/flexrunner/check-result
|
|
548
|
-
* 检查结果文件的成功/失败状态
|
|
549
|
-
*
|
|
550
|
-
* Body: { taskId, stepId, resultFilePath }
|
|
551
|
-
*/
|
|
552
|
-
async checkResult(req, res) {
|
|
553
|
-
const body = req.body || {};
|
|
554
|
-
const taskId = body.taskId;
|
|
555
|
-
const stepId = Number(body.stepId);
|
|
556
|
-
const { resultFilePath } = body;
|
|
557
|
-
|
|
558
|
-
let runner;
|
|
559
|
-
let filePath = resultFilePath || null;
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
runner = this._getRunner(taskId, stepId);
|
|
563
|
-
if (!filePath) filePath = runner.resultPath;
|
|
564
|
-
} catch (err) {
|
|
565
|
-
// 没有活跃 runner(磁盘恢复的任务),从编排器步骤元信息获取 resultPath
|
|
566
|
-
if (this._orchestratorController) {
|
|
567
|
-
const stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
568
|
-
if (stepInfo && stepInfo.resultPath) {
|
|
569
|
-
filePath = stepInfo.resultPath;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if (!filePath) {
|
|
573
|
-
return res.error(404, err.message);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (!filePath) {
|
|
578
|
-
return res.error(400, 'resultFilePath 为空且实例无记录的 resultPath');
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// 读取文件内容并解析执行状态
|
|
582
|
-
let content = null;
|
|
583
|
-
let success = false;
|
|
584
|
-
try {
|
|
585
|
-
content = await fs.readFile(filePath, 'utf-8');
|
|
586
|
-
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
587
|
-
const match = content.match(/##\s*执行状态\s*\n\s*(成功|失败)/i);
|
|
588
|
-
success = match ? match[1] === '成功' : false;
|
|
589
|
-
} catch {
|
|
590
|
-
/* 文件可能不存在 */
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
res.json({
|
|
594
|
-
status: 'ok',
|
|
595
|
-
success,
|
|
596
|
-
resultFilePath: filePath,
|
|
597
|
-
content,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* GET /api/flexrunner/steps?taskId=xxx[&stepId=yyy][&loop=N]
|
|
603
|
-
* 列出任务目录下的步骤记录文件
|
|
604
|
-
*
|
|
605
|
-
* - 可选 stepId:只返回该步骤的文件
|
|
606
|
-
* - 可选 loop:只返回该大循环轮次的文件(匹配文件名中的 _loop{N}_ 段)
|
|
607
|
-
*/
|
|
608
|
-
async steps(req, res) {
|
|
609
|
-
const { taskId } = req.query;
|
|
610
|
-
const filterStepId = req.query.stepId != null && req.query.stepId !== '' ? String(req.query.stepId) : null;
|
|
611
|
-
const filterLoop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : null;
|
|
612
|
-
|
|
613
|
-
if (!taskId) {
|
|
614
|
-
return res.error(400, 'taskId 为必填项');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// 从已注册的实例中查找该 taskId 的所有 step
|
|
618
|
-
const stepList = [];
|
|
619
|
-
const seenStepKeys = new Set();
|
|
620
|
-
for (const [key, runner] of this._runners) {
|
|
621
|
-
if (runner.taskId === taskId) {
|
|
622
|
-
stepList.push(this._runnerToJSON(runner));
|
|
623
|
-
seenStepKeys.add(this._key(runner.taskId, runner.stepId));
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// 从 OrchestratorController 补充编排器管理的步骤(覆盖编排执行创建的实例)
|
|
628
|
-
if (this._orchestratorController) {
|
|
629
|
-
const orchSteps = this._orchestratorController.getTaskSteps(taskId);
|
|
630
|
-
for (const s of orchSteps) {
|
|
631
|
-
const k = this._key(taskId, s.stepId);
|
|
632
|
-
if (!seenStepKeys.has(k)) {
|
|
633
|
-
stepList.push(s);
|
|
634
|
-
seenStepKeys.add(k);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// 扫描该 task 自己的目录下的 .md 文件
|
|
640
|
-
// taskDir 直接就是 db/task/{taskId},文件直接放在该目录下(不在子目录中)
|
|
641
|
-
let files = [];
|
|
642
|
-
const taskDir = stepList[0]?.taskDir || null;
|
|
643
|
-
if (taskDir) {
|
|
644
|
-
try {
|
|
645
|
-
const allFiles = await fs.readdir(taskDir);
|
|
646
|
-
for (const f of allFiles) {
|
|
647
|
-
if (!f.endsWith('.md')) continue;
|
|
648
|
-
const fp = path.join(taskDir, f);
|
|
649
|
-
const fstat = await fs.stat(fp);
|
|
650
|
-
if (!fstat.isFile()) continue;
|
|
651
|
-
|
|
652
|
-
// 从文件名中解析 stepId 和 loop
|
|
653
|
-
// 文件命名格式: stepId{N}_loop{L}_result_{R}.md 或 stepId{N}_loop{L}_Summary_{R}.md
|
|
654
|
-
const stepIdMatch = f.match(/^stepId(\d+)/);
|
|
655
|
-
const loopMatch = f.match(/_loop(\d+)_/);
|
|
656
|
-
const fileStepId = stepIdMatch ? stepIdMatch[1] : null;
|
|
657
|
-
const fileLoop = loopMatch != null ? Number(loopMatch[1]) : null;
|
|
658
|
-
|
|
659
|
-
// 按 stepId 筛选
|
|
660
|
-
if (filterStepId && fileStepId !== filterStepId) continue;
|
|
661
|
-
// 按 loop 筛选
|
|
662
|
-
if (filterLoop != null && fileLoop !== filterLoop) continue;
|
|
663
|
-
|
|
664
|
-
files.push({
|
|
665
|
-
stepId: fileStepId || f,
|
|
666
|
-
loop: fileLoop,
|
|
667
|
-
filename: f,
|
|
668
|
-
path: fp,
|
|
669
|
-
size: fstat.size,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
} catch { /* 目录不存在 */ }
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
res.json({
|
|
676
|
-
status: 'ok',
|
|
677
|
-
taskId,
|
|
678
|
-
steps: stepList,
|
|
679
|
-
files,
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* GET /api/flexrunner/file?path=xxx
|
|
685
|
-
* 读取指定文件的内容(用于查看 result/summary .md)
|
|
686
|
-
*/
|
|
687
|
-
async file(req, res) {
|
|
688
|
-
const filePath = req.query.path;
|
|
689
|
-
if (!filePath) {
|
|
690
|
-
return res.error(400, 'path 为必填项');
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
try {
|
|
694
|
-
let content = await fs.readFile(filePath, 'utf-8');
|
|
695
|
-
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
696
|
-
res.json({
|
|
697
|
-
status: 'ok',
|
|
698
|
-
path: filePath,
|
|
699
|
-
content,
|
|
700
|
-
});
|
|
701
|
-
} catch (err) {
|
|
702
|
-
res.error(404, `文件读取失败: ${err.message}`);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* GET /api/flexrunner/archive?taskId=xxx
|
|
708
|
-
* 打包下载任务目录下的全部 .md 报告文件(ZIP)。
|
|
709
|
-
*
|
|
710
|
-
* - 扫描 taskDir 根目录(不递归,排除 source/ 克隆仓库),收集所有 .md 文件
|
|
711
|
-
* - 检视报告在前、框架产物(stepId*_result/Summary)在后,同类自然排序
|
|
712
|
-
* - ZIP 内以 {taskId}/ 目录前缀组织,便于解压归档
|
|
713
|
-
* - 任务进行中调用时打包当前已生成的文件(实时快照)
|
|
714
|
-
*/
|
|
715
|
-
async archive(req, res) {
|
|
716
|
-
const { taskId } = req.query;
|
|
717
|
-
if (!taskId) {
|
|
718
|
-
return res.error(400, 'taskId 为必填项');
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
try {
|
|
722
|
-
// ── 1. 解析 taskDir ──
|
|
723
|
-
const taskDir = await this._resolveTaskDir(taskId);
|
|
724
|
-
if (!taskDir) {
|
|
725
|
-
return res.error(404, `未找到任务目录: taskId=${taskId}`);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// ── 2. 收集根目录 .md 文件(withFileTypes + isFile,天然排除子目录)──
|
|
729
|
-
const entries = await fs.readdir(taskDir, { withFileTypes: true });
|
|
730
|
-
const mdFiles = entries
|
|
731
|
-
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
732
|
-
.map(e => e.name);
|
|
733
|
-
|
|
734
|
-
if (mdFiles.length === 0) {
|
|
735
|
-
return res.error(404, '暂无报告文件(任务尚未生成任何 .md 产物)');
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ── 3. 排序:检视报告在前,框架产物(stepId*)在后 ──
|
|
739
|
-
const isFrameworkFile = (n) => /^stepId/i.test(n);
|
|
740
|
-
mdFiles.sort((a, b) => {
|
|
741
|
-
const fa = isFrameworkFile(a) ? 1 : 0;
|
|
742
|
-
const fb = isFrameworkFile(b) ? 1 : 0;
|
|
743
|
-
if (fa !== fb) return fa - fb;
|
|
744
|
-
return a.localeCompare(b, 'zh-CN', { numeric: true });
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// ── 4. 读取并打包(单文件读取失败跳过,不中断整体打包)──
|
|
748
|
-
const files = [];
|
|
749
|
-
for (const name of mdFiles) {
|
|
750
|
-
const fp = path.join(taskDir, name);
|
|
751
|
-
try {
|
|
752
|
-
const [data, st] = await Promise.all([fs.readFile(fp), fs.stat(fp)]);
|
|
753
|
-
files.push({ name: `${taskId}/${name}`, data, mtime: st.mtime });
|
|
754
|
-
} catch (err) {
|
|
755
|
-
console.warn(`[FlexRunnerController] archive 跳过读取失败文件: ${fp}, ${err.message}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (files.length === 0) {
|
|
759
|
-
return res.error(404, '报告文件读取失败');
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const zip = createZipBuffer(files);
|
|
763
|
-
|
|
764
|
-
// ── 5. 二进制响应(Content-Disposition 双写,兼容 ASCII 回退与 UTF-8)──
|
|
765
|
-
const raw = res.raw;
|
|
766
|
-
raw.writeHead(200, {
|
|
767
|
-
'Content-Type': 'application/zip',
|
|
768
|
-
'Content-Length': zip.length,
|
|
769
|
-
'Content-Disposition': `attachment; filename="${taskId}.zip"; filename*=UTF-8''${encodeURIComponent(taskId)}.zip`
|
|
770
|
-
});
|
|
771
|
-
raw.end(zip);
|
|
772
|
-
} catch (err) {
|
|
773
|
-
if (!res.raw.headersSent) {
|
|
774
|
-
return res.error(500, `打包失败: ${err.message}`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* 解析任务工作目录(供 archive 打包等场景使用)。
|
|
781
|
-
*
|
|
782
|
-
* 查找顺序:
|
|
783
|
-
* 1. 内存中活跃 FlexRunner 实例的 taskDir
|
|
784
|
-
* 2. OrchestratorController 中编排步骤元信息的 taskDir(含重启后恢复的实例)
|
|
785
|
-
* 3. 磁盘兜底:{rootDir}/task/{taskId}(taskId 白名单校验 + 路径边界检查,防目录穿越)
|
|
786
|
-
*
|
|
787
|
-
* @param {string} taskId
|
|
788
|
-
* @returns {Promise<string|null>} taskDir 绝对路径;未找到返回 null
|
|
789
|
-
* @private
|
|
790
|
-
*/
|
|
791
|
-
async _resolveTaskDir(taskId) {
|
|
792
|
-
// 1. 活跃 runner
|
|
793
|
-
for (const runner of this._runners.values()) {
|
|
794
|
-
if (runner.taskId === taskId && runner.taskDir) {
|
|
795
|
-
return runner.taskDir;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// 2. 编排器步骤元信息
|
|
800
|
-
if (this._orchestratorController) {
|
|
801
|
-
const steps = this._orchestratorController.getTaskSteps(taskId);
|
|
802
|
-
const withDir = steps.find(s => s && s.taskDir);
|
|
803
|
-
if (withDir) return withDir.taskDir;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// 3. 磁盘兜底(白名单 + 边界校验)
|
|
807
|
-
const rootDir = this._storage?.manager?.rootDir;
|
|
808
|
-
if (rootDir) {
|
|
809
|
-
if (!/^[A-Za-z0-9._-]+$/.test(taskId) || taskId === '.' || taskId === '..') {
|
|
810
|
-
return null;
|
|
811
|
-
}
|
|
812
|
-
const taskRoot = path.join(rootDir, 'task');
|
|
813
|
-
const resolved = path.resolve(taskRoot, taskId);
|
|
814
|
-
if (!resolved.startsWith(taskRoot + path.sep)) {
|
|
815
|
-
return null;
|
|
816
|
-
}
|
|
817
|
-
try {
|
|
818
|
-
const st = await fs.stat(resolved);
|
|
819
|
-
if (st.isDirectory()) return resolved;
|
|
820
|
-
} catch { /* 目录不存在 */ }
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* GET /api/flexrunner/runners
|
|
828
|
-
* 列出所有已注册的 FlexRunner 实例
|
|
829
|
-
*/
|
|
830
|
-
async runners(req, res) {
|
|
831
|
-
const list = [];
|
|
832
|
-
for (const runner of this._runners.values()) {
|
|
833
|
-
list.push(this._runnerToJSON(runner));
|
|
834
|
-
}
|
|
835
|
-
res.json({
|
|
836
|
-
status: 'ok',
|
|
837
|
-
count: list.length,
|
|
838
|
-
runners: list,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ─── 单次执行 ─────────────────────────────────────────────────────
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* POST /api/flexrunner/start-single
|
|
846
|
-
* 启动单次执行 — 创建一个只含单步骤的编排任务,不走 Brain 拆解和 LoopEngine 循环。
|
|
847
|
-
*
|
|
848
|
-
* 内部流程:
|
|
849
|
-
* 1. 生成 taskId(如果未指定)
|
|
850
|
-
* 2. 构造单步骤 taskDef(stepId=1, dependId=0, nextStepId=0, failStepId=0)
|
|
851
|
-
* 3. 委托 OrchestratorController.load() 加载任务 → 写 orchestration.json
|
|
852
|
-
* 4. 委托 OrchestratorController.start() 异步启动编排
|
|
853
|
-
* 5. 写精简版 loop.json(isSingleExecution=true)使任务列表可见
|
|
854
|
-
* 6. 编排结束后更新 loop.json 终态 + 释放重型引用
|
|
855
|
-
*
|
|
856
|
-
* Body: {
|
|
857
|
-
* taskDescription: string, // 必填
|
|
858
|
-
* skillDirectory?: string, // 可选
|
|
859
|
-
* specifiedSkill?: string, // 可选,指定 SKILL(name 或目录名),跳过自动匹配
|
|
860
|
-
* taskDir?: string, // 可选
|
|
861
|
-
* inputReport?: string, // 可选
|
|
862
|
-
* maxRetries?: number, // 可选,默认 3
|
|
863
|
-
* taskId?: string // 可选
|
|
864
|
-
* }
|
|
865
|
-
*
|
|
866
|
-
* 响应:异步启动,立即返回 taskId 和 stepId。
|
|
867
|
-
*/
|
|
868
|
-
async startSingle(req, res) {
|
|
869
|
-
const body = req.body || {};
|
|
870
|
-
const { taskDescription } = body;
|
|
871
|
-
|
|
872
|
-
if (!taskDescription) {
|
|
873
|
-
return res.error(400, 'taskDescription 为必填项');
|
|
874
|
-
}
|
|
875
|
-
if (!this._orchestratorController) {
|
|
876
|
-
return res.error(500, 'orchestratorController 未初始化');
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
try {
|
|
880
|
-
// ── 1. 生成 taskId ──
|
|
881
|
-
const taskId = body.taskId ||
|
|
882
|
-
`single_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`;
|
|
883
|
-
|
|
884
|
-
// 幂等保护:同一 taskId 若已有编排在运行,直接返回
|
|
885
|
-
let existingOrch;
|
|
886
|
-
try {
|
|
887
|
-
existingOrch = this._orchestratorController._getOrchestrator(taskId);
|
|
888
|
-
} catch { /* 不存在,正常 */ }
|
|
889
|
-
if (existingOrch && existingOrch.status === OrchestratorStatus.RUNNING) {
|
|
890
|
-
return res.json({
|
|
891
|
-
status: 'ok',
|
|
892
|
-
message: `单次执行已在运行中: taskId=${taskId}`,
|
|
893
|
-
taskId,
|
|
894
|
-
stepId: 1,
|
|
895
|
-
progress: existingOrch.getProgress()
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// 若已存在同 taskId 的旧编排(已结束),先终止并移除
|
|
900
|
-
if (existingOrch) {
|
|
901
|
-
try { await existingOrch.abort(); } catch { /* 已结束 */ }
|
|
902
|
-
this._orchestratorController.disposeOrchestrator(taskId, { removeInstance: true });
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// ── 2. 构造单步骤 taskDef ──
|
|
906
|
-
const maxRetries = typeof body.maxRetries === 'number' && body.maxRetries > 0
|
|
907
|
-
? body.maxRetries
|
|
908
|
-
: 3;
|
|
909
|
-
const skillDirectory = body.skillDirectory || '';
|
|
910
|
-
|
|
911
|
-
// taskDir 默认推导(Orchestrator.loadTask 内部也会推导,但 loop.json 需要提前知道)
|
|
912
|
-
const rootDir = this._storage?.manager?.rootDir;
|
|
913
|
-
const taskDir = body.taskDir ||
|
|
914
|
-
(rootDir
|
|
915
|
-
? path.resolve(rootDir, 'task', taskId)
|
|
916
|
-
: path.resolve(process.cwd(), 'db', 'task', taskId));
|
|
917
|
-
|
|
918
|
-
// 确保 taskDir 存在(opencode session 需要工作目录存在)
|
|
919
|
-
await fs.mkdir(taskDir, { recursive: true });
|
|
920
|
-
|
|
921
|
-
const taskDef = {
|
|
922
|
-
taskId,
|
|
923
|
-
taskDir,
|
|
924
|
-
skillDirectory,
|
|
925
|
-
loopCount: 0, // 单次执行固定为第 0 轮
|
|
926
|
-
steps: [{
|
|
927
|
-
stepId: 1,
|
|
928
|
-
taskDescription,
|
|
929
|
-
dependId: 0, // 入口步骤
|
|
930
|
-
nextStepId: 0, // 无后续
|
|
931
|
-
failStepId: 0, // 失败即终止
|
|
932
|
-
maxRetries,
|
|
933
|
-
skillDirectory,
|
|
934
|
-
specifiedSkill: body.specifiedSkill || '',
|
|
935
|
-
taskDir,
|
|
936
|
-
inputReport: body.inputReport || ''
|
|
937
|
-
}]
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
// ── 3. 委托 OrchestratorController 加载任务 ──
|
|
941
|
-
const loadReq = { body: taskDef };
|
|
942
|
-
const loadRes = this._createMockResponse();
|
|
943
|
-
await this._orchestratorController.load(loadReq, loadRes);
|
|
944
|
-
|
|
945
|
-
if (loadRes._status >= 400) {
|
|
946
|
-
return res.error(loadRes._status, loadRes._data?.error || loadRes._data?.message || '编排任务加载失败');
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// ── 4. 启动编排(直接调用 orch.start(),便于跟踪完成) ──
|
|
950
|
-
const orch = this._orchestratorController._getOrchestrator(taskId);
|
|
951
|
-
|
|
952
|
-
// ── 5. 写精简版 loop.json(使任务列表可见) ──
|
|
953
|
-
await this._writeSingleLoopJson(taskId, taskDescription, taskDir, skillDirectory, 'executing', null, null);
|
|
954
|
-
|
|
955
|
-
// ── 6. 异步启动编排,完成后更新 loop.json 终态 + 释放重型引用 ──
|
|
956
|
-
orch.start()
|
|
957
|
-
.catch(err => {
|
|
958
|
-
console.error(`[FlexRunnerController] 单次执行异常: taskId=${taskId}, ${err.message}`);
|
|
959
|
-
})
|
|
960
|
-
.finally(() => {
|
|
961
|
-
this._onSingleExecutionComplete(taskId, orch).catch(err => {
|
|
962
|
-
console.warn(`[FlexRunnerController] 单次执行收尾失败: taskId=${taskId}, ${err.message}`);
|
|
963
|
-
});
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
res.json({
|
|
967
|
-
status: 'ok',
|
|
968
|
-
message: `单次执行已启动: taskId=${taskId}`,
|
|
969
|
-
taskId,
|
|
970
|
-
stepId: 1,
|
|
971
|
-
progress: orch.getProgress()
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
} catch (err) {
|
|
975
|
-
res.error(400, err.message);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* 创建模拟 Response 对象(供调用 OrchestratorController 的 load/start)。
|
|
981
|
-
* @private
|
|
982
|
-
*/
|
|
983
|
-
_createMockResponse() {
|
|
984
|
-
const mock = {
|
|
985
|
-
_status: 200,
|
|
986
|
-
_data: null,
|
|
987
|
-
statusCode: 200,
|
|
988
|
-
headersSent: false,
|
|
989
|
-
writableEnded: false,
|
|
990
|
-
json(data) {
|
|
991
|
-
this._data = data;
|
|
992
|
-
this.writableEnded = true;
|
|
993
|
-
},
|
|
994
|
-
error(code, message) {
|
|
995
|
-
this._status = code;
|
|
996
|
-
this._data = { status: 'error', error: message };
|
|
997
|
-
this.writableEnded = true;
|
|
998
|
-
}
|
|
999
|
-
};
|
|
1000
|
-
return mock;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* 写精简版 loop.json(单次执行任务专用)。
|
|
1005
|
-
*
|
|
1006
|
-
* 格式与 LoopEngine._persistLoopState 兼容,附加 isSingleExecution 标记。
|
|
1007
|
-
*
|
|
1008
|
-
* @param {string} taskId
|
|
1009
|
-
* @param {string} taskDescription
|
|
1010
|
-
* @param {string} taskDir
|
|
1011
|
-
* @param {string} skillDirectory
|
|
1012
|
-
* @param {string} status - executing | completed | failed | aborted
|
|
1013
|
-
* @param {string|null} startTime
|
|
1014
|
-
* @param {string|null} endTime
|
|
1015
|
-
* @private
|
|
1016
|
-
*/
|
|
1017
|
-
async _writeSingleLoopJson(taskId, taskDescription, taskDir, skillDirectory, status, startTime, endTime) {
|
|
1018
|
-
if (!this._storage) return;
|
|
1019
|
-
try {
|
|
1020
|
-
// 映射 loop 状态到 orchestrator 状态(用于前端 rounds 渲染)
|
|
1021
|
-
var orchStatus;
|
|
1022
|
-
if (status === 'executing') orchStatus = 'running';
|
|
1023
|
-
else if (status === 'passed') orchStatus = 'completed';
|
|
1024
|
-
else if (status === 'aborted') orchStatus = 'aborted';
|
|
1025
|
-
else orchStatus = 'failed';
|
|
1026
|
-
|
|
1027
|
-
const state = {
|
|
1028
|
-
taskId,
|
|
1029
|
-
taskDescription,
|
|
1030
|
-
userAcceptanceCriteria: '',
|
|
1031
|
-
taskDir,
|
|
1032
|
-
skillDirectory,
|
|
1033
|
-
maxLoops: 1,
|
|
1034
|
-
currentLoop: 1,
|
|
1035
|
-
status,
|
|
1036
|
-
startTime: startTime || nowCST(),
|
|
1037
|
-
endTime,
|
|
1038
|
-
error: null,
|
|
1039
|
-
aborted: status === 'aborted',
|
|
1040
|
-
isSingleExecution: true,
|
|
1041
|
-
rounds: [{
|
|
1042
|
-
loop: 0,
|
|
1043
|
-
startTime: startTime || nowCST(),
|
|
1044
|
-
endTime,
|
|
1045
|
-
passed: status === 'passed',
|
|
1046
|
-
error: null,
|
|
1047
|
-
planningIsReplan: false,
|
|
1048
|
-
planningStepCount: 1,
|
|
1049
|
-
orchestratorStatus: orchStatus
|
|
1050
|
-
}],
|
|
1051
|
-
persistedAt: nowCST()
|
|
1052
|
-
};
|
|
1053
|
-
await this._storage.json('task').write(`${taskId}/loop`, state);
|
|
1054
|
-
} catch (err) {
|
|
1055
|
-
console.warn(`[FlexRunnerController] 写 loop.json 失败: taskId=${taskId}, ${err.message}`);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* 单次执行编排完成后的收尾处理。
|
|
1061
|
-
*
|
|
1062
|
-
* 1. 更新 loop.json 的终态状态
|
|
1063
|
-
* 2. 释放 Orchestrator 重型引用(FlexRunner/CLI/LLM 等)
|
|
1064
|
-
*
|
|
1065
|
-
* @param {string} taskId
|
|
1066
|
-
* @param {object} orch - Orchestrator 实例
|
|
1067
|
-
* @private
|
|
1068
|
-
*/
|
|
1069
|
-
async _onSingleExecutionComplete(taskId, orch) {
|
|
1070
|
-
const status = orch.status;
|
|
1071
|
-
|
|
1072
|
-
// 映射编排终态到 loop.json 状态
|
|
1073
|
-
let loopStatus;
|
|
1074
|
-
if (status === OrchestratorStatus.COMPLETED) {
|
|
1075
|
-
loopStatus = 'passed';
|
|
1076
|
-
} else if (status === OrchestratorStatus.ABORTED) {
|
|
1077
|
-
loopStatus = 'aborted';
|
|
1078
|
-
} else {
|
|
1079
|
-
loopStatus = 'failed';
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
const startTime = orch.startTime;
|
|
1083
|
-
const endTime = orch.endTime;
|
|
1084
|
-
|
|
1085
|
-
// 读取 taskDescription(从编排器获取)
|
|
1086
|
-
const progress = orch.getProgress();
|
|
1087
|
-
const step = progress.steps && progress.steps[0];
|
|
1088
|
-
const taskDescription = step ? step.taskDescription : '';
|
|
1089
|
-
const taskDir = orch._taskDir || '';
|
|
1090
|
-
const skillDirectory = orch._skillDirectory || '';
|
|
1091
|
-
|
|
1092
|
-
// 更新 loop.json
|
|
1093
|
-
await this._writeSingleLoopJson(
|
|
1094
|
-
taskId, taskDescription, taskDir, skillDirectory,
|
|
1095
|
-
loopStatus, startTime, endTime
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
// 释放重型引用
|
|
1099
|
-
if (this._orchestratorController) {
|
|
1100
|
-
try {
|
|
1101
|
-
this._orchestratorController.disposeOrchestrator(taskId, { removeInstance: true });
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
console.warn(`[FlexRunnerController] 释放 Orchestrator 失败: taskId=${taskId}, ${err.message}`);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* DELETE /api/flexrunner/runner?taskId=xxx&stepId=yyy
|
|
1110
|
-
* 移除一个已注册的 FlexRunner 实例
|
|
1111
|
-
*/
|
|
1112
|
-
async removeRunner(req, res) {
|
|
1113
|
-
const taskId = req.query.taskId;
|
|
1114
|
-
const stepId = Number(req.query.stepId);
|
|
1115
|
-
const key = this._key(taskId, stepId);
|
|
1116
|
-
const existed = this._runners.delete(key);
|
|
1117
|
-
res.json({
|
|
1118
|
-
status: 'ok',
|
|
1119
|
-
removed: existed,
|
|
1120
|
-
message: existed ? '实例已移除' : '实例不存在',
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
export default FlexRunnerController;
|
|
1
|
+
import { FlexRunner, ControlType } from '../../FlexRunner/FlexRunner.js';
|
|
2
|
+
import { OrchestratorStatus } from '../../Orchestrator/Orchestrator.js';
|
|
3
|
+
import { nowCST } from '../../library/cstTime.js';
|
|
4
|
+
import { createZipBuffer } from '../library/zipArchive.js';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FlexRunnerController —— FlexRunner 执行控制器
|
|
10
|
+
*
|
|
11
|
+
* 职责:封装 FlexRunner 的 HTTP 接口,供前端测试和操作。
|
|
12
|
+
* 不感知 HTTP 层细节,只接收 RequestContext / ResponseContext。
|
|
13
|
+
*
|
|
14
|
+
* 管理多个 FlexRunner 实例(按 taskId + stepId 区分),支持:
|
|
15
|
+
* - 初始化任务
|
|
16
|
+
* - 执行单步(executeStart)
|
|
17
|
+
* - 查询状态
|
|
18
|
+
* - 控制执行(暂停/继续/终止)
|
|
19
|
+
* - 获取实时消息
|
|
20
|
+
* - 获取会话内容
|
|
21
|
+
* - 检查结果文件
|
|
22
|
+
* - 列出任务步骤记录
|
|
23
|
+
*/
|
|
24
|
+
class FlexRunnerController {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} [deps] - 共享依赖
|
|
27
|
+
* @param {object} [deps.orchestratorController] - OrchestratorController 实例(用于从编排器中查找 FlexRunner)
|
|
28
|
+
* @param {object} [deps.storage] - Storage 实例(用于在没有活跃 runner 时直接读取 session 归档)
|
|
29
|
+
*/
|
|
30
|
+
constructor(deps = {}) {
|
|
31
|
+
/** @type {Map<string, FlexRunner>} key = `${taskId}::${stepId}` */
|
|
32
|
+
this._runners = new Map();
|
|
33
|
+
/** @type {Map<string, {logs:string[], status:string}>} key = taskId */
|
|
34
|
+
this._taskLogs = new Map();
|
|
35
|
+
/** @type {object|null} OrchestratorController 实例 */
|
|
36
|
+
this._orchestratorController = deps.orchestratorController || null;
|
|
37
|
+
/** @type {object|null} Storage 实例 */
|
|
38
|
+
this._storage = deps.storage || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── 辅助方法 ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 生成实例缓存 key。
|
|
45
|
+
*/
|
|
46
|
+
_key(taskId, stepId) {
|
|
47
|
+
return `${taskId}::${stepId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取已有实例,不存在则报错。
|
|
52
|
+
*
|
|
53
|
+
* 查找顺序:
|
|
54
|
+
* 1. 本地 _runners Map(通过 /init 注册的实例)
|
|
55
|
+
* 2. OrchestratorController 中编排器步骤节点的 _runner(编排执行时创建的实例)
|
|
56
|
+
*/
|
|
57
|
+
_getRunner(taskId, stepId) {
|
|
58
|
+
// 1. 本地 Map
|
|
59
|
+
const key = this._key(taskId, stepId);
|
|
60
|
+
const localRunner = this._runners.get(key);
|
|
61
|
+
if (localRunner) return localRunner;
|
|
62
|
+
|
|
63
|
+
// 2. 从 OrchestratorController 的编排器实例中查找
|
|
64
|
+
if (this._orchestratorController) {
|
|
65
|
+
const runner = this._orchestratorController.getStepRunner(taskId, stepId);
|
|
66
|
+
if (runner) return runner;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`未找到任务实例: taskId=${taskId}, stepId=${stepId},请先初始化或执行编排`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 在没有活跃 FlexRunner 实例时,从编排器步骤元信息中提取 sessionId,
|
|
74
|
+
* 然后直接读取本地 session 归档返回消息列表。
|
|
75
|
+
*
|
|
76
|
+
* 用于服务重启后(runner 不在内存中)仍能查看已完成步骤的对话记录。
|
|
77
|
+
*
|
|
78
|
+
* @param {string} taskId
|
|
79
|
+
* @param {number} stepId
|
|
80
|
+
* @param {number} [loop] - 大循环轮次(0-based)。指定时从磁盘 orchestration_loop{N}.json 读取历史轮次步骤信息
|
|
81
|
+
* @returns {Promise<{sessionId:string, messages:Array, meta:object|null}|null>}
|
|
82
|
+
*/
|
|
83
|
+
async _readSessionArchiveFromStep(taskId, stepId, loop) {
|
|
84
|
+
if (!this._orchestratorController || !this._storage) return null;
|
|
85
|
+
|
|
86
|
+
// 指定轮次:从磁盘读取历史轮次的步骤信息
|
|
87
|
+
let stepInfo;
|
|
88
|
+
if (loop != null) {
|
|
89
|
+
stepInfo = await this._orchestratorController.getStepInfoByLoop(taskId, stepId, loop);
|
|
90
|
+
} else {
|
|
91
|
+
stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
92
|
+
}
|
|
93
|
+
if (!stepInfo || !stepInfo.sessionId) return null;
|
|
94
|
+
|
|
95
|
+
const sessionId = stepInfo.sessionId;
|
|
96
|
+
const sessionStore = this._storage.sessionStore();
|
|
97
|
+
|
|
98
|
+
const [meta, messages] = await Promise.all([
|
|
99
|
+
sessionStore.readMeta(sessionId).catch(() => null),
|
|
100
|
+
sessionStore.readMessages(sessionId).catch(() => [])
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
return { sessionId, messages, meta };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 将 FlexRunner 实例的状态序列化为 JSON。
|
|
108
|
+
*/
|
|
109
|
+
_runnerToJSON(runner) {
|
|
110
|
+
return {
|
|
111
|
+
taskId: runner.taskId,
|
|
112
|
+
stepId: runner.stepId,
|
|
113
|
+
dependId: runner.dependId,
|
|
114
|
+
nextStepId: runner.nextStepId,
|
|
115
|
+
taskDescription: runner.taskDescription,
|
|
116
|
+
taskDir: runner.taskDir,
|
|
117
|
+
skillDirectory: runner.skillDirectory,
|
|
118
|
+
inputReport: runner.inputReport,
|
|
119
|
+
maxRetries: runner.maxRetries,
|
|
120
|
+
executeStep: runner.executeStep,
|
|
121
|
+
retryTimes: runner.retryTimes,
|
|
122
|
+
sessionId: runner.sessionId,
|
|
123
|
+
startTime: runner.startTime,
|
|
124
|
+
endTime: runner.endTime,
|
|
125
|
+
resultPath: runner.resultPath,
|
|
126
|
+
initialized: runner.initialized,
|
|
127
|
+
canRetry: runner.canRetry(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── HTTP Handlers ─────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* POST /api/flexrunner/init
|
|
135
|
+
* 初始化一个 FlexRunner 实例
|
|
136
|
+
*
|
|
137
|
+
* Body: {
|
|
138
|
+
* taskId, stepId, dependId, nextStepId, failStepId, taskDescription,
|
|
139
|
+
* skillDirectory, taskDir, inputReport, maxRetries
|
|
140
|
+
* }
|
|
141
|
+
*/
|
|
142
|
+
async init(req, res) {
|
|
143
|
+
const body = req.body || {};
|
|
144
|
+
const { taskId } = body;
|
|
145
|
+
|
|
146
|
+
if (!taskId) {
|
|
147
|
+
return res.error(400, 'taskId 为必填项');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const stepId = Number(body.stepId);
|
|
151
|
+
if (!Number.isInteger(stepId) || stepId <= 0) {
|
|
152
|
+
return res.error(400, 'stepId 必须为正整数');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 如果同 key 实例已存在,覆盖(重新初始化)
|
|
156
|
+
const runner = new FlexRunner();
|
|
157
|
+
await runner.initContext({
|
|
158
|
+
taskId,
|
|
159
|
+
stepId,
|
|
160
|
+
dependId: Number(body.dependId) || 0,
|
|
161
|
+
nextStepId: Number(body.nextStepId) || 0,
|
|
162
|
+
failStepId: Number(body.failStepId) || 0,
|
|
163
|
+
taskDescription: body.taskDescription || '',
|
|
164
|
+
skillDirectory: body.skillDirectory || '',
|
|
165
|
+
taskDir: body.taskDir || '',
|
|
166
|
+
inputReport: body.inputReport || '',
|
|
167
|
+
maxRetries: typeof body.maxRetries === 'number' ? body.maxRetries : undefined,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const key = this._key(taskId, stepId);
|
|
171
|
+
this._runners.set(key, runner);
|
|
172
|
+
|
|
173
|
+
res.json({
|
|
174
|
+
status: 'ok',
|
|
175
|
+
message: `FlexRunner 已初始化: taskId=${taskId}, stepId=${stepId}`,
|
|
176
|
+
runner: this._runnerToJSON(runner),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* POST /api/flexrunner/execute
|
|
182
|
+
* 执行单次 executeStart(不内部循环,由调用方决定是否重试)
|
|
183
|
+
*
|
|
184
|
+
* Body: { taskId, stepId }
|
|
185
|
+
*/
|
|
186
|
+
async execute(req, res) {
|
|
187
|
+
const body = req.body || {};
|
|
188
|
+
const taskId = body.taskId;
|
|
189
|
+
const stepId = Number(body.stepId);
|
|
190
|
+
|
|
191
|
+
let runner;
|
|
192
|
+
try {
|
|
193
|
+
runner = this._getRunner(taskId, stepId);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return res.error(404, err.message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!runner.canRetry()) {
|
|
199
|
+
return res.error(409, `已达最大重试次数或已终止,无法继续执行`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = await runner.executeStart();
|
|
203
|
+
|
|
204
|
+
res.json({
|
|
205
|
+
status: 'ok',
|
|
206
|
+
result,
|
|
207
|
+
runner: this._runnerToJSON(runner),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* POST /api/flexrunner/execute-loop
|
|
213
|
+
* 自动循环执行直到成功或不可重试
|
|
214
|
+
*
|
|
215
|
+
* Body: { taskId, stepId }
|
|
216
|
+
*/
|
|
217
|
+
async executeLoop(req, res) {
|
|
218
|
+
const body = req.body || {};
|
|
219
|
+
const taskId = body.taskId;
|
|
220
|
+
const stepId = Number(body.stepId);
|
|
221
|
+
|
|
222
|
+
let runner;
|
|
223
|
+
try {
|
|
224
|
+
runner = this._getRunner(taskId, stepId);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return res.error(404, err.message);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const results = [];
|
|
230
|
+
let lastResult;
|
|
231
|
+
|
|
232
|
+
while (runner.canRetry()) {
|
|
233
|
+
lastResult = await runner.executeStart();
|
|
234
|
+
results.push({
|
|
235
|
+
attempt: runner.executeStep,
|
|
236
|
+
success: lastResult.success,
|
|
237
|
+
nextStepId: lastResult.nextStepId,
|
|
238
|
+
resultPath: lastResult.resultPath,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (lastResult.success) break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
res.json({
|
|
245
|
+
status: lastResult?.success ? 'ok' : 'error',
|
|
246
|
+
totalAttempts: results.length,
|
|
247
|
+
results,
|
|
248
|
+
finalResult: lastResult,
|
|
249
|
+
runner: this._runnerToJSON(runner),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* GET /api/flexrunner/status?taskId=xxx&stepId=yyy
|
|
255
|
+
* 查询 FlexRunner 实例状态
|
|
256
|
+
*
|
|
257
|
+
* 如果本地无实例,尝试从编排器恢复步骤状态后返回。
|
|
258
|
+
*/
|
|
259
|
+
async status(req, res) {
|
|
260
|
+
const taskId = req.query.taskId;
|
|
261
|
+
const stepId = Number(req.query.stepId);
|
|
262
|
+
|
|
263
|
+
let runner;
|
|
264
|
+
try {
|
|
265
|
+
runner = this._getRunner(taskId, stepId);
|
|
266
|
+
res.json({
|
|
267
|
+
status: 'ok',
|
|
268
|
+
runner: this._runnerToJSON(runner),
|
|
269
|
+
});
|
|
270
|
+
} catch (err) {
|
|
271
|
+
// 编排器中存在该任务但步骤已结束(runner 未存活),返回步骤元信息
|
|
272
|
+
if (this._orchestratorController) {
|
|
273
|
+
const stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
274
|
+
if (stepInfo) {
|
|
275
|
+
return res.json({
|
|
276
|
+
status: 'ok',
|
|
277
|
+
runner: { ...stepInfo, initialized: false },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return res.error(404, err.message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* GET /api/flexrunner/live-messages?taskId=xxx&stepId=yyy[&loop=N]
|
|
287
|
+
* 获取当前会话的实时对话消息
|
|
288
|
+
*
|
|
289
|
+
* - 不传 loop:从内存编排器实例查找(最新轮次)
|
|
290
|
+
* - 传 loop=N:从磁盘 orchestration_loop{N}.json 读取历史轮次步骤的 sessionId,再读取本地归档
|
|
291
|
+
*/
|
|
292
|
+
async liveMessages(req, res) {
|
|
293
|
+
const taskId = req.query.taskId;
|
|
294
|
+
const stepId = Number(req.query.stepId);
|
|
295
|
+
const loop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : undefined;
|
|
296
|
+
|
|
297
|
+
// 指定历史轮次:直接从磁盘读取该轮次的步骤归档(不查内存最新实例)
|
|
298
|
+
if (loop != null) {
|
|
299
|
+
const archive = await this._readSessionArchiveFromStep(taskId, stepId, loop);
|
|
300
|
+
if (archive) {
|
|
301
|
+
return res.json({
|
|
302
|
+
status: 'ok',
|
|
303
|
+
sessionId: archive.sessionId,
|
|
304
|
+
count: archive.messages.length,
|
|
305
|
+
messages: archive.messages,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return res.error(404, `未找到第 ${loop} 轮步骤归档: taskId=${taskId}, stepId=${stepId}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let runner;
|
|
312
|
+
try {
|
|
313
|
+
runner = this._getRunner(taskId, stepId);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
// 没有活跃 runner(如服务重启后从磁盘恢复的任务),
|
|
316
|
+
// 尝试从编排器步骤元信息中获取 sessionId 并读取本地归档
|
|
317
|
+
const archive = await this._readSessionArchiveFromStep(taskId, stepId);
|
|
318
|
+
if (archive) {
|
|
319
|
+
return res.json({
|
|
320
|
+
status: 'ok',
|
|
321
|
+
sessionId: archive.sessionId,
|
|
322
|
+
count: archive.messages.length,
|
|
323
|
+
messages: archive.messages,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return res.error(404, err.message);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 有活跃 runner,优先从远端拉取实时消息
|
|
330
|
+
try {
|
|
331
|
+
const messages = await runner.getLiveMessages();
|
|
332
|
+
return res.json({
|
|
333
|
+
status: 'ok',
|
|
334
|
+
sessionId: runner.sessionId,
|
|
335
|
+
count: messages.length,
|
|
336
|
+
messages,
|
|
337
|
+
});
|
|
338
|
+
} catch (liveErr) {
|
|
339
|
+
// 远端不可用(opencode 服务关闭/session 过期),回退到本地归档
|
|
340
|
+
const sessionId = runner.sessionId;
|
|
341
|
+
if (sessionId && this._storage) {
|
|
342
|
+
const sessionStore = this._storage.sessionStore();
|
|
343
|
+
const messages = await sessionStore.readMessages(sessionId).catch(() => []);
|
|
344
|
+
return res.json({
|
|
345
|
+
status: 'ok',
|
|
346
|
+
sessionId,
|
|
347
|
+
count: messages.length,
|
|
348
|
+
messages,
|
|
349
|
+
warning: '远端不可用,已回退到本地归档',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return res.error(500, `获取实时消息失败: ${liveErr.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* POST /api/flexrunner/inject-prompt
|
|
358
|
+
* 向当前会话注入提示词/上下文(不触发 AI 回复)
|
|
359
|
+
*
|
|
360
|
+
* Body: { taskId, stepId, message, [loop] }
|
|
361
|
+
*/
|
|
362
|
+
async injectPrompt(req, res) {
|
|
363
|
+
const body = req.body || {};
|
|
364
|
+
const taskId = body.taskId;
|
|
365
|
+
const stepId = Number(body.stepId);
|
|
366
|
+
const { message } = body;
|
|
367
|
+
const loop = body.loop != null ? Number(body.loop) : undefined;
|
|
368
|
+
|
|
369
|
+
if (!message || typeof message !== 'string') {
|
|
370
|
+
return res.error(400, 'message 为必填项且必须为字符串');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 指定历史轮次:历史会话已结束,无法注入(仅支持查看归档)
|
|
374
|
+
if (loop != null) {
|
|
375
|
+
return res.error(409, `历史轮次(${loop})的会话已结束,不支持注入提示词`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let runner;
|
|
379
|
+
try {
|
|
380
|
+
runner = this._getRunner(taskId, stepId);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
return res.error(404, err.message);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = await runner.injectPrompt(message);
|
|
386
|
+
|
|
387
|
+
res.json({
|
|
388
|
+
status: result.success ? 'ok' : 'error',
|
|
389
|
+
sessionId: runner.sessionId,
|
|
390
|
+
success: result.success,
|
|
391
|
+
message: result.success
|
|
392
|
+
? '提示词已注入'
|
|
393
|
+
: '注入失败(可能无活跃会话或 CLI 不支持)',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* POST /api/flexrunner/send-and-wait
|
|
399
|
+
* 向当前会话发送消息并同步等待 AI 回复完成
|
|
400
|
+
*
|
|
401
|
+
* Body: { taskId, stepId, message, [loop] }
|
|
402
|
+
*/
|
|
403
|
+
async sendAndWaitMessage(req, res) {
|
|
404
|
+
const body = req.body || {};
|
|
405
|
+
const taskId = body.taskId;
|
|
406
|
+
const stepId = Number(body.stepId);
|
|
407
|
+
const { message } = body;
|
|
408
|
+
const loop = body.loop != null ? Number(body.loop) : undefined;
|
|
409
|
+
|
|
410
|
+
if (!message || typeof message !== 'string') {
|
|
411
|
+
return res.error(400, 'message 为必填项且必须为字符串');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 指定历史轮次:历史会话已结束,无法发送消息
|
|
415
|
+
if (loop != null) {
|
|
416
|
+
return res.error(409, `历史轮次(${loop})的会话已结束,不支持发送消息`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let runner;
|
|
420
|
+
try {
|
|
421
|
+
runner = this._getRunner(taskId, stepId);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return res.error(404, err.message);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const result = await runner.sendAndWaitMessage(message);
|
|
427
|
+
|
|
428
|
+
res.json({
|
|
429
|
+
status: result.success ? 'ok' : 'error',
|
|
430
|
+
sessionId: runner.sessionId,
|
|
431
|
+
success: result.success,
|
|
432
|
+
response: result.response,
|
|
433
|
+
content: result.content,
|
|
434
|
+
message: result.success
|
|
435
|
+
? '已收到 AI 回复'
|
|
436
|
+
: '发送失败(可能无活跃会话或 CLI 不支持)',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* GET /api/flexrunner/session?taskId=xxx&stepId=yyy[&loop=N]
|
|
442
|
+
* 获取当前会话的完整归档内容
|
|
443
|
+
*
|
|
444
|
+
* - 不传 loop:从内存编排器实例查找(最新轮次)
|
|
445
|
+
* - 传 loop=N:从磁盘 orchestration_loop{N}.json 读取历史轮次步骤的 sessionId,再读取本地归档
|
|
446
|
+
*/
|
|
447
|
+
async session(req, res) {
|
|
448
|
+
const taskId = req.query.taskId;
|
|
449
|
+
const stepId = Number(req.query.stepId);
|
|
450
|
+
const loop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : undefined;
|
|
451
|
+
|
|
452
|
+
// 指定历史轮次:直接从磁盘读取该轮次的步骤归档
|
|
453
|
+
if (loop != null) {
|
|
454
|
+
const archive = await this._readSessionArchiveFromStep(taskId, stepId, loop);
|
|
455
|
+
if (archive) {
|
|
456
|
+
return res.json({
|
|
457
|
+
status: 'ok',
|
|
458
|
+
sessionId: archive.sessionId,
|
|
459
|
+
meta: archive.meta,
|
|
460
|
+
messages: archive.messages,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return res.error(404, `未找到第 ${loop} 轮步骤归档: taskId=${taskId}, stepId=${stepId}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let runner;
|
|
467
|
+
try {
|
|
468
|
+
runner = this._getRunner(taskId, stepId);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
// 没有活跃 runner,从编排器步骤元信息获取 sessionId 并读取本地归档
|
|
471
|
+
const archive = await this._readSessionArchiveFromStep(taskId, stepId);
|
|
472
|
+
if (archive) {
|
|
473
|
+
return res.json({
|
|
474
|
+
status: 'ok',
|
|
475
|
+
sessionId: archive.sessionId,
|
|
476
|
+
meta: archive.meta,
|
|
477
|
+
messages: archive.messages,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return res.error(404, err.message);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const jsonStr = await runner.getSessionContent();
|
|
485
|
+
const data = JSON.parse(jsonStr);
|
|
486
|
+
|
|
487
|
+
res.json({
|
|
488
|
+
status: 'ok',
|
|
489
|
+
sessionId: runner.sessionId,
|
|
490
|
+
...data,
|
|
491
|
+
});
|
|
492
|
+
} catch (err) {
|
|
493
|
+
// runner 存在但读取失败,尝试直接从本地归档恢复
|
|
494
|
+
const sessionId = runner.sessionId;
|
|
495
|
+
if (sessionId && this._storage) {
|
|
496
|
+
const sessionStore = this._storage.sessionStore();
|
|
497
|
+
const [meta, messages] = await Promise.all([
|
|
498
|
+
sessionStore.readMeta(sessionId).catch(() => null),
|
|
499
|
+
sessionStore.readMessages(sessionId).catch(() => [])
|
|
500
|
+
]);
|
|
501
|
+
return res.json({
|
|
502
|
+
status: 'ok',
|
|
503
|
+
sessionId,
|
|
504
|
+
meta,
|
|
505
|
+
messages,
|
|
506
|
+
warning: '已回退到本地归档',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return res.error(500, `获取会话内容失败: ${err.message}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* POST /api/flexrunner/control
|
|
515
|
+
* 控制执行(暂停/继续/终止)
|
|
516
|
+
*
|
|
517
|
+
* Body: { taskId, stepId, type: 'pause'|'resume'|'abort' }
|
|
518
|
+
*/
|
|
519
|
+
async control(req, res) {
|
|
520
|
+
const body = req.body || {};
|
|
521
|
+
const taskId = body.taskId;
|
|
522
|
+
const stepId = Number(body.stepId);
|
|
523
|
+
const { type } = body;
|
|
524
|
+
|
|
525
|
+
let runner;
|
|
526
|
+
try {
|
|
527
|
+
runner = this._getRunner(taskId, stepId);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return res.error(404, err.message);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const validTypes = ['pause', 'resume', 'abort'];
|
|
533
|
+
if (!validTypes.includes(type)) {
|
|
534
|
+
return res.error(400, `type 必须为: ${validTypes.join(', ')}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const ok = await runner.controlExecution(type);
|
|
538
|
+
|
|
539
|
+
res.json({
|
|
540
|
+
status: ok ? 'ok' : 'error',
|
|
541
|
+
message: `控制指令 "${type}" 已${ok ? '执行' : '失败'}`,
|
|
542
|
+
runner: this._runnerToJSON(runner),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* POST /api/flexrunner/check-result
|
|
548
|
+
* 检查结果文件的成功/失败状态
|
|
549
|
+
*
|
|
550
|
+
* Body: { taskId, stepId, resultFilePath }
|
|
551
|
+
*/
|
|
552
|
+
async checkResult(req, res) {
|
|
553
|
+
const body = req.body || {};
|
|
554
|
+
const taskId = body.taskId;
|
|
555
|
+
const stepId = Number(body.stepId);
|
|
556
|
+
const { resultFilePath } = body;
|
|
557
|
+
|
|
558
|
+
let runner;
|
|
559
|
+
let filePath = resultFilePath || null;
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
runner = this._getRunner(taskId, stepId);
|
|
563
|
+
if (!filePath) filePath = runner.resultPath;
|
|
564
|
+
} catch (err) {
|
|
565
|
+
// 没有活跃 runner(磁盘恢复的任务),从编排器步骤元信息获取 resultPath
|
|
566
|
+
if (this._orchestratorController) {
|
|
567
|
+
const stepInfo = this._orchestratorController.getStepInfo(taskId, stepId);
|
|
568
|
+
if (stepInfo && stepInfo.resultPath) {
|
|
569
|
+
filePath = stepInfo.resultPath;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!filePath) {
|
|
573
|
+
return res.error(404, err.message);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!filePath) {
|
|
578
|
+
return res.error(400, 'resultFilePath 为空且实例无记录的 resultPath');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// 读取文件内容并解析执行状态
|
|
582
|
+
let content = null;
|
|
583
|
+
let success = false;
|
|
584
|
+
try {
|
|
585
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
586
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
587
|
+
const match = content.match(/##\s*执行状态\s*\n\s*(成功|失败)/i);
|
|
588
|
+
success = match ? match[1] === '成功' : false;
|
|
589
|
+
} catch {
|
|
590
|
+
/* 文件可能不存在 */
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
res.json({
|
|
594
|
+
status: 'ok',
|
|
595
|
+
success,
|
|
596
|
+
resultFilePath: filePath,
|
|
597
|
+
content,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* GET /api/flexrunner/steps?taskId=xxx[&stepId=yyy][&loop=N]
|
|
603
|
+
* 列出任务目录下的步骤记录文件
|
|
604
|
+
*
|
|
605
|
+
* - 可选 stepId:只返回该步骤的文件
|
|
606
|
+
* - 可选 loop:只返回该大循环轮次的文件(匹配文件名中的 _loop{N}_ 段)
|
|
607
|
+
*/
|
|
608
|
+
async steps(req, res) {
|
|
609
|
+
const { taskId } = req.query;
|
|
610
|
+
const filterStepId = req.query.stepId != null && req.query.stepId !== '' ? String(req.query.stepId) : null;
|
|
611
|
+
const filterLoop = req.query.loop != null && req.query.loop !== '' ? Number(req.query.loop) : null;
|
|
612
|
+
|
|
613
|
+
if (!taskId) {
|
|
614
|
+
return res.error(400, 'taskId 为必填项');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 从已注册的实例中查找该 taskId 的所有 step
|
|
618
|
+
const stepList = [];
|
|
619
|
+
const seenStepKeys = new Set();
|
|
620
|
+
for (const [key, runner] of this._runners) {
|
|
621
|
+
if (runner.taskId === taskId) {
|
|
622
|
+
stepList.push(this._runnerToJSON(runner));
|
|
623
|
+
seenStepKeys.add(this._key(runner.taskId, runner.stepId));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 从 OrchestratorController 补充编排器管理的步骤(覆盖编排执行创建的实例)
|
|
628
|
+
if (this._orchestratorController) {
|
|
629
|
+
const orchSteps = this._orchestratorController.getTaskSteps(taskId);
|
|
630
|
+
for (const s of orchSteps) {
|
|
631
|
+
const k = this._key(taskId, s.stepId);
|
|
632
|
+
if (!seenStepKeys.has(k)) {
|
|
633
|
+
stepList.push(s);
|
|
634
|
+
seenStepKeys.add(k);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// 扫描该 task 自己的目录下的 .md 文件
|
|
640
|
+
// taskDir 直接就是 db/task/{taskId},文件直接放在该目录下(不在子目录中)
|
|
641
|
+
let files = [];
|
|
642
|
+
const taskDir = stepList[0]?.taskDir || null;
|
|
643
|
+
if (taskDir) {
|
|
644
|
+
try {
|
|
645
|
+
const allFiles = await fs.readdir(taskDir);
|
|
646
|
+
for (const f of allFiles) {
|
|
647
|
+
if (!f.endsWith('.md')) continue;
|
|
648
|
+
const fp = path.join(taskDir, f);
|
|
649
|
+
const fstat = await fs.stat(fp);
|
|
650
|
+
if (!fstat.isFile()) continue;
|
|
651
|
+
|
|
652
|
+
// 从文件名中解析 stepId 和 loop
|
|
653
|
+
// 文件命名格式: stepId{N}_loop{L}_result_{R}.md 或 stepId{N}_loop{L}_Summary_{R}.md
|
|
654
|
+
const stepIdMatch = f.match(/^stepId(\d+)/);
|
|
655
|
+
const loopMatch = f.match(/_loop(\d+)_/);
|
|
656
|
+
const fileStepId = stepIdMatch ? stepIdMatch[1] : null;
|
|
657
|
+
const fileLoop = loopMatch != null ? Number(loopMatch[1]) : null;
|
|
658
|
+
|
|
659
|
+
// 按 stepId 筛选
|
|
660
|
+
if (filterStepId && fileStepId !== filterStepId) continue;
|
|
661
|
+
// 按 loop 筛选
|
|
662
|
+
if (filterLoop != null && fileLoop !== filterLoop) continue;
|
|
663
|
+
|
|
664
|
+
files.push({
|
|
665
|
+
stepId: fileStepId || f,
|
|
666
|
+
loop: fileLoop,
|
|
667
|
+
filename: f,
|
|
668
|
+
path: fp,
|
|
669
|
+
size: fstat.size,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
} catch { /* 目录不存在 */ }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
res.json({
|
|
676
|
+
status: 'ok',
|
|
677
|
+
taskId,
|
|
678
|
+
steps: stepList,
|
|
679
|
+
files,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* GET /api/flexrunner/file?path=xxx
|
|
685
|
+
* 读取指定文件的内容(用于查看 result/summary .md)
|
|
686
|
+
*/
|
|
687
|
+
async file(req, res) {
|
|
688
|
+
const filePath = req.query.path;
|
|
689
|
+
if (!filePath) {
|
|
690
|
+
return res.error(400, 'path 为必填项');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
695
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
696
|
+
res.json({
|
|
697
|
+
status: 'ok',
|
|
698
|
+
path: filePath,
|
|
699
|
+
content,
|
|
700
|
+
});
|
|
701
|
+
} catch (err) {
|
|
702
|
+
res.error(404, `文件读取失败: ${err.message}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* GET /api/flexrunner/archive?taskId=xxx
|
|
708
|
+
* 打包下载任务目录下的全部 .md 报告文件(ZIP)。
|
|
709
|
+
*
|
|
710
|
+
* - 扫描 taskDir 根目录(不递归,排除 source/ 克隆仓库),收集所有 .md 文件
|
|
711
|
+
* - 检视报告在前、框架产物(stepId*_result/Summary)在后,同类自然排序
|
|
712
|
+
* - ZIP 内以 {taskId}/ 目录前缀组织,便于解压归档
|
|
713
|
+
* - 任务进行中调用时打包当前已生成的文件(实时快照)
|
|
714
|
+
*/
|
|
715
|
+
async archive(req, res) {
|
|
716
|
+
const { taskId } = req.query;
|
|
717
|
+
if (!taskId) {
|
|
718
|
+
return res.error(400, 'taskId 为必填项');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
// ── 1. 解析 taskDir ──
|
|
723
|
+
const taskDir = await this._resolveTaskDir(taskId);
|
|
724
|
+
if (!taskDir) {
|
|
725
|
+
return res.error(404, `未找到任务目录: taskId=${taskId}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ── 2. 收集根目录 .md 文件(withFileTypes + isFile,天然排除子目录)──
|
|
729
|
+
const entries = await fs.readdir(taskDir, { withFileTypes: true });
|
|
730
|
+
const mdFiles = entries
|
|
731
|
+
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
732
|
+
.map(e => e.name);
|
|
733
|
+
|
|
734
|
+
if (mdFiles.length === 0) {
|
|
735
|
+
return res.error(404, '暂无报告文件(任务尚未生成任何 .md 产物)');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ── 3. 排序:检视报告在前,框架产物(stepId*)在后 ──
|
|
739
|
+
const isFrameworkFile = (n) => /^stepId/i.test(n);
|
|
740
|
+
mdFiles.sort((a, b) => {
|
|
741
|
+
const fa = isFrameworkFile(a) ? 1 : 0;
|
|
742
|
+
const fb = isFrameworkFile(b) ? 1 : 0;
|
|
743
|
+
if (fa !== fb) return fa - fb;
|
|
744
|
+
return a.localeCompare(b, 'zh-CN', { numeric: true });
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// ── 4. 读取并打包(单文件读取失败跳过,不中断整体打包)──
|
|
748
|
+
const files = [];
|
|
749
|
+
for (const name of mdFiles) {
|
|
750
|
+
const fp = path.join(taskDir, name);
|
|
751
|
+
try {
|
|
752
|
+
const [data, st] = await Promise.all([fs.readFile(fp), fs.stat(fp)]);
|
|
753
|
+
files.push({ name: `${taskId}/${name}`, data, mtime: st.mtime });
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.warn(`[FlexRunnerController] archive 跳过读取失败文件: ${fp}, ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (files.length === 0) {
|
|
759
|
+
return res.error(404, '报告文件读取失败');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const zip = createZipBuffer(files);
|
|
763
|
+
|
|
764
|
+
// ── 5. 二进制响应(Content-Disposition 双写,兼容 ASCII 回退与 UTF-8)──
|
|
765
|
+
const raw = res.raw;
|
|
766
|
+
raw.writeHead(200, {
|
|
767
|
+
'Content-Type': 'application/zip',
|
|
768
|
+
'Content-Length': zip.length,
|
|
769
|
+
'Content-Disposition': `attachment; filename="${taskId}.zip"; filename*=UTF-8''${encodeURIComponent(taskId)}.zip`
|
|
770
|
+
});
|
|
771
|
+
raw.end(zip);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
if (!res.raw.headersSent) {
|
|
774
|
+
return res.error(500, `打包失败: ${err.message}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* 解析任务工作目录(供 archive 打包等场景使用)。
|
|
781
|
+
*
|
|
782
|
+
* 查找顺序:
|
|
783
|
+
* 1. 内存中活跃 FlexRunner 实例的 taskDir
|
|
784
|
+
* 2. OrchestratorController 中编排步骤元信息的 taskDir(含重启后恢复的实例)
|
|
785
|
+
* 3. 磁盘兜底:{rootDir}/task/{taskId}(taskId 白名单校验 + 路径边界检查,防目录穿越)
|
|
786
|
+
*
|
|
787
|
+
* @param {string} taskId
|
|
788
|
+
* @returns {Promise<string|null>} taskDir 绝对路径;未找到返回 null
|
|
789
|
+
* @private
|
|
790
|
+
*/
|
|
791
|
+
async _resolveTaskDir(taskId) {
|
|
792
|
+
// 1. 活跃 runner
|
|
793
|
+
for (const runner of this._runners.values()) {
|
|
794
|
+
if (runner.taskId === taskId && runner.taskDir) {
|
|
795
|
+
return runner.taskDir;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 2. 编排器步骤元信息
|
|
800
|
+
if (this._orchestratorController) {
|
|
801
|
+
const steps = this._orchestratorController.getTaskSteps(taskId);
|
|
802
|
+
const withDir = steps.find(s => s && s.taskDir);
|
|
803
|
+
if (withDir) return withDir.taskDir;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 3. 磁盘兜底(白名单 + 边界校验)
|
|
807
|
+
const rootDir = this._storage?.manager?.rootDir;
|
|
808
|
+
if (rootDir) {
|
|
809
|
+
if (!/^[A-Za-z0-9._-]+$/.test(taskId) || taskId === '.' || taskId === '..') {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const taskRoot = path.join(rootDir, 'task');
|
|
813
|
+
const resolved = path.resolve(taskRoot, taskId);
|
|
814
|
+
if (!resolved.startsWith(taskRoot + path.sep)) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const st = await fs.stat(resolved);
|
|
819
|
+
if (st.isDirectory()) return resolved;
|
|
820
|
+
} catch { /* 目录不存在 */ }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* GET /api/flexrunner/runners
|
|
828
|
+
* 列出所有已注册的 FlexRunner 实例
|
|
829
|
+
*/
|
|
830
|
+
async runners(req, res) {
|
|
831
|
+
const list = [];
|
|
832
|
+
for (const runner of this._runners.values()) {
|
|
833
|
+
list.push(this._runnerToJSON(runner));
|
|
834
|
+
}
|
|
835
|
+
res.json({
|
|
836
|
+
status: 'ok',
|
|
837
|
+
count: list.length,
|
|
838
|
+
runners: list,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ─── 单次执行 ─────────────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* POST /api/flexrunner/start-single
|
|
846
|
+
* 启动单次执行 — 创建一个只含单步骤的编排任务,不走 Brain 拆解和 LoopEngine 循环。
|
|
847
|
+
*
|
|
848
|
+
* 内部流程:
|
|
849
|
+
* 1. 生成 taskId(如果未指定)
|
|
850
|
+
* 2. 构造单步骤 taskDef(stepId=1, dependId=0, nextStepId=0, failStepId=0)
|
|
851
|
+
* 3. 委托 OrchestratorController.load() 加载任务 → 写 orchestration.json
|
|
852
|
+
* 4. 委托 OrchestratorController.start() 异步启动编排
|
|
853
|
+
* 5. 写精简版 loop.json(isSingleExecution=true)使任务列表可见
|
|
854
|
+
* 6. 编排结束后更新 loop.json 终态 + 释放重型引用
|
|
855
|
+
*
|
|
856
|
+
* Body: {
|
|
857
|
+
* taskDescription: string, // 必填
|
|
858
|
+
* skillDirectory?: string, // 可选
|
|
859
|
+
* specifiedSkill?: string, // 可选,指定 SKILL(name 或目录名),跳过自动匹配
|
|
860
|
+
* taskDir?: string, // 可选
|
|
861
|
+
* inputReport?: string, // 可选
|
|
862
|
+
* maxRetries?: number, // 可选,默认 3
|
|
863
|
+
* taskId?: string // 可选
|
|
864
|
+
* }
|
|
865
|
+
*
|
|
866
|
+
* 响应:异步启动,立即返回 taskId 和 stepId。
|
|
867
|
+
*/
|
|
868
|
+
async startSingle(req, res) {
|
|
869
|
+
const body = req.body || {};
|
|
870
|
+
const { taskDescription } = body;
|
|
871
|
+
|
|
872
|
+
if (!taskDescription) {
|
|
873
|
+
return res.error(400, 'taskDescription 为必填项');
|
|
874
|
+
}
|
|
875
|
+
if (!this._orchestratorController) {
|
|
876
|
+
return res.error(500, 'orchestratorController 未初始化');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
// ── 1. 生成 taskId ──
|
|
881
|
+
const taskId = body.taskId ||
|
|
882
|
+
`single_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`;
|
|
883
|
+
|
|
884
|
+
// 幂等保护:同一 taskId 若已有编排在运行,直接返回
|
|
885
|
+
let existingOrch;
|
|
886
|
+
try {
|
|
887
|
+
existingOrch = this._orchestratorController._getOrchestrator(taskId);
|
|
888
|
+
} catch { /* 不存在,正常 */ }
|
|
889
|
+
if (existingOrch && existingOrch.status === OrchestratorStatus.RUNNING) {
|
|
890
|
+
return res.json({
|
|
891
|
+
status: 'ok',
|
|
892
|
+
message: `单次执行已在运行中: taskId=${taskId}`,
|
|
893
|
+
taskId,
|
|
894
|
+
stepId: 1,
|
|
895
|
+
progress: existingOrch.getProgress()
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 若已存在同 taskId 的旧编排(已结束),先终止并移除
|
|
900
|
+
if (existingOrch) {
|
|
901
|
+
try { await existingOrch.abort(); } catch { /* 已结束 */ }
|
|
902
|
+
this._orchestratorController.disposeOrchestrator(taskId, { removeInstance: true });
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ── 2. 构造单步骤 taskDef ──
|
|
906
|
+
const maxRetries = typeof body.maxRetries === 'number' && body.maxRetries > 0
|
|
907
|
+
? body.maxRetries
|
|
908
|
+
: 3;
|
|
909
|
+
const skillDirectory = body.skillDirectory || '';
|
|
910
|
+
|
|
911
|
+
// taskDir 默认推导(Orchestrator.loadTask 内部也会推导,但 loop.json 需要提前知道)
|
|
912
|
+
const rootDir = this._storage?.manager?.rootDir;
|
|
913
|
+
const taskDir = body.taskDir ||
|
|
914
|
+
(rootDir
|
|
915
|
+
? path.resolve(rootDir, 'task', taskId)
|
|
916
|
+
: path.resolve(process.cwd(), 'db', 'task', taskId));
|
|
917
|
+
|
|
918
|
+
// 确保 taskDir 存在(opencode session 需要工作目录存在)
|
|
919
|
+
await fs.mkdir(taskDir, { recursive: true });
|
|
920
|
+
|
|
921
|
+
const taskDef = {
|
|
922
|
+
taskId,
|
|
923
|
+
taskDir,
|
|
924
|
+
skillDirectory,
|
|
925
|
+
loopCount: 0, // 单次执行固定为第 0 轮
|
|
926
|
+
steps: [{
|
|
927
|
+
stepId: 1,
|
|
928
|
+
taskDescription,
|
|
929
|
+
dependId: 0, // 入口步骤
|
|
930
|
+
nextStepId: 0, // 无后续
|
|
931
|
+
failStepId: 0, // 失败即终止
|
|
932
|
+
maxRetries,
|
|
933
|
+
skillDirectory,
|
|
934
|
+
specifiedSkill: body.specifiedSkill || '',
|
|
935
|
+
taskDir,
|
|
936
|
+
inputReport: body.inputReport || ''
|
|
937
|
+
}]
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// ── 3. 委托 OrchestratorController 加载任务 ──
|
|
941
|
+
const loadReq = { body: taskDef };
|
|
942
|
+
const loadRes = this._createMockResponse();
|
|
943
|
+
await this._orchestratorController.load(loadReq, loadRes);
|
|
944
|
+
|
|
945
|
+
if (loadRes._status >= 400) {
|
|
946
|
+
return res.error(loadRes._status, loadRes._data?.error || loadRes._data?.message || '编排任务加载失败');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ── 4. 启动编排(直接调用 orch.start(),便于跟踪完成) ──
|
|
950
|
+
const orch = this._orchestratorController._getOrchestrator(taskId);
|
|
951
|
+
|
|
952
|
+
// ── 5. 写精简版 loop.json(使任务列表可见) ──
|
|
953
|
+
await this._writeSingleLoopJson(taskId, taskDescription, taskDir, skillDirectory, 'executing', null, null);
|
|
954
|
+
|
|
955
|
+
// ── 6. 异步启动编排,完成后更新 loop.json 终态 + 释放重型引用 ──
|
|
956
|
+
orch.start()
|
|
957
|
+
.catch(err => {
|
|
958
|
+
console.error(`[FlexRunnerController] 单次执行异常: taskId=${taskId}, ${err.message}`);
|
|
959
|
+
})
|
|
960
|
+
.finally(() => {
|
|
961
|
+
this._onSingleExecutionComplete(taskId, orch).catch(err => {
|
|
962
|
+
console.warn(`[FlexRunnerController] 单次执行收尾失败: taskId=${taskId}, ${err.message}`);
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
res.json({
|
|
967
|
+
status: 'ok',
|
|
968
|
+
message: `单次执行已启动: taskId=${taskId}`,
|
|
969
|
+
taskId,
|
|
970
|
+
stepId: 1,
|
|
971
|
+
progress: orch.getProgress()
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
} catch (err) {
|
|
975
|
+
res.error(400, err.message);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* 创建模拟 Response 对象(供调用 OrchestratorController 的 load/start)。
|
|
981
|
+
* @private
|
|
982
|
+
*/
|
|
983
|
+
_createMockResponse() {
|
|
984
|
+
const mock = {
|
|
985
|
+
_status: 200,
|
|
986
|
+
_data: null,
|
|
987
|
+
statusCode: 200,
|
|
988
|
+
headersSent: false,
|
|
989
|
+
writableEnded: false,
|
|
990
|
+
json(data) {
|
|
991
|
+
this._data = data;
|
|
992
|
+
this.writableEnded = true;
|
|
993
|
+
},
|
|
994
|
+
error(code, message) {
|
|
995
|
+
this._status = code;
|
|
996
|
+
this._data = { status: 'error', error: message };
|
|
997
|
+
this.writableEnded = true;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
return mock;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* 写精简版 loop.json(单次执行任务专用)。
|
|
1005
|
+
*
|
|
1006
|
+
* 格式与 LoopEngine._persistLoopState 兼容,附加 isSingleExecution 标记。
|
|
1007
|
+
*
|
|
1008
|
+
* @param {string} taskId
|
|
1009
|
+
* @param {string} taskDescription
|
|
1010
|
+
* @param {string} taskDir
|
|
1011
|
+
* @param {string} skillDirectory
|
|
1012
|
+
* @param {string} status - executing | completed | failed | aborted
|
|
1013
|
+
* @param {string|null} startTime
|
|
1014
|
+
* @param {string|null} endTime
|
|
1015
|
+
* @private
|
|
1016
|
+
*/
|
|
1017
|
+
async _writeSingleLoopJson(taskId, taskDescription, taskDir, skillDirectory, status, startTime, endTime) {
|
|
1018
|
+
if (!this._storage) return;
|
|
1019
|
+
try {
|
|
1020
|
+
// 映射 loop 状态到 orchestrator 状态(用于前端 rounds 渲染)
|
|
1021
|
+
var orchStatus;
|
|
1022
|
+
if (status === 'executing') orchStatus = 'running';
|
|
1023
|
+
else if (status === 'passed') orchStatus = 'completed';
|
|
1024
|
+
else if (status === 'aborted') orchStatus = 'aborted';
|
|
1025
|
+
else orchStatus = 'failed';
|
|
1026
|
+
|
|
1027
|
+
const state = {
|
|
1028
|
+
taskId,
|
|
1029
|
+
taskDescription,
|
|
1030
|
+
userAcceptanceCriteria: '',
|
|
1031
|
+
taskDir,
|
|
1032
|
+
skillDirectory,
|
|
1033
|
+
maxLoops: 1,
|
|
1034
|
+
currentLoop: 1,
|
|
1035
|
+
status,
|
|
1036
|
+
startTime: startTime || nowCST(),
|
|
1037
|
+
endTime,
|
|
1038
|
+
error: null,
|
|
1039
|
+
aborted: status === 'aborted',
|
|
1040
|
+
isSingleExecution: true,
|
|
1041
|
+
rounds: [{
|
|
1042
|
+
loop: 0,
|
|
1043
|
+
startTime: startTime || nowCST(),
|
|
1044
|
+
endTime,
|
|
1045
|
+
passed: status === 'passed',
|
|
1046
|
+
error: null,
|
|
1047
|
+
planningIsReplan: false,
|
|
1048
|
+
planningStepCount: 1,
|
|
1049
|
+
orchestratorStatus: orchStatus
|
|
1050
|
+
}],
|
|
1051
|
+
persistedAt: nowCST()
|
|
1052
|
+
};
|
|
1053
|
+
await this._storage.json('task').write(`${taskId}/loop`, state);
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
console.warn(`[FlexRunnerController] 写 loop.json 失败: taskId=${taskId}, ${err.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* 单次执行编排完成后的收尾处理。
|
|
1061
|
+
*
|
|
1062
|
+
* 1. 更新 loop.json 的终态状态
|
|
1063
|
+
* 2. 释放 Orchestrator 重型引用(FlexRunner/CLI/LLM 等)
|
|
1064
|
+
*
|
|
1065
|
+
* @param {string} taskId
|
|
1066
|
+
* @param {object} orch - Orchestrator 实例
|
|
1067
|
+
* @private
|
|
1068
|
+
*/
|
|
1069
|
+
async _onSingleExecutionComplete(taskId, orch) {
|
|
1070
|
+
const status = orch.status;
|
|
1071
|
+
|
|
1072
|
+
// 映射编排终态到 loop.json 状态
|
|
1073
|
+
let loopStatus;
|
|
1074
|
+
if (status === OrchestratorStatus.COMPLETED) {
|
|
1075
|
+
loopStatus = 'passed';
|
|
1076
|
+
} else if (status === OrchestratorStatus.ABORTED) {
|
|
1077
|
+
loopStatus = 'aborted';
|
|
1078
|
+
} else {
|
|
1079
|
+
loopStatus = 'failed';
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const startTime = orch.startTime;
|
|
1083
|
+
const endTime = orch.endTime;
|
|
1084
|
+
|
|
1085
|
+
// 读取 taskDescription(从编排器获取)
|
|
1086
|
+
const progress = orch.getProgress();
|
|
1087
|
+
const step = progress.steps && progress.steps[0];
|
|
1088
|
+
const taskDescription = step ? step.taskDescription : '';
|
|
1089
|
+
const taskDir = orch._taskDir || '';
|
|
1090
|
+
const skillDirectory = orch._skillDirectory || '';
|
|
1091
|
+
|
|
1092
|
+
// 更新 loop.json
|
|
1093
|
+
await this._writeSingleLoopJson(
|
|
1094
|
+
taskId, taskDescription, taskDir, skillDirectory,
|
|
1095
|
+
loopStatus, startTime, endTime
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
// 释放重型引用
|
|
1099
|
+
if (this._orchestratorController) {
|
|
1100
|
+
try {
|
|
1101
|
+
this._orchestratorController.disposeOrchestrator(taskId, { removeInstance: true });
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.warn(`[FlexRunnerController] 释放 Orchestrator 失败: taskId=${taskId}, ${err.message}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* DELETE /api/flexrunner/runner?taskId=xxx&stepId=yyy
|
|
1110
|
+
* 移除一个已注册的 FlexRunner 实例
|
|
1111
|
+
*/
|
|
1112
|
+
async removeRunner(req, res) {
|
|
1113
|
+
const taskId = req.query.taskId;
|
|
1114
|
+
const stepId = Number(req.query.stepId);
|
|
1115
|
+
const key = this._key(taskId, stepId);
|
|
1116
|
+
const existed = this._runners.delete(key);
|
|
1117
|
+
res.json({
|
|
1118
|
+
status: 'ok',
|
|
1119
|
+
removed: existed,
|
|
1120
|
+
message: existed ? '实例已移除' : '实例不存在',
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
export default FlexRunnerController;
|