@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.
- package/MANIFEST.json +307 -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 +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -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 +377 -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/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 +134 -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 +46 -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 +260 -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/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -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 +1024 -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 +2275 -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 +194 -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.py +262 -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 +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -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/run-tests.sh +256 -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 +136 -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 +73 -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 +34 -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 +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -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 +706 -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 +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* repair-unresolved-tools.mjs — 修复"step/end 时存在未解决 tool call"的 v4 session。
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-09-29):0.1.7 初版加固补丁 `dsh-session--tool-result-block-017.patch` 的
|
|
6
|
+
* "message 必须含 tool-result block" 规则与 v4 形态矛盾(v4 中该 wrapper 是 retired syntax,
|
|
7
|
+
* 迁移器会把 tool/result 展开为 text block),导致合法的 tool/result 在 append 边界被拒:
|
|
8
|
+
* - tool/result 写入失败 → step/end 时悬挂未解决的 tool call
|
|
9
|
+
* - session 只能加载失败("stored log is corrupt: step/end leaves unresolved tool call")
|
|
10
|
+
* 修正版补丁已上线;本脚本用于修复已被毒化的存量 session。
|
|
11
|
+
*
|
|
12
|
+
* 修复原理:对每个未解决点,把前缀交给 dsh 官方 `interruptedTurnClosers` 生成
|
|
13
|
+
* 补齐事件(tool/result、isError、ToolOutcomeUnknownError / ToolNotStartedError),
|
|
14
|
+
* 插入后对后续事件 seq 顺移并修正内部引用(surfaceOp/sourceEventSeqs/messageSeqs/headerSeq)。
|
|
15
|
+
*
|
|
16
|
+
* 用法:
|
|
17
|
+
* node repair-unresolved-tools.mjs --scan 扫描全部
|
|
18
|
+
* node repair-unresolved-tools.mjs --repair <id> [--dir <path>] 预演(不写盘)
|
|
19
|
+
* node repair-unresolved-tools.mjs --repair <id> --apply 写盘(自动备份 .bak)
|
|
20
|
+
*/
|
|
21
|
+
// 2026-09-29 修:原实现写死开发机路径 `/opt/homebrew/lib/...` ⇒ 客户机(node 官方包,
|
|
22
|
+
// 树在 ~/node/lib/node_modules)一跑就 ERR_MODULE_NOT_FOUND。按仓库规矩走共享解析器,
|
|
23
|
+
// 且用**动态 import**(scope 要到运行时才知道)。
|
|
24
|
+
// 0.6.0 F1 修复批(2026-10-14):本脚本**必须**有宿主树(`interruptedTurnClosers` 从它 import)
|
|
25
|
+
// ⇒ 探不到就**响亮失败**(⛔ 不用假路径 —— 旧写法在 npx 宿主上会拿一个不存在的树去 import)。
|
|
26
|
+
import { requireDshScope } from './lib/dsh-scope.mjs';
|
|
27
|
+
let _SCOPE;
|
|
28
|
+
try { _SCOPE = requireDshScope({ what: '未决工具修复(repair-unresolved-tools)' }); }
|
|
29
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(1); }
|
|
30
|
+
const { interruptedTurnClosers } = await import(`${_SCOPE}/dsh-session/lib/index.js`);
|
|
31
|
+
import { zstdCompress, zstdDecompress, constants } from 'node:zlib';
|
|
32
|
+
import { promisify } from 'node:util';
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import os from 'node:os';
|
|
36
|
+
import { execFileSync } from 'node:child_process';
|
|
37
|
+
|
|
38
|
+
const zstdCompressAsync = promisify(zstdCompress);
|
|
39
|
+
const CHECKSUM_OPTIONS = { params: { [constants.ZSTD_c_checksumFlag]: 1 } };
|
|
40
|
+
const SESSIONS_ROOT = path.join(os.homedir(), '.dsh', 'sessions');
|
|
41
|
+
|
|
42
|
+
// ---------- 工具 ----------
|
|
43
|
+
function findSessionDir(id) {
|
|
44
|
+
for (const proj of fs.readdirSync(SESSIONS_ROOT)) {
|
|
45
|
+
const p = path.join(SESSIONS_ROOT, proj, id);
|
|
46
|
+
if (fs.existsSync(path.join(p, 'session.v4.jsonl.zstd'))) return p;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readLog(file) {
|
|
52
|
+
// 注意:node:zlib 的 zstdDecompress 只解第一帧;多帧文件须用 zstd CLI 全量解压
|
|
53
|
+
const raw = execFileSync('zstd', ['-d', '-c', file], { maxBuffer: 1 << 30 });
|
|
54
|
+
const lines = raw.toString('utf8').trim().split('\n');
|
|
55
|
+
return { header: JSON.parse(lines[0]), events: lines.slice(1).map(l => JSON.parse(l)) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeLog(file, header, events) {
|
|
59
|
+
const headerLine = JSON.stringify(header) + '\n';
|
|
60
|
+
const body = events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
61
|
+
const f1 = await zstdCompressAsync(Buffer.from(headerLine), CHECKSUM_OPTIONS);
|
|
62
|
+
const f2 = await zstdCompressAsync(Buffer.from(body), CHECKSUM_OPTIONS);
|
|
63
|
+
fs.writeFileSync(file, Buffer.concat([f1, f2]));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 检测未解决点:step/end(或 turn/end)时 pendingCalls 非空 */
|
|
67
|
+
function analyze(events) {
|
|
68
|
+
const pending = new Map();
|
|
69
|
+
const issues = [];
|
|
70
|
+
for (let i = 0; i < events.length; i++) {
|
|
71
|
+
const ev = events[i], t = ev.type, d = ev.data || {};
|
|
72
|
+
if (t === 'turn/start') pending.clear();
|
|
73
|
+
else if (t === 'turn/end') { if (pending.size) issues.push({ index: i, type: 'turn/end', pending: [...pending.entries()] }); pending.clear(); }
|
|
74
|
+
else if (t === 'step/end') { if (pending.size) issues.push({ index: i, type: 'step/end', pending: [...pending.entries()] }); pending.clear(); }
|
|
75
|
+
else if (t === 'assistant/message') {
|
|
76
|
+
for (const b of (d.message?.content || [])) if (b.type === 'tool-call') pending.set(b.id, { step: d.step });
|
|
77
|
+
} else if (t === 'tool/call') {
|
|
78
|
+
const e = pending.get(d.callId); if (e) e.callSeq = ev.seq;
|
|
79
|
+
} else if (t === 'tool/result') {
|
|
80
|
+
pending.delete(d.message?.source?.callId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return issues;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 顺移事件内部引用 */
|
|
87
|
+
function shiftRefs(ev, fromSeq, delta) {
|
|
88
|
+
const d = ev.data || {};
|
|
89
|
+
if (d.surfaceOp && typeof d.surfaceOp === 'object') {
|
|
90
|
+
if (typeof d.surfaceOp.startSeq === 'number' && d.surfaceOp.startSeq >= fromSeq) d.surfaceOp.startSeq += delta;
|
|
91
|
+
if (typeof d.surfaceOp.endSeq === 'number' && d.surfaceOp.endSeq >= fromSeq) d.surfaceOp.endSeq += delta;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(d.sourceEventSeqs)) d.sourceEventSeqs = d.sourceEventSeqs.map(s => (typeof s === 'number' && s >= fromSeq) ? s + delta : s);
|
|
94
|
+
if (Array.isArray(d.messageSeqs)) d.messageSeqs = d.messageSeqs.map(s => (typeof s === 'number' && s >= fromSeq) ? s + delta : s);
|
|
95
|
+
if (typeof d.headerSeq === 'number' && d.headerSeq >= fromSeq) d.headerSeq += delta;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 修复一个会话的事件流(返回修复后事件数组 + 修复报告) */
|
|
99
|
+
function repair(events) {
|
|
100
|
+
const issues = analyze(events);
|
|
101
|
+
if (issues.length === 0) return { events, fixes: [] };
|
|
102
|
+
let fixed = structuredClone(events);
|
|
103
|
+
const fixes = [];
|
|
104
|
+
// 从后往前修,避免编号漂移
|
|
105
|
+
for (const issue of issues.reverse()) {
|
|
106
|
+
const insertAt = issue.index; // 该 step/end(或 turn/end)在 fixed 中的位置
|
|
107
|
+
const prefix = fixed.slice(0, insertAt);
|
|
108
|
+
const closers = interruptedTurnClosers(prefix);
|
|
109
|
+
const results = closers.filter(c => c.type === 'tool/result');
|
|
110
|
+
if (results.length === 0) { fixes.push({ at: insertAt, note: 'closers 未产出(跳过)' }); continue; }
|
|
111
|
+
const insertSeq = prefix.length > 0 ? prefix.at(-1).seq + 1 : 0;
|
|
112
|
+
// 原事件(step/end 起)顺移
|
|
113
|
+
const tail = fixed.slice(insertAt).map(ev => {
|
|
114
|
+
const copy = structuredClone(ev);
|
|
115
|
+
copy.seq += results.length;
|
|
116
|
+
shiftRefs(copy, insertSeq, results.length);
|
|
117
|
+
return copy;
|
|
118
|
+
});
|
|
119
|
+
fixed = [...prefix, ...results, ...tail];
|
|
120
|
+
fixes.push({ at: insertAt, inserted: results.length, seq: insertSeq, calls: results.map(r => r.data.message.source.callId) });
|
|
121
|
+
}
|
|
122
|
+
// 连续性校验
|
|
123
|
+
for (let i = 0; i < fixed.length; i++) {
|
|
124
|
+
if (fixed[i].seq !== i) throw new Error(`修复后 seq 断裂 at index ${i}: got ${fixed[i].seq}`);
|
|
125
|
+
}
|
|
126
|
+
return { events: fixed, fixes };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------- 主流程 ----------
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const mode = args.includes('--scan') ? 'scan' : args.includes('--repair') ? 'repair' : null;
|
|
132
|
+
const apply = args.includes('--apply');
|
|
133
|
+
const id = args[args.indexOf('--repair') + 1];
|
|
134
|
+
|
|
135
|
+
if (mode === 'scan') {
|
|
136
|
+
let checked = 0, broken = 0;
|
|
137
|
+
for (const proj of fs.readdirSync(SESSIONS_ROOT)) {
|
|
138
|
+
for (const sess of fs.readdirSync(path.join(SESSIONS_ROOT, proj))) {
|
|
139
|
+
const f = path.join(SESSIONS_ROOT, proj, sess, 'session.v4.jsonl.zstd');
|
|
140
|
+
if (!fs.existsSync(f)) continue;
|
|
141
|
+
checked++;
|
|
142
|
+
try {
|
|
143
|
+
const { events } = await readLog(f);
|
|
144
|
+
const issues = analyze(events);
|
|
145
|
+
if (issues.length) {
|
|
146
|
+
broken++;
|
|
147
|
+
console.log(`☠ ${sess} (${issues.length} 个未解决点)`);
|
|
148
|
+
for (const is of issues) console.log(` ${is.type} · 缺失: ${is.pending.map(([c]) => c).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.log(`?! ${sess}: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(`\n扫描 ${checked} 个 v4 文件;损坏 ${broken} 个`);
|
|
156
|
+
} else if (mode === 'repair' && id) {
|
|
157
|
+
const dir = findSessionDir(id);
|
|
158
|
+
if (!dir) { console.error(`找不到会话: ${id}`); process.exit(1); }
|
|
159
|
+
const file = path.join(dir, 'session.v4.jsonl.zstd');
|
|
160
|
+
const { header, events } = await readLog(file);
|
|
161
|
+
console.log(`会话 ${id}: ${events.length} 事件`);
|
|
162
|
+
const issues = analyze(events);
|
|
163
|
+
if (!issues.length) { console.log('✓ 健康,无需修复'); process.exit(0); }
|
|
164
|
+
console.log(`发现 ${issues.length} 个未解决点`);
|
|
165
|
+
const { events: fixed, fixes } = repair(events);
|
|
166
|
+
for (const fx of fixes) console.log(' 修复:', JSON.stringify(fx));
|
|
167
|
+
console.log(`修复后: ${fixed.length} 事件,seq 0..${fixed.at(-1).seq}`);
|
|
168
|
+
if (!apply) {
|
|
169
|
+
console.log('\n[预演] 未写盘。加 --apply 执行(自动备份 .bak-<ts>)');
|
|
170
|
+
} else {
|
|
171
|
+
const bak = `${file}.bak-${Date.now()}`;
|
|
172
|
+
fs.copyFileSync(file, bak);
|
|
173
|
+
await writeLog(file, header, fixed);
|
|
174
|
+
console.log(`\n✓ 已写盘。备份: ${bak}`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log('用法:\n node repair-unresolved-tools.mjs --scan\n node repair-unresolved-tools.mjs --repair <session-id> [--apply]');
|
|
178
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 全量测试入口(2026-09-30 补:本仓库此前有 123 个测试却无总入口 ⇒ 门禁无法一键复验)
|
|
3
|
+
#
|
|
4
|
+
# 用法:
|
|
5
|
+
# bash scripts/run-tests.sh [--filter <关键字>] [--skip <子串>]...
|
|
6
|
+
# bash scripts/run-tests.sh --clean-tree-check [--filter <关键字>]
|
|
7
|
+
# bash scripts/run-tests.sh --print-git-skips [<树>] # 测试钩子:打印该树里因缺 .git 要跳过的用例
|
|
8
|
+
# · *.test.mjs → node
|
|
9
|
+
# · *.test.py → venv python(缺 venv 则回落系统 python3)
|
|
10
|
+
# 退出码:任一失败即 1;末尾打印通过/失败计数与失败清单。
|
|
11
|
+
#
|
|
12
|
+
# `--clean-tree-check` = **干净树自检**:`git archive HEAD` 解到临时目录后跑**同一套**判据,
|
|
13
|
+
# 用来判"HEAD 既存红"(提交本身就是红的)。它**自动规避三类环境性假红**(2026-10-08 固化):
|
|
14
|
+
# ① `/tmp` → `/private/tmp` 符号链接(用 `pwd -P` 的物理路径建树并 `cd` 进去);
|
|
15
|
+
# ② git 不跟踪的空目录(`git archive` 不带 ⇒ `ENOENT`),自动镜像 —— **通用兜底**;
|
|
16
|
+
# `scripts/flow-prompts/` 曾是唯一实例,该用例的根因已于 2026-10-08 修(用例容忍缺目录);
|
|
17
|
+
# ③ **需要 `.git` 的用例**(裸检出没有 `.git`)按**判据 `[ -d .git ]`** 跳过、点名原因
|
|
18
|
+
# (判据在 `git_index_skips()` 一处;候选见 `NEED_GIT_CANDIDATES`)。
|
|
19
|
+
# 判据与两条假红的区别见 docs/CI-自建runner.md §8.1「两类假红」。保树调试:`ARGENT_CLEAN_TREE_KEEP=1`。
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
cd "$REPO"
|
|
23
|
+
PY="$HOME/.dsh/argent/venv/bin/python3"; [ -x "$PY" ] || PY=python3
|
|
24
|
+
# 测试期间**绝不发遥测**(2026-09-30:夹具流程 flowA 的失败事件曾被推到生产告警)
|
|
25
|
+
export ARGENT_TELEMETRY_OFF=1
|
|
26
|
+
export ARGENT_TESTS_OFFLINE=1 # 测试期**零网络**(2026-10-11):监听类用例(dsh-release-watch 等)见到它就**拒绝**真发请求 ⇒ "夹具回归零网络"由开关兜住,不靠自觉
|
|
27
|
+
# 测试进程**不继承会话/实例的 DSH_HOME**(2026-10-02):
|
|
28
|
+
# DSH_HOME 是**实例级**开关,而数据目录三级链把它排在"默认 ~/.dsh/argent"之前 ⇒
|
|
29
|
+
# 测试用假 HOME 隔离时会被会话的 DSH_HOME 覆盖,**静默写进真实实例**(实测:本机
|
|
30
|
+
# DSH_HOME=~/.dsh 时,role-mapping/skin/设置页用例把 roles.json、skin.json 写进了生产数据目录)。
|
|
31
|
+
# 故跑测试时清掉它(venv 解释器先落成 ARGENT_PY,避免清掉后找不到 venv)。
|
|
32
|
+
if [ -z "${ARGENT_PY:-}" ] && [ -x "${DSH_HOME:-$HOME/.dsh}/argent/venv/bin/python3" ]; then
|
|
33
|
+
export ARGENT_PY="${DSH_HOME:-$HOME/.dsh}/argent/venv/bin/python3"
|
|
34
|
+
fi
|
|
35
|
+
unset DSH_HOME
|
|
36
|
+
|
|
37
|
+
# ── 「需要 git 索引」的用例:**按判据跳过**,⛔ 不是一份"永远跳过"的静态名单 ──────────────
|
|
38
|
+
# 判据只有一个:**被跑的树里有没有 `.git`**(`[ -d .git ]`)。
|
|
39
|
+
# · 树里有 `.git` ⇒ 这些用例**照常跑**(它们的判据本身就是"仓库/工作树状态");
|
|
40
|
+
# · 树里没有(`git archive HEAD` 出来的裸检出)⇒ 显式跳过并**点名原因**(这才是"干净树不适用")。
|
|
41
|
+
# 为什么必须判据驱动(2026-10-14 批):③ 的实例里 `agent-title-naming` / `dead-pointer-scan`
|
|
42
|
+
# 靠 `git ls-files` 列受控文件 ⇒ 裸检出(没有索引)里恒红,是**检查器**的缺陷,不是用例的问题。
|
|
43
|
+
# ⛔ 不许因此改那两条用例的判据、⛔ 不许把它们删掉 —— 只在这里按 `.git` 的有无跳过。
|
|
44
|
+
NEED_GIT_CANDIDATES=(
|
|
45
|
+
# release-dl:跑 release-argent.sh,它做 git status / git rev-parse refs/tags
|
|
46
|
+
release-dl
|
|
47
|
+
# install-capabilities:【7b】完整安装以本仓为克隆源(ARGENT_REPO_URL=<repo>)⇒ 需 .git
|
|
48
|
+
install-capabilities
|
|
49
|
+
# install-registry-loadable:按设计要读工作树/仓库身份(`git rev-parse` 一类)
|
|
50
|
+
install-registry-loadable
|
|
51
|
+
# agent-title-naming:`git ls-files -z` 列受控文本文件(裸检出没有索引 ⇒ execFileSync 抛)
|
|
52
|
+
agent-title-naming
|
|
53
|
+
# dead-pointer-scan:同上(`git ls-files -z` 列受控文件)
|
|
54
|
+
dead-pointer-scan
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 该树里**因缺 `.git`** 要跳过的用例子串(一行一个;有 `.git` ⇒ 零行)。
|
|
58
|
+
# ⭐ 这是判据的**唯一实现**:干净树分支与回归夹具(`scripts/tests/clean-tree-git-skips.test.mjs`)
|
|
59
|
+
# 都调它,⛔ 不写第二份名单。
|
|
60
|
+
git_index_skips() {
|
|
61
|
+
local tree="${1:-.}"
|
|
62
|
+
[ -d "$tree/.git" ] || printf '%s\n' "${NEED_GIT_CANDIDATES[@]}"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ── 参数解析(**默认行为不变**:不带参数 = 跑全量套件)──────────────────────────────
|
|
66
|
+
MODE="run"; FILTER=""; SKIP=()
|
|
67
|
+
while [ $# -gt 0 ]; do
|
|
68
|
+
case "${1:-}" in
|
|
69
|
+
--filter) FILTER="${2:-}"; if [ $# -ge 2 ]; then shift 2; else shift; fi ;;
|
|
70
|
+
--skip) if [ $# -ge 2 ] && [ -n "${2:-}" ]; then SKIP+=("$2"); shift 2
|
|
71
|
+
else echo " ✗ --skip 需要一个非空子串(用法见 bash scripts/run-tests.sh --help)"; exit 2; fi ;;
|
|
72
|
+
--clean-tree-check) MODE="clean-tree"; shift ;;
|
|
73
|
+
--print-git-skips) git_index_skips "${2:-.}"; exit 0 ;;
|
|
74
|
+
-h|--help) sed -n '4,19p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
75
|
+
*) echo " ✗ 未知参数:$1(用法见 bash scripts/run-tests.sh --help)"; exit 2 ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# 命中任一 --skip 子串 ⇒ 跳过(与 --filter 同样按路径子串匹配)
|
|
80
|
+
skip_match() {
|
|
81
|
+
local f="$1" s
|
|
82
|
+
for s in ${SKIP[@]+"${SKIP[@]}"}; do [ -n "$s" ] && [[ "$f" == *"$s"* ]] && return 0; done
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ── `--clean-tree-check`:先建干净树并规避三类环境性假红,再把自己重跑一遍(判据不变)──────
|
|
87
|
+
if [ "$MODE" = "clean-tree" ]; then
|
|
88
|
+
if [ "${ARGENT_CLEAN_TREE_PHASE:-}" = "1" ]; then
|
|
89
|
+
echo " ✗ 已经在干净树自检的子里程里了(不要嵌套)——直接跑 bash scripts/run-tests.sh 即可"; exit 2
|
|
90
|
+
fi
|
|
91
|
+
command -v git >/dev/null 2>&1 || { echo " ✗ 需要 git(干净树自检靠 git archive HEAD 取树)"; exit 2; }
|
|
92
|
+
git -C "$REPO" rev-parse --verify HEAD >/dev/null 2>&1 || {
|
|
93
|
+
echo " ✗ $REPO 不是一个带 HEAD 的 git 仓 —— 干净树自检要在**有 .git 的检出**里发起"
|
|
94
|
+
echo " (它取的是 HEAD 的内容;在裸检出里无从取树)"; exit 2; }
|
|
95
|
+
|
|
96
|
+
CLEAN_DIR="$(mktemp -d "${TMPDIR:-/tmp}argent-clean-tree-XXXXXX")" || exit 2
|
|
97
|
+
if [ "${ARGENT_CLEAN_TREE_KEEP:-}" != "1" ]; then
|
|
98
|
+
trap 'rm -rf "$CLEAN_DIR"' EXIT INT TERM
|
|
99
|
+
fi
|
|
100
|
+
git -C "$REPO" archive HEAD | tar -x -C "$CLEAN_DIR" || { echo " ✗ git archive HEAD 取树失败"; exit 2; }
|
|
101
|
+
|
|
102
|
+
# 闸门:**取出来的那棵树里的 `run-tests.sh` 必须认识 `--skip`**。
|
|
103
|
+
# 为什么必须有它(2026-10-08 实测踩到):干净树取自 `HEAD`,若本次改动**还没提交**,
|
|
104
|
+
# 树里就是**旧版**脚本 —— 而旧版**静默忽略**未知参数 ⇒ `--skip`/`--filter` 全部失效、
|
|
105
|
+
# 跑成"全量 + 那 2 条按设计跳过的用例照样红" ⇒ 输出看起来像"干净树有 4 条红",
|
|
106
|
+
# **把环境性假红伪装成 HEAD 既存红**(正是本自检要消灭的误导)。宁可当场停下,也不给假结论。
|
|
107
|
+
if ! grep -q -- '--skip)' "$CLEAN_DIR/scripts/run-tests.sh" 2>/dev/null; then
|
|
108
|
+
echo " ✗ HEAD 里的 scripts/run-tests.sh 还是旧版(不认识 --skip)——多半是本次改动尚未提交。"
|
|
109
|
+
echo " 裸检出取的是 HEAD 的内容 ⇒ 现在跑只会得到**误导性**结果(--skip/--filter 被静默忽略)。"
|
|
110
|
+
echo " 处置:先 commit(或先把新版 run-tests.sh 提交),再跑 --clean-tree-check。"
|
|
111
|
+
exit 2
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ① 真实路径:`/tmp` → `/private/tmp`(macOS 亦含 `/var` → `/private/var`)是符号链接。
|
|
115
|
+
# 若沿用逻辑路径 `$PWD`,node 侧(`import.meta.url` 走 realpath)与 bash 侧(`pwd` 走 `$PWD`)
|
|
116
|
+
# 会各说各话 ⇒ `repo-root` 用例句"开发机:解析到本仓"恒红。故一律 `pwd -P` 取物理路径再 cd 进去。
|
|
117
|
+
REAL="$(cd "$CLEAN_DIR" && pwd -P)" || exit 2
|
|
118
|
+
cd "$REAL"
|
|
119
|
+
|
|
120
|
+
# ② git **不跟踪空目录** ⇒ `git archive` 自然不带 ⇒ 裸检出里没有该目录。
|
|
121
|
+
# **历史实例**:`scripts/flow-prompts/`(`check-sop-consistency.mjs` 的 C2 曾无条件 readdirSync 它
|
|
122
|
+
# ⇒ 真·新克隆上 ENOENT 判红)。**该条根因已于 2026-10-08 修**(用例容忍缺目录,
|
|
123
|
+
# 见 `legacyPromptFiles()`)⇒ 本步骤现在只是**通用兜底**:对**其它**未跟踪空目录仍然必要。
|
|
124
|
+
# 只镜像**空目录**:内容为空 ⇒ 不可能夹带任何在制品,纯粹补回"目录存在"这个事实。
|
|
125
|
+
MIRRORED=()
|
|
126
|
+
while IFS= read -r d; do
|
|
127
|
+
[ -n "$d" ] || continue
|
|
128
|
+
rel="${d#./}"
|
|
129
|
+
[ -d "$REAL/$rel" ] && continue
|
|
130
|
+
mkdir -p "$REAL/$rel" && MIRRORED+=("$rel")
|
|
131
|
+
done < <(cd "$REPO" && find . -type d -empty -not -name .git -not -name node_modules -not -path '*/.git/*' -not -path '*/node_modules/*' 2>/dev/null | sort)
|
|
132
|
+
|
|
133
|
+
# ③ 需要 `.git` 的用例:**干净树不适用**(裸检出没有 `.git`,它们查的是"仓库/工作树状态",
|
|
134
|
+
# 不是被测代码本身)。⭐ **判据 = `[ -d .git ]`**(实现在 `git_index_skips()` 一处):
|
|
135
|
+
# 树里有 `.git` ⇒ 照常跑;没有 ⇒ 跳过并点名原因。⛔ **不因此改用例语义**、⛔ 不删用例。
|
|
136
|
+
NEED_GIT=()
|
|
137
|
+
while IFS= read -r g; do [ -n "$g" ] && NEED_GIT+=("$g"); done < <(git_index_skips "$REAL")
|
|
138
|
+
|
|
139
|
+
echo
|
|
140
|
+
echo " ═══ 干净树自检(HEAD=$(git -C "$REPO" rev-parse --short HEAD),不含工作树在制品)═══"
|
|
141
|
+
echo " 树位置:$REAL"
|
|
142
|
+
echo " ① 符号链接:已用 pwd -P 的**物理路径**建树并 cd(规避 /tmp → /private/tmp)"
|
|
143
|
+
if [ "${#MIRRORED[@]}" -gt 0 ]; then
|
|
144
|
+
echo " ② 未跟踪空目录:已镜像 ${#MIRRORED[@]} 个 —— ${MIRRORED[*]}"
|
|
145
|
+
else
|
|
146
|
+
echo " ② 未跟踪空目录:无(无需镜像)"
|
|
147
|
+
fi
|
|
148
|
+
if [ -d "$REAL/.git" ]; then
|
|
149
|
+
echo " ③ 需要 .git 的用例:判据 \`[ -d .git ]\` ⇒ 这棵树**有** .git ⇒ 全部照常跑(0 条跳过)"
|
|
150
|
+
else
|
|
151
|
+
echo " ③ 需要 .git 的用例:判据 \`[ -d .git ]\` ⇒ 这棵树**没有** .git ⇒ 按设计跳过 ${#NEED_GIT[@]} 条 —— ${NEED_GIT[*]}"
|
|
152
|
+
echo " · release-dl:跑 release-argent.sh --help,该脚本做 git status / git rev-parse refs/tags"
|
|
153
|
+
echo " · install-capabilities:【7b】完整安装以本仓为克隆源(ARGENT_REPO_URL=<repo>)⇒ 需 .git"
|
|
154
|
+
echo " · install-registry-loadable:按设计要读工作树/仓库身份(\`git rev-parse\` 一类)"
|
|
155
|
+
echo " · agent-title-naming / dead-pointer-scan:靠 \`git ls-files\` 列受控文件 ⇒ 裸检出(无索引)里必然抛(2026-10-14 复现;检查器按判据跳过,⛔ 不改用例判据、⛔ 不删用例)"
|
|
156
|
+
fi
|
|
157
|
+
echo " ⛔ 这是「干净树不适用」,**不是红**;不得据此放宽这些用例的判据(docs/CI-自建runner.md §8.1)"
|
|
158
|
+
echo " · 判据的唯一实现:\`bash scripts/run-tests.sh --print-git-skips <树>\`(一行一个;有 .git ⇒ 零行)"
|
|
159
|
+
echo " ═══════════════════════════════════════════════════════════════════════"
|
|
160
|
+
|
|
161
|
+
inner=()
|
|
162
|
+
for g in "${NEED_GIT[@]}"; do inner+=(--skip "$g"); done
|
|
163
|
+
for s in ${SKIP[@]+"${SKIP[@]}"}; do inner+=(--skip "$s"); done
|
|
164
|
+
[ -n "$FILTER" ] && inner+=(--filter "$FILTER")
|
|
165
|
+
env ARGENT_CLEAN_TREE_PHASE=1 bash "$REAL/scripts/run-tests.sh" ${inner[@]+"${inner[@]}"}
|
|
166
|
+
rc=$?
|
|
167
|
+
if [ "${ARGENT_CLEAN_TREE_KEEP:-}" = "1" ]; then echo " (树已保留:${REAL})"; fi
|
|
168
|
+
exit $rc
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
pass=0; fail=0; failed=(); skipped=()
|
|
172
|
+
shopt -s nullglob
|
|
173
|
+
|
|
174
|
+
# ── 测试环境卫生:**结果判据**(2026-10-03 两次事故后补;2026-10-04 扩成三层)──────────────
|
|
175
|
+
# 为什么在这里:跑测试**前**给真实数据目录拍一份只读快照,跑完**后**比对 ⇒ 只要有任何一条
|
|
176
|
+
# 路径漏了隔离、写了真实目录,就在**这一轮**被抓(不靠"下次注意")。三层判据:
|
|
177
|
+
# ① 关键文件(skin.json / flow-ledger.jsonl / license-gate.jsonl …)的 size/mtime/sha256 —— **判红**;
|
|
178
|
+
# ② 目录级发现:全树**路径集合差**,只报"新增" —— 抓"在真实目录里新建文件"这个盲区
|
|
179
|
+
# (1016 份夹具备份就是这么溜过去的:当时①全程绿);
|
|
180
|
+
# ③ 配置形态(`*.json`/`*.yml`/`*.yaml`/`*.toml`,排除体积区)的**内容** sha256。
|
|
181
|
+
# 姿态(起步):②③ **只报告、不改退出码**(允许表 `CHURN_ALLOW` 稳定前不判红;要判红得显式
|
|
182
|
+
# `--verify <snap> --strict`)⇒ 本脚本的失败判据仍只由①与各用例决定。
|
|
183
|
+
# 快照文件落在 ${TMPDIR}(不在数据目录里);判据实现见 scripts/check-test-env-safety.mjs。
|
|
184
|
+
SNAP="$(mktemp -t argent-tests-env-snapshot)"
|
|
185
|
+
node scripts/check-test-env-safety.mjs --snapshot "$SNAP" >/dev/null 2>&1 || true
|
|
186
|
+
|
|
187
|
+
for f in scripts/tests/*.test.mjs; do
|
|
188
|
+
[ -n "$FILTER" ] && [[ "$f" != *"$FILTER"* ]] && continue
|
|
189
|
+
if skip_match "$f"; then skipped+=("$f"); continue; fi
|
|
190
|
+
if out=$(env -u DSH_HOME node "$f" 2>&1); then pass=$((pass+1)); else fail=$((fail+1)); failed+=("$f"); printf ' ❌ %s\n%s\n' "$f" "$(printf '%s' "$out" | tail -6 | sed 's/^/ /')"; fi
|
|
191
|
+
done
|
|
192
|
+
for f in scripts/tests/*.test.py; do
|
|
193
|
+
[ -n "$FILTER" ] && [[ "$f" != *"$FILTER"* ]] && continue
|
|
194
|
+
if skip_match "$f"; then skipped+=("$f"); continue; fi
|
|
195
|
+
if out=$(env -u DSH_HOME "$PY" "$f" 2>&1); then pass=$((pass+1)); else fail=$((fail+1)); failed+=("$f"); printf ' ❌ %s\n%s\n' "$f" "$(printf '%s' "$out" | tail -6 | sed 's/^/ /')"; fi
|
|
196
|
+
done
|
|
197
|
+
echo
|
|
198
|
+
if [ "${#skipped[@]}" -gt 0 ]; then
|
|
199
|
+
printf ' ⏭ 按 --skip 跳过 %d 条(**不计入失败**,但它们没有在本轮被验证):%s\n' "${#skipped[@]}" "${skipped[*]}"
|
|
200
|
+
fi
|
|
201
|
+
echo " ============================================="
|
|
202
|
+
printf ' 测试结果:%d 通过 / %d 失败' "$pass" "$fail"
|
|
203
|
+
[ "$fail" -eq 0 ] && printf ' ✓\n' || printf '\n 失败清单:%s\n' "${failed[*]}"
|
|
204
|
+
echo " ============================================="
|
|
205
|
+
# 红了先分清"真问题"与"环境/在制品":**干净树(= git archive HEAD,不含工作树在制品)也应全绿**。
|
|
206
|
+
# 仍红 ⇒ HEAD 既存红(提交本身的问题,必须修)。判据与**两类假红**的区分见 docs/CI-自建runner.md §8.1。
|
|
207
|
+
# 下面这条命令是**可复现**的:它自己规避符号链接、镜像未跟踪空目录、并按设计跳过需要 .git 的用例。
|
|
208
|
+
[ "$fail" -eq 0 ] || echo " ↳ 干净树判据(可复现):bash scripts/run-tests.sh --clean-tree-check"
|
|
209
|
+
|
|
210
|
+
# ── 跑完立刻比对真实数据目录(只读比较)──────────────────────────────────────────────
|
|
211
|
+
# ① 变了 ⇒ 判红并点名文件(**退出码语义与以前一致**);②③ 的发现只打印(起步姿态),不在这里判红。
|
|
212
|
+
if [ -f "$SNAP" ]; then
|
|
213
|
+
if ! env -u DSH_HOME node scripts/check-test-env-safety.mjs --verify "$SNAP"; then
|
|
214
|
+
fail=$((fail+1)); failed+=("(真实数据目录被写)")
|
|
215
|
+
fi
|
|
216
|
+
rm -f "$SNAP"
|
|
217
|
+
fi
|
|
218
|
+
[ "$fail" -eq 0 ] || exit 1
|
|
219
|
+
|
|
220
|
+
# ── 能力包闭包体检(只报告;**不**参与失败判据,2026-10-04)────────────────────────────
|
|
221
|
+
# 判据:`scripts/check-pack-closure.mjs` —— 按每个能力包的 pack.engine + files 清单解析包内代码的
|
|
222
|
+
# 静态 import / Python import / 子进程调用图,报"引用到、仓库里有、但不在包内"的运行期文件。
|
|
223
|
+
# 为什么接在**非失败**位置:缺件可能常年存在(输出里带修法),接进失败判据就是把门禁变成恒红陷阱;
|
|
224
|
+
# 真要有非零判据请单独跑 `node scripts/check-pack-closure.mjs --strict`(或接进 CI)。
|
|
225
|
+
# 全量跑时打一次基线,让"包内容漂移"不会悄悄发生;`--filter` 迭代时不跑(省时间)。
|
|
226
|
+
# 位置在 provider 装配巡检之前:那一段是最后一块且不改变退出码(本段同样不改变)。
|
|
227
|
+
if [ -z "$FILTER" ] && [ -f "$REPO/scripts/check-pack-closure.mjs" ]; then
|
|
228
|
+
echo
|
|
229
|
+
if closure=$(env -u DSH_HOME node "$REPO/scripts/check-pack-closure.mjs" --quiet 2>&1); then
|
|
230
|
+
printf '%s\n' "$closure" | sed '/^[[:space:]]*$/d; s/^/ /'
|
|
231
|
+
case "$closure" in
|
|
232
|
+
*"缺件 0 个"*) ;;
|
|
233
|
+
*) echo " ↳ 缺件修法:写进 scripts/flow-requirements.json#pack.engine(core 件另进 CORE_KEY),"
|
|
234
|
+
echo " 再提三个带 flow 的能力包版本 + python3 scripts/capability_pack.py --stamp;详见 docs/README.md §六.13" ;;
|
|
235
|
+
esac
|
|
236
|
+
else
|
|
237
|
+
echo " ⚠️ 能力包闭包体检没跑成(exit≠0,多半是 python 不可用)—— 手动跑 node scripts/check-pack-closure.mjs"
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# ── 生产状态守卫(2026-09-30 加;2026-10-02 口径更新):跑完测试立刻做一次 **provider 装配巡检** ──
|
|
242
|
+
# 背景(历史):`~/.dsh/settings.yaml` 会被"自身 schema 重写"丢掉未知的 `llm-deepseek` 段 ⇒ 当时那套
|
|
243
|
+
# 「计费模式」静默退回直连(实测:文件只剩 1 字节)。**那套机制已移除** —— 现在是两个独立 provider
|
|
244
|
+
# 并存(官方直连 / 问述代理),由用户在输入框的模型选择器里选 ⇒ 判据变成「**组合声明了的** provider
|
|
245
|
+
# 有没有装上」:exit 1 = 声明了却没装上;exit 2 = 无法判定(组合树读不到)。
|
|
246
|
+
if [ -x "$REPO/scripts/check-billing-mode.sh" ]; then
|
|
247
|
+
if ! bash "$REPO/scripts/check-billing-mode.sh" >/tmp/billing-guard.out 2>&1; then
|
|
248
|
+
echo
|
|
249
|
+
echo " ⚠️ provider 装配守卫:组合**声明了**的 provider 没装上(exit 1)或无法判定(exit 2)"
|
|
250
|
+
sed 's/^/ /' /tmp/billing-guard.out
|
|
251
|
+
echo " ↳ 处置(**请手动执行**):bash scripts/provider-select.sh status 看当前默认 provider;"
|
|
252
|
+
echo " 要切换用 bash scripts/provider-select.sh {whyshu|deepseek}(改完需重启 dsh)。"
|
|
253
|
+
echo " ⛔ 不要用 check-billing-mode.sh --fix(已熔断 exit 3):它会诱使人改官方 provider 的 baseURL。"
|
|
254
|
+
# 不再因此失败:**用户有权选择官方直连**(2026-09-30 纠正)。只大声提示,不阻断。
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* self-sign-license —— **离线自签**一份许可(演练 / 测试 / 无服务端时跑通离线激活闭环),2026-10-03
|
|
4
|
+
*
|
|
5
|
+
* ## 它解决什么
|
|
6
|
+
* 离线激活闭环是「`license request` 拿设备码 → 服务端签发 v2 许可 → `install --file`」。
|
|
7
|
+
* 没有服务端在场时(开发机、隔离环境、客户现场演练)这条链路跑不通 ⇒ 本工具用**本地密钥**
|
|
8
|
+
* 按**同一份载荷契约**签一份,让闭环可验(`core/lib/license-file.mjs#signLicenseFile` 是唯一实现,
|
|
9
|
+
* 本脚本不重写载荷构造)。
|
|
10
|
+
*
|
|
11
|
+
* ## ⛔ 三条硬约束(都是刻意的摩擦,不是遗漏)
|
|
12
|
+
* ① **密钥必须显式**:只认 `--secret-file`(或 `ARGENT_LICENSE_SECRET`);**不**读本机已装的
|
|
13
|
+
* `license.secret` —— 那会让"拿生产密钥自签"变成一次无意识的默认动作。
|
|
14
|
+
* ② **产物路径必须显式**:必须给 `--out <路径>`;`--out` 落在**本机现役许可位**
|
|
15
|
+
* (`$ARGENT_DATA_DIR/license.json`)时**拒绝**,除非显式 `--allow-live-target`
|
|
16
|
+
* (那条路应该走 `install-license.mjs`,它带备份/读回验收/设备比对)。
|
|
17
|
+
* ③ **不扩权**:`capabilities/verticals/seats/expiresAt/graceDays` 全部来自命令行**显式**参数,
|
|
18
|
+
* 默认最小(无能力、30 天、席位 1)—— 本工具不会"读一份现有许可然后放宽它"。
|
|
19
|
+
*
|
|
20
|
+
* ## ⚠️ 它不是"防伪"的一环
|
|
21
|
+
* 许可签名是 **HMAC-SHA256(对称密钥)**:验签方必须持有同一把密钥 ⇒ 能读 `license.secret` 的人
|
|
22
|
+
* **本来就能自签**。本工具只是把这件事**摆到明面上**(工具化、留痕、口径唯一),不构成新的能力。
|
|
23
|
+
* 真防伪造需要**非对称签名**(Ed25519,客户端只拿公钥)—— 见 §3.6③ 与实施方案的服务端方案。
|
|
24
|
+
*
|
|
25
|
+
* 用法:
|
|
26
|
+
* node scripts/self-sign-license.mjs --user <账号id> --secret-file <密钥文件> --out <许可文件>
|
|
27
|
+
* [--cap <能力id>]… [--vertical <业务线id>]… [--seats N] [--days N | --expires <ISO>] [--grace N]
|
|
28
|
+
* [--instance <提示串>] [--device-code <64hex> | --device-from-local | --no-device]
|
|
29
|
+
* [--sign-v1 | --sign-v2] [--json] [--dry-run] [--force]
|
|
30
|
+
* 缺省:**v2 + 本机设备码**(D1:新签发默认带 `device`);`--no-device` ⇒ v2 但不声明设备(unbound,
|
|
31
|
+
* 缺位一样进签名);`--sign-v1` ⇒ 旧形态(9 项载荷,**不含** device)。
|
|
32
|
+
* 退出码:0 已签(含 dry-run)| 1 写入失败/自验失败 | 2 用法或环境错误
|
|
33
|
+
*/
|
|
34
|
+
import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
35
|
+
import { dirname, join, resolve } from 'node:path'
|
|
36
|
+
import { DEVICE_ALGO, currentDevice } from '../core/lib/license-device.mjs'
|
|
37
|
+
import { defaultLicensePath, resolveSecret, signLicenseFile, verifyLicenseFile } from '../core/lib/license-file.mjs'
|
|
38
|
+
|
|
39
|
+
const argv = process.argv.slice(2)
|
|
40
|
+
const has = (f) => argv.includes(f)
|
|
41
|
+
const allOf = (f) => argv.reduce((acc, a, i) => (a === f && argv[i + 1] !== undefined ? [...acc, argv[i + 1]] : acc), [])
|
|
42
|
+
const argOf = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] ?? null : null }
|
|
43
|
+
const die = (m) => { console.error(`✗ ${m}`); process.exit(2) }
|
|
44
|
+
const fail = (m) => { console.error(`✗ ${m}`); process.exit(1) }
|
|
45
|
+
|
|
46
|
+
if (has('--help') || has('-h')) {
|
|
47
|
+
console.log('用法:node scripts/self-sign-license.mjs --user <id> --secret-file <密钥文件> --out <许可文件>')
|
|
48
|
+
console.log(' [--cap <id>]… [--vertical <id>]… [--seats N] [--days N|--expires <ISO>] [--grace N] [--instance <hint>]')
|
|
49
|
+
console.log(' [--device-code <64hex>|--device-from-local|--no-device] [--sign-v1|--sign-v2] [--json] [--dry-run] [--force]')
|
|
50
|
+
console.log('⚠️ 离线**自签**(演练/测试用);真实签发在服务端(whyshu-svr)。签名是 HMAC 对称密钥 ⇒ 不是防伪边界。')
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const dryRun = has('--dry-run')
|
|
55
|
+
const jsonOut = has('--json')
|
|
56
|
+
const asJson = jsonOut
|
|
57
|
+
|
|
58
|
+
const userId = String(argOf('--user') || '').trim()
|
|
59
|
+
if (!userId) die('必须给 --user <账号id>(许可里的 userId;空值会被判 malformed)')
|
|
60
|
+
|
|
61
|
+
const secretSrc = argOf('--secret-file') ? resolve(argOf('--secret-file')) : null
|
|
62
|
+
if (!secretSrc && !String(process.env.ARGENT_LICENSE_SECRET || '').trim()) {
|
|
63
|
+
die('必须显式给密钥来源:--secret-file <密钥文件> 或 ARGENT_LICENSE_SECRET'
|
|
64
|
+
+ '(⛔ 不读本机已装的 license.secret:自签必须是**刻意**的动作)')
|
|
65
|
+
}
|
|
66
|
+
if (secretSrc && !existsSync(secretSrc)) die(`密钥文件不存在:${secretSrc}`)
|
|
67
|
+
const key = resolveSecret({ secret: undefined, secretPath: secretSrc || undefined })
|
|
68
|
+
if (!key.secret) die(`读不到密钥:${secretSrc || 'ARGENT_LICENSE_SECRET'}`)
|
|
69
|
+
|
|
70
|
+
const outArg = argOf('--out')
|
|
71
|
+
if (!outArg) die('必须给 --out <许可文件路径>(本工具**不**默认往数据目录写)')
|
|
72
|
+
const out = resolve(outArg)
|
|
73
|
+
const live = defaultLicensePath()
|
|
74
|
+
if (out === live && !has('--allow-live-target')) {
|
|
75
|
+
die(`--out 指向**本机现役许可位**(${live}):这条路请走 \`install-license.mjs\`(带备份 + 读回验收 + 设备比对);`
|
|
76
|
+
+ '确实要直接覆盖请显式加 --allow-live-target')
|
|
77
|
+
}
|
|
78
|
+
if (existsSync(out) && !has('--force') && !dryRun) die(`--out 已存在:${out}(要覆盖请加 --force)`)
|
|
79
|
+
|
|
80
|
+
// 版本(缺省 v2 —— D1:新签发默认带 device)
|
|
81
|
+
const signV1 = has('--sign-v1')
|
|
82
|
+
const signV2 = has('--sign-v2')
|
|
83
|
+
if (signV1 && signV2) die('--sign-v1 与 --sign-v2 只能给一个')
|
|
84
|
+
const version = signV1 ? 1 : 2
|
|
85
|
+
|
|
86
|
+
// 设备声明(v2 才有意义):缺省 = **本机设备码**
|
|
87
|
+
const noDevice = has('--no-device')
|
|
88
|
+
const deviceCodeArg = argOf('--device-code')
|
|
89
|
+
let device = null
|
|
90
|
+
if (version === 2 && !noDevice) {
|
|
91
|
+
if (deviceCodeArg) {
|
|
92
|
+
const code = String(deviceCodeArg).trim()
|
|
93
|
+
if (!/^[0-9a-f]{64}$/i.test(code)) {
|
|
94
|
+
die(`--device-code 必须是 64 位十六进制的**设备码**(\`argent license request\` 打印的那串);`
|
|
95
|
+
+ '⛔ 不要给主机名/MAC/IP(D3:指纹只认硬件 UUID 一项)')
|
|
96
|
+
}
|
|
97
|
+
device = { code: code.toLowerCase(), algo: DEVICE_ALGO, issuedAt: new Date().toISOString() }
|
|
98
|
+
} else {
|
|
99
|
+
const dev = currentDevice({})
|
|
100
|
+
if (!dev.ok) {
|
|
101
|
+
die(`读不到本机设备码(${dev.reason}):${dev.error} ⇒ 要么 --device-code <64hex>,要么显式 --no-device`)
|
|
102
|
+
}
|
|
103
|
+
device = { code: dev.code, algo: dev.algo, issuedAt: dev.issuedAt }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 期限:`--days`(缺省 30)或 `--expires`(显式 ISO)
|
|
108
|
+
const now = new Date()
|
|
109
|
+
const days = argOf('--days') === null ? 30 : Number(argOf('--days'))
|
|
110
|
+
if (!Number.isFinite(days) || days <= 0) die(`--days 必须是正数:${argOf('--days')}`)
|
|
111
|
+
const expiresArg = argOf('--expires')
|
|
112
|
+
let expiresAt = new Date(now.getTime() + days * 86400000).toISOString()
|
|
113
|
+
if (expiresArg) {
|
|
114
|
+
const t = Date.parse(String(expiresArg))
|
|
115
|
+
if (!Number.isFinite(t)) die(`--expires 不是可解析时间:${expiresArg}`)
|
|
116
|
+
expiresAt = new Date(t).toISOString()
|
|
117
|
+
}
|
|
118
|
+
const graceDays = argOf('--grace') === null ? 14 : Number(argOf('--grace'))
|
|
119
|
+
if (!Number.isFinite(graceDays) || graceDays < 0) die(`--grace 必须 ≥ 0:${argOf('--grace')}`)
|
|
120
|
+
|
|
121
|
+
const file = {
|
|
122
|
+
v: version,
|
|
123
|
+
userId,
|
|
124
|
+
capabilities: [...new Set(allOf('--cap').map(String).filter(Boolean))].sort(),
|
|
125
|
+
verticals: [...new Set(allOf('--vertical').map(String).filter(Boolean))].sort(),
|
|
126
|
+
seats: argOf('--seats') === null ? 1 : Number(argOf('--seats')),
|
|
127
|
+
issuedAt: now.toISOString(),
|
|
128
|
+
expiresAt,
|
|
129
|
+
graceDays,
|
|
130
|
+
instanceHint: String(argOf('--instance') || 'self-sign'),
|
|
131
|
+
...(device ? { device } : {}),
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isFinite(file.seats) || file.seats < 1) die(`--seats 必须是正数:${argOf('--seats')}`)
|
|
134
|
+
|
|
135
|
+
const signed = signLicenseFile(file, key.secret, { version })
|
|
136
|
+
if (!signed.ok) fail(`签名失败(${signed.reason}${signed.warning ? `:${signed.warning}` : ''})`)
|
|
137
|
+
// **自验**:产物必须能在**独立入口**(verifyLicenseFile)验过 —— "我签完了"不算数
|
|
138
|
+
const check = verifyLicenseFile(signed.file, { secret: key.secret, now: now.getTime() })
|
|
139
|
+
if (!check.ok) fail(`自验失败(${check.reason}):拒绝写出(别把一份自己都验不过的许可交给别人)`)
|
|
140
|
+
|
|
141
|
+
const result = {
|
|
142
|
+
ok: true, empty: false, code: 0, reason: null,
|
|
143
|
+
tool: 'scripts/self-sign-license.mjs',
|
|
144
|
+
warning: '离线**自签**:真实签发在服务端(whyshu-svr);签名是 HMAC 对称密钥 ⇒ 本工具不构成安全边界',
|
|
145
|
+
out, dryRun, written: false,
|
|
146
|
+
signatureVersion: version,
|
|
147
|
+
deviceSigned: version === 2,
|
|
148
|
+
payload: signed.payload,
|
|
149
|
+
licenseFile: signed.file,
|
|
150
|
+
keySource: secretSrc ? `secret-file:${secretSrc}` : 'env:ARGENT_LICENSE_SECRET',
|
|
151
|
+
verified: { ok: check.ok, reason: check.reason, state: check.inGrace ? 'grace' : (check.ok ? 'valid' : 'denied') },
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(dirname(out), { recursive: true, mode: 0o700 })
|
|
157
|
+
const tmp = `${out}.tmp`
|
|
158
|
+
writeFileSync(tmp, JSON.stringify(signed.file, null, 2) + '\n', { mode: 0o600 })
|
|
159
|
+
chmodSync(tmp, 0o600)
|
|
160
|
+
renameSync(tmp, out)
|
|
161
|
+
result.written = true
|
|
162
|
+
} catch (e) {
|
|
163
|
+
rmSync(`${out}.tmp`, { force: true })
|
|
164
|
+
fail(`写入失败:${e.message}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (asJson) {
|
|
169
|
+
console.log(JSON.stringify(result, null, 2))
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`${dryRun ? '🔎 [dry-run] 会自签' : '✅ 已自签'}离线许可(v${version}):→ ${out}`)
|
|
172
|
+
console.log(` 账号:${userId} | 席位:${file.seats} | 到期:${expiresAt} | 宽限 ${graceDays} 天`)
|
|
173
|
+
console.log(` 能力:${file.capabilities.join(',') || '(空 ⇒ 客户端拒一切能力)'} | 业务线:${file.verticals.join(',') || '(空 = 不限)'}`)
|
|
174
|
+
console.log(` 设备:${device ? `已声明(${device.code.slice(0, 12)}…,进签名)` : '未声明(unbound;缺位也已进签名)'}`)
|
|
175
|
+
console.log(` 自验:${check.reason}(独立入口 verifyLicenseFile)`)
|
|
176
|
+
console.log(` ⚠️ ${result.warning}`)
|
|
177
|
+
console.log(' 下一步:node scripts/install-license.mjs --file <该文件> [--dry-run]')
|
|
178
|
+
}
|
|
179
|
+
process.exit(0)
|