@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.
- package/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-bootcheck 的**隔离装配计划**与**启动日志判据**(唯一真源,纯 L1)。
|
|
3
|
+
*
|
|
4
|
+
* 由来(2026-10-12 P1-0,依据 `docs/dsh-0.2.x-适配审计.md` §B2/§D1 + 契约 §3.1/§3.3):
|
|
5
|
+
* 原 bootcheck 把**真实 profile** 原样软链进隔离 home ⇒ 插件仍解析到**真实 profile 指的那个
|
|
6
|
+
* scope**。在 0.2 宿主上,那个 scope 是 0.1.7 ⇒ 0.2 的 peer 门禁把**每一行** disable
|
|
7
|
+
* (`dsh: disabling profile plugin row "…"` ×110 + `startup failed: 1 required plugin did not
|
|
8
|
+
* activate`)⇒ **假红**,与"Argent 坏了"在输出上无法区分。
|
|
9
|
+
*
|
|
10
|
+
* 本模块把两件事从命令式脚本里抽成**可断言的纯数据**:
|
|
11
|
+
* ① `planProfileWiring()` —— 把真实 profile **浅镜像**进临时 home,并把 `@deepseek-ai`
|
|
12
|
+
* 农场**重建**为指向目标 scope 的链接(⛔ 绝不写真实 profile;⛔ 不 `cp -R` node_modules);
|
|
13
|
+
* ② `evaluateBootLog()` —— 三条判据(零禁用行 / 提供行 / preset 探针)+ **三态**判定;
|
|
14
|
+
* ③ `compareBinScopeVersions()` —— **硬前置**「被测 dsh 二进制必须与目标 scope 同源」(P1-0 实测的坑:
|
|
15
|
+
* 宿主 0.1.7 + 插件 0.2 ⇒ 110 条 `disabling profile plugin row`,与"Argent 坏了"无法区分)。
|
|
16
|
+
*
|
|
17
|
+
* ## 为什么"浅镜像"而不是"原样软链"
|
|
18
|
+
* 原样软链会把 dsh 的**组合产物** `cordis.yml` 写回**真实** profile(实测:内容 sha 不变、
|
|
19
|
+
* mtime 变)⇒ 违反"真实 `~/.dsh` 零增量"。镜像后所有写操作落在临时 home。
|
|
20
|
+
*
|
|
21
|
+
* ## 为什么不 `cp -R` node_modules
|
|
22
|
+
* 284 个包整树复制既慢又会**副本漂移**(改仓库插件后副本还是旧字节 ⇒ "重启即生效"是假的)。
|
|
23
|
+
* 镜像里 node_modules 的每个条目仍是**软链**(只读共享真实文件),只有 `@deepseek-ai` 这一项
|
|
24
|
+
* 被换成指向目标 scope 的链接。
|
|
25
|
+
*
|
|
26
|
+
* ## 两套 profile 农场形态都要支持
|
|
27
|
+
* | 形态 | 真实 profile 里 `node_modules/@deepseek-ai` | 重建方式 |
|
|
28
|
+
* |---|---|---|
|
|
29
|
+
* | **整树链接**(本机 0.1.7 web profile 现状) | 一个软链 | 一个软链 → 目标 scope 农场 |
|
|
30
|
+
* | **行式**(0.1.7 的 `profiles/node_modules` 农场) | 真目录,逐包软链 | 真目录 + 逐包软链 → 目标 scope |
|
|
31
|
+
* | **目录式**(0.1.5 的真目录/整树复制) | 真目录,包是真目录 | 真目录 + 逐包软链 → 目标 scope(⛔ 不复制) |
|
|
32
|
+
* 目标 scope 自身是**平铺**(`<scope>/<pkg>`)还是**嵌套**(`<scope>/dsh/node_modules/@deepseek-ai/<pkg>`)
|
|
33
|
+
* 由 `locateScopeFarm()` 探测。
|
|
34
|
+
*
|
|
35
|
+
* ⛔ 本文件是 L1:**零宿主 import**(只有 node 内建)。
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, symlinkSync, copyFileSync } from 'node:fs'
|
|
39
|
+
import { dirname, join, resolve } from 'node:path'
|
|
40
|
+
|
|
41
|
+
/** 判定"这个目录是不是 `@deepseek-ai` 农场"的探针包(按顺序试)。 */
|
|
42
|
+
export const SCOPE_PROBE_PKGS = ['dsh-base', 'dsh', 'dsh-agent-preset']
|
|
43
|
+
|
|
44
|
+
/** 目标 scope 的布局标签(`locateScopeFarm().layout`)。 */
|
|
45
|
+
export const SCOPE_LAYOUT = {
|
|
46
|
+
/** scopeDir 自己就是 `@deepseek-ai`(本机 / 靶子树:`…/node_modules/@deepseek-ai`)。 */
|
|
47
|
+
FLAT: 'flat',
|
|
48
|
+
/** scopeDir 下还有一层 `@deepseek-ai`(`<prefix>/@deepseek-ai`)。 */
|
|
49
|
+
PREFIXED: 'prefixed',
|
|
50
|
+
/** 主包内嵌:`<scope>/dsh/node_modules/@deepseek-ai`(rc.6 式 nested)。 */
|
|
51
|
+
NESTED: 'nested',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 真实 fs 适配器(测试可注入假实现)。 */
|
|
55
|
+
export const defaultIo = {
|
|
56
|
+
exists: (p) => existsSync(p),
|
|
57
|
+
isDir: (p) => { try { return statSync(p).isDirectory() } catch { return false } },
|
|
58
|
+
isSymlink: (p) => { try { return lstatSync(p).isSymbolicLink() } catch { return false } },
|
|
59
|
+
readdir: (p) => { try { return readdirSync(p) } catch { return [] } },
|
|
60
|
+
realpath: (p) => { try { return realpathSync(p) } catch { return null } },
|
|
61
|
+
readlink: (p) => { try { return readlinkSync(p) } catch { return null } },
|
|
62
|
+
readText: (p) => { try { return readFileSync(p, 'utf8') } catch { return null } },
|
|
63
|
+
join,
|
|
64
|
+
dirname,
|
|
65
|
+
resolve,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 读某包的 version(读不到回 null,绝不抛)。 */
|
|
69
|
+
export function readPackageVersion(pkgDir, io = defaultIo) {
|
|
70
|
+
const t = io.readText(io.join(pkgDir, 'package.json'))
|
|
71
|
+
if (!t) return null
|
|
72
|
+
try { return JSON.parse(t).version ?? null } catch { return null }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 定位目标 scope 的 `@deepseek-ai` 农场目录(平铺 / 前缀式 / 嵌套三种都探)。
|
|
77
|
+
* @returns {{farm: string, layout: string, probe: string} | null}
|
|
78
|
+
*/
|
|
79
|
+
export function locateScopeFarm(scopeDir, io = defaultIo) {
|
|
80
|
+
if (!scopeDir) return null
|
|
81
|
+
const cands = [
|
|
82
|
+
{ farm: scopeDir, layout: SCOPE_LAYOUT.FLAT },
|
|
83
|
+
{ farm: io.join(scopeDir, '@deepseek-ai'), layout: SCOPE_LAYOUT.PREFIXED },
|
|
84
|
+
{ farm: io.join(scopeDir, 'node_modules', '@deepseek-ai'), layout: SCOPE_LAYOUT.PREFIXED },
|
|
85
|
+
{ farm: io.join(scopeDir, 'dsh', 'node_modules', '@deepseek-ai'), layout: SCOPE_LAYOUT.NESTED },
|
|
86
|
+
]
|
|
87
|
+
for (const c of cands) {
|
|
88
|
+
for (const probe of SCOPE_PROBE_PKGS) {
|
|
89
|
+
if (io.exists(io.join(c.farm, probe, 'package.json'))) return { ...c, probe }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 农场里的包名清单(跳过点开头;排序保证计划可比对)。 */
|
|
96
|
+
export function listFarmPackages(farm, io = defaultIo) {
|
|
97
|
+
return io.readdir(farm).filter((n) => !n.startsWith('.')).sort()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 探测真实 profile 里 `node_modules/@deepseek-ai` 的形态。
|
|
102
|
+
* @returns {{entry:string, exists:boolean, kind:'symlink'|'dir'|'missing', real:string|null, linkTo:string|null, packages:string[]}}
|
|
103
|
+
*/
|
|
104
|
+
export function inspectProfileScope(profileDir, io = defaultIo) {
|
|
105
|
+
const entry = io.join(profileDir, 'node_modules', '@deepseek-ai')
|
|
106
|
+
if (!io.exists(entry)) return { entry, exists: false, kind: 'missing', real: null, linkTo: null, packages: [] }
|
|
107
|
+
const linkTo = io.isSymlink(entry) ? io.readlink(entry) : null
|
|
108
|
+
const kind = io.isSymlink(entry) ? 'symlink' : (io.isDir(entry) ? 'dir' : 'missing')
|
|
109
|
+
return {
|
|
110
|
+
entry,
|
|
111
|
+
exists: true,
|
|
112
|
+
kind,
|
|
113
|
+
real: io.realpath(entry),
|
|
114
|
+
linkTo: linkTo ? io.resolve(io.dirname(entry), linkTo) : null,
|
|
115
|
+
packages: kind === 'missing' ? [] : listFarmPackages(entry, io),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 生成**隔离 home 的装配计划**(纯数据,不落盘 —— 落盘在 {@link applyProfileWiring})。
|
|
121
|
+
*
|
|
122
|
+
* @param {object} o
|
|
123
|
+
* @param {string} o.profileDir 真实 profile 目录(**只读**)
|
|
124
|
+
* @param {string} o.tmpProfileDir 临时 home 里的目标 profile 目录(计划里所有写操作都在它下面)
|
|
125
|
+
* @param {string} o.targetFarm 目标 scope 的 `@deepseek-ai` 农场
|
|
126
|
+
* @returns {{mode:string, scopeMode:string, actions:Array, notes:string[], missing:string[], packages:string[],
|
|
127
|
+
* sameFarm:boolean, profileFarm:string|null, targetFarm:string}}
|
|
128
|
+
*/
|
|
129
|
+
export function planProfileWiring({ profileDir, tmpProfileDir, targetFarm }, io = defaultIo) {
|
|
130
|
+
const actions = []
|
|
131
|
+
const notes = []
|
|
132
|
+
const scope = inspectProfileScope(profileDir, io)
|
|
133
|
+
const targetReal = io.realpath(targetFarm) || targetFarm
|
|
134
|
+
const profileFarmReal = scope.real
|
|
135
|
+
const sameFarm = !!(profileFarmReal && io.realpath(profileFarmReal) === targetReal)
|
|
136
|
+
|
|
137
|
+
// ── 顶层:文件**复制**(小、且可能是 dsh 的写目标,如 cordis.yml);目录/软链**软链** ──
|
|
138
|
+
actions.push({ op: 'mkdir', path: tmpProfileDir })
|
|
139
|
+
for (const e of io.readdir(profileDir).sort()) {
|
|
140
|
+
if (e === 'node_modules') continue
|
|
141
|
+
const from = io.join(profileDir, e)
|
|
142
|
+
const at = io.join(tmpProfileDir, e)
|
|
143
|
+
if (io.isSymlink(from) || io.isDir(from)) {
|
|
144
|
+
if (io.isDir(from) && !io.isSymlink(from)) {
|
|
145
|
+
// 真目录:镜像一层(条目软链),这样目录内新增文件也落在 tmp
|
|
146
|
+
actions.push({ op: 'mkdir', path: at })
|
|
147
|
+
for (const sub of io.readdir(from).sort()) {
|
|
148
|
+
actions.push({ op: 'symlink', at: io.join(at, sub), to: io.join(from, sub) })
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
actions.push({ op: 'symlink', at, to: from })
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
actions.push({ op: 'copyFile', from, to: at })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── node_modules:除 @deepseek-ai 外全部软链(只读共享,零副本)──────────────
|
|
159
|
+
const realNm = io.join(profileDir, 'node_modules')
|
|
160
|
+
const tmpNm = io.join(tmpProfileDir, 'node_modules')
|
|
161
|
+
actions.push({ op: 'mkdir', path: tmpNm })
|
|
162
|
+
if (io.isDir(realNm)) {
|
|
163
|
+
for (const e of io.readdir(realNm).sort()) {
|
|
164
|
+
if (e === '@deepseek-ai') continue
|
|
165
|
+
actions.push({ op: 'symlink', at: io.join(tmpNm, e), to: io.join(realNm, e) })
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
notes.push(`真实 profile 没有 node_modules 目录(${realNm})—— 只重建 @deepseek-ai`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── @deepseek-ai:按目标 scope 重建 ──────────────────────────────────────
|
|
172
|
+
// 链接目标:同 scope(passthrough)时保持**现役**农场(= 逐字不回归);否则指向目标 scope。
|
|
173
|
+
const linkFarm = sameFarm ? (profileFarmReal || targetFarm) : targetFarm
|
|
174
|
+
const scopeMode = scope.kind === 'symlink' || scope.kind === 'missing' ? 'dir-link' : 'per-pkg'
|
|
175
|
+
const missing = []
|
|
176
|
+
const at = io.join(tmpNm, '@deepseek-ai')
|
|
177
|
+
if (scopeMode === 'dir-link') {
|
|
178
|
+
// 整树链接:一个软链顶替整个 scope(本机 0.1.7 web profile 与审计的手搭 home 都是这一形态)。
|
|
179
|
+
// passthrough 时**原样保留** profile 现役那条软链的字面目标(逐字不回归)。
|
|
180
|
+
const linkTarget = sameFarm && scope.kind === 'symlink' && scope.linkTo ? scope.linkTo : linkFarm
|
|
181
|
+
actions.push({ op: 'symlink', at, to: linkTarget, scopeEntry: true })
|
|
182
|
+
} else {
|
|
183
|
+
actions.push({ op: 'mkdir', path: at, scopeEntry: true })
|
|
184
|
+
for (const pkg of scope.packages) {
|
|
185
|
+
const tgt = io.join(linkFarm, pkg)
|
|
186
|
+
if (!io.exists(tgt)) { missing.push(pkg); continue }
|
|
187
|
+
actions.push({ op: 'symlink', at: io.join(at, pkg), to: tgt, scopeEntry: true })
|
|
188
|
+
}
|
|
189
|
+
if (missing.length) notes.push(`目标 scope 缺 ${missing.length} 个 profile 农场里的包:${missing.slice(0, 8).join(', ')}${missing.length > 8 ? ' …' : ''}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
mode: sameFarm ? 'passthrough' : 'wired',
|
|
194
|
+
scopeMode,
|
|
195
|
+
sameFarm,
|
|
196
|
+
profileFarm: profileFarmReal,
|
|
197
|
+
targetFarm,
|
|
198
|
+
linkFarm,
|
|
199
|
+
packages: scope.packages,
|
|
200
|
+
missing,
|
|
201
|
+
notes,
|
|
202
|
+
actions,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 「计划绝不写真实 profile」的机器判据(防止后来者加一条 `copyFile` 到真实树里)。
|
|
208
|
+
* @returns {{ok:boolean, violations:string[]}}
|
|
209
|
+
*/
|
|
210
|
+
export function planStaysInsideTmp(plan, { profileDir, tmpProfileDir }, io = defaultIo) {
|
|
211
|
+
const tmp = io.realpath(tmpProfileDir) || tmpProfileDir
|
|
212
|
+
const inside = (p) => { const s = String(p); return s === tmp || s.startsWith(tmp + '/') }
|
|
213
|
+
const violations = []
|
|
214
|
+
for (const a of plan.actions) {
|
|
215
|
+
if (a.op === 'symlink') {
|
|
216
|
+
if (!inside(a.at)) violations.push(`symlink.at 不在临时 home: ${a.at}`)
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
const target = a.op === 'mkdir' ? a.path : a.to
|
|
220
|
+
if (!inside(target)) violations.push(`${a.op} 的目标不在临时 home: ${target}`)
|
|
221
|
+
}
|
|
222
|
+
const realProfile = io.realpath(profileDir) || profileDir
|
|
223
|
+
const insideReal = (p) => { const s = String(p); return s === realProfile || s.startsWith(realProfile + '/') }
|
|
224
|
+
for (const a of plan.actions) {
|
|
225
|
+
if (a.op === 'copyFile' && insideReal(a.to)) violations.push(`copyFile 写回真实 profile: ${a.to}`)
|
|
226
|
+
}
|
|
227
|
+
return { ok: violations.length === 0, violations }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 落盘(真实 fs)。调用前应先跑 {@link planStaysInsideTmp}。 */
|
|
231
|
+
export function applyProfileWiring(plan, io = defaultIo) {
|
|
232
|
+
const done = []
|
|
233
|
+
for (const a of plan.actions) {
|
|
234
|
+
if (a.op === 'mkdir') {
|
|
235
|
+
if (!io.exists(a.path)) { mkdirSync(a.path, { recursive: true }); done.push(a) }
|
|
236
|
+
} else if (a.op === 'copyFile') {
|
|
237
|
+
if (io.exists(a.from)) { copyFileSync(a.from, a.to); done.push(a) }
|
|
238
|
+
} else if (a.op === 'symlink') {
|
|
239
|
+
try { symlinkSync(a.to, a.at) } catch (e) { if (e.code !== 'EEXIST') throw e }
|
|
240
|
+
done.push(a)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return done
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 自证:从**临时 profile** 解析探针包,必须落到**目标 scope** 里。
|
|
248
|
+
* @param {object} o
|
|
249
|
+
* @param {string} o.tmpProfileDir
|
|
250
|
+
* @param {string} o.targetFarm
|
|
251
|
+
* @param {(spec:string)=>string|null} o.resolveFrom 注入的解析器(避免本 L1 文件 import 宿主)
|
|
252
|
+
* @returns {{ok:boolean, spec:string|null, resolved:string|null, expected:string, why:string}}
|
|
253
|
+
*/
|
|
254
|
+
export function verifyScopeResolution({ tmpProfileDir, targetFarm, resolveFrom }, io = defaultIo) {
|
|
255
|
+
const expected = io.realpath(targetFarm) || targetFarm
|
|
256
|
+
for (const pkg of SCOPE_PROBE_PKGS) {
|
|
257
|
+
const spec = `@deepseek-ai/${pkg}`
|
|
258
|
+
let resolved = null
|
|
259
|
+
try { resolved = resolveFrom(spec) } catch { resolved = null }
|
|
260
|
+
if (!resolved) continue
|
|
261
|
+
const real = io.realpath(resolved) || resolved
|
|
262
|
+
const ok = real === expected || real.startsWith(expected + '/')
|
|
263
|
+
return {
|
|
264
|
+
ok, spec, resolved: real, expected,
|
|
265
|
+
why: ok
|
|
266
|
+
? `${spec} ⇒ ${real}(在目标 scope 内)`
|
|
267
|
+
: `${spec} 解析到 ${real},**不在**目标 scope ${expected} 内 ⇒ 目标 scope 未接好`,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
ok: false, spec: null, resolved: null, expected,
|
|
272
|
+
why: `临时 profile 里 ${SCOPE_PROBE_PKGS.map((p) => `@deepseek-ai/${p}`).join(' / ')} 一个都解析不到 ⇒ 目标 scope 不可用`,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── 硬前置:被测 dsh 二进制必须与**目标 scope 同源** ────────────────────────
|
|
277
|
+
//
|
|
278
|
+
// 由来(2026-10-12 P1-0 实测):隔离 profile 的 `@deepseek-ai` 已按**目标 scope** 重接线,
|
|
279
|
+
// 而被测的 `dsh` 可执行文件却是**本机全局**那一个(`resolveDshBin()` 先 PATH、后硬编码
|
|
280
|
+
// `/opt/homebrew/bin/dsh`)⇒ 出现「插件 0.2.0-rc.2 + 宿主 0.1.7-rc.2」的混搭:
|
|
281
|
+
//
|
|
282
|
+
// dsh: disabling profile plugin row "plugin-manager": Plugin @deepseek-ai/dsh-plugin-manager@0.2.0-rc.2
|
|
283
|
+
// is incompatible with dsh 0.1.7-rc.2: peerDependencies {…}. …
|
|
284
|
+
// dsh: startup failed: 1 required plugin did not activate
|
|
285
|
+
//
|
|
286
|
+
// 老输出只能把 110 条禁用行归因成「目标 scope 自身与本次 dsh 不兼容」——**方向对但没点名根因**,
|
|
287
|
+
// 排障时会让人去怀疑目标 scope(其实它才是我们想要的那个)。⇒ 固化成**判据**:两者版本不一致
|
|
288
|
+
// 就**响亮判红**(附双方版本 + 出路),⛔ 不许静默跑出一个"看起来像 Argent 坏了"的红。
|
|
289
|
+
|
|
290
|
+
/** 同源判据的三态。 */
|
|
291
|
+
export const BIN_SOURCE_LEVEL = { OK: 'ok', WARN: 'warn', FAIL: 'fail' }
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* ⚠️ 候选序列的唯一实现(含**平铺档** `<dir>/node_modules/.bin/dsh`)在真源
|
|
295
|
+
* `scripts/lib/dsh-scope.mjs#scopeBinCandidates()`,而"取第一个存在的"那个取用口
|
|
296
|
+
* (`resolveBootcheckBin()`)也在同一文件(P1-3 2026-10-12 上移 + 搬家)。
|
|
297
|
+
*
|
|
298
|
+
* **本文件刻意不 import 它**:本文件被 `scripts/tests/kernel-purity.test.mjs` 认领为 **L1**
|
|
299
|
+
* (理由原文:"纯 node 内建、零宿主 import"),而该门禁的既有约定是 `scripts/lib/dsh-*.mjs`
|
|
300
|
+
* 属 **L2**(宿主运维档)⇒ **L1 不得引用 `dsh-*` 模块**。要"这个 scope 自带的 dsh"的调用方
|
|
301
|
+
* (`plugin-bootcheck.mjs`,L2)直接从真源取 —— 两条纪律("候选序列只许一份实现" +
|
|
302
|
+
* "L1 零宿主 import")都成立,且**不必为绕过门禁抬任何基线**。
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 「被测 dsh 与目标 scope 是否同源」的**纯判据**(比对双方自报的 dsh 版本)。
|
|
307
|
+
*
|
|
308
|
+
* · 两边版本都拿到且**一致** ⇒ `ok`
|
|
309
|
+
* · 两边版本都拿到且**不一致** ⇒ `fail`(硬前置不满足);`allowMismatch` 时降级成 `warn`
|
|
310
|
+
* · 任一边拿不到 ⇒ `warn`(⛔ 不静默:明说"无法证明同源",而不是假装通过)
|
|
311
|
+
*
|
|
312
|
+
* @param {object} o
|
|
313
|
+
* @param {string|null} o.binVersion 被测 dsh 自报版本(`dsh --version`)
|
|
314
|
+
* @param {string|null} o.scopeVersion 目标 scope 里 `@deepseek-ai/dsh` 的版本
|
|
315
|
+
* @param {string|null} [o.binPath] 被测 dsh 路径(证据原文用)
|
|
316
|
+
* @param {string|null} [o.scopeDir] 目标 scope 目录(证据原文用)
|
|
317
|
+
* @param {boolean} [o.allowMismatch] 显式接受风险(`BOOTCHECK_ALLOW_VERSION_MISMATCH=1`)⇒ warn
|
|
318
|
+
* @returns {{level:'ok'|'warn'|'fail', why:string}}
|
|
319
|
+
*/
|
|
320
|
+
export function compareBinScopeVersions({ binVersion = null, scopeVersion = null, binPath = null, scopeDir = null, allowMismatch = false } = {}) {
|
|
321
|
+
const detail = `被测 dsh=${binPath ?? '?'}(自报 ${binVersion ?? '拿不到'})|目标 scope=${scopeDir ?? '?'}(dsh=${scopeVersion ?? '拿不到'})`
|
|
322
|
+
if (binVersion && scopeVersion) {
|
|
323
|
+
if (binVersion === scopeVersion) return { level: BIN_SOURCE_LEVEL.OK, why: `同源(双方 dsh 版本一致 = ${binVersion}):${detail}` }
|
|
324
|
+
const why = `${detail}\n 两侧**不同源**:隔离 profile 会解析到目标 scope(${scopeVersion}),而宿主可执行文件是 ${binVersion}` +
|
|
325
|
+
` ⇒ 0.2 的 peer 门禁会把 profile 插件行**逐条** disable(\`disabling profile plugin row\` + \`startup failed\`),` +
|
|
326
|
+
`输出与"Argent 坏了"无法区分。\n 出路:① 设 \`DSH_BIN=<目标 scope 自带的 dsh>\`` +
|
|
327
|
+
`(平铺树 \`<dir>/node_modules/.bin/dsh\`;标准布局 \`<prefix>/bin/dsh\`;客户机 \`~/.local/node/bin/dsh\`);` +
|
|
328
|
+
`② 或把 \`DSH_SCOPE\` 指向被测 dsh 所属的那个 scope。` +
|
|
329
|
+
`\n (已实测:把 bin 换成目标 scope 自带的那个 ⇒ 全绿;这不是目标 scope 的问题。)`
|
|
330
|
+
return { level: allowMismatch ? BIN_SOURCE_LEVEL.WARN : BIN_SOURCE_LEVEL.FAIL, why }
|
|
331
|
+
}
|
|
332
|
+
if (!binVersion) return { level: BIN_SOURCE_LEVEL.WARN, why: `拿不到被测 dsh 的自报版本 ⇒ **无法证明**它与目标 scope 同源(⛔ 不静默,按"未证明"记):${detail}` }
|
|
333
|
+
return { level: BIN_SOURCE_LEVEL.WARN, why: `拿不到目标 scope 的 dsh 版本 ⇒ 无法与宿主对照(⛔ 不静默):${detail}` }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── 启动日志判据 ──────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/** 禁用行(0.2 peer 门禁)原文匹配。 */
|
|
339
|
+
export const DISABLE_ROW_RE = /disabling profile plugin row "([^"]+)"/
|
|
340
|
+
/** 装配失败行。 */
|
|
341
|
+
export const STARTUP_FAILED_RE = /^\s*dsh: startup failed:/
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* **提供行判据表**(不是硬编码一个字符串)。
|
|
345
|
+
* 每条 = 某插件 `apply()` 走到底时自己打的那行;`required` 为 true 的必须命中。
|
|
346
|
+
* 来源列写明**仓库内的出处**,改动插件日志时同步改这张表(门禁 `bootcheck-wiring.test.mjs` 会核出处文件里真有该串)。
|
|
347
|
+
*/
|
|
348
|
+
export const PROVIDER_LINES = [
|
|
349
|
+
{
|
|
350
|
+
id: 'argent-core',
|
|
351
|
+
required: true,
|
|
352
|
+
re: /\[argent-core\]\s*已启动/,
|
|
353
|
+
sample: '[argent-core] 已启动,数据目录: …',
|
|
354
|
+
sourceFile: 'plugins/argent-core/src/index.js',
|
|
355
|
+
literals: ["[argent-core] 已启动"],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'argent-flow',
|
|
359
|
+
required: false,
|
|
360
|
+
re: /\[argent-flow\][^\n]*失败回路(?:已装载|已启用)/,
|
|
361
|
+
sample: '[argent-flow] ⏸ 失败回路已装载但**未运行**:… / [argent-flow] 失败回路已启用:…',
|
|
362
|
+
sourceFile: 'plugins/argent-flow/src/index.js',
|
|
363
|
+
literals: ['失败回路已装载但', '失败回路已启用:'],
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: 'argent-gateway',
|
|
367
|
+
required: false,
|
|
368
|
+
re: /\[argent-gateway\][^\n]*(?:已启动,|跳过启动)/,
|
|
369
|
+
sample: '[argent-gateway] 无可用 Bot 配置(…)——跳过启动 / [argent-gateway] 已启动,N 个 Bot 路由(…)',
|
|
370
|
+
sourceFile: 'plugins/argent-gateway/src/index.js',
|
|
371
|
+
literals: ['——跳过启动', '[argent-gateway] 已启动,'],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'argent-ml',
|
|
375
|
+
required: false,
|
|
376
|
+
re: /\[argent-ml\]\s*已启动/,
|
|
377
|
+
sample: '[argent-ml] 已启动,scriptsDir: …',
|
|
378
|
+
sourceFile: 'plugins/argent-ml/src/index.js',
|
|
379
|
+
literals: ['[argent-ml] 已启动'],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'argent-drive',
|
|
383
|
+
required: false,
|
|
384
|
+
re: /\[argent-drive\]\s*v0\.8-B\s*执行内核已启动/,
|
|
385
|
+
sample: '[argent-drive] v0.8-B 执行内核已启动',
|
|
386
|
+
sourceFile: null, // 插件已退场(仓库内无实现文件)⇒ 只留历史判据,明示无出处
|
|
387
|
+
literals: [],
|
|
388
|
+
},
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
/** 装配级错误特征(与既有脚本同义)。 */
|
|
392
|
+
export const FATAL_RE = /fatal load failure|is not defined|has been registered at|Duplicate service|Cannot find (module|name)|is not a function/
|
|
393
|
+
|
|
394
|
+
/** 逐行抽取 `disabling profile plugin row` 的原文与结构化字段。 */
|
|
395
|
+
export function extractDisableRows(log) {
|
|
396
|
+
const out = []
|
|
397
|
+
for (const line of String(log).split('\n')) {
|
|
398
|
+
const m = DISABLE_ROW_RE.exec(line)
|
|
399
|
+
if (!m) continue
|
|
400
|
+
const p = /Plugin ((?:@[^@\s/]+\/)?[^@\s]+)@([^\s]+) is incompatible with dsh ([^\s:]+)/.exec(line)
|
|
401
|
+
out.push({ row: m[1], pkg: p ? p[1] : null, version: p ? p[2] : null, dshVersion: p ? p[3] : null, line: line.trim() })
|
|
402
|
+
}
|
|
403
|
+
return out
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 把禁用行归因成"**目标 scope 未接好**"还是"目标 scope 本身不兼容"。
|
|
408
|
+
* 判据 = 该行自称的版本 vs **从临时 profile 真解析出来的版本**:
|
|
409
|
+
* · 两者**不一致** ⇒ 这行读的不是我们接的那个 scope(scope 未接好)
|
|
410
|
+
* · 两者**一致** ⇒ 我们有意识地接上的 scope 就是供这个版本(目标 scope 自身不兼容,属真结论)
|
|
411
|
+
*/
|
|
412
|
+
export function classifyDisableRows(rows, { resolveVersion } = {}) {
|
|
413
|
+
if (!rows.length) return { kind: 'none', total: 0, mismatched: [], consistent: [], unresolved: [], summary: '零条禁用行' }
|
|
414
|
+
const mismatched = []
|
|
415
|
+
const consistent = []
|
|
416
|
+
const unresolved = []
|
|
417
|
+
for (const r of rows) {
|
|
418
|
+
const v = r.pkg && resolveVersion ? resolveVersion(r.pkg) : null
|
|
419
|
+
if (!v) unresolved.push(r)
|
|
420
|
+
else if (v !== r.version) mismatched.push({ ...r, resolvedVersion: v })
|
|
421
|
+
else consistent.push({ ...r, resolvedVersion: v })
|
|
422
|
+
}
|
|
423
|
+
let kind
|
|
424
|
+
if (mismatched.length) kind = 'scope-not-wired'
|
|
425
|
+
else if (consistent.length) kind = 'target-scope-incompatible'
|
|
426
|
+
else kind = 'unattributed'
|
|
427
|
+
const summary = kind === 'scope-not-wired'
|
|
428
|
+
? `有 ${mismatched.length} 条禁用行读到的版本**不是**临时 profile 接的那个 scope 的版本 ⇒ 目标 scope 未接好`
|
|
429
|
+
: kind === 'target-scope-incompatible'
|
|
430
|
+
? `有 ${consistent.length} 条禁用行的版本**正是**目标 scope 供的版本 ⇒ 目标 scope 自身与本次 dsh 不兼容`
|
|
431
|
+
: `有 ${rows.length} 条禁用行,但无法解析其包版本 ⇒ 原因未归因`
|
|
432
|
+
return { kind, total: rows.length, mismatched, consistent, unresolved, summary }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** 检查提供行判据表;返回逐条命中情况。 */
|
|
436
|
+
export function matchProviderLines(log) {
|
|
437
|
+
const text = String(log)
|
|
438
|
+
return PROVIDER_LINES.map((c) => ({ id: c.id, required: c.required, hit: c.re.test(text), sample: c.sample, sourceFile: c.sourceFile, literals: c.literals }))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** preset 探针结果。 */
|
|
442
|
+
export function readPresetProbe(log) {
|
|
443
|
+
const text = String(log)
|
|
444
|
+
const done = /\[preset-probe\] DONE fail=(\d+) total=(\d+)/.exec(text)
|
|
445
|
+
const fails = text.split('\n').filter((l) => l.includes('[preset-probe] FAIL')).map((l) => l.trim())
|
|
446
|
+
const oks = text.split('\n').filter((l) => l.includes('[preset-probe] OK')).length
|
|
447
|
+
return { done: !!done, fail: done ? Number(done[1]) : null, total: done ? Number(done[2]) : null, fails, oks }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* **三态判定**。
|
|
452
|
+
* @param {string} log 子进程完整 stdout+stderr
|
|
453
|
+
* @param {object} ctx
|
|
454
|
+
* @param {number} ctx.expectedPresets 期望挂载的 preset 数(0 ⇒ 该判据 n/a)
|
|
455
|
+
* @param {boolean} [ctx.httpReady] HTTP 是否就绪
|
|
456
|
+
* @param {number|null} [ctx.exitCode] 子进程退出码
|
|
457
|
+
* @param {(pkg:string)=>string|null} [ctx.resolveVersion] 从临时 profile 解析包版本
|
|
458
|
+
* @returns {{verdict:'PASS'|'FAIL'|'EVIDENCE-INSUFFICIENT', criteria:Array, reasons:string[], disable:object, providers:Array, probe:object}}
|
|
459
|
+
*/
|
|
460
|
+
export function evaluateBootLog(log, ctx = {}) {
|
|
461
|
+
const text = String(log ?? '')
|
|
462
|
+
const expectedPresets = ctx.expectedPresets ?? 0
|
|
463
|
+
const reasons = []
|
|
464
|
+
const rows = extractDisableRows(text)
|
|
465
|
+
const disable = classifyDisableRows(rows, { resolveVersion: ctx.resolveVersion })
|
|
466
|
+
const providers = matchProviderLines(text)
|
|
467
|
+
const probe = readPresetProbe(text)
|
|
468
|
+
const fatal = FATAL_RE.test(text)
|
|
469
|
+
const startupFailed = text.split('\n').some((l) => STARTUP_FAILED_RE.test(l))
|
|
470
|
+
|
|
471
|
+
// ── 判据 ① 零条 `disabling profile plugin row` / 零条 `startup failed` ──
|
|
472
|
+
const c1 = rows.length === 0 && !startupFailed
|
|
473
|
+
if (!c1) {
|
|
474
|
+
if (rows.length) reasons.push(`判据①失败:${rows.length} 条 \`disabling profile plugin row\`(归因:${disable.summary})`)
|
|
475
|
+
if (startupFailed) reasons.push('判据①失败:出现 `dsh: startup failed:` 行')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── 判据 ② 提供行(判据表,required 全中 + 至少 1 条)──
|
|
479
|
+
const hit = providers.filter((p) => p.hit)
|
|
480
|
+
const missRequired = providers.filter((p) => p.required && !p.hit)
|
|
481
|
+
const c2 = hit.length > 0 && missRequired.length === 0
|
|
482
|
+
if (!c2) {
|
|
483
|
+
reasons.push(missRequired.length
|
|
484
|
+
? `判据②失败:必需提供行缺失 [${missRequired.map((p) => p.id).join(', ')}](表见 PROVIDER_LINES)`
|
|
485
|
+
: '判据②失败:日志里一条提供行都没有(判据表零命中)')
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── 判据 ③ preset 探针(既有判据,⛔ 不放宽成"任一哨兵命中")──
|
|
489
|
+
let c3 = true
|
|
490
|
+
if (expectedPresets > 0) {
|
|
491
|
+
c3 = probe.done && probe.fail === 0 && probe.total === expectedPresets
|
|
492
|
+
if (!c3) {
|
|
493
|
+
reasons.push(probe.done
|
|
494
|
+
? `判据③失败:[preset-probe] DONE fail=${probe.fail} total=${probe.total}(期望 fail=0 total=${expectedPresets})`
|
|
495
|
+
: `判据③失败:preset 挂载探针未完成(期望 ${expectedPresets} 个,实收 OK ${probe.oks} 个)`)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (fatal) reasons.push('日志命中装配级错误特征(FATAL_RE)')
|
|
500
|
+
|
|
501
|
+
// ── 三态 ──
|
|
502
|
+
// 「证据不足」= 拿不到可用日志:零提供行 + 零探针结果 + 零禁用行(子进程没能给出任何判据面)。
|
|
503
|
+
const noEvidence = hit.length === 0 && !probe.done && rows.length === 0 && !fatal && !startupFailed
|
|
504
|
+
let verdict
|
|
505
|
+
if (noEvidence) verdict = 'EVIDENCE-INSUFFICIENT'
|
|
506
|
+
else if (c1 && c2 && c3 && !fatal) verdict = 'PASS'
|
|
507
|
+
else verdict = 'FAIL'
|
|
508
|
+
|
|
509
|
+
return { verdict, criteria: { zeroDisable: c1, providerLine: c2, presetProbe: c3, noFatal: !fatal }, reasons, disable, providers, probe, startupFailed, fatal }
|
|
510
|
+
}
|