@whyshu/argent-engine 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/MANIFEST.json +307 -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 +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -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 +377 -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/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env bash
2
+ # ══════════════════════════════════════════════════════════════════════════════
3
+ # release-argent.sh <版本> — Argent 发布版本流程(KB-082,2026-09-01)
4
+ #
5
+ # 用途:把开发 HEAD 的稳定改动作为正式版本发布——校验 → 打 tag → 同步分发文件
6
+ # 到服务器 dl/ → 验证 → 输出发布报告。替代手动同步(曾发生 dl/ 落后漏同步)。
7
+ #
8
+ # 用法:
9
+ # bash release-argent.sh 0.7.1 # 正式发布(校验 → tag+push → 同步 → 验证)
10
+ # bash release-argent.sh 0.7.1 --dry-run # 演练:只校验+打印计划,不 tag 不同步
11
+ # bash release-argent.sh 0.7.1 --full # 额外同步仓库 skills/、plugins/ 目录
12
+ # bash release-argent.sh --check-dl # 只体检:仓库 vs 服务器 dl/ 是否一致(不改任何东西)
13
+ # bash release-argent.sh --publish-dl # 只同步分发文件(不打 tag、不 push)——应急修 dl 漂移
14
+ #
15
+ # 为什么有 --check-dl:2026-09-20 实测发现服务器 dl/install-argent.mjs 停在 9-07,
16
+ # 而仓库已是 9-15(缺 dsh 0.1.5-rc.1 已验版本 + profile-doctor 收尾门禁)——**整整漂了 8 天**。
17
+ # 官网那条 `curl https://whyshu.com/dl/install-argent.mjs | node` 下发的就是它。
18
+ # dl/ 与仓库没有任何自动连接,只能靠"发布时同步 + 平时可体检"两道:本模式就是体检那道。
19
+ # (默认同步 CORE_FILES 四件安装器
20
+ # install-argent.mjs / install.sh / install-dsh.sh / install.ps1;
21
+ # 服务器 dl/skills、dl/plugins 有独立静态资源
22
+ # (whyshu-ppt-template/argent-account),
23
+ # 与仓库目录不同源,--full 才覆盖,谨慎使用)
24
+ #
25
+ # 前置条件:
26
+ # - 本机可 ssh ubuntu@81.69.222.211(~/.ssh/whyshu.pem)
27
+ # - 发布门槛(docs/release.md):对应改动已在看板 review→done 验收通过(PM 批准)
28
+ # - VERSION 文件已更新为目标版本、CHANGELOG.md 已含对应条目(可先跑 --dry-run 检查)
29
+ #
30
+ # 流程:
31
+ # 1. 校验:VERSION 与参数一致 / git working tree clean / CHANGELOG 含 ## v<版本>
32
+ # (缺 CHANGELOG 条目仅警告)/ tag v<版本> 未存在
33
+ # 2. 打 git tag v<版本> + push origin v<版本>(含 tag)
34
+ # 3. 同步分发文件到 ubuntu@81.69.222.211:/home/whyshu/web/dl/:
35
+ # install-argent.mjs、install.sh、install-dsh.sh、install.ps1(默认);skills/、plugins/(--full)
36
+ # 4. 验证:本地/服务器 md5 一致 + curl -sI https://whyshu.com/dl/install-argent.mjs = 200
37
+ # 5. 输出发布报告(版本/tag/文件清单/md5/验证结果)
38
+ #
39
+ # ⛔ 投递纪律(本仓红线,2026-10-06 定、2026-10-06 收口):**客户机/服务器的任何投递禁用 `scp`**,一律用 `rsync`
40
+ # (或 `cat | ssh`)。原因:scp 的协议/选项面在新旧 OpenSSH 之间不兼容、不支持增量 ⇒ 大包重传、失败静默半截。
41
+ # **本脚本三条投递路径已全部走 rsync**:模式 P 的能力包上传 / CORE_FILES / `--full` 的 DIR_FILES。
42
+ # ⚠️ 且投递后都**显式归一远端模式**(`remote_chmod`):dl/ 是 nginx 静态面,源文件 600(开发树 umask)
43
+ # 被 `rsync -a` 原样带过去 ⇒ **403**,官网那条 `curl …/dl/install-argent.mjs | node` 当场废
44
+ # (2026-10-09 实测命中,当场 chmod 644 修复;体检现在把"HTTP 取不到"也判红)。
45
+ # ⛔ 为什么不用 rsync 的 `--chmod=F644`:macOS 自带的 openrsync("rsync version 2.6.9 compatible")
46
+ # 不认 `F`/`D` 前缀(实测报 `--chmod=F600: invalid argument`)⇒ 走远端 `chmod`,与 rsync 版本无关。
47
+ # `grep -n scp` 只应命中本段注释。⚠️ 清单口径(2026-10-06 修脚枪):模式 P 第 ④ 步的
48
+ # `--emit-server-manifest` **默认 merge**(只刷新注册表里认识的能力,现网独有的条目原样保留、键序不动;
49
+ # 删条目须显式 `--prune`)⇒ **工具已保证不误删**;仍建议发布前 `--dry-run` 复核 diff
50
+ # (脚本已自动先 dry-run 打印、再落盘)。
51
+ # ══════════════════════════════════════════════════════════════════════════════
52
+ set -euo pipefail
53
+
54
+ # ── 常量 ──────────────────────────────────────────────────────────────────────
55
+ SSH_HOST="ubuntu@81.69.222.211"
56
+ SSH_KEY="$HOME/.ssh/whyshu.pem"
57
+ DL_DIR="/home/whyshu/web/dl"
58
+ DL_URL="https://whyshu.com/dl"
59
+ # whyshu-svr 在服务器上的**扁平**部署目录(pm2 cwd;能力包清单就部署在这里)
60
+ SRV_DEPLOY="${SRV_DEPLOY:-/home/whyshu/server}"
61
+ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
62
+ # push 目标 remote:默认仓库第一个 remote(本机为 whyshu);可用 ARGENT_RELEASE_REMOTE 覆盖
63
+ REMOTE="${ARGENT_RELEASE_REMOTE:-$(git -C "$REPO_DIR" remote | head -1)}"
64
+
65
+ # 核心分发文件(与服务器 dl/ 直接对应)
66
+ # 2026-09-20:补 install.ps1 —— 官网 Windows PowerShell 一行命令 `irm .../dl/install.ps1 | iex`
67
+ # 直接吃这个文件;漏了它就等于 Windows 用户在下载"没人管"的副本。
68
+ # 2026-10-09:补 install.sh —— 官网 macOS/Linux 一行命令 `curl .../dl/install.sh | bash` 的入口
69
+ # (旧的 `curl .../dl/install-argent.mjs | node` 因模块级相对 import 必然 ERR_MODULE_NOT_FOUND,
70
+ # 已被 install.sh 取代)。`install-dsh.sh` 留在这里只为**存量老命令**仍能取到,是**遗留**件。
71
+ CORE_FILES=("install-argent.mjs" "install.sh" "install-dsh.sh" "install.ps1")
72
+ # 目录级分发(--full 时启用;服务器对应目录有独立静态资源,非同名覆盖不删旧)
73
+ DIR_FILES=("skills" "plugins")
74
+
75
+ # ── 参数 ──────────────────────────────────────────────────────────────────────
76
+ VERSION_ARG=""
77
+ DRY_RUN=0
78
+ FULL=0
79
+ MODE="release" # release | check-dl | publish-dl
80
+ for a in "$@"; do
81
+ case "$a" in
82
+ --dry-run) DRY_RUN=1 ;;
83
+ --full) FULL=1 ;;
84
+ --check-dl) MODE="check-dl" ;;
85
+ --publish-packs) MODE="publish-packs" ;;
86
+ --publish-dl) MODE="publish-dl" ;;
87
+ -h|--help) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
88
+ *) VERSION_ARG="$a" ;;
89
+ esac
90
+ done
91
+ if [[ "$MODE" == "release" && -z "$VERSION_ARG" ]]; then
92
+ echo "✗ 缺少版本参数(用法:bash release-argent.sh <版本> [--dry-run] [--full])"
93
+ echo " 或:bash release-argent.sh --check-dl | --publish-dl | --publish-packs"
94
+ exit 1
95
+ fi
96
+ if [[ "$MODE" != "release" ]]; then
97
+ VERSION_ARG="${VERSION_ARG:-current}"
98
+ fi
99
+
100
+ # ── md5 / 拉取校验的小工具(本地 / 服务器 / 真正下发的 HTTP 内容)──────────────
101
+ md5_of() { md5 -q "$1" 2>/dev/null || md5sum "$1" | awk '{print $1}'; }
102
+ md5_stdin() { md5 -q 2>/dev/null || md5sum | awk '{print $1}'; }
103
+ remote_md5_of() { ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$SSH_HOST" \
104
+ "md5sum ${DL_DIR}/$1 2>/dev/null | awk '{print \$1}'" 2>/dev/null || echo ''; }
105
+ # 真正"用户会拿到什么":绕过缓存拉一遍内容算 md5(只有 200 + 内容一致才算数)
106
+ # ⚠️ 2026-10-09 实测事故:旧实现是 `curl -fsSL … | md5_stdin` —— curl 失败时下游**照样**算出一个
107
+ # "空内容的 md5"(d41d8cd9…)⇒ 调用方看到的是"内容不同"而不是"取不到";且 `set -e` 下 curl 的
108
+ # 退出码(56)会**直接结束脚本**(现象 = 只打印一行"── dl/ 体检 ──"就没了)。
109
+ # 现在:先探 HTTP 状态码,**非 200 ⇒ 返回空串**(调用方按"HTTP 取不到"报红 + 给 chmod 修法);
110
+ # 状态码 200 才流式取内容(⛔ 不做命令替换:那会剥掉尾换行 ⇒ md5 与实际下发字节不符 ⇒ 恒假红)。
111
+ http_md5_of() {
112
+ local code
113
+ code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 60 -H 'Cache-Control: no-cache' "${DL_URL}/$1" 2>/dev/null || echo 000)"
114
+ [ "$code" = "200" ] || return 0
115
+ curl -fsSL --max-time 60 -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' "${DL_URL}/$1" 2>/dev/null | md5_stdin || true
116
+ }
117
+
118
+ # 投递后**归一远端文件模式**(2026-10-09 实测事故,见脚本头"投递纪律"):
119
+ # `rsync -a` 会保留开发树的模式 ⇒ 源文件 600 时 dl/ 上也 600 ⇒ nginx 403 ⇒ 官网一行命令当场废。
120
+ # 用法:remote_chmod <模式> <远端路径…>(模式与路径都交给远端 chmod,路径里的 glob 由远端展开)
121
+ remote_chmod() {
122
+ local mode="$1"; shift
123
+ ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$SSH_HOST" "chmod ${mode} $*" 2>/dev/null || true
124
+ }
125
+
126
+ # ── 解释器解析(两个档)─────────────────────────────────────────────────────────
127
+ # 2026-09-22 重启事故:默认值写死 ~/.dsh/argent/venv ⇒ /tmp 被清后静默空转。
128
+ # 候选(2026-09-22):ARGENT_PY → DSH_HOME/argent/venv → ~/.dsh/argent/venv → 仓库 .venv → PATH
129
+ py_candidates() {
130
+ printf '%s\n' "${ARGENT_PY:-}" "${DSH_HOME:-$HOME/.dsh}/argent/venv/bin/python3" \
131
+ "$HOME/.dsh/argent/venv/bin/python3" "${REPO:-}/.venv/bin/python3" \
132
+ "$(command -v python3 2>/dev/null || true)"
133
+ }
134
+ # 严格档:配置/发票生成硬要求 openpyxl + yaml(--publish-packs 走这一档)
135
+ pick_py() {
136
+ local c
137
+ for c in $(py_candidates); do
138
+ [ -n "$c" ] || continue
139
+ [ -x "$c" ] || continue
140
+ "$c" -c 'import openpyxl, yaml' 2>/dev/null || continue
141
+ "$c" -c 'import PIL, playwright' 2>/dev/null || \
142
+ echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:node install-argent.mjs --venv-only)" >&2
143
+ printf '%s' "$c"; return 0
144
+ done
145
+ return 1
146
+ }
147
+ # 轻档(2026-10-09):能力包档**只跑 stdlib**(`capability_pack.py --digest-tgz` / `--emit-server-manifest`)
148
+ # ⇒ 不该被 openpyxl 挡在门外(体检机器上没装办公依赖是常态)。
149
+ pick_py_light() {
150
+ local c
151
+ for c in $(py_candidates); do
152
+ [ -n "$c" ] || continue
153
+ [ -x "$c" ] || continue
154
+ "$c" -c 'import json,tarfile,hashlib' 2>/dev/null || continue
155
+ printf '%s' "$c"; return 0
156
+ done
157
+ return 1
158
+ }
159
+
160
+ # ── 能力包档(2026-10-09 新增)─────────────────────────────────────────────────
161
+ # 为什么要有它:`CORE_FILES` 只有三个安装器 ⇒ **能力包从来没人核**。而 hr 今天就发生过
162
+ # 「**同 URL 覆盖发布**」(版本没提、字节变了)—— 文件名与 HTTP 200 都没变,谁都没发现。
163
+ # 判据(任一不满足即判红,复用本脚本既有的「HTTP 取不到即红」风格):
164
+ # ① HTTP 取得到(状态码 200);② 取回来的 tgz **重算内容摘要 == 清单 sha256**。
165
+ # ⛔ 摘要**只用 CLI 算**(`capability_pack.py --digest-tgz`):importlib 加载那个模块时
166
+ # `scripts/` 不在 sys.path ⇒ `import flow_registry` 静默降级成未合并的 base 注册表 ⇒
167
+ # 带流程的能力(full/invoice/daily-report)摘要会**算错**(2026-10-08 实测)。
168
+ # 另外做一次**仓库 ↔ 线上清单**对账(同一份 CLI 的 `--emit-server-manifest` 输出):
169
+ # · 版本不同 ⇒ ⚠(发布在途/落后,**不判红**,否则开发中恒红);
170
+ # · **版本相同但摘要不同** ⇒ ✗ 判红 —— 那正是「同 URL 覆盖发布」的形态:客户拿到的字节
171
+ # 与仓库里那一版对不上,而版本号骗过了所有人。
172
+ # 用法:check_packs <light-py> ⇒ 设 PACK_DRIFT=0/1(返回值恒 0,判据走变量,免得被 set -e 咬)
173
+ check_packs() {
174
+ local py="$1"
175
+ local tmpd; tmpd="$(mktemp -d -t argent-packs-check)"
176
+ local rc=0
177
+ PACK_DRIFT=0
178
+ echo
179
+ echo "── 能力包档(服务端清单 → 逐个 HTTP 取回 + **重算内容摘要**)──"
180
+ if [[ -z "$py" ]]; then
181
+ echo " ✗ 找不到可用解释器(stdlib 即可)⇒ 无法重算包摘要(⛔ 不用 importlib 兜底:会静默算错)"
182
+ rm -rf "$tmpd"; PACK_DRIFT=1; return 0
183
+ fi
184
+ local remote_manifest
185
+ # 部署形态是**扁平**的(`/home/whyshu/server/config/…`,服务按 `services/../config` 解析);
186
+ # 仓库形态则是 `src/config/…` ⇒ 两个都试,取第一个存在的(别把"我猜错了目录"报成"清单没了")。
187
+ remote_manifest="$(ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$SSH_HOST" \
188
+ "for p in ${SRV_DEPLOY}/config/capability-packs.json ${SRV_DEPLOY}/src/config/capability-packs.json; do \
189
+ if [ -f \"\$p\" ]; then cat \"\$p\"; break; fi; done" 2>/dev/null || echo '')"
190
+ if [[ -z "$remote_manifest" ]]; then
191
+ echo " ✗ 取不到服务器上部署的能力清单(试过 ${SRV_DEPLOY}/config/ 与 ${SRV_DEPLOY}/src/config/)"
192
+ rm -rf "$tmpd"; PACK_DRIFT=1; return 0
193
+ fi
194
+ printf '%s' "$remote_manifest" > "$tmpd/manifest.json"
195
+ # 发布侧真源(本机 whyshu-svr 仓库那一份):与**已部署**的那份不一致 = 改了没部署。
196
+ # ⚠️ 比对前两边都去掉**尾随换行**:`$( )` 会剥掉命令替换结果的所有尾换行,而文件末尾有一个
197
+ # ⇒ 直接 `diff <(printf '%s' "$remote") <file>` 会**恒报不等**(2026-10-09 实测的假告警)。
198
+ local local_manifest="${WHYSHU_SVR_DIR:-$HOME/Code/whyshu-svr}/src/config/capability-packs.json"
199
+ if [[ -f "$local_manifest" ]]; then
200
+ if [[ "$remote_manifest" == "$(cat "$local_manifest")" ]]; then
201
+ echo " ✓ 已部署清单 == 本机 whyshu-svr 工作树那一份(未部署改动 = 0)"
202
+ else
203
+ echo " ⚠ 已部署清单 ≠ 本机 whyshu-svr 工作树那一份 —— 有改动没部署(⚠ 不判红;本脚本只核 dl/)"
204
+ fi
205
+ else
206
+ echo " · 本机没有 ${local_manifest}(跳过「发布侧真源」对账)"
207
+ fi
208
+ local rows
209
+ rows="$("$py" -c 'import json,sys
210
+ d=json.load(open(sys.argv[1],encoding="utf-8"))
211
+ for cid,e in (d.get("capabilities") or {}).items():
212
+ print("\t".join([cid,str(e.get("version","")),str(e.get("sha256","")),str(e.get("url",""))]))' \
213
+ "$tmpd/manifest.json" 2>/dev/null || true)"
214
+ if [[ -z "$rows" ]]; then
215
+ echo " ✗ 服务端清单里**一个能力都没有**(清单空/结构非法)—— 客户机拿到的会是空清单"
216
+ rm -rf "$tmpd"; PACK_DRIFT=1; return 0
217
+ fi
218
+ local cid ver sha url code got f
219
+ while IFS=$'\t' read -r cid ver sha url; do
220
+ [[ -n "$cid" ]] || continue
221
+ if [[ -z "$url" ]]; then
222
+ echo " ✗ ${cid} v${ver}:清单没有 url(客户端无从取包)"; PACK_DRIFT=1; continue
223
+ fi
224
+ code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 60 -H 'Cache-Control: no-cache' "$url" 2>/dev/null || echo 000)"
225
+ if [[ "$code" != "200" ]]; then
226
+ echo " ✗ ${cid} v${ver}:**HTTP ${code}**(${url})—— 包取不到;修:bash scripts/release-argent.sh --publish-packs"
227
+ PACK_DRIFT=1; continue
228
+ fi
229
+ f="$tmpd/${cid}.tgz"
230
+ if ! curl -fsSL --max-time 300 -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' "$url" -o "$f" 2>/dev/null; then
231
+ echo " ✗ ${cid} v${ver}:状态码 200 但**内容取不回来**(多半是权限 ⇒ nginx 403 / 传输中断)"
232
+ PACK_DRIFT=1; continue
233
+ fi
234
+ got="$("$py" scripts/capability_pack.py --digest-tgz "$f" 2>/dev/null | head -1 | tr -d '[:space:]' || true)"
235
+ if [[ -z "$got" ]]; then
236
+ echo " ✗ ${cid} v${ver}:摘要重算失败(--digest-tgz 非零退出)"; PACK_DRIFT=1; continue
237
+ fi
238
+ if [[ "$got" == "$sha" ]]; then
239
+ echo " ✓ ${cid} v${ver} 内容摘要一致(${got:0:8},$(wc -c < "$f" | tr -d ' ') 字节)"
240
+ else
241
+ echo " ✗ ${cid} v${ver} **内容摘要不符**:清单 ${sha:0:8} vs HTTP 取回重算 ${got:0:8}"
242
+ echo " ↳ 这正是「同 URL 覆盖发布」的形态(包换了、版本没提,HTTP 与文件名都还是好的)"
243
+ PACK_DRIFT=1
244
+ fi
245
+ done <<< "$rows"
246
+
247
+ # ── 仓库 ↔ 线上清单对账(同一份 CLI 派生,不手抄任何值)──
248
+ echo " ── 仓库 ↔ 线上清单对账(capability_pack.py --emit-server-manifest)──"
249
+ local derived; derived="$("$py" scripts/capability_pack.py --emit-server-manifest 2>/dev/null || true)"
250
+ if [[ -z "$derived" ]]; then
251
+ echo " ⚠ 派生不出仓库侧清单(注册表读不出来?)—— 跳过对账"
252
+ else
253
+ printf '%s' "$derived" > "$tmpd/derived.json"
254
+ local cmp_out; cmp_out="$("$py" -c 'import json,sys
255
+ der=(json.load(open(sys.argv[1],encoding="utf-8")).get("capabilities") or {})
256
+ live=(json.load(open(sys.argv[2],encoding="utf-8")).get("capabilities") or {})
257
+ warn=red=0
258
+ for cid in sorted(set(der)|set(live)):
259
+ d,l=der.get(cid),live.get(cid)
260
+ if d is None:
261
+ print(" ⚠ %s: 线上有、仓库注册表不认识(%s)—— 孤儿条目,无法重建/提版" % (cid,l.get("version"))); warn+=1; continue
262
+ if l is None:
263
+ print(" ⚠ %s: 仓库有、线上**没有**(%s)—— 还没发布?" % (cid,d.get("version"))); warn+=1; continue
264
+ if str(d.get("version"))!=str(l.get("version")):
265
+ print(" ⚠ %s: 版本不同 仓库 %s vs 线上 %s(发布在途/落后;**不判红**)" % (cid,d.get("version"),l.get("version"))); warn+=1; continue
266
+ if str(d.get("sha256"))!=str(l.get("sha256")):
267
+ print(" ✗ %s: **版本相同(%s)但内容摘要不同** —— 仓库 %s vs 线上 %s ⇒ 同版本覆盖发布的形态,客户拿到的字节与仓库这一版对不上" % (cid,d.get("version"),str(d.get("sha256"))[:8],str(l.get("sha256"))[:8])); red+=1
268
+ print(" ⇒ 对账:✗ %d | ⚠ %d%s" % (red,warn,"(✗ 即判红)" if red else "(⚠ 不判红)"))
269
+ print("__RED__" if red else "__OK__")' "$tmpd/derived.json" "$tmpd/manifest.json" 2>/dev/null || true)"
270
+ printf '%s\n' "$cmp_out" | grep -v '^__' || true
271
+ if printf '%s' "$cmp_out" | grep -q '__RED__'; then PACK_DRIFT=1; fi
272
+ fi
273
+ rm -rf "$tmpd"
274
+ if [[ "$PACK_DRIFT" -eq 0 ]]; then
275
+ echo " ✅ 能力包档:清单里每个包都 HTTP 可达且内容摘要与清单一致"
276
+ else
277
+ echo " ✗ 能力包档**不通过** —— 面向客户的下载面与清单对不上"
278
+ fi
279
+ return 0
280
+ }
281
+
282
+ # ── 模式 P:发布能力包(--publish-packs)── 缺口③:把包托管到 dl/packs/ 并生成服务端清单 ──
283
+ # 为什么需要:客户端的 `--from-packs <url>` 早就支持按 url 装包,但包一直没地方放(清单 url 是占位)。
284
+ # 本模式:① 构建每个能力的**确定性 tgz** → ② rsync 到 `${DL_DIR}/packs/` → ③ 复核 HTTP 可达
285
+ # → ④ 生成 whyshu-svr 的清单(含整包内容摘要 + url):**默认 merge**(只刷新注册表认识的能力,
286
+ # 现网独有的条目原样保留;删条目须显式 --prune)⇒ 先 `--dry-run` 打印 diff,再落盘。
287
+ # ⛔ 不再"整表替换":2026-10-06 前的整表输出会抹掉现网独有条目(实测会把 daily-report-lite 抹掉)。
288
+ if [[ "$MODE" == "publish-packs" ]]; then
289
+ cd "$REPO_DIR"
290
+ # 解释器解析(2026-09-22 重启事故:默认值写死 ~/.dsh/argent/venv ⇒ /tmp 被清后静默空转)
291
+ pick_py() {
292
+ local c
293
+ # 候选(2026-09-22):ARGENT_PY → DSH_HOME/argent/venv → ~/.dsh/argent/venv → 仓库 .venv → PATH
294
+ for c in "${ARGENT_PY:-}" "${DSH_HOME:-$HOME/.dsh}/argent/venv/bin/python3" \
295
+ "$HOME/.dsh/argent/venv/bin/python3" "${REPO:-}/.venv/bin/python3" \
296
+ "$(command -v python3 2>/dev/null || true)"; do
297
+ [ -n "$c" ] || continue
298
+ [ -x "$c" ] || continue
299
+ "$c" -c 'import openpyxl, yaml' 2>/dev/null || continue # 硬要求:配置/发票生成必需
300
+ "$c" -c 'import PIL, playwright' 2>/dev/null || \
301
+ echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:node install-argent.mjs --venv-only)" >&2
302
+ printf '%s' "$c"; return 0
303
+ done
304
+ return 1
305
+ }
306
+ if ! PY="$(pick_py)"; then
307
+ echo "❌ 找不到可用解释器(需含 openpyxl + yaml):ARGENT_PY / ~/.dsh/argent/venv/bin/python3 / PATH 的 python3" >&2
308
+ echo " 修复: python3 -m venv ~/.dsh/argent/venv && ~/.dsh/argent/venv/bin/pip install openpyxl pyyaml" >&2
309
+ exit 2
310
+ fi
311
+ OUT="${PACKS_OUT:-$(mktemp -d -t argent-packs)}"
312
+ DL_BASE="${DL_BASE:-https://whyshu.com/dl}"
313
+ echo "── 构建能力包(确定性 tgz)→ ${OUT} ──"
314
+ CAPS="$("$PY" scripts/capability_pack.py --list 2>/dev/null | awk '/^ [a-z]/{print $1}')"
315
+ [[ -n "$CAPS" ]] || CAPS="invoice full daily-report hr"
316
+ for c in $CAPS; do
317
+ "$PY" scripts/capability_pack.py --build "$c" --out-dir "${OUT}/${c}" --tar --force >/dev/null 2>&1 \
318
+ && echo " ✓ ${c}" || { echo " ✗ ${c} 构建失败"; exit 1; }
319
+ done
320
+ echo
321
+ echo "── 上传 → ${SSH_HOST}:${DL_DIR}/packs/ ──"
322
+ ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$SSH_HOST" "mkdir -p ${DL_DIR}/packs"
323
+ # ⛔ 红线(2026-10-06):客户机/服务器投递禁用 `scp` ⇒ 改 `rsync`(同一份字节、同一目标目录,
324
+ # 行为等价;rsync 走 ssh 且支持增量,避免 scp 在新旧 OpenSSH 间的兼容差异与半截失败)。
325
+ rsync -az --no-owner --no-group -e "ssh -i $SSH_KEY -o ConnectTimeout=10" "${OUT}"/*.tgz "${SSH_HOST}:${DL_DIR}/packs/"
326
+ # 模式归一(600 ⇒ nginx 403;见 remote_chmod 注释):包是 HTTP 直接下发物,必须 world-readable。
327
+ remote_chmod 644 "${DL_DIR}/packs/*.tgz"
328
+ echo " ✓ 已上传 $(ls "${OUT}"/*.tgz | wc -l | tr -d ' ') 个包"
329
+ echo
330
+ echo "── 复核(HTTP 可达 + 与本地字节一致)──"
331
+ OK=1
332
+ for f in "${OUT}"/*.tgz; do
333
+ n="$(basename "$f")"; l="$(md5_of "$f")"
334
+ code="$(curl -s -o /dev/null -w '%{http_code}' -I "${DL_BASE}/packs/${n}" || echo 000)"
335
+ h="$(curl -s "${DL_BASE}/packs/${n}" | md5 -q 2>/dev/null || curl -s "${DL_BASE}/packs/${n}" | md5sum | awk '{print $1}')"
336
+ if [[ "$code" == "200" && "$h" == "$l" ]]; then echo " ✓ ${n}(HTTP 200,md5 ${l:0:8})"; else
337
+ echo " ✗ ${n}(HTTP ${code},md5 本地 ${l:0:8} vs 远端 ${h:0:8})"; OK=0; fi
338
+ done
339
+ [[ "$OK" -eq 1 ]] || { echo "✗ 部分包复核不通过——先别改服务端清单"; exit 1; }
340
+ echo
341
+ echo "── 生成服务端能力清单(whyshu-svr;默认 merge,只刷新注册表认识的能力)──"
342
+ SRV="${WHYSHU_SVR_DIR:-$HOME/Code/whyshu-svr}/src/config/capability-packs.json"
343
+ # ④a 先 dry-run **看 diff**(不写盘):工具已保证不误删现网独有条目,这一步是发布前的第二双眼睛。
344
+ "$PY" scripts/capability_pack.py --emit-server-manifest --dl-base "$DL_BASE" --out "$SRV" --dry-run
345
+ # ④b 复核无误 ⇒ 落盘(仍是 merge 语义:不认识的条目原样保留、键序不动)
346
+ "$PY" scripts/capability_pack.py --emit-server-manifest --dl-base "$DL_BASE" --out "$SRV"
347
+ echo " ⇒ 下一步:把 ${SRV} 随 whyshu-svr 一起部署(rsync + pm2 startOrReload whyshu-svr)"
348
+ exit 0
349
+ fi
350
+
351
+ # ── 模式 A:只体检(--check-dl)/ 只同步(--publish-dl)——不打 tag、不 push ──
352
+ if [[ "$MODE" != "release" ]]; then
353
+ cd "$REPO_DIR"
354
+ echo "── dl/ 体检(仓库 ↔ ${SSH_HOST}:${DL_DIR})──"
355
+ DRIFT=0
356
+ for f in "${CORE_FILES[@]}"; do
357
+ [[ -f "$f" ]] || { echo " ⚠ 仓库无 ${f}(跳过)"; continue; }
358
+ l="$(md5_of "$f")"; r="$(remote_md5_of "$f")"; h="$(http_md5_of "$f")"
359
+ tag="✓"
360
+ if [[ -z "$r" ]]; then tag="✗"; DRIFT=1; echo " ✗ ${f}:服务器上没有这个文件"; continue; fi
361
+ [[ "$l" == "$r" ]] || { tag="✗"; DRIFT=1; }
362
+ note=""
363
+ [[ "$l" == "$r" ]] || note="(服务器 ${r:0:8} vs 仓库 ${l:0:8})"
364
+ if [[ -n "$h" && "$h" != "$l" ]]; then note="${note}(HTTP 下发 ${h:0:8})"; DRIFT=1; tag="✗"; fi
365
+ # 2026-10-09:HTTP 拉不到内容同样判红(实测事故:rsync -a 把开发树的 600 带进生产 ⇒ nginx 403
366
+ # ⇒ 官网那条 `curl .../dl/install-argent.mjs | node` 一行命令当场废掉,而旧逻辑把空串当"没有证据"放过)。
367
+ if [[ -z "$h" ]]; then tag="✗"; DRIFT=1; note="${note}(**HTTP 取不到下发内容**:多为文件权限 ⇒ nginx 403;修:chmod 644)"; fi
368
+ echo " ${tag} ${f} ${l:0:8}${note}"
369
+ done
370
+ # ── 能力包档(2026-10-09 新增):安装器之外,**能力包也要逐个核**。见 check_packs 注释 ──
371
+ # 只在"体检"语义下把结果并进 DRIFT;--publish-dl 不改包(要重发用 --publish-packs),
372
+ # 所以那一档只报告、不据此早退(否则会挡住它本来要干的"修安装器 403")。
373
+ check_packs "$(pick_py_light || true)"
374
+ if [[ "$MODE" == "check-dl" && "$PACK_DRIFT" -ne 0 ]]; then DRIFT=1; fi
375
+ if [[ "$DRIFT" -eq 0 ]]; then
376
+ echo "✅ dl/ 与仓库一致(含 HTTP 下发内容一致 + 能力包内容摘要一致)"
377
+ exit 0
378
+ fi
379
+ if [[ "$MODE" == "check-dl" ]]; then
380
+ echo "✗ dl/ 与仓库**不一致**——修法:bash scripts/release-argent.sh --publish-dl(应急同步安装器,不打 tag)"
381
+ echo " 能力包不符则用:bash scripts/release-argent.sh --publish-packs(重建+上传+刷新服务端清单)"
382
+ echo " 或走正式发布:bash scripts/release-argent.sh <版本>"
383
+ exit 1
384
+ fi
385
+ echo
386
+ echo "── 同步分发文件(--publish-dl:不打 tag、不 push)──"
387
+ for f in "${CORE_FILES[@]}"; do
388
+ [[ -f "$f" ]] || continue
389
+ if [[ "$(md5_of "$f")" == "$(remote_md5_of "$f")" ]]; then
390
+ echo " · ${f} 已一致,跳过"
391
+ else
392
+ # ⛔ 红线:投递禁用 `scp`(见脚本头"投递纪律")⇒ 一律 rsync。
393
+ rsync -az --no-owner --no-group -e "ssh -i $SSH_KEY -o ConnectTimeout=10" "$f" "${SSH_HOST}:${DL_DIR}/${f}"
394
+ echo " ✓ ${f} → ${DL_DIR}/${f}"
395
+ fi
396
+ # ⭐ 模式归一**无条件**执行(2026-10-09 实测):内容一致 ⇒ rsync 跳过 ⇒ **权限照旧**,
397
+ # 600 的文件会一直 403(本次反证就是"已一致,跳过"之后 HTTP 仍取不到 ⇒ 复核判红)。
398
+ # "内容对"与"用户拿得到"是两件事,投递必须两件都保证。
399
+ remote_chmod 644 "${DL_DIR}/${f}"
400
+ done
401
+ echo
402
+ echo "── 复核(服务器 md5 + HTTP 下发内容)──"
403
+ OK=1
404
+ for f in "${CORE_FILES[@]}"; do
405
+ [[ -f "$f" ]] || continue
406
+ l="$(md5_of "$f")"; r="$(remote_md5_of "$f")"; h="$(http_md5_of "$f")"
407
+ if [[ "$l" == "$r" && -n "$h" && "$h" == "$l" ]]; then
408
+ echo " ✓ ${f} 一致(${l:0:8};HTTP 下发同字节)"
409
+ else
410
+ echo " ✗ ${f} 仍不一致:仓库 ${l:0:8} / 服务器 ${r:0:8} / HTTP ${h:0:8}"
411
+ [[ -n "$h" ]] || echo " ↳ HTTP 取不到内容(403?)—— 面向用户的那条命令会直接失败:服务器上 chmod 644 ${DL_DIR}/${f}"
412
+ OK=0
413
+ fi
414
+ done
415
+ [[ "$OK" -eq 1 ]] || exit 1
416
+ if [[ "$PACK_DRIFT" -ne 0 ]]; then
417
+ echo "✅ 安装器三点已与仓库对齐;但**能力包档有 ✗**(本模式不改包)⇒ 跑:bash scripts/release-argent.sh --publish-packs"
418
+ exit 0
419
+ fi
420
+ echo "✅ dl/ 已与仓库对齐(下次正式发布仍会再同步一遍,不冲突)"
421
+ exit 0
422
+ fi
423
+ VERSION="${VERSION_ARG#v}" # 容错:0.7.1 或 v0.7.1 均可
424
+ TAG="v${VERSION}"
425
+
426
+ # ── 1. 校验 ──────────────────────────────────────────────────────────────────
427
+ cd "$REPO_DIR"
428
+ echo "── 发布校验(版本 ${VERSION})──"
429
+ FAIL=0
430
+
431
+ # 1a. VERSION 文件与参数一致
432
+ if [[ -f VERSION ]]; then
433
+ CUR="$(cat VERSION | tr -d '[:space:]')"
434
+ if [[ "$CUR" != "$VERSION" ]]; then
435
+ echo "✗ VERSION 文件内容(${CUR})与参数(${VERSION})不一致——请先更新 VERSION 并提交"
436
+ FAIL=1
437
+ else
438
+ echo "✓ VERSION = ${VERSION}(一致)"
439
+ fi
440
+ else
441
+ echo "✗ 仓库无 VERSION 文件"
442
+ FAIL=1
443
+ fi
444
+
445
+ # 1b. git working tree clean
446
+ if [[ -n "$(git status --porcelain)" ]]; then
447
+ echo "✗ git working tree 有未提交改动:"
448
+ git status --porcelain | head -10
449
+ FAIL=1
450
+ else
451
+ echo "✓ git working tree clean"
452
+ fi
453
+
454
+ # 1c. CHANGELOG.md 含对应版本条目(缺则警告,不阻断——发布候选可后补)
455
+ # 判据兼容两种抬头:`## v0.9.9 xxx`(旧写法)与 `## 0.9.9(日期)`(≥0.7.3 现行写法)。
456
+ # 原判据写死 `^## v${VERSION} `(v + 半角空格)⇒ 自 0.7.3 起恒不匹配 ⇒ 门槛 3 实际"永远只警告"。
457
+ V_RE="${VERSION//./\\.}"
458
+ if [[ -f CHANGELOG.md ]] && grep -qE "^## (v)?${V_RE}([ ((]|$)" CHANGELOG.md; then
459
+ echo "✓ CHANGELOG.md 含 ## ${VERSION} 条目"
460
+ else
461
+ echo "⚠ CHANGELOG.md 缺 ## ${VERSION} 条目——建议先补充(发布门槛,docs/release.md)"
462
+ fi
463
+
464
+ # 1d. tag 未存在
465
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
466
+ echo "✗ tag ${TAG} 已存在($(git log -1 --format='%h %s' "${TAG}"))——如需重新发布请删除旧 tag"
467
+ FAIL=1
468
+ else
469
+ echo "✓ tag ${TAG} 可创建"
470
+ fi
471
+
472
+ # 1e. push 目标 remote 存在
473
+ if [[ -z "$REMOTE" ]]; then
474
+ echo "✗ 仓库无 git remote(无法 push tag)——可用 ARGENT_RELEASE_REMOTE 指定"
475
+ FAIL=1
476
+ else
477
+ echo "✓ push 目标 remote = ${REMOTE}"
478
+ fi
479
+
480
+ if [[ $FAIL -eq 1 ]]; then
481
+ echo "✗ 校验未通过,发布中止(可先 --dry-run 排查)"
482
+ exit 1
483
+ fi
484
+
485
+ # ── 2. 计划 / dry-run ────────────────────────────────────────────────────────
486
+ echo
487
+ echo "── 发布计划(版本 ${VERSION},tag ${TAG})──"
488
+ echo " 1. git tag ${TAG} + git push ${REMOTE} ${TAG}"
489
+ echo " 2. rsync → ${SSH_HOST}:${DL_DIR}/:${CORE_FILES[*]}"
490
+ if [[ $FULL -eq 1 ]]; then
491
+ echo " 3. rsync 目录 → ${SSH_HOST}:${DL_DIR}/:${DIR_FILES[*]}(--full,覆盖服务器同名内容)"
492
+ else
493
+ echo " 3. (未指定 --full:skills/ plugins/ 目录不同步——服务器有独立静态资源)"
494
+ fi
495
+ echo " 4. 验证:本地/服务器 md5 + curl -sI ${DL_URL}/install-argent.mjs"
496
+
497
+ if [[ $DRY_RUN -eq 1 ]]; then
498
+ echo
499
+ echo "✅ [dry-run] 校验通过,计划如上(未打 tag、未同步)。确认后执行:bash release-argent.sh ${VERSION}"
500
+ exit 0
501
+ fi
502
+
503
+ # ── 3. 执行 ──────────────────────────────────────────────────────────────────
504
+ echo
505
+ echo "── 打 tag + push ──"
506
+ git tag "${TAG}"
507
+ git push "${REMOTE}" "${TAG}"
508
+ echo "✓ tag ${TAG} 已 push"
509
+
510
+ echo
511
+ echo "── 同步分发文件到服务器 ──"
512
+ SSH_ARGS=(-i "$SSH_KEY" -o ConnectTimeout=10 "$SSH_HOST")
513
+ # ⛔ 红线:投递禁用 `scp`(见脚本头"投递纪律")⇒ 一律 rsync;目录投递(--full)用的是同一形态。
514
+ RSYNC_SSH="ssh -i $SSH_KEY -o ConnectTimeout=10"
515
+ for f in "${CORE_FILES[@]}"; do
516
+ if [[ ! -f "$f" ]]; then
517
+ echo "⚠ 仓库无 ${f}(跳过)"
518
+ continue
519
+ fi
520
+ rsync -az --no-owner --no-group -e "$RSYNC_SSH" "$f" "${SSH_HOST}:${DL_DIR}/${f}"
521
+ remote_chmod 644 "${DL_DIR}/${f}" # 模式归一(600 ⇒ nginx 403)
522
+ echo " ✓ ${f} → ${DL_DIR}/${f}"
523
+ done
524
+ if [[ $FULL -eq 1 ]]; then
525
+ for d in "${DIR_FILES[@]}"; do
526
+ if [[ ! -d "$d" ]]; then
527
+ echo "⚠ 仓库无 ${d}/(跳过)"
528
+ continue
529
+ fi
530
+ rsync -az --no-owner --no-group -e "$RSYNC_SSH" "$d/" "${SSH_HOST}:${DL_DIR}/${d}/"
531
+ # 目录面:`a+rX` = 目录可进入/可列举、文件可读(可执行位只给本来就有的)——同样"开发树模式不进生产"
532
+ remote_chmod -R a+rX "${DL_DIR}/${d}"
533
+ echo " ✓ ${d}/ → ${DL_DIR}/${d}/(--full 覆盖)"
534
+ done
535
+ fi
536
+
537
+ # ── 4. 验证 ──────────────────────────────────────────────────────────────────
538
+ echo
539
+ echo "── 验证 ──"
540
+ VERIFY_OK=1
541
+ for f in "${CORE_FILES[@]}"; do
542
+ [[ -f "$f" ]] || continue
543
+ local_md5="$(md5 -q "$f" 2>/dev/null || md5sum "$f" | awk '{print $1}')"
544
+ remote_md5="$(ssh "${SSH_ARGS[@]}" "md5sum ${DL_DIR}/${f} 2>/dev/null | awk '{print \$1}'" 2>/dev/null || echo '')"
545
+ if [[ "$local_md5" == "$remote_md5" && -n "$remote_md5" ]]; then
546
+ echo " ✓ ${f} md5 一致(${local_md5})"
547
+ else
548
+ echo " ✗ ${f} md5 不一致:本地 ${local_md5} vs 服务器 ${remote_md5}"
549
+ VERIFY_OK=0
550
+ fi
551
+ done
552
+ http_code="$(curl -sI -o /dev/null -w '%{http_code}' "${DL_URL}/install-argent.mjs")"
553
+ if [[ "$http_code" == "200" ]]; then
554
+ echo " ✓ ${DL_URL}/install-argent.mjs HTTP ${http_code}"
555
+ else
556
+ echo " ✗ ${DL_URL}/install-argent.mjs HTTP ${http_code}(期望 200)"
557
+ VERIFY_OK=0
558
+ fi
559
+
560
+ # ── 5. 发布报告 ─────────────────────────────────────────────────────────────
561
+ echo
562
+ echo "════════ 发布报告 ════════"
563
+ echo "版本: ${VERSION}"
564
+ echo "tag: ${TAG}($(git log -1 --format='%h %s' "${TAG}"))"
565
+ echo "提交: $(git log -1 --format='%h %s' HEAD)"
566
+ if [[ $FULL -eq 1 ]]; then
567
+ echo "同步: ${CORE_FILES[*]} + 目录 ${DIR_FILES[*]}"
568
+ else
569
+ echo "同步: ${CORE_FILES[*]}(skills/ plugins/ 未同步,--full 可选)"
570
+ fi
571
+ echo "验证: $([[ $VERIFY_OK -eq 1 ]] && echo '全部通过' || echo '存在失败(见上)')"
572
+ echo "════════════════════════"
573
+ [[ $VERIFY_OK -eq 1 ]] || exit 1
574
+ echo "✅ 发布完成:${TAG} 已发布(dl/ 分发文件已同步并验证)"