@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,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* argent-doctor — Argent 运行时巡检(v0.8 M1 雏形;只读,不改任何文件)
|
|
4
|
+
*
|
|
5
|
+
* 检查项:
|
|
6
|
+
* 1. 三副本漂移:dev 仓库 / ~/.argent-dsh 基座 / 运行库 node_modules 的插件 src+dist md5 一致性
|
|
7
|
+
* 2. 敏感文件权限:~/.dsh/argent 下 env/config 是否 600(世界可读 = 告警)
|
|
8
|
+
* 3. 双调度检测:crontab 与 LaunchAgent 是否同时注册同一跑批(poll-fulfillment / poll-invoice / invoice)
|
|
9
|
+
* 4. 数据卫生:kanban-events.jsonl 体量、invoice 假绿探测(最近 run-*.log 尾含 Traceback/JSONDecodeError)、
|
|
10
|
+
* 顶层调试 png 数量
|
|
11
|
+
* 5. gateway.json 基础结构(roles 数组、role 唯一)
|
|
12
|
+
* 6. 代码卫生(KB-105):dev 仓库 TODO/FIXME/HACK/XXX 计数(阈值 0,防回归)
|
|
13
|
+
* 7. 单文件增长(KB-105):invoice cron.log / fulfillment-logs failures.jsonl / kanban-events.jsonl 超 5MB → WARN
|
|
14
|
+
* 8. .bak 轮转(KB-105):~/.dsh/argent 顶层 .bak-* 数量 >6 或权限非 600 → 提示
|
|
15
|
+
* 9. **排程面体检**(P1-A,2026-10-12;只读;**最高 WARN,从不 FAIL**):宿主原生排程的
|
|
16
|
+
* ① 域文件(`<DSH_HOME>/storages/schedule*`)可解析性 —— 坏了会让**整个宿主拒绝启动**;
|
|
17
|
+
* ② 注册表里的 `trigger.schedule` 声明面(读不到 ⇒ WARN,**不能**当成"没声明")。
|
|
18
|
+
* 判据 = `scripts/lib/schedule-probe.mjs`(⚠️ 同一份实现;⛔ 这里不另写一套)。
|
|
19
|
+
* ⚠️ 本项**不做**在线探针(不访问宿主、不读凭据)—— "宿主挂载了没 / 定时还活着没"要跑
|
|
20
|
+
* `node scripts/argent-schedule.mjs check`(四道闸,exit 0/1/2)。
|
|
21
|
+
*
|
|
22
|
+
* 用法:node scripts/argent-doctor.mjs [--json]
|
|
23
|
+
* 退出码:0=全过 1=有 FAIL 2=有 WARN 无 FAIL(默认也返回 2 供肉眼区分)
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
27
|
+
import { homedir } from 'node:os'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
30
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
31
|
+
import { readdirSync, existsSync, readFileSync } from 'node:fs'
|
|
32
|
+
import { resolveDataDir } from './lib/argent-datadir.mjs'
|
|
33
|
+
|
|
34
|
+
const HOME = homedir()
|
|
35
|
+
const DEV = fileURLToPath(new URL('..', import.meta.url)) // 本机开发仓库(脚本所在仓库上级)
|
|
36
|
+
const BASE = join(HOME, '.argent-dsh')
|
|
37
|
+
// 运行时插件根可覆盖(2026-09-21):回归测试用隔离夹具目录跑同一个 doctor,
|
|
38
|
+
// 否则"符号引用"这类静态检查只能靠真机环境验,容易漏掉误报修复的回归。
|
|
39
|
+
const RUNTIME = process.env.ARGENT_DOCTOR_RUNTIME || join(HOME, '.dsh', 'profiles', 'web', 'node_modules')
|
|
40
|
+
// 数据目录:内核三级链(原为硬编码 ~/.dsh/argent ⇒ 隔离实例查的是生产目录,结论会骗人)
|
|
41
|
+
const DATA = resolveDataDir({ env: process.env }).dir
|
|
42
|
+
const jsonOut = process.argv.includes('--json')
|
|
43
|
+
|
|
44
|
+
const results = []
|
|
45
|
+
const report = (level, check, msg) => results.push({ level, check, msg })
|
|
46
|
+
|
|
47
|
+
function md5(buf) {
|
|
48
|
+
return createHash('md5').update(buf).digest('hex')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function md5File(p) {
|
|
52
|
+
try {
|
|
53
|
+
return md5(await readFile(p))
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── 1. 三副本漂移 ──────────────────────────────
|
|
60
|
+
async function checkDrift() {
|
|
61
|
+
const plugins = []
|
|
62
|
+
try {
|
|
63
|
+
for (const d of await readdir(join(RUNTIME))) {
|
|
64
|
+
if (d.startsWith('argent-') && !d.startsWith('argent-bak')) plugins.push(d)
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
report('FAIL', '三副本漂移', `读运行库失败: ${err.message}`)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
let bad = 0
|
|
71
|
+
for (const name of plugins.sort()) {
|
|
72
|
+
for (const rel of ['src/index.js', 'dist/client.js']) {
|
|
73
|
+
const dev = await md5File(join(DEV, 'plugins', name, rel))
|
|
74
|
+
const base = await md5File(join(BASE, 'plugins', name, rel))
|
|
75
|
+
const run = await md5File(join(RUNTIME, name, rel))
|
|
76
|
+
if (dev === null && run === null && base === null) continue
|
|
77
|
+
if (dev !== run || (dev !== null && base !== null && dev !== base)) {
|
|
78
|
+
bad++
|
|
79
|
+
report('FAIL', '三副本漂移', `${name}/${rel}: dev=${dev ? dev.slice(0, 7) : '无'} base=${base ? base.slice(0, 7) : '无'} run=${run ? run.slice(0, 7) : '无'}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!bad) report('PASS', '三副本漂移', `检查 ${plugins.length} 个插件 src+dist,三副本一致`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 2. 敏感文件权限 ────────────────────────────
|
|
87
|
+
async function checkPerms() {
|
|
88
|
+
const secrets = ['fulfillment.env', 'daily-report.env', 'ziniao-webdriver.env', 'config.json', '.credentials.yaml']
|
|
89
|
+
for (const f of secrets) {
|
|
90
|
+
try {
|
|
91
|
+
const st = await stat(join(DATA, f))
|
|
92
|
+
const mode = st.mode & 0o777
|
|
93
|
+
const worldReadable = (mode & 0o004) !== 0
|
|
94
|
+
if (worldReadable) report('WARN', '文件权限', `${f} 权限 ${mode.toString(8)} 世界可读(建议 600)`)
|
|
95
|
+
} catch {
|
|
96
|
+
/* 不存在跳过 */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const creds = await stat(join(HOME, '.dsh', '.credentials.yaml')).catch(() => null)
|
|
100
|
+
if (creds) {
|
|
101
|
+
const mode = creds.mode & 0o777
|
|
102
|
+
if ((mode & 0o004) !== 0) report('WARN', '文件权限', `.credentials.yaml 权限 ${mode.toString(8)} 世界可读`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── 3. 双调度检测 ──────────────────────────────
|
|
107
|
+
function checkScheduling() {
|
|
108
|
+
let cron = ''
|
|
109
|
+
try {
|
|
110
|
+
cron = execSync('crontab -l 2>/dev/null || true', { encoding: 'utf8', shell: '/bin/bash' })
|
|
111
|
+
} catch {
|
|
112
|
+
cron = ''
|
|
113
|
+
}
|
|
114
|
+
let launch = ''
|
|
115
|
+
try {
|
|
116
|
+
launch = execSync('launchctl list 2>/dev/null | grep -i "com.whyshu" || true', { encoding: 'utf8', shell: '/bin/bash' })
|
|
117
|
+
} catch {
|
|
118
|
+
launch = ''
|
|
119
|
+
}
|
|
120
|
+
const cronJobs = [...new Set((cron.match(/poll-fulfillment|poll-invoice|invoice|daily-report/g) || []).map((x) => x))]
|
|
121
|
+
const launchJobs = [...new Set((launch.match(/com\.whyshu\.[a-z-]+/g) || []).map((x) => x))]
|
|
122
|
+
for (const key of ['poll-fulfillment', 'poll-invoice', 'invoice', 'daily']) {
|
|
123
|
+
const inCron = cron.includes(key)
|
|
124
|
+
const inLaunch = launchJobs.some((l) => l.includes(key) || (key === 'invoice' && l.includes('invoice')))
|
|
125
|
+
if (inCron && inLaunch) report('WARN', '双调度', `${key}: crontab 与 LaunchAgent 同时注册(可能双跑)`)
|
|
126
|
+
}
|
|
127
|
+
if (cronJobs.length || launchJobs.length) {
|
|
128
|
+
report('PASS', '调度注册', `cron: [${cronJobs.join(', ') || '无'}] launchd: [${launchJobs.join(', ') || '无'}]`)
|
|
129
|
+
} else {
|
|
130
|
+
report('PASS', '调度注册', '未发现 whyshu 相关调度项(可能全部在远程机)')
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── 4. 数据卫生 ────────────────────────────────
|
|
135
|
+
async function checkHygiene() {
|
|
136
|
+
try {
|
|
137
|
+
const ev = await stat(join(DATA, 'kanban-events.jsonl'))
|
|
138
|
+
const mb = ev.size / 1024 / 1024
|
|
139
|
+
if (mb > 5) report('WARN', '数据卫生', `kanban-events.jsonl ${mb.toFixed(1)}MB(建议轮转/归档)`)
|
|
140
|
+
} catch { /* 无文件 */ }
|
|
141
|
+
// invoice 假绿探测:最近一个 run-*.log 尾部的异常
|
|
142
|
+
try {
|
|
143
|
+
const dir = join(DATA, 'invoice-logs')
|
|
144
|
+
const files = (await readdir(dir)).filter((f) => f.startsWith('run-') && f.endsWith('.log'))
|
|
145
|
+
if (files.length) {
|
|
146
|
+
const sorted = files.sort().reverse()
|
|
147
|
+
const tail = await readFile(join(dir, sorted[0]), 'utf8').catch(() => '')
|
|
148
|
+
const lines = tail.split('\n').slice(-15)
|
|
149
|
+
// 判定取「最后出现的运行特征」:日志为追加式,历史失败残留后若已有更新成功输出
|
|
150
|
+
// (2026-09-06 修复后「无待上传…」跟在旧 traceback 之后)应判 PASS,不得因残留误报
|
|
151
|
+
let verdict = null
|
|
152
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
153
|
+
const ln = lines[i]
|
|
154
|
+
if (/Traceback|JSONDecodeError|Error|失败/.test(ln)) { verdict = 'FAIL'; break }
|
|
155
|
+
if (/无待上传|轮询完成|上传|下载标签|阶段 B|成功/.test(ln)) { verdict = 'PASS'; break }
|
|
156
|
+
}
|
|
157
|
+
// 陈旧证据容忍(2026-09-24):判定只能代表**当时**的运行。日志若已 N 小时没更新,
|
|
158
|
+
// 异常多半是"环境已修好但还没跑过"的残留(实测:09-22 的 openpyxl 缺失 traceback 一直
|
|
159
|
+
// 把本门禁挂在 FAIL,挡住了 sync-plugins.sh 的整条同步门禁)⇒ 降为 WARN 并写明复核时机。
|
|
160
|
+
let staleH = 0
|
|
161
|
+
try {
|
|
162
|
+
const st = await stat(join(dir, sorted[0]))
|
|
163
|
+
staleH = (Date.now() - st.mtimeMs) / 3600000
|
|
164
|
+
} catch { /* 取不到 mtime 按新鲜处理(保守) */ }
|
|
165
|
+
const STALE_H = Number(process.env.ARGENT_DOCTOR_LOG_STALE_H || 24)
|
|
166
|
+
if (verdict === 'FAIL' && staleH > STALE_H) {
|
|
167
|
+
report('WARN', '发票假绿',
|
|
168
|
+
`最近日志 ${sorted[0]} 尾部含异常,但已 ${staleH.toFixed(1)}h 未更新(>${STALE_H}h)`
|
|
169
|
+
+ ' ⇒ 无法代表当前状态;下次发票运行后复核')
|
|
170
|
+
} else if (verdict === 'FAIL') {
|
|
171
|
+
report('FAIL', '发票假绿', `最近日志 ${sorted[0]} 尾部含异常:${lines.slice(-200)}`)
|
|
172
|
+
} else {
|
|
173
|
+
report('PASS', '发票假绿', `最近日志 ${sorted[0]} 尾部${verdict === 'PASS' ? '最近输出正常' : '无异常特征'}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch { /* 无目录 */ }
|
|
177
|
+
try {
|
|
178
|
+
const pngs = (await readdir(DATA)).filter((f) => f.endsWith('.png'))
|
|
179
|
+
if (pngs.length > 5) report('WARN', '数据卫生', `~/.dsh/argent 顶层 ${pngs.length} 张调试 png(建议清理)`)
|
|
180
|
+
} catch { /* 忽略 */ }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── 5. gateway.json 基础结构 ───────────────────
|
|
184
|
+
async function checkGateway() {
|
|
185
|
+
try {
|
|
186
|
+
const gw = JSON.parse(await readFile(join(DATA, 'gateway.json'), 'utf8'))
|
|
187
|
+
const roles = gw.roles
|
|
188
|
+
if (!Array.isArray(roles) || !roles.length) {
|
|
189
|
+
report('FAIL', 'gateway.json', 'roles 缺失或为空')
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const ids = roles.map((r) => r.role)
|
|
193
|
+
const dup = ids.filter((v, i) => ids.indexOf(v) !== i)
|
|
194
|
+
if (dup.length) report('FAIL', 'gateway.json', `role 重复: ${[...new Set(dup)].join(',')}`)
|
|
195
|
+
else report('PASS', 'gateway.json', `${roles.length} 个角色:${ids.join(', ')}`)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
report('FAIL', 'gateway.json', `解析失败: ${err.message}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── 6. 代码卫生:TODO/FIXME 计数(KB-105)────────────────
|
|
202
|
+
// 扫描 dev 仓库 scripts/plugins/presets/skills(排除 node_modules/.git/dist),
|
|
203
|
+
// 只匹配「注释形态」的 TODO/FIXME/HACK/XXX 标记(排除 mktemp/URL 的 XXX 占位)。
|
|
204
|
+
async function checkCodeHygiene() {
|
|
205
|
+
const rootDirs = ['scripts', 'plugins', 'presets', 'skills']
|
|
206
|
+
const exts = ['py', 'js', 'mjs', 'ts', 'sh', 'yml', 'yaml']
|
|
207
|
+
try {
|
|
208
|
+
const out = execSync(
|
|
209
|
+
`grep -rn --include='*.${exts.join("' --include='*.")}' -E '(#|//|/\\*|<!--|--)\\s*(TODO|FIXME|HACK|XXX)\\s*[::]?\\s*[^Xx]{3,}' ${rootDirs.join(' ')} 2>/dev/null | grep -vE 'node_modules|\\.git/|__pycache__|/dist/' | wc -l`,
|
|
210
|
+
{ encoding: 'utf8', shell: '/bin/bash', cwd: DEV }
|
|
211
|
+
)
|
|
212
|
+
const n = parseInt(out.trim(), 10) || 0
|
|
213
|
+
if (n > 0) report('WARN', '代码卫生', `dev 仓库 ${n} 条 TODO/FIXME/HACK/XXX 注释(阈值 0,建议清理防回归)`)
|
|
214
|
+
else report('PASS', '代码卫生', 'dev 仓库 0 条 TODO/FIXME/HACK/XXX 注释(无卫生欠账)')
|
|
215
|
+
} catch {
|
|
216
|
+
report('PASS', '代码卫生', '扫描跳过(grep 不可用)')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 7. 单文件增长(KB-105):>5MB → WARN ─────────────
|
|
221
|
+
async function checkGrowth() {
|
|
222
|
+
const targets = [
|
|
223
|
+
['invoice cron.log', join(DATA, 'invoice-logs', 'cron.log')],
|
|
224
|
+
['fulfillment failures.jsonl', join(DATA, 'fulfillment-logs', 'failures.jsonl')],
|
|
225
|
+
['kanban-events.jsonl', join(DATA, 'kanban-events.jsonl')],
|
|
226
|
+
]
|
|
227
|
+
for (const [label, p] of targets) {
|
|
228
|
+
try {
|
|
229
|
+
const st = await stat(p)
|
|
230
|
+
const mb = st.size / 1024 / 1024
|
|
231
|
+
if (mb > 5) report('WARN', '数据卫生', `${label} ${mb.toFixed(1)}MB(建议轮转/归档)`)
|
|
232
|
+
else report('PASS', '文件增长', `${label} ${mb.toFixed(1)}MB(<5MB)`)
|
|
233
|
+
} catch { /* 无文件:跳过(非 WARN,文件可能不存在) */ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── 8. .bak 轮转(KB-105):>6 份或权限非 600 → 提示 ──
|
|
238
|
+
async function checkBakRotation() {
|
|
239
|
+
try {
|
|
240
|
+
const files = (await readdir(DATA)).filter((f) => f.includes('.bak-'))
|
|
241
|
+
if (!files.length) {
|
|
242
|
+
report('PASS', '备份轮转', '无 .bak- 备份文件(干净)')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
let permBad = 0
|
|
246
|
+
for (const f of files) {
|
|
247
|
+
const st = await stat(join(DATA, f)).catch(() => null)
|
|
248
|
+
if (st && (st.mode & 0o004) !== 0) permBad++
|
|
249
|
+
}
|
|
250
|
+
const hints = []
|
|
251
|
+
if (files.length > 6) hints.push(`${files.length} 份 .bak-(建议保留近期 3-5 份,超期归档/清理)`)
|
|
252
|
+
if (permBad) hints.push(`${permBad} 份权限非 600(含敏感配置副本,建议 chmod 600)`)
|
|
253
|
+
if (hints.length) report('WARN', '备份轮转', hints.join(';'))
|
|
254
|
+
else report('PASS', '备份轮转', `${files.length} 份 .bak-(≤6 且权限 600)`)
|
|
255
|
+
} catch {
|
|
256
|
+
report('PASS', '备份轮转', '检查跳过')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── 9/10 插件静态自检(v0.8-B1 事故复盘 2026-09-04:gateway/drive 双 ctx.provide('argentRoles')
|
|
261
|
+
// 曾致 dsh web 启动崩溃——服务重复注册与语法错误均可静态检出,host 改动发布前必跑)──
|
|
262
|
+
function checkPluginSyntax() {
|
|
263
|
+
let bad = 0
|
|
264
|
+
try {
|
|
265
|
+
const files = []
|
|
266
|
+
for (const d of readdirSync(RUNTIME)) {
|
|
267
|
+
if (!d.startsWith('argent-')) continue
|
|
268
|
+
for (const rel of ['src/index.js', 'dist/client.js']) {
|
|
269
|
+
const f = join(RUNTIME, d, rel)
|
|
270
|
+
if (existsSync(f)) files.push(f)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const f of files) {
|
|
274
|
+
const r = spawnSync(process.execPath, ['--check', f], { encoding: 'utf8' })
|
|
275
|
+
if (r.status !== 0) {
|
|
276
|
+
bad++
|
|
277
|
+
report('FAIL', '插件语法', `${f}: ${(r.stderr || '').slice(0, 200)}`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!bad) report('PASS', '插件语法', `node --check ${files.length} 个插件文件全部通过`)
|
|
281
|
+
} catch (e) {
|
|
282
|
+
report('FAIL', '插件语法', e.message)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function checkServiceConflicts() {
|
|
286
|
+
try {
|
|
287
|
+
const svcOf = {}
|
|
288
|
+
const dirs = readdirSync(RUNTIME).filter((d) => d.startsWith('argent-') && !d.startsWith('argent-bak'))
|
|
289
|
+
for (const d of dirs) {
|
|
290
|
+
const f = join(RUNTIME, d, 'src', 'index.js')
|
|
291
|
+
if (!existsSync(f)) continue
|
|
292
|
+
const lines = readFileSync(f, 'utf8').split('\n')
|
|
293
|
+
for (const ln of lines) {
|
|
294
|
+
const t = ln.trim()
|
|
295
|
+
if (!t || t.startsWith('//') || t.startsWith('*')) continue
|
|
296
|
+
const m = /ctx\.provide\(\s*['"]([\w]+)['"]/.exec(t)
|
|
297
|
+
if (m) {
|
|
298
|
+
const key = m[1]
|
|
299
|
+
;(svcOf[key] = svcOf[key] || []).push(d)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
let dup = 0
|
|
304
|
+
for (const [name, plugins] of Object.entries(svcOf)) {
|
|
305
|
+
const uniq = [...new Set(plugins)]
|
|
306
|
+
if (uniq.length > 1) {
|
|
307
|
+
dup++
|
|
308
|
+
report('FAIL', '服务重复注册', `ctx.provide('${name}') 出现在多个插件: ${uniq.join(', ')}(cordis 启动将崩溃)`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!dup) report('PASS', '服务重复注册', `共 ${Object.keys(svcOf).length} 个服务名,无跨插件重复`)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
report('FAIL', '服务重复注册', e.message)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── 11 符号引用完整性(v0.8-B2 事故复盘 2026-09-04:瘦身误删 MENTION_NAMES/ROLE_NAMES/
|
|
318
|
+
// ROLE_PREFIX 与 CONFIG_FILE/GATEWAY_FILE——引用仍在而定义被删,前者 ReferenceError、
|
|
319
|
+
// 后者在 try/catch 内静默失败。host 插件重构后跑本检查可拦截此类误删)──
|
|
320
|
+
const JS_BUILTIN_UPPER = new Set(('JSON Math Date Promise Buffer URL URLSearchParams AbortController AbortSignal TextEncoder TextDecoder Map Set WeakMap WeakSet Symbol Error TypeError RangeError SyntaxError ReferenceError RegExp Proxy Reflect Object Array String Number Boolean BigInt Function Atomics ArrayBuffer DataView Intl WebAssembly Headers Request Response FormData Blob File Event EventTarget setTimeout clearTimeout setInterval clearInterval queueMicrotask structuredClone crypto performance navigator window document React globalThis fetch').split(' '))
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 逐字符 JS 扫描器(2026-10-02 重写词法器 + 加**作用域**建模)。
|
|
324
|
+
* 返回 `{ defs: Map<名字, [定义处的大括号深度…]>, refs: [{name, depth}] }`。
|
|
325
|
+
*
|
|
326
|
+
* ## 为什么必须换成状态机(真因,实测)
|
|
327
|
+
* 旧管线是"正则互相踩":先按块注释 `/* … *\/` 删,**再**删行注释。而
|
|
328
|
+
* `plugins/argent-core/src/index.js:1290` 的**行注释**里有一个 `/*`(`` …/env/*.env` ``)⇒
|
|
329
|
+
* 被当成块注释开头,一路吞到下一个 `*\/`(:1424),把中间 **134 行**(含 :1387 的
|
|
330
|
+
* `const BILLING_SETTINGS = …`)从"已定义集合"里删掉,而 :1427 的**使用**还在
|
|
331
|
+
* ⇒ 假阳性「引用未定义常量 BILLING_SETTINGS」。
|
|
332
|
+
* (实测佐证:原文 2075 行;只跑块注释那一步就剩 1912 行,BILLING_SETTINGS 出现次数 2 → 1。)
|
|
333
|
+
*
|
|
334
|
+
* ## 作用域(第二条判据)
|
|
335
|
+
* 定义记在**它所在的大括号深度**上;一个引用只有在**同层或外层**(`定义深度 <= 引用深度`)
|
|
336
|
+
* 才能算已定义 —— 于是"同一 apply/闭包里的 const 被内层嵌套函数引用"是合法的(不再误报),
|
|
337
|
+
* 而"只在内层/兄弟作用域定义过"仍会被抓住。注意这条比旧版**更严**(旧版是全文级 defined 集合)。
|
|
338
|
+
*
|
|
339
|
+
* ## 有意保守之处(宁可漏报,不可误报)
|
|
340
|
+
* · `深度 <= ` 不区分兄弟块 ⇒ `{ const A = 1 } { A }` 仍判"已定义"(漏报,不是误报);
|
|
341
|
+
* · 函数/箭头**形参**不建定义(实测本仓插件无大写形参;真出现会退化成"引用",属已知保守边界)。
|
|
342
|
+
*/
|
|
343
|
+
function scanJsSymbols(src) {
|
|
344
|
+
const defs = new Map()
|
|
345
|
+
const refs = []
|
|
346
|
+
const n = src.length
|
|
347
|
+
const ID_START = /[A-Za-z_$]/
|
|
348
|
+
const ID_PART = /[A-Za-z0-9_$]/
|
|
349
|
+
// 这些"上一个 token"之后出现的 `/` 只可能是**正则字面量**(除法不会出现在这些位置)
|
|
350
|
+
const REGEX_OK = new Set(['', '(', '=', ',', ':', '[', '!', '&', '|', '?', '{', '}', ';',
|
|
351
|
+
'return', 'typeof', 'case', '=>', '+', '-', '*', '%', '<', '>', '~', '^'])
|
|
352
|
+
const skipString = (i, q) => {
|
|
353
|
+
let j = i + 1
|
|
354
|
+
while (j < n) {
|
|
355
|
+
const c = src[j]
|
|
356
|
+
if (c === '\\') { j += 2; continue }
|
|
357
|
+
if (c === q) return j + 1
|
|
358
|
+
if (c === '\n') return j // 未闭合(多半不是字符串)⇒ 止损,别吞掉后面整片
|
|
359
|
+
j++
|
|
360
|
+
}
|
|
361
|
+
return j
|
|
362
|
+
}
|
|
363
|
+
const skipRegex = (i) => {
|
|
364
|
+
let j = i + 1, inClass = false
|
|
365
|
+
while (j < n) {
|
|
366
|
+
const c = src[j]
|
|
367
|
+
if (c === '\\') { j += 2; continue }
|
|
368
|
+
if (c === '\n') break // 未闭合 ⇒ 当除法止损
|
|
369
|
+
if (c === '[') inClass = true
|
|
370
|
+
else if (c === ']') inClass = false
|
|
371
|
+
else if (c === '/' && !inClass) { j++; break }
|
|
372
|
+
j++
|
|
373
|
+
}
|
|
374
|
+
while (j < n && /[gimsuy]/.test(src[j])) j++
|
|
375
|
+
return j
|
|
376
|
+
}
|
|
377
|
+
const addDef = (name, d) => { const a = defs.get(name) || []; a.push(d); defs.set(name, a) }
|
|
378
|
+
|
|
379
|
+
let i = 0, depth = 0, prev = '', decl = null, importing = false
|
|
380
|
+
const tmpl = [] // 模板栈:{mode:'body'|'expr', depth}
|
|
381
|
+
while (i < n) {
|
|
382
|
+
const top = tmpl.length ? tmpl[tmpl.length - 1] : null
|
|
383
|
+
if (top && top.mode === 'body') { // 模板正文态:只认 `\` / 反引号 / `${`
|
|
384
|
+
const c = src[i]
|
|
385
|
+
if (c === '\\') { i += 2; continue }
|
|
386
|
+
if (c === '`') { tmpl.pop(); i++; prev = '`'; continue }
|
|
387
|
+
if (c === '$' && src[i + 1] === '{') { top.mode = 'expr'; i += 2; prev = '('; continue }
|
|
388
|
+
i++
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
const c = src[i]
|
|
392
|
+
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { i++; continue }
|
|
393
|
+
if (c === '/' && src[i + 1] === '/') { while (i < n && src[i] !== '\n') i++; continue } // 行注释
|
|
394
|
+
if (c === '/' && src[i + 1] === '*') { // 块注释
|
|
395
|
+
i += 2
|
|
396
|
+
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i++
|
|
397
|
+
i += 2
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
if (c === '/' && REGEX_OK.has(prev)) { i = skipRegex(i); prev = 'RE'; continue } // 正则字面量
|
|
401
|
+
if (c === '"' || c === "'") { i = skipString(i, c); prev = 'STR'; importing = false; continue } // 字符串
|
|
402
|
+
if (c === '`') { tmpl.push({ mode: 'body', depth }); i++; continue } // 模板开始
|
|
403
|
+
if (ID_START.test(c)) {
|
|
404
|
+
let j = i + 1
|
|
405
|
+
while (j < n && ID_PART.test(src[j])) j++
|
|
406
|
+
const name = src.slice(i, j)
|
|
407
|
+
i = j
|
|
408
|
+
let k = i
|
|
409
|
+
while (k < n && /\s/.test(src[k])) k++
|
|
410
|
+
const isKey = src[k] === ':' && prev !== 'case' && prev !== '?' // 对象键/标签;`case X:`、`a ? X :` 不算
|
|
411
|
+
if (prev === '.') { /* 成员访问:不是引用 */ }
|
|
412
|
+
else if (importing) { if (name !== 'from' && name !== 'as' && name !== 'type') addDef(name, depth) }
|
|
413
|
+
else if (prev === 'import') importing = true
|
|
414
|
+
else if (prev === 'function' || prev === 'class') addDef(name, depth)
|
|
415
|
+
else if (prev === 'const' || prev === 'let' || prev === 'var') { if (!isKey) addDef(name, depth); decl = depth }
|
|
416
|
+
else if (decl !== null) {
|
|
417
|
+
if (name === 'of' || name === 'in') decl = null // for (const K of …)
|
|
418
|
+
else if (!isKey) addDef(name, depth) // 解构/多声明:const { A, B } = …
|
|
419
|
+
} else if (!isKey) refs.push({ name, depth })
|
|
420
|
+
prev = name
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (c === '{') { depth++; prev = '{'; i++; continue }
|
|
424
|
+
if (c === '}') {
|
|
425
|
+
if (top && top.mode === 'expr' && depth === top.depth) top.mode = 'body' // 关掉 ${…},回模板正文
|
|
426
|
+
else { depth--; if (decl !== null && decl > depth) decl = null }
|
|
427
|
+
prev = '}'
|
|
428
|
+
i++
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
if (c === '(' || c === ')' || c === '[' || c === ']' || c === ';' || c === '=') {
|
|
432
|
+
if (decl !== null && (c === '=' || c === ';' || c === ')')) decl = null
|
|
433
|
+
prev = c
|
|
434
|
+
i++
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
prev = c
|
|
438
|
+
i++
|
|
439
|
+
}
|
|
440
|
+
return { defs, refs }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function checkSymbolIntegrity() {
|
|
444
|
+
try {
|
|
445
|
+
// 词法/作用域:见上方 scanJsSymbols(逐字符状态机 + 大括号作用域)
|
|
446
|
+
const dirs = readdirSync(RUNTIME).filter((d) => d.startsWith('argent-') && !d.startsWith('argent-bak'))
|
|
447
|
+
let totalBad = 0
|
|
448
|
+
// ⚠️ 这里**没有白名单**(2026-10-02 删掉旧的 `FEISHU_WEBHOOK_URL` 条目):新扫描器下它只出现在
|
|
449
|
+
// 行注释/字符串/正则字面量里(argent-core:1664/1667/1673)⇒ 根本不会成为 token,无需豁免。
|
|
450
|
+
// 修假阳性的正确姿势是**修判据**(见 scanJsSymbols),不是把名字加进豁免表。
|
|
451
|
+
const IS_CONST = /^[A-Z][A-Z0-9_]{2,}$/
|
|
452
|
+
for (const d of dirs) {
|
|
453
|
+
const f = join(RUNTIME, d, 'src', 'index.js')
|
|
454
|
+
if (!existsSync(f)) continue
|
|
455
|
+
const { defs, refs } = scanJsSymbols(readFileSync(f, 'utf8'))
|
|
456
|
+
// 已定义 = 存在 `定义深度 <= 引用深度` 的定义(本层或**外层**作用域 ⇒ 闭包里的 const 合法)
|
|
457
|
+
const definedAt = (name, depth) => (defs.get(name) || []).some((dep) => dep <= depth)
|
|
458
|
+
const bad = new Map()
|
|
459
|
+
for (const r of refs) {
|
|
460
|
+
if (!IS_CONST.test(r.name)) continue
|
|
461
|
+
if (JS_BUILTIN_UPPER.has(r.name)) continue
|
|
462
|
+
if (definedAt(r.name, r.depth)) continue
|
|
463
|
+
bad.set(r.name, (bad.get(r.name) || 0) + 1)
|
|
464
|
+
}
|
|
465
|
+
for (const [name, cnt] of bad) {
|
|
466
|
+
totalBad++
|
|
467
|
+
report('FAIL', '符号引用', `${d}: 引用未定义常量「${name}」(${cnt} 处;疑似重构误删定义——host 启动将崩溃或静默失败)`)
|
|
468
|
+
}
|
|
469
|
+
for (const name of defs.keys()) {
|
|
470
|
+
if (!IS_CONST.test(name)) continue
|
|
471
|
+
if (refs.some((r) => r.name === name)) continue
|
|
472
|
+
report('WARN', '符号死代码', `${d}: 常量 ${name} 无引用(可清理)`)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!totalBad) report('PASS', '符号引用完整', `扫描 ${dirs.length} 个插件 src:无「引用未定义」大写常量`)
|
|
476
|
+
} catch (e) {
|
|
477
|
+
report('FAIL', '符号引用完整', e.message)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 看板写路径收口(§8.5 P0-③):把"看板是投影窗口"变成**发布门禁**,而不只是单测守卫。
|
|
483
|
+
* 判据(扫 dev 仓库所有插件源文件):
|
|
484
|
+
* ① 裸写 `kb.(comment|move|create|update|remove)(` 只允许出现在**收口文件**里(CHOKE 白名单,含理由);
|
|
485
|
+
* ② 收口文件必须同时打审计行(writeAuditLine)——否则无法复盘"这条写是投影还是意图";
|
|
486
|
+
* ③ 除 kanban 服务自身外,**任何插件不得出现 `kb.create(`/`kb.remove(`**(自动化不得建卡/删卡);
|
|
487
|
+
* ④ 源码里出现的每个 `comment./move./create.` op 字面量都必须已在 WRITE_SURFACE 登记类别。
|
|
488
|
+
* 写面清单的**真源是插件模块本身**(这里直接 import,避免 doctor 抄一份而漂移)。
|
|
489
|
+
*/
|
|
490
|
+
async function checkKanbanWrites() {
|
|
491
|
+
try {
|
|
492
|
+
const pluginsDir = join(DEV, 'plugins')
|
|
493
|
+
// 2026-09-15 P4-4c:argent-drive / argent-kanban 已退场 ⇒ 该检查失去对象,
|
|
494
|
+
// 直接跳过(否则 import 崩溃会被误报成 FAIL)。
|
|
495
|
+
{
|
|
496
|
+
const { existsSync } = await import('node:fs')
|
|
497
|
+
if (!existsSync(join(pluginsDir, 'argent-drive', 'src', 'kanban-writes.js'))) {
|
|
498
|
+
report('PASS', '看板写收口', 'argent-drive/kanban 已退场,跳过该检查')
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const driveMod = await import(pathToFileURL(join(pluginsDir, 'argent-drive', 'src', 'kanban-writes.js')).href)
|
|
503
|
+
const registered = new Set(Object.keys(driveMod.WRITE_SURFACE || {}))
|
|
504
|
+
const CHOKE = new Map([
|
|
505
|
+
['argent-drive/src/index.js', 'drive 执行内核:kbWrite 收口 + kbComment/kbMove'],
|
|
506
|
+
['argent-drive/src/observe.js', 'observe 投影:project() 是其唯一写入口'],
|
|
507
|
+
['argent-kanban/src/index.js', 'kanban 服务自身实现'],
|
|
508
|
+
['argent-notify/src/index.js', '渠道层:飞书全链失败时的卡评论留痕(kbComment 是唯一写入口)'],
|
|
509
|
+
])
|
|
510
|
+
const RAW_WRITE = /\bkb\.(comment|move|create|update|remove)\(/g
|
|
511
|
+
const stripComments = (s) => s.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n')
|
|
512
|
+
let bad = 0
|
|
513
|
+
let scanned = 0
|
|
514
|
+
const chokeSeen = new Set()
|
|
515
|
+
for (const d of readdirSync(pluginsDir).filter((x) => x.startsWith('argent-') && !x.startsWith('argent-bak'))) {
|
|
516
|
+
const srcDir = join(pluginsDir, d, 'src')
|
|
517
|
+
if (!existsSync(srcDir)) continue
|
|
518
|
+
for (const f of readdirSync(srcDir).filter((x) => x.endsWith('.js'))) {
|
|
519
|
+
const rel = `${d}/src/${f}`
|
|
520
|
+
const raw = readFileSync(join(srcDir, f), 'utf8')
|
|
521
|
+
const code = stripComments(raw)
|
|
522
|
+
scanned++
|
|
523
|
+
const rawHits = [...code.matchAll(RAW_WRITE)]
|
|
524
|
+
if (rawHits.length) {
|
|
525
|
+
if (CHOKE.has(rel)) {
|
|
526
|
+
chokeSeen.add(rel)
|
|
527
|
+
// 审计行判据二选一:① 调用共享 helper `writeAuditLine(`(格式集中在 kanban-writes.js);
|
|
528
|
+
// ② 自带一个同格式字面量 `[write] class=`(跨插件不便 import 时,格式仍必须统一)。
|
|
529
|
+
// 这样 `grep '\[write\] class='` 能一次看到全系统写面。
|
|
530
|
+
// 注意:必须查**剥掉注释**后的代码——否则文件里的文档注释("须打 [write] class=…")会假通过
|
|
531
|
+
const auditOk = code.includes('writeAuditLine(') || /\[write\] class=/.test(code)
|
|
532
|
+
if (!auditOk) {
|
|
533
|
+
bad++
|
|
534
|
+
report('FAIL', '看板写收口', `${rel}: 有裸写但没有审计行(须打 "[<who>][write] class=… op=… card=…")→ 无法复盘写类别`)
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
bad++
|
|
538
|
+
report('FAIL', '看板写收口', `${rel}: 出现裸写 ${rawHits[0][0]} —— 必须经收口(或在 doctor 的 CHOKE 白名单登记并写明理由)`)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (rel !== 'argent-kanban/src/index.js' && /\bkb\.(create|remove)\(/.test(code)) {
|
|
542
|
+
bad++
|
|
543
|
+
report('FAIL', '看板写收口', `${rel}: 出现 kb.create(/kb.remove( —— 自动化不得建卡/删卡(§8.5 P0-③ origination 禁用)`)
|
|
544
|
+
}
|
|
545
|
+
for (const m of code.matchAll(/'(comment|move|create)\.[a-z][a-z-]*'/g)) {
|
|
546
|
+
const op = m[0].slice(1, -1)
|
|
547
|
+
if (!registered.has(op)) {
|
|
548
|
+
bad++
|
|
549
|
+
report('FAIL', '看板写收口', `${rel}: 未登记的写 op「${op}」(须在 kanban-writes.WRITE_SURFACE 登记类别)`)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!bad) {
|
|
555
|
+
report('PASS', '看板写收口', `扫描 ${scanned} 个插件源文件:裸写仅在 ${chokeSeen.size} 个收口处且都有审计行;无未登记 op、无建卡/删卡`)
|
|
556
|
+
}
|
|
557
|
+
} catch (e) {
|
|
558
|
+
report('FAIL', '看板写收口', e.message)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ── 13. 排程面体检(P1-A,2026-10-12;只读;最高 WARN,**从不 FAIL**)──
|
|
563
|
+
// 判据 = `scripts/lib/schedule-probe.mjs`(同一份实现)。离线档:只读域文件 + 注册表声明。
|
|
564
|
+
// ⛔ 不访问宿主、不读凭据("挂载/新鲜度/执行"三闸在 `scripts/argent-schedule.mjs check` 里)。
|
|
565
|
+
async function checkScheduleFace() {
|
|
566
|
+
let probe
|
|
567
|
+
try {
|
|
568
|
+
probe = await import('./lib/schedule-probe.mjs')
|
|
569
|
+
} catch (e) {
|
|
570
|
+
report('WARN', '排程面', `判据模块取不到(scripts/lib/schedule-probe.mjs):${e.message} ⇒ **不能**当成"排程面没问题"`)
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
// ① 域文件(坏了 ⇒ 宿主拒绝启动;不存在 ⇒ 排程从未启用,**不是故障**)
|
|
574
|
+
let inspection
|
|
575
|
+
try {
|
|
576
|
+
const paths = probe.domainPaths(probe.storageRoot({ dshHome: process.env.DSH_HOME || undefined }))
|
|
577
|
+
inspection = probe.inspectDomain(paths)
|
|
578
|
+
if (inspection.state === 'corrupt') {
|
|
579
|
+
report('WARN', '排程面', `排程域文件非法(${inspection.problems.length} 处)⇒ **宿主会拒绝启动整个 domain(连对话一起)**:`
|
|
580
|
+
+ inspection.problems.slice(0, 3).map((p) => `${p.path}:${p.issue}`).join(';')
|
|
581
|
+
+ `|修法:先整份备份 ${inspection.paths.unitDir} 与 ${inspection.paths.legacyFile},再按点名文件修`
|
|
582
|
+
+ '(逐记录文件可直接删:宿主把读不出的记录**当作不存在**丢弃)')
|
|
583
|
+
} else if (inspection.state === 'unreadable') {
|
|
584
|
+
report('WARN', '排程面', `排程域文件读不了(${inspection.problems[0] ? inspection.problems[0].issue : '权限/IO'})⇒ 不能判好坏`)
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
report('WARN', '排程面', `域文件检查没跑成:${e.message}`)
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
// ② 声明面(注册表读不到 ⇒ WARN;⛔ 不静默当"没声明")
|
|
591
|
+
let declared = []
|
|
592
|
+
try {
|
|
593
|
+
const { loadRegistryDoc } = await import('./lib/registry.mjs')
|
|
594
|
+
const doc = loadRegistryDoc({ root: DEV })
|
|
595
|
+
declared = Object.entries(doc.flows || {})
|
|
596
|
+
.map(([id, spec]) => ({ id, s: spec && spec.trigger && spec.trigger.schedule }))
|
|
597
|
+
.filter((x) => x.s && typeof x.s === 'object')
|
|
598
|
+
} catch (e) {
|
|
599
|
+
report('WARN', '排程面', `注册表读不到 ⇒ **不能**下"没有流程声明排程"的结论:${String((e && (e.stderr || e.message)) || e).split('\n')[0]}`)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
const enabled = declared.filter((d) => d.s.enabled === true)
|
|
603
|
+
if (inspection.state === 'absent' && !enabled.length) {
|
|
604
|
+
report('PASS', '排程面', `排程未启用(域文件未落盘;声明 ${declared.length} 条、enabled ${enabled.length} 条)⇒ 无需判`)
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
report('PASS', '排程面', `域文件 ${inspection.state}(${inspection.files.length} 个)|声明 ${declared.length} 条、enabled ${enabled.length} 条`
|
|
608
|
+
+ `${enabled.length ? `(${enabled.map((d) => d.id).join('、')})` : ''}`
|
|
609
|
+
+ '|"挂载了没 / 定时还活着没"跑 `node scripts/argent-schedule.mjs check`')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const checks = [checkDrift, checkPerms, checkScheduling, checkHygiene, checkGateway, checkCodeHygiene, checkGrowth, checkBakRotation, checkPluginSyntax, checkServiceConflicts, checkSymbolIntegrity, checkKanbanWrites, checkScheduleFace]
|
|
613
|
+
for (const c of checks) await c()
|
|
614
|
+
|
|
615
|
+
const fails = results.filter((r) => r.level === 'FAIL')
|
|
616
|
+
const warns = results.filter((r) => r.level === 'WARN')
|
|
617
|
+
const passes = results.filter((r) => r.level === 'PASS')
|
|
618
|
+
if (jsonOut) {
|
|
619
|
+
process.stdout.write(JSON.stringify({ summary: { pass: passes.length, warn: warns.length, fail: fails.length }, results }, null, 2) + '\n')
|
|
620
|
+
} else {
|
|
621
|
+
for (const r of results) process.stdout.write(`[${r.level}] ${r.check}: ${r.msg}\n`)
|
|
622
|
+
process.stdout.write(`\nargent-doctor: PASS ${passes.length} / WARN ${warns.length} / FAIL ${fails.length}\n`)
|
|
623
|
+
}
|
|
624
|
+
process.exit(fails.length ? 1 : warns.length ? 2 : 0)
|