@whyshu/argent-engine 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +378 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ """ensure-store-windows —— 一条命令把「该开的店铺窗口」开好、CDP 端口对齐、可驱动性断言过。
3
+
4
+ python3 scripts/ensure-store-windows.py # 预演(**默认 dry-run**,零写入)
5
+ python3 scripts/ensure-store-windows.py --apply # 真做(开窗 + 写 cdp-port-map.json)
6
+ python3 scripts/ensure-store-windows.py --apply --json
7
+
8
+ 退出码:`0` 全部就绪(含本次已开/已对齐);`1` 有店不可用或**还需要动作**(逐店打印状态与原因);
9
+ `2` 用法错误(未知 `--store` 名 / 未实现的开关 / 缺 stores.json 的条目)。
10
+
11
+ ## 为什么要有它(2026-10-11 生产现场,已恢复,但过程暴露了缺口)
12
+
13
+ 客户机 FULL 触发器**瞬时**报「Bridge 正常但**没有任何店铺**」(13:13/13:18),随后自愈
14
+ (13:35/13:41 又报「就绪(6 个店铺)」)。恢复窗口里 Lead 是用**引擎已有能力**一步步拼出来的:
15
+
16
+ ① 用引擎既有的「安全重启客户端」实现 + `ensure_webdriver_client` 重启客户端;
17
+ ② **WebDriver API 仍不可用**:`getBrowserList` 返回 `statusCode=0, data=None`(0 个店铺)
18
+ ⇒ `startBrowser` 报 `-10004 browserId或browserOauth缺失`;
19
+ ③ 改用 **CLI 通道**(客户端自带的命令行)`store open --id <storeId> --expected-name <店名>`
20
+ ⇒ `✓ 店铺已打开`(`launchSource: cli`);
21
+ ④ 但 `~/.dsh/argent/cdp-port-map.json` 里还是**旧端口**(1 店 32826 已随重启消失、
22
+ 2 店 15079 过期)⇒ **引擎找不到窗口**;
23
+ ⑤ 用引擎**自己的对齐判据**收口:`_discover_cdp_ports()` → `[13018, 27893]`,
24
+ `_match_port_by_user_data_dir(store_id, ports)` → 1 店=27893 / 2 店=13018,
25
+ 再 `write_cdp_port_map()` 写映射;
26
+ ⑥ 复测(**不给 `--cdp-url`**,让引擎自解析):两店 `query_runner --mode shipment` 都 `found=True`。
27
+
28
+ **缺的不是能力,而是「CLI 开窗后没人把新端口写进映射」这一步**——正常路径是引擎 `startBrowser`
29
+ 时顺手写,而 CLI 开的窗口**绕过了它**。本脚本把那 ⑥ 步收成一条**幂等**命令。
30
+
31
+ ## 只复用引擎既有实现(⛔ 不新造第二套)
32
+
33
+ | 用到的既有实现 | 干什么 |
34
+ |---|---|
35
+ | `orchestrator._discover_cdp_ports()` | 端口发现(`lsof` 客户端进程 + `/json/version` 逐个校验)—— **薄委托**到 `scripts/lib/cdp_ports.py` |
36
+ | `orchestrator._match_port_by_user_data_dir()` | **归属判据**:进程命令行含 `chrome_<store_id>` —— 同上,**薄委托** |
37
+ | `orchestrator.write_cdp_port_map()` | 写 `cdp-port-map.json`(**唯一**写入口) |
38
+ | `orchestrator.Orchestrator._port_from_cdp_map()` | 读映射(同一份解析,⛔ 不另写第二份) |
39
+ | `orchestrator._is_cdp_port()` | `/json/version` 校验(端口是不是浏览器调试端口)—— **薄委托** |
40
+ | `orchestrator._acquire_open_port_lock()` / `_release_open_port_lock()` | 全局开店互斥(差集不被别进程污染) |
41
+ | `orchestrator.close_store_bridge()` | 关窗 —— **仅** `--force-reopen <店>` 显式点名时 |
42
+
43
+ ⚠️ 2026-10-11 起**端口发现/归属/`/json/list` 探针的实现**都在 `scripts/lib/cdp_ports.py`
44
+ (编排器与认领前环境预检的第二层回退共用同一份)⇒ 上面两个 `orchestrator._*` 名字仍在、语义不变,
45
+ 但它们是**薄委托**;本脚本的调用点**一个都没改**(回归里 `esw.orch is orchestrator` 的断言照旧成立)。
46
+
47
+ 开窗的**唯一**出口 = 客户端自带的命令行 `store open --id <storeId> --expected-name <店名>`;
48
+ `--expected-name` **必带**(id 与店名对不上时客户端自己拒绝 ⇒ 防「按 id 开错店」)。
49
+ 它的可执行名**不在本文件里写第二份字面量**:见下面 `CLI_BIN` 的取值注释。
50
+
51
+ ## ⛔ 不做的事
52
+
53
+ · ⛔ **不 `startBrowser`**(API 通道今天实测不可用;`--api-first` 会**明确拒绝**而不是假装已做);
54
+ · ⛔ 不关任何窗口(**只有** `--force-reopen <店>` 才关,且只在 `--apply` 下);
55
+ · ⛔ 不写任何业务流程数据、不碰票、不连飞书、不碰客户机;
56
+ · ⛔ 归属确认不到就**拒绝乱连**(绝不"猜一个端口"),且**不写映射**。
57
+
58
+ ## 可注入(离线回归用:⛔ 不碰真客户端、⛔ 不联网)
59
+
60
+ · **引擎桩**:本脚本一律**按模块属性**调用引擎(`orch.X(...)`,取属性发生在每次调用时)
61
+ ⇒ 回归替换 `orchestrator._discover_cdp_ports` / `._match_port_by_user_data_dir` /
62
+ `._is_cdp_port` / `.write_cdp_port_map` / `.close_store_bridge` 即可。
63
+ · **本模块的两个出口**:`cli_open()`(外部命令)与 `cdp_page_targets()`(`/json/list` 只读探针 ——
64
+ 自 2026-10-11 起它是 `scripts/lib/cdp_ports.py#page_targets` 的**薄委托**,`fetch(url, timeout)`
65
+ 形参原样保留,故回归仍按本模块属性替换 `esw.cdp_page_targets`)。
66
+ · **隔离点**(既有 env,非本脚本新造):`ARGENT_DATA_DIR` / `STORES_JSON` / `CDP_PORT_MAP`。
67
+
68
+ 回归:`python3 scripts/tests/ensure-store-windows.test.py`(52 断言纯夹具,见该文件页首)。
69
+
70
+ ## 为什么本文件里看不到客户端/平台的名字
71
+
72
+ `verticals/_engine-words.json` 规定引擎文件(`scripts/**`,不含 `scripts/tests/`)不得**新增**业务词,
73
+ `node scripts/vertical-lint.mjs` 的 engine-words 档按 `verticals/_engine-words.baseline.txt` 只拦新增
74
+ (⛔ 不抬基线 —— 2026-10-09 的 `scripts/ml_hub_status.py` 就是这么把平台域名移出去的)。
75
+ 所以:① 客户端可执行名**引用**既有唯一字面量(见 `CLI_BIN`);② 其余一律用中性说法
76
+ (「客户端」「店铺浏览器」「店铺窗口」)。这不是洁癖:它保证这个文件可以随任一能力包分发而不带业务绑定。
77
+ """
78
+ from __future__ import annotations
79
+
80
+ import argparse
81
+ import json
82
+ import pathlib
83
+ import subprocess
84
+ import sys
85
+ import time
86
+ from typing import Any, Optional
87
+
88
+ HERE = pathlib.Path(__file__).resolve().parent
89
+ # `scripts/` 与 `scripts/lib/` 进 sys.path:让 `import orchestrator` 及其同级依赖
90
+ # (`flow_registry` / `argent_datadir` / `argent_filelock` / `webdriver_ipc`…)都能解析。
91
+ # ⚠️ 顺序**必须先于** import orchestrator:否则它内部的 `import flow_registry` 会静默走
92
+ # fallback(读到未合并的 base 注册表),那种"降级产出错值"在本仓库已踩过多次。
93
+ sys.path.insert(0, str(HERE))
94
+ sys.path.insert(0, str(HERE / "lib"))
95
+
96
+ import env_probe # noqa: E402 —— 只为借它的 `DEFAULT_STORE_CMD` 取客户端可执行名(见下)
97
+ import cdp_ports # noqa: E402 —— `/json/list` 只读探针的**唯一实现**(与 `env_probe` 共用)
98
+ import orchestrator as orch # noqa: E402 —— 引擎既有实现的**唯一**来源(⛔ 本文件不复制一份)
99
+
100
+ # 客户端 CLI 的可执行名:**引用** `env_probe.DEFAULT_STORE_CMD` 的第 0 项,⛔ 不在本文件里写第二份字面量。
101
+ # 为什么必须绕这一下:该名字是 `verticals/_engine-words.json` 里的业务词,engine-words 门禁
102
+ # **只拦新增且不许抬基线** ⇒ 新引擎文件里出现它就是红的。而 `env_probe.py` 里那份字面量早已带基线记账,
103
+ # 且它自己的注释就写着「**唯一字面量**:多处默认值都引用它 —— 第二份字面量会被 engine-words 记成新增」。
104
+ # ⇒ 引用它,而不是复制它。(`env_probe` 只依赖 stdlib + `scripts/lib/*`,import 无副作用。)
105
+ CLI_BIN = env_probe.DEFAULT_STORE_CMD[0]
106
+
107
+ # ── 每店状态(**稳定字符串**,`--json` 与退出码都按它判)──────────────────────────────
108
+ ST_READY = "ready" # 已就绪(映射对 + 可驱动)⇒ 零副作用跳过
109
+ ST_ALIGNED = "aligned" # 窗口在跑、映射过期 ⇒ 本次只对齐映射(⛔ 没开窗/关窗)
110
+ ST_OPENED = "opened" # 本次开了窗并写入映射
111
+ ST_REOPENED = "reopened" # 本次按 --force-reopen 关掉再开
112
+ ST_WOULD_ALIGN = "would-align" # dry-run:需要对齐映射
113
+ ST_WOULD_OPEN = "would-open" # dry-run:需要开窗
114
+ ST_WOULD_REOPEN = "would-reopen" # dry-run:需要关掉再开
115
+ ST_FAILED = "failed" # 不可用(原因见 reason)
116
+ DONE_STATES = (ST_READY, ST_ALIGNED, ST_OPENED, ST_REOPENED)
117
+
118
+ # `--api-first` 的拒绝文案(见模块 docstring「⛔ 不做的事」)。
119
+ API_FIRST_REFUSAL = (
120
+ "WebDriver API 通道(startBrowser)**当前不可用**,本脚本不实现它。"
121
+ "依据 = 2026-10-11 生产实测:`getBrowserList` 返回 `statusCode=0, data=None`(0 个店铺)"
122
+ "⇒ `startBrowser` 报 `-10004 browserId或browserOauth缺失`;而 CLI 通道"
123
+ "(客户端自带的命令行 `store open --id … --expected-name …`)当场可用。"
124
+ "⇒ 现在只用 CLI 通道。将来 API 恢复后若要加成**优先**路径,须先在 orchestrator 侧验通"
125
+ "`getBrowserList` 非空,再在此实现、并保留 `--apply` 门。此刻给出 `--api-first` 只为一件事:"
126
+ "**绝不假装已做**。"
127
+ )
128
+
129
+
130
+ def log(msg: str) -> None:
131
+ """进度一律走 stderr(stdout 只留最终结论,`--json` 时更是**只有**一个 JSON 对象)。"""
132
+ print(f"[{time.strftime('%H:%M:%S')}] {msg}", file=sys.stderr, flush=True)
133
+
134
+
135
+ # ── 只读探针:/json/list(**唯一实现**已下沉到 `scripts/lib/cdp_ports.py`)──────────────
136
+ def cdp_page_targets(port: int, timeout: float = 2.0,
137
+ fetch=None) -> list[dict[str, Any]]:
138
+ """只读 `GET http://127.0.0.1:<port>/json/list` ⇒ 可附加的 `type=="page"` 目标。
139
+
140
+ 「**页面可达**」判据用它:`/json/version` 只证明浏览器进程在,**不证明有可驱动的标签页**。
141
+
142
+ **薄委托**:实现在 `scripts/lib/cdp_ports.py#page_targets` —— 认领前环境预检的第二层回退
143
+ (`scripts/env_probe.py`)也要判「页面可达」,**两处共用同一份**(⛔ 不复制)。
144
+ `fetch(url, timeout)` 仍是本模块的注入点(既有回归按本模块属性替换本函数)。
145
+ """
146
+ return cdp_ports.page_targets(int(port), timeout=timeout, fetch=fetch)
147
+
148
+
149
+ def cdp_drivable(port: Optional[int], timeout: float) -> tuple[bool, str]:
150
+ """**可驱动性**判据(两条都要过):`/json/version` 可达 ∧ `/json/list` 有 page 目标。"""
151
+ if not port:
152
+ return False, "没有端口"
153
+ if not orch._is_cdp_port(int(port), timeout):
154
+ return False, f"CDP `/json/version` 不可达(127.0.0.1:{port} 没在监听,或不是浏览器调试端口)"
155
+ pages = cdp_page_targets(int(port), timeout)
156
+ if not pages:
157
+ return False, f"CDP `/json/list` 没有可附加的 page 目标(127.0.0.1:{port} 的窗口还没就绪?)"
158
+ return True, f"CDP 可驱动({len(pages)} 个 page 目标)"
159
+
160
+
161
+ def cli_open(store_id: str, expected_name: str, timeout: float) -> tuple[int, str]:
162
+ """客户端命令行 `store open --id <storeId> --expected-name <店名>`(**唯一**开窗出口,可注入)。
163
+
164
+ `--expected-name` **必带**:CLI 会逐字比对 id 与店名,对不上就拒绝 ⇒ 防"按 id 开错店"。
165
+ 返回 `(returncode, 合并输出)`,**不抛**(命令缺失/超时一律折成非零 + 文案)。
166
+ """
167
+ cmd = [CLI_BIN, "store", "open",
168
+ "--id", str(store_id), "--expected-name", str(expected_name)]
169
+ try:
170
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout,
171
+ stdin=subprocess.DEVNULL)
172
+ except FileNotFoundError as exc:
173
+ return 127, f"{CLI_BIN} 不在 PATH 上:{exc}"
174
+ except subprocess.TimeoutExpired as exc:
175
+ return 124, f"{CLI_BIN} store open 超时({timeout:g}s):{exc}"
176
+ except Exception as exc: # noqa: BLE001
177
+ return 1, f"{type(exc).__name__}: {exc}"
178
+ return p.returncode, ((p.stdout or "") + (p.stderr or "")).strip()
179
+
180
+
181
+ # ── 映射读回(复用引擎解析,⛔ 不另写第二份)──────────────────────────────────────
182
+ def map_port(store_id: str) -> Optional[int]:
183
+ """从 `cdp-port-map.json` 按 store_id 读端口。
184
+
185
+ 直接调引擎的 `Orchestrator._port_from_cdp_map` —— 它**不使用 self**(只调
186
+ `_cdp_port_map_path()` 读文件),故以 `None` 作 self 调用是安全的;这么做的唯一理由是
187
+ **解析只有一份**(⛔ 不在本文件里重写一遍"读 JSON + 取 cdp_port")。
188
+ """
189
+ try:
190
+ return orch.Orchestrator._port_from_cdp_map(None, store_id) # type: ignore[arg-type]
191
+ except Exception: # noqa: BLE001
192
+ return None
193
+
194
+
195
+ def _assert_aligned(store_id: str, port: int, probe_timeout: float) -> tuple[bool, str]:
196
+ """写映射后的**读回验收**(三条都要过):映射读回 == port ∧ 可驱动 ∧ 归属仍指向该店。
197
+
198
+ "写进去了" ≠ "写得对":映射是**运行时事实源**,写错等于把引擎指向别人家的窗口。
199
+ """
200
+ back = map_port(store_id)
201
+ if back != port:
202
+ return False, f"写后读回不一致:映射 {back} ≠ 目标 {port}"
203
+ ok, why = cdp_drivable(port, probe_timeout)
204
+ if not ok:
205
+ return False, f"写后断言失败:{why}"
206
+ try:
207
+ owned = orch._match_port_by_user_data_dir(store_id, [port])
208
+ except Exception as exc: # noqa: BLE001
209
+ return False, f"写后归属复核失败:{type(exc).__name__}: {exc}"
210
+ if owned != port:
211
+ return False, f"写后断言失败:端口 {port} 的归属确认不到 `chrome_{store_id}`"
212
+ return True, f"映射已写并读回一致({port});{why};归属 `chrome_{store_id}` 已确认"
213
+
214
+
215
+ def _wait_for_owned_port(store_id: str, before: list[int],
216
+ args: argparse.Namespace) -> tuple[Optional[int], str]:
217
+ """开窗后**轮询直到确认归属**(归属 + 可驱动),或到点/撞歧义。
218
+
219
+ 归属判据 = 引擎的 `_match_port_by_user_data_dir`(`chrome_<store_id>`)。
220
+ `before` 是开窗前快照,仅用来在原因里区分"本次新增"与"复用既有窗口"。
221
+ **多个新端口且一个都匹配不上 ⇒ 立刻拒绝**(⛔ 不猜一个、⛔ 不写映射)。
222
+ """
223
+ deadline = time.time() + max(0.0, float(args.timeout))
224
+ last_ports: list[int] = []
225
+ last_reason = "还没有可用的端口快照"
226
+ while True:
227
+ ports = orch._discover_cdp_ports()
228
+ new = [p for p in ports if p not in before]
229
+ last_ports = ports
230
+ try:
231
+ owned = orch._match_port_by_user_data_dir(store_id, ports)
232
+ except Exception as exc: # noqa: BLE001
233
+ owned = None
234
+ last_reason = f"归属匹配查询异常:{type(exc).__name__}: {exc}"
235
+ if owned is not None:
236
+ ok, why = cdp_drivable(owned, args.probe_timeout)
237
+ if ok:
238
+ tag = "本次新增" if owned in new else "复用的既有窗口"
239
+ return int(owned), f"端口 {owned}({tag})"
240
+ last_reason = f"端口 {owned} 归属已确认但还不可驱动:{why}"
241
+ else:
242
+ last_reason = f"端口归属未确认(`chrome_{store_id}` 一个都没命中)"
243
+ if len(new) >= 2:
244
+ return None, (f"**归属歧义**:本次开窗新增 {len(new)} 个端口 {new},"
245
+ f"其中没有一个属于 `chrome_{store_id}` ⇒ **拒绝乱连**"
246
+ f"(⛔ 不猜一个端口,⛔ 不写映射)")
247
+ if time.time() >= deadline:
248
+ return None, (f"{args.timeout:g}s 内没能确认可驱动的店铺窗口:{last_reason}"
249
+ f"(已发现端口 {last_ports};开窗前 {before})")
250
+ time.sleep(max(0.01, float(args.interval)))
251
+
252
+
253
+ # ── 店铺解析 ────────────────────────────────────────────────────────────────────
254
+ def _pick_store(stores: dict[str, dict], name: str) -> tuple[Optional[str], Optional[dict]]:
255
+ """按 stores.json 的 **key**(= 客户端环境名)或 value 的 `name` 字段解析(与
256
+ `orchestrator._resolve_store` 同一口径:两种写法都认)。找不到 ⇒ `(None, None)`。"""
257
+ if name in stores:
258
+ return name, stores[name]
259
+ for k, v in stores.items():
260
+ if str((v or {}).get("name") or "") == name:
261
+ return k, v
262
+ return None, None
263
+
264
+
265
+ def build_targets(args: argparse.Namespace, stores: dict[str, dict]
266
+ ) -> tuple[list[dict[str, Any]], list[str]]:
267
+ """要处理的店铺清单。缺省 = stores.json **全部**(排序保证可复现);`--store` 收窄。
268
+
269
+ 返回 `(targets, problems)`;`problems` 非空 ⇒ 用法错误(exit 2)。
270
+ """
271
+ names = list(dict.fromkeys(list(args.store) + list(args.force_reopen))) # 保序去重
272
+ names = names or sorted(stores)
273
+ targets: list[dict[str, Any]] = []
274
+ problems: list[str] = []
275
+ for n in names:
276
+ key, info = _pick_store(stores, n)
277
+ if key is None:
278
+ problems.append(f"店铺 {n!r} 不在 stores.json 里(可用:{'、'.join(sorted(stores))})")
279
+ continue
280
+ sid = str((info or {}).get("store_id") or "").strip()
281
+ if not sid:
282
+ problems.append(f"店铺 {key!r} 的 stores.json 条目缺 `store_id`(开不了窗)")
283
+ continue
284
+ targets.append({
285
+ "env_name": key, # stores.json 的 key
286
+ "store_id": sid,
287
+ "name": str((info or {}).get("name") or key), # 传给 CLI 的店名(--expected-name)
288
+ "platform": str((info or {}).get("platform") or ""),
289
+ })
290
+ return targets, problems
291
+
292
+
293
+ # ── 每店:对齐 / 开窗 / 断言 ───────────────────────────────────────────────────────
294
+ def process_store(t: dict[str, Any], args: argparse.Namespace,
295
+ apply: bool, force: bool) -> dict[str, Any]:
296
+ """幂等处理一个店,返回状态 dict(**不抛**:所有失败都折成 ST_FAILED + reason)。"""
297
+ sid = str(t["store_id"])
298
+ name = str(t["name"])
299
+ r: dict[str, Any] = {"store": t["env_name"], "storeId": sid, "name": name,
300
+ "status": ST_FAILED, "port": None, "mapPort": None,
301
+ "changed": False, "reason": ""}
302
+ try:
303
+ ports = orch._discover_cdp_ports()
304
+ except Exception as exc: # noqa: BLE001
305
+ r["reason"] = f"CDP 端口发现失败:{type(exc).__name__}: {exc}"
306
+ return r
307
+ r["mapPort"] = map_port(sid)
308
+
309
+ # ── ① 本机已有窗口 ⇒ 只对齐映射(⛔ 不开窗、⛔ 不关窗;这是"CLI 开窗后没人写映射"的正解)──
310
+ try:
311
+ owned = orch._match_port_by_user_data_dir(sid, ports)
312
+ except Exception as exc: # noqa: BLE001
313
+ owned = None
314
+ log(f" ⚠️ 归属匹配查询异常(按未确认处理):{type(exc).__name__}: {exc}")
315
+ if owned is not None:
316
+ ok, why = cdp_drivable(owned, args.probe_timeout)
317
+ # `--force-reopen` 是**显式要求重开**:窗口此刻再健康也照办(⛔ 不被幂等短路掉)。
318
+ if ok and not force:
319
+ if r["mapPort"] == owned:
320
+ r.update(status=ST_READY, port=int(owned),
321
+ reason=f"跳过(幂等,零副作用):映射已是实际端口 {owned};{why}")
322
+ return r
323
+ if not apply:
324
+ r.update(status=ST_WOULD_ALIGN, port=int(owned),
325
+ reason=f"端口漂移:映射 {r['mapPort']} → 实际 {owned}"
326
+ f"(dry-run 未写;{why})")
327
+ return r
328
+ orch.write_cdp_port_map(store_id=sid, store_name=t["env_name"],
329
+ env_name=t["platform"], port=int(owned))
330
+ good, why2 = _assert_aligned(sid, int(owned), args.probe_timeout)
331
+ r.update(status=ST_ALIGNED if good else ST_FAILED, port=int(owned), changed=True,
332
+ reason=(f"端口漂移已对齐:映射 {r['mapPort']} → {owned};{why}"
333
+ if good else why2))
334
+ return r
335
+ need = (f"端口 {owned} 归属已确认、{why}"
336
+ if ok else f"端口 {owned} 归属已确认但不可驱动:{why}")
337
+ else:
338
+ need = (f"本机没有属于该店的 CDP 端口(`chrome_{sid}` 未命中;"
339
+ f"已发现 {ports})")
340
+
341
+ # ── ② 需要开窗(dry-run 到此为止)──────────────────────────────────────────────
342
+ if not apply:
343
+ r.update(status=ST_WOULD_REOPEN if force else ST_WOULD_OPEN,
344
+ reason=f"{need} ⇒ 需要开窗(dry-run 未开)")
345
+ return r
346
+
347
+ # ⚠️ 这里**刻意不调**引擎的云端店铺清单硬校验(`store list` 那条「店名必须逐字在云端清单里」)。
348
+ # 不是忘了,两条理由:① 它的判据是**云端清单**,而 2026-10-11 现场正是清单拉不到(0 项)
349
+ # ⇒ 它恰好在"最需要恢复"的那一刻不构成判据;② 那份硬校验的名字是业务词,本文件写不得
350
+ # (见文件头「为什么本文件里看不到客户端/平台的名字」)。
351
+ # 防「开错店」改由**在开窗路径上真正生效**的两道判据兜住:
352
+ # ① 客户端自己的 `--expected-name`:逐字校验 id ↔ 店名,对不上直接拒绝;
353
+ # ② 开窗后的归属断言:端口必须命中 `chrome_<store_id>`,否则**不写映射**(见 `_assert_aligned`)。
354
+ # 若将来要把云端清单门也加上,请把它加在**引擎侧**(与 `store list` 的既有实现放一起),
355
+ # ⛔ 不要在引擎文件里写下第二份客户端字面量。
356
+
357
+ # ── ③ 关窗:**只有** --force-reopen 显式点名才关 ────────────────────────────────
358
+ if force:
359
+ try:
360
+ orch.close_store_bridge(sid, log=lambda m: log(f" {m}"))
361
+ except Exception as exc: # noqa: BLE001
362
+ r["reason"] = f"关窗失败(--force-reopen):{exc}"
363
+ return r
364
+
365
+ # ── ④ 开窗:**持全局开店互斥**(差集临界区,⛔ 无锁绝不开)──────────────────────
366
+ try:
367
+ lock = orch._acquire_open_port_lock()
368
+ except Exception as exc: # noqa: BLE001
369
+ r["reason"] = f"拿不到全局开店互斥锁(fail-closed,⛔ 不无锁开店):{exc}"
370
+ return r
371
+ try:
372
+ before = orch._discover_cdp_ports()
373
+ rc, out = cli_open(sid, name, args.timeout)
374
+ if rc != 0:
375
+ r["reason"] = f"{CLI_BIN} store open 失败(rc={rc}):{out[:300]}"
376
+ return r
377
+ log(f" ✓ 店铺已打开({name},launchSource=cli)")
378
+ port, why = _wait_for_owned_port(sid, before, args)
379
+ if port is None:
380
+ r["reason"] = why
381
+ return r
382
+ orch.write_cdp_port_map(store_id=sid, store_name=t["env_name"],
383
+ env_name=t["platform"], port=int(port))
384
+ good, why2 = _assert_aligned(sid, int(port), args.probe_timeout)
385
+ r.update(status=(ST_REOPENED if force else ST_OPENED) if good else ST_FAILED,
386
+ port=int(port), changed=True,
387
+ reason=(f"已开窗并写入映射:{why};{why2}" if good else why2))
388
+ return r
389
+ finally:
390
+ orch._release_open_port_lock(lock)
391
+
392
+
393
+ # ── CLI ────────────────────────────────────────────────────────────────────────
394
+ def build_parser() -> argparse.ArgumentParser:
395
+ ap = argparse.ArgumentParser(
396
+ prog="ensure-store-windows",
397
+ formatter_class=argparse.RawDescriptionHelpFormatter,
398
+ description="幂等地把该开的店铺窗口开好、CDP 端口对齐、可驱动性断言过"
399
+ "(**默认 dry-run**,真做要 --apply;只复用 orchestrator 既有实现)",
400
+ epilog="例子:\n"
401
+ " python3 scripts/ensure-store-windows.py # 预演(零写入)\n"
402
+ " python3 scripts/ensure-store-windows.py --apply # 恢复窗口 + 写映射\n"
403
+ " python3 scripts/ensure-store-windows.py --apply --store <店名>\n")
404
+ ap.add_argument("--store", action="append", default=[], metavar="名",
405
+ help="店铺名(stores.json 的 key 或 name 字段);可重复;缺省 = stores.json 全部")
406
+ ap.add_argument("--force-reopen", action="append", default=[], metavar="名",
407
+ help="显式关掉再开(复用 close_store_bridge);可重复。"
408
+ "⛔ 不带它绝不关任何窗口")
409
+ ap.add_argument("--apply", action="store_true",
410
+ help="真做(开窗 + 写 cdp-port-map.json)。缺省 = dry-run")
411
+ ap.add_argument("--dry-run", action="store_true",
412
+ help="显式 dry-run(**本来就是默认**;A3:写类动作默认安全态)。"
413
+ "与 --apply 同时给时 dry-run 赢")
414
+ ap.add_argument("--timeout", type=float, default=60.0, metavar="秒",
415
+ help="每店预算:CLI 调用超时 + 开窗后轮询确认归属的总时长(默认 60)")
416
+ ap.add_argument("--interval", type=float, default=1.0, metavar="秒",
417
+ help=argparse.SUPPRESS)
418
+ ap.add_argument("--probe-timeout", type=float, default=2.0, metavar="秒",
419
+ help=argparse.SUPPRESS)
420
+ ap.add_argument("--json", action="store_true", help="stdout 只输出一个 JSON 对象")
421
+ ap.add_argument("--api-first", action="store_true",
422
+ help="⛔ 未实现:走 WebDriver API(startBrowser)优先路径。"
423
+ "今天该通道不可用 ⇒ 本脚本明确拒绝(绝不假装已做)")
424
+ return ap
425
+
426
+
427
+ def _emit(args: argparse.Namespace, results: list[dict[str, Any]], apply: bool,
428
+ timeout: float) -> int:
429
+ keys = ("ready", "aligned", "opened", "reopened",
430
+ "would-align", "would-open", "would-reopen", "failed")
431
+ counts = {k: sum(1 for r in results if r["status"] == k) for k in keys}
432
+ actionable = counts["would-align"] + counts["would-open"] + counts["would-reopen"]
433
+ ok = counts["failed"] == 0 and actionable == 0
434
+ if args.json:
435
+ print(json.dumps({
436
+ "ok": ok, "dryRun": not apply, "applied": apply, "timeout": timeout,
437
+ "stores": results,
438
+ "summary": dict(counts, total=len(results)),
439
+ }, ensure_ascii=False, indent=2))
440
+ return 0 if ok else 1
441
+
442
+ icons = {ST_READY: "✅", ST_ALIGNED: "🛠", ST_OPENED: "🪟", ST_REOPENED: "♻️",
443
+ ST_WOULD_ALIGN: "•", ST_WOULD_OPEN: "•", ST_WOULD_REOPEN: "•", ST_FAILED: "❌"}
444
+ print(f"{'预演(dry-run,零写入)' if not apply else '真做(--apply)'}"
445
+ f":{len(results)} 个店铺")
446
+ for r in results:
447
+ port = r["port"] if r["port"] is not None else "-"
448
+ print(f" {icons.get(r['status'], '?')} {r['store']}(storeId={r['storeId']})"
449
+ f" [{r['status']}] 端口={port}")
450
+ print(f" {r['reason']}")
451
+ tail = "、".join(f"{k}={counts[k]}" for k in keys if counts[k]) or "无"
452
+ print(f" ⇒ {'全部就绪' if ok else '**未全部就绪**'}({tail})")
453
+ if not apply and actionable:
454
+ print(" ⇒ 预演发现需要动作:加 `--apply` 真做(会开窗 / 写 cdp-port-map.json)")
455
+ return 0 if ok else 1
456
+
457
+
458
+ def main(argv: Optional[list[str]] = None) -> int:
459
+ args = build_parser().parse_args(argv)
460
+ if args.api_first:
461
+ print(f"❌ --api-first:{API_FIRST_REFUSAL}", file=sys.stderr)
462
+ return 2
463
+ # A3:**默认 dry-run**;`--dry-run` 显式给出时永远赢(安全方向)。
464
+ apply = bool(args.apply) and not bool(args.dry_run)
465
+ if args.timeout <= 0:
466
+ print("❌ --timeout 必须 > 0", file=sys.stderr)
467
+ return 2
468
+
469
+ try:
470
+ stores = orch._load_stores()
471
+ except Exception as exc: # noqa: BLE001
472
+ print(f"❌ 读 stores.json 失败:{type(exc).__name__}: {exc}", file=sys.stderr)
473
+ return 1
474
+ if not stores:
475
+ print("❌ stores.json 里没有任何店铺(文件缺失 / 损坏?)——先跑初始化向导"
476
+ f"(`{orch.STORES_JSON}`)", file=sys.stderr)
477
+ return 1
478
+
479
+ targets, problems = build_targets(args, stores)
480
+ if problems:
481
+ for p in problems:
482
+ print(f"❌ {p}", file=sys.stderr)
483
+ return 2
484
+ if not targets:
485
+ print("❌ 没有要处理的店铺", file=sys.stderr)
486
+ return 2
487
+
488
+ forced = set(args.force_reopen)
489
+ log(f"{'APPLY' if apply else 'DRY-RUN'}:{len(targets)} 个店铺"
490
+ f"(每店超时 {args.timeout:g}s)"
491
+ + (f";--force-reopen {sorted(forced)}" if forced else ""))
492
+ results: list[dict[str, Any]] = []
493
+ for t in targets:
494
+ force = t["env_name"] in forced or str(t["name"]) in forced
495
+ log(f"── {t['env_name']}(storeId={t['store_id']})"
496
+ + (" [force-reopen]" if force else ""))
497
+ try:
498
+ r = process_store(t, args, apply, force)
499
+ except Exception as exc: # noqa: BLE001
500
+ r = {"store": t["env_name"], "storeId": t["store_id"], "name": t["name"],
501
+ "status": ST_FAILED, "port": None, "mapPort": None, "changed": False,
502
+ "reason": f"未预期异常:{type(exc).__name__}: {exc}"}
503
+ results.append(r)
504
+ log(f" {r['status']}:{r['reason']}")
505
+ return _emit(args, results, apply, float(args.timeout))
506
+
507
+
508
+ if __name__ == "__main__":
509
+ sys.exit(main())