@whyshu/argent-engine 0.0.0-stage → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh 离线包打包器 —— **打包/开发工具**(2026-10-12 起:安装器**不再**代装宿主,
|
|
4
|
+
* 也就**不再**下载/引用本工具产物;保留它是为了快照留档 / 断网机人工送装)。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* node scripts/dsh-offline-pack.mjs <版本> [--from-dir <已装目录>]
|
|
8
|
+
*
|
|
9
|
+
* 做什么:
|
|
10
|
+
* 1. 校验版本与快照(dsh-version-lock/<版本>.json 必须存在 = 已验证版本)
|
|
11
|
+
* 2. 校验来源目录的 @deepseek-ai 子包版本分布 == 快照(防打包脏树)
|
|
12
|
+
* 3. 打包整个 node_modules(197 scope 包 + 顶层非 scope 依赖)→ /tmp/dsh-<版本>-offline.tgz
|
|
13
|
+
*
|
|
14
|
+
* 输出文件上传到 https://whyshu.com/dl/dsh-<版本>-offline.tgz(服务器 /home/whyshu/web/dl/)
|
|
15
|
+
*/
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const REPO = join(__dirname, "..");
|
|
23
|
+
const LOCK_DIR = join(REPO, "dsh-version-lock");
|
|
24
|
+
|
|
25
|
+
const target = process.argv[2];
|
|
26
|
+
if (!target) {
|
|
27
|
+
console.error("用法: node scripts/dsh-offline-pack.mjs <版本> [--from-dir <已装目录>]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const fromIdx = process.argv.indexOf("--from-dir");
|
|
31
|
+
const fromDir = fromIdx > -1 ? process.argv[fromIdx + 1] : null;
|
|
32
|
+
const ok = (m) => console.log(" ✅ " + m);
|
|
33
|
+
const fail = (m) => { console.error(" ❌ " + m); process.exit(1); };
|
|
34
|
+
|
|
35
|
+
// ── 1. 快照校验 ──────────────────────────────────────────
|
|
36
|
+
const snapFile = join(LOCK_DIR, `${target}.json`);
|
|
37
|
+
if (!existsSync(snapFile)) fail(`快照不存在: ${snapFile}(只有已验证版本才能打包分发)`);
|
|
38
|
+
const snapshot = JSON.parse(readFileSync(snapFile, "utf8"));
|
|
39
|
+
ok(`快照 ${target}: ${Object.keys(snapshot.resolved).length} 个子包`);
|
|
40
|
+
|
|
41
|
+
// ── 2. 来源目录校验 ──────────────────────────────────────
|
|
42
|
+
const src = fromDir || join("/tmp", `dsh-snap-${target.replace(/\./g, "").replace(/-/g, "")}`);
|
|
43
|
+
const srcNM = join(src, "node_modules");
|
|
44
|
+
if (!existsSync(srcNM)) fail(`来源目录不存在: ${srcNM}(先用 dsh-upgrade.mjs 或手工本地安装好目标版本)`);
|
|
45
|
+
const scope = join(srcNM, "@deepseek-ai");
|
|
46
|
+
if (!existsSync(scope)) fail(`缺少 @deepseek-ai: ${scope}`);
|
|
47
|
+
|
|
48
|
+
console.log("\n校验 @deepseek-ai 版本分布 == 快照 ...");
|
|
49
|
+
let bad = 0, total = 0;
|
|
50
|
+
for (const name of readdirSync(scope)) {
|
|
51
|
+
const pj = join(scope, name, "package.json");
|
|
52
|
+
if (!existsSync(pj)) continue;
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pj, "utf8"));
|
|
54
|
+
total++;
|
|
55
|
+
const expect = snapshot.resolved[name];
|
|
56
|
+
if (expect && pkg.version !== expect) {
|
|
57
|
+
console.log(` ⚠️ ${name}: 期望 ${expect} 实际 ${pkg.version}`);
|
|
58
|
+
bad++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (bad) fail(`${bad}/${total} 个子包版本与快照不一致——不要打包脏树`);
|
|
62
|
+
ok(`${total} 个子包版本与快照一致`);
|
|
63
|
+
|
|
64
|
+
// ── 3. 打包 ──────────────────────────────────────────────
|
|
65
|
+
const out = `/tmp/dsh-${target}-offline.tgz`;
|
|
66
|
+
rmSync(out, { force: true });
|
|
67
|
+
console.log(`\n打包 ${srcNM} → ${out} ...`);
|
|
68
|
+
// -h(--dereference):node_modules/.bin 的 symlink 转真实文件——
|
|
69
|
+
// Windows 创建 symlink 需要管理员特权,保留链接会导致 PC 解压失败(实测 2026-08-24)
|
|
70
|
+
execSync(`tar -czhf "${out}" -C "${src}" node_modules`, { stdio: "inherit" });
|
|
71
|
+
const mb = (statSync(out).size / 1048576).toFixed(1);
|
|
72
|
+
console.log(`\n✅ 离线包: ${out}(${mb} MB)`);
|
|
73
|
+
console.log(`
|
|
74
|
+
上传到服务器 /home/whyshu/web/dl/(whyshu.com/dl/):
|
|
75
|
+
scp -i ~/.ssh/whyshu.pem "${out}" ubuntu@81.69.222.211:/home/whyshu/web/dl/
|
|
76
|
+
验证: curl -sI https://whyshu.com/dl/dsh-${target}-offline.tgz | head -3
|
|
77
|
+
(已无自动引用:安装器自 2026-10-12 起⛔ 不代装宿主。要人工送装请自行解包到目标 node_modules)`);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dsh-overlay-pin.sh —— 固定本机在 EasyTier 覆盖网里的地址(默认 10.126.126.2),并验证到位。
|
|
3
|
+
#
|
|
4
|
+
# 为什么要有它(2026-09-22 事故 + 用户实操踩坑):
|
|
5
|
+
# · EasyTier 用 `-d`(DHCP) 时地址**每次重启可能变**(.2 → .1),而端口转发与 dsh web 的
|
|
6
|
+
# trusted-host 都按地址写 ⇒ PC 直接连不上;
|
|
7
|
+
# · 手工"杀 + 重启"很容易**看起来做了、其实没做**:`sudo pkill -x easytier-core` 在 macOS 上
|
|
8
|
+
# **匹配不上**(该进程的 comm 是完整路径 `/Users/chester/.local/bin/easytier-core`),
|
|
9
|
+
# 于是旧进程照旧跑着,启动脚本又因"already running"直接退出 —— 地址当然没变。
|
|
10
|
+
# 本脚本把这件事做成可验证的一步:停(按模式匹配并确认已退出)→ 起(固定地址)→ 等地址生效 → 复核。
|
|
11
|
+
#
|
|
12
|
+
# 用法:
|
|
13
|
+
# bash scripts/dsh-overlay-pin.sh # 固定到 10.126.126.2/24(默认)
|
|
14
|
+
# bash scripts/dsh-overlay-pin.sh 10.126.126.9/24
|
|
15
|
+
# bash scripts/dsh-overlay-pin.sh --status # 只看当前地址(不改)
|
|
16
|
+
# 需要管理员权限(EasyTier 以 root 运行)——会用 sudo,按提示输入密码。
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
19
|
+
LAUNCHER="$HOME/.local/bin/start-easytier.sh"
|
|
20
|
+
CIDR_PREFIX="${ARGENT_OVERLAY_CIDR:-10.126.126.}"
|
|
21
|
+
WANT="${1:-10.126.126.2/24}"
|
|
22
|
+
|
|
23
|
+
overlay_ips() { ifconfig 2>/dev/null | awk '/inet /{print $2}' | grep "^${CIDR_PREFIX}" || true; }
|
|
24
|
+
|
|
25
|
+
if [ "${1:-}" = "--status" ]; then
|
|
26
|
+
echo "── EasyTier"
|
|
27
|
+
pgrep -f easytier-core >/dev/null && echo " ✅ 在跑($(pgrep -f easytier-core | head -1)):$(ps -o command= -p "$(pgrep -f easytier-core | head -1)" | cut -c1-120)" \
|
|
28
|
+
|| echo " ❌ 没在跑"
|
|
29
|
+
echo " 覆盖网地址:$(overlay_ips | tr '\n' ' ')"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ ! -x "$LAUNCHER" ]; then
|
|
34
|
+
echo "❌ 找不到启动脚本 $LAUNCHER" >&2; exit 2
|
|
35
|
+
fi
|
|
36
|
+
if [ "$(id -u)" -ne 0 ] && ! sudo -n true 2>/dev/null; then
|
|
37
|
+
echo "→ 需要管理员权限(EasyTier 以 root 运行),接下来会提示输入密码"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "── ① 停掉现有 easytier(按**模式**匹配,不用 -x:它的 comm 是完整路径,-x 匹配不上)"
|
|
41
|
+
PIDS="$(pgrep -f easytier-core || true)"
|
|
42
|
+
if [ -n "$PIDS" ]; then
|
|
43
|
+
echo " 终止 PID: $PIDS"
|
|
44
|
+
sudo pkill -f easytier-core || true
|
|
45
|
+
for _ in 1 2 3 4 5 6; do
|
|
46
|
+
pgrep -f easytier-core >/dev/null || break
|
|
47
|
+
sleep 1
|
|
48
|
+
done
|
|
49
|
+
if pgrep -f easytier-core >/dev/null; then
|
|
50
|
+
echo " ⚠️ 仍在运行,强制终止"; sudo pkill -9 -f easytier-core || true; sleep 2
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
if pgrep -f easytier-core >/dev/null; then
|
|
54
|
+
echo "❌ 停不掉(还活着:$(pgrep -f easytier-core | tr '\n' ' '))——请手工处理" >&2; exit 1
|
|
55
|
+
fi
|
|
56
|
+
echo " ✅ 已停止"
|
|
57
|
+
|
|
58
|
+
echo "── ② 以固定地址启动:$WANT"
|
|
59
|
+
# 启动脚本里的地址是可编辑的;这里按需改写(保持单一入口:launchd 也调它)
|
|
60
|
+
if ! grep -q -- "--ipv4" "$LAUNCHER"; then
|
|
61
|
+
echo " ⚠️ $LAUNCHER 里没有 --ipv4(DHCP 模式)⇒ 自动补上并去掉 -d"
|
|
62
|
+
sed -i '' 's/ -d \\/ \\/' "$LAUNCHER" 2>/dev/null || true
|
|
63
|
+
sed -i '' "s#--hostname chester-mac \\\\#--hostname chester-mac \\\\\n --ipv4 $WANT \\\\#" "$LAUNCHER" 2>/dev/null || true
|
|
64
|
+
else
|
|
65
|
+
sed -i '' "s#--ipv4 [0-9./]*#--ipv4 $WANT#" "$LAUNCHER"
|
|
66
|
+
fi
|
|
67
|
+
grep -q -- "--ipv4 $WANT" "$LAUNCHER" || { echo "❌ 未能写入固定地址,请手工检查 $LAUNCHER" >&2; exit 1; }
|
|
68
|
+
|
|
69
|
+
# 以 root 起(与 launchd 的 osascript 提权等价)
|
|
70
|
+
sudo bash -c "nohup '$LAUNCHER' >/dev/null 2>&1 &" || { echo "❌ 启动失败" >&2; exit 1; }
|
|
71
|
+
|
|
72
|
+
echo "── ③ 等地址生效(最多 30s)"
|
|
73
|
+
GOT=""
|
|
74
|
+
for _ in $(seq 1 30); do
|
|
75
|
+
GOT="$(overlay_ips | head -1)"
|
|
76
|
+
[ "$GOT" = "${WANT%%/*}" ] && break
|
|
77
|
+
sleep 1
|
|
78
|
+
done
|
|
79
|
+
echo " 当前覆盖网地址:${GOT:-(无)}"
|
|
80
|
+
if [ "$GOT" != "${WANT%%/*}" ]; then
|
|
81
|
+
echo " ❌ 未拿到 ${WANT}(实际 ${GOT:-无})——查日志:$HOME/Library/Logs/argent/easytier.log" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
echo " ✅ 已固定到 $GOT"
|
|
85
|
+
|
|
86
|
+
echo "── ④ 远程访问链路复核"
|
|
87
|
+
bash "$REPO/scripts/dsh-remote-check.sh"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-plane —— 三平面里的**侧车 / 救援面**登记与自检(P5,2026-09-29)
|
|
4
|
+
*
|
|
5
|
+
* 背景(评审稿 §5):主面追新、救援面滞后一版、客户对齐侧车跟客户机同版;三者必须
|
|
6
|
+
* **各有名字 / 各有 home / 各有端口 / 各有启动项**,否则"救援"和"被救援"会互相踩。
|
|
7
|
+
* 本工具只做三件事,**都不碰服务**:
|
|
8
|
+
* ① `--add`:登记一个平面(校验树布局、独立 home、端口不撞),打印**plist 片段**(不安装);
|
|
9
|
+
* ② `--list`:列出已登记平面与它们的状态;
|
|
10
|
+
* ③ `--check`:只读自检(树还能不能跑、home 在不在、端口是否被占、与主面是否隔离)。
|
|
11
|
+
*
|
|
12
|
+
* 为什么不让脚本自己装 LaunchAgent / 起服务:那是"激活"动作,会重启服务、会抢端口 ——
|
|
13
|
+
* 必须由人来做(AGENTS.md「严禁自杀式重启」)。本工具把**该写什么**打印清楚,人复制即可。
|
|
14
|
+
*
|
|
15
|
+
* 用法:
|
|
16
|
+
* node scripts/dsh-plane.mjs --add sidecar-0.1.5 --scope ~/dsh-0.1.5/lib/node_modules/@deepseek-ai \
|
|
17
|
+
* --home ~/.dsh015 --port 3081 [--label com.whyshu.dsh015-web] [--title "客户对齐 0.1.5"]
|
|
18
|
+
* node scripts/dsh-plane.mjs --list
|
|
19
|
+
* node scripts/dsh-plane.mjs --check all
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { dirname, join, resolve } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import { resolveDshScope } from './lib/dsh-scope.mjs'
|
|
26
|
+
import { dshHome, looksLikeScope } from './lib/scope-pointer.mjs'
|
|
27
|
+
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
const REPO = resolve(HERE, '..')
|
|
30
|
+
const argOf = (n, d) => { const i = process.argv.indexOf(n); return i > -1 ? process.argv[i + 1] ?? d : d }
|
|
31
|
+
const PLANE_DIR = join(dshHome(), 'argent', 'planes')
|
|
32
|
+
const REGISTRY = join(PLANE_DIR, 'planes.json')
|
|
33
|
+
|
|
34
|
+
const load = () => {
|
|
35
|
+
try { return JSON.parse(readFileSync(REGISTRY, 'utf8')) } catch { return { planes: {} } }
|
|
36
|
+
}
|
|
37
|
+
const save = (doc) => {
|
|
38
|
+
mkdirSync(PLANE_DIR, { recursive: true })
|
|
39
|
+
writeFileSync(REGISTRY, `${JSON.stringify(doc, null, 2)}\n`)
|
|
40
|
+
}
|
|
41
|
+
const expand = (p) => (p ? resolve(String(p).replace(/^~(?=\/|$)/, homedir())) : p)
|
|
42
|
+
|
|
43
|
+
const MODE = process.argv.includes('--add') ? 'add'
|
|
44
|
+
: process.argv.includes('--list') ? 'list'
|
|
45
|
+
: process.argv.includes('--check') ? 'check' : ''
|
|
46
|
+
|
|
47
|
+
const PRIMARY_PORT = 3080
|
|
48
|
+
|
|
49
|
+
// 主面的 scope(**可选增强**:登记/合并时用得上,⛔ 探不到不该让 `--list` 崩)。
|
|
50
|
+
// 0.6.0 F1 修复批(2026-10-14):`resolveDshScope()` 探不到 ⇒ `null`(未知);这里**降级但打印原因**,
|
|
51
|
+
// ⛔ 不再拿一个可能不存在的路径当"主面 scope"(那会把平面的端口/home 冲突判据算错)。
|
|
52
|
+
const PRIMARY_SCOPE = (() => {
|
|
53
|
+
const hit = resolveDshScope()
|
|
54
|
+
if (hit && !hit.stale) return hit.scope
|
|
55
|
+
console.error(hit
|
|
56
|
+
? ` ⚠️ 主面 scope 是**空壳**(${hit.scope}:目录在、里面没有 dsh 包)⇒ 主面 scope 记为空(原因见上)`
|
|
57
|
+
: ' ⚠️ 探不到任何宿主 scope(可能是 **npx 形态**)⇒ 主面 scope 记为空;如需要请显式给 `--scope-dir <…/@deepseek-ai>`')
|
|
58
|
+
return ''
|
|
59
|
+
})()
|
|
60
|
+
const KNOWN_PLANES = { primary: { port: PRIMARY_PORT, home: dshHome(), scope: PRIMARY_SCOPE, note: '主面(当前服务)' } }
|
|
61
|
+
|
|
62
|
+
if (MODE === 'add') {
|
|
63
|
+
const name = argOf('--add')
|
|
64
|
+
const scope = expand(argOf('--scope'))
|
|
65
|
+
const home = expand(argOf('--home'))
|
|
66
|
+
const port = Number(argOf('--port', '0'))
|
|
67
|
+
const label = argOf('--label', `com.whyshu.dsh-${name}-web`)
|
|
68
|
+
const title = argOf('--title', name)
|
|
69
|
+
const problems = []
|
|
70
|
+
// 允许点:侧车名常带版本(如 sidecar-0.1.5);只要求小写字母/数字/点/连字符开头合法
|
|
71
|
+
if (!name || !/^[a-z0-9][a-z0-9.-]*$/.test(name)) problems.push('--add 需要一个 kebab-case 名字(允许点,如 sidecar-0.1.5)')
|
|
72
|
+
if (!scope) problems.push('--scope 必填(平面用的引擎树)')
|
|
73
|
+
else if (!looksLikeScope(scope)) problems.push(`--scope 不是可运行的 dsh 树(需要在 node_modules/ 下且含 dsh/lib/bin.js):${scope}`)
|
|
74
|
+
if (!home) problems.push('--home 必填(该平面自己的 DSH_HOME,必须与主面隔离)')
|
|
75
|
+
else if (resolve(home) === resolve(dshHome())) problems.push(`--home 不能与主面相同(${dshHome()})—— 平面之间必须各自一套 home`)
|
|
76
|
+
if (!port) problems.push('--port 必填(各平面各占端口)')
|
|
77
|
+
else if (port === PRIMARY_PORT) problems.push(`--port 不能与主面相同(${PRIMARY_PORT})`)
|
|
78
|
+
const doc = load()
|
|
79
|
+
for (const [n, p] of Object.entries({ ...KNOWN_PLANES, ...doc.planes })) {
|
|
80
|
+
if (p.port === port) problems.push(`端口 ${port} 已被平面 ${n} 占用(登记里)`)
|
|
81
|
+
if (p.home && home && resolve(p.home) === resolve(home)) problems.push(`home ${home} 已被平面 ${n} 占用`)
|
|
82
|
+
}
|
|
83
|
+
if (problems.length) {
|
|
84
|
+
console.error('✗ 不能登记:')
|
|
85
|
+
for (const p of problems) console.error(` · ${p}`)
|
|
86
|
+
process.exit(2)
|
|
87
|
+
}
|
|
88
|
+
doc.planes[name] = { scope, home, port, label, title, addedAt: new Date().toISOString() }
|
|
89
|
+
save(doc)
|
|
90
|
+
// 建 home 骨架(**只建目录**:profiles/ 与 argent/ 由安装器/人工继续填;不安装、不起服务)
|
|
91
|
+
for (const d of [join(home, 'argent'), join(home, 'profiles')]) {
|
|
92
|
+
if (!existsSync(d)) mkdirSync(d, { recursive: true })
|
|
93
|
+
}
|
|
94
|
+
const bin = join(scope, 'dsh', 'lib', 'bin.js')
|
|
95
|
+
console.log(`✅ 已登记平面 \`${name}\`(${title})`)
|
|
96
|
+
console.log(` 树:${scope}`)
|
|
97
|
+
console.log(` home:${home}(已建骨架 argent/ + profiles/;其余由安装器/人工填)`)
|
|
98
|
+
console.log(` 端口:${port} 启动项 label:${label}`)
|
|
99
|
+
console.log(`
|
|
100
|
+
下一步(**人工激活**,工具不代劳 —— 会重启/抢端口):
|
|
101
|
+
1) 目录:mkdir -p ${home}/argent ${join(dirname(scope), '..')} 2>/dev/null || true
|
|
102
|
+
2) 写 ~/Library/LaunchAgents/${label}.plist,ProgramArguments 用**绝对路径**:
|
|
103
|
+
${process.execPath} 或 ${bin}
|
|
104
|
+
web --no-open --port ${port}
|
|
105
|
+
且设置 EnvironmentVariables:DSH_HOME=${home}
|
|
106
|
+
3) 加载:launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/${label}.plist
|
|
107
|
+
4) 验证:curl -s -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:${port}/ # 401/200 都算起来了
|
|
108
|
+
5) 自检:node scripts/dsh-plane.mjs --check ${name}`)
|
|
109
|
+
process.exit(0)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (MODE === 'list' || (!MODE && process.argv.length <= 2)) {
|
|
113
|
+
const doc = load()
|
|
114
|
+
console.log(`平面登记:${REGISTRY}\n`)
|
|
115
|
+
const rows = [{ name: 'primary(主面)', ...KNOWN_PLANES.primary, label: 'com.whyshu.dsh-web' },
|
|
116
|
+
...Object.entries(doc.planes).map(([n, p]) => ({ name: n, ...p }))]
|
|
117
|
+
for (const r of rows) {
|
|
118
|
+
const okTree = looksLikeScope(r.scope) ? '✅' : (r.scope ? '❌' : 'ℹ️')
|
|
119
|
+
console.log(` ${okTree} ${r.name}|端口 ${r.port}|home ${r.home}|label ${r.label}`)
|
|
120
|
+
if (r.scope) console.log(` 树 ${r.scope}`)
|
|
121
|
+
}
|
|
122
|
+
console.log('\n自检:node scripts/dsh-plane.mjs --check all')
|
|
123
|
+
process.exit(0)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (MODE === 'check') {
|
|
127
|
+
const which = argOf('--check', 'all')
|
|
128
|
+
const doc = load()
|
|
129
|
+
const all = { primary: { ...KNOWN_PLANES.primary, label: 'com.whyshu.dsh-web' }, ...doc.planes }
|
|
130
|
+
const targets = which === 'all' ? Object.keys(all) : [which]
|
|
131
|
+
let bad = 0
|
|
132
|
+
for (const t of targets) {
|
|
133
|
+
const p = all[t]
|
|
134
|
+
if (!p) { console.error(` ❌ 未登记平面:${t}`); bad += 1; continue }
|
|
135
|
+
const issues = []
|
|
136
|
+
if (!looksLikeScope(p.scope)) issues.push(`树不可运行:${p.scope}`)
|
|
137
|
+
if (p.home && !existsSync(p.home)) issues.push(`home 不存在(还没建/还没装):${p.home}`)
|
|
138
|
+
if (p.home && resolve(p.home) === resolve(dshHome()) && t !== 'primary') issues.push('home 与主面相同(未隔离)')
|
|
139
|
+
console.log(` ${issues.length ? '❌' : '✅'} ${t}(端口 ${p.port})`)
|
|
140
|
+
for (const i of issues) console.log(` · ${i}`)
|
|
141
|
+
if (issues.length) bad += 1
|
|
142
|
+
}
|
|
143
|
+
process.exit(bad ? 1 : 0)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`dsh-plane —— 侧车/救援面登记与自检
|
|
147
|
+
|
|
148
|
+
node scripts/dsh-plane.mjs --add <名> --scope <树> --home <dir> --port <n> [--label <label>] [--title <标题>]
|
|
149
|
+
node scripts/dsh-plane.mjs --list
|
|
150
|
+
node scripts/dsh-plane.mjs --check [<名>|all]
|
|
151
|
+
|
|
152
|
+
只做登记与只读自检;**安装启动项/起服务由人来做**(会重启、会抢端口)。`)
|
|
153
|
+
process.exit(process.argv.length <= 2 ? 0 : 2)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dsh-portforward — dsh web 对外入口(EasyTier 组网内访问)
|
|
4
|
+
|
|
5
|
+
背景:dsh 0.1.5+ 出于安全设计只允许监听 127.0.0.1(--host 0.0.0.0 被 CLI 拒绝,
|
|
6
|
+
自定义 IP 被 schema 拒绝)。组网设备(手机/其他电脑)通过本转发器访问:
|
|
7
|
+
|
|
8
|
+
<本机 EasyTier IP>:3080 → 127.0.0.1:3080 (dsh web)
|
|
9
|
+
|
|
10
|
+
配套:dsh web 启动参数需加 `--trusted-host <本机 EasyTier IP>:3080`
|
|
11
|
+
(browser-trust fence 白名单,否则 /api 请求 403);token 鉴权照旧
|
|
12
|
+
(首次访问用带 `?token=` 的 URL 换取 30 天 cookie)。
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
dsh-portforward.py [--listen-host 10.126.126.2] [--listen-port 3080]
|
|
16
|
+
[--target-host 127.0.0.1] [--target-port 3080]
|
|
17
|
+
|
|
18
|
+
特性:bind 失败自动重试(EasyTier 接口未就绪时自愈);多连接并发;
|
|
19
|
+
半开/静默连接在一侧 EOF 后限时回收,不泄漏 fd(2026-09-22 修复,见下方注释)。
|
|
20
|
+
"""
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import socket
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
BUF = 65536
|
|
29
|
+
RETRY_INTERVAL = 5
|
|
30
|
+
# 一侧已 EOF 后,留给另一侧把剩余数据排完的时间;超时即强制关两端。
|
|
31
|
+
# 不能无限等:对端静默(EasyTier 抖动、浏览器挂死、隧道半开)时另一侧会永久阻塞在
|
|
32
|
+
# recv(),handle() 就永远走不到 close() —— 每条连接泄漏 2 个 fd,最终耗尽
|
|
33
|
+
# launchd 下发的 256 句柄上限,accept() 开始 EMFILE,远程将彻底进不来。
|
|
34
|
+
DRAIN_GRACE = float(os.environ.get("DSH_PORTFORWARD_DRAIN_GRACE", "10"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def close_quietly(sock):
|
|
38
|
+
"""幂等关闭:先 shutdown 唤醒阻塞在对端的线程,再 close 释放 fd。"""
|
|
39
|
+
try:
|
|
40
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
try:
|
|
44
|
+
sock.close()
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def handle(client, target):
|
|
50
|
+
try:
|
|
51
|
+
upstream = socket.create_connection(target, timeout=10)
|
|
52
|
+
except OSError as e:
|
|
53
|
+
print(f"[portforward] 上游连接失败 {target}: {e}", flush=True)
|
|
54
|
+
close_quietly(client)
|
|
55
|
+
return
|
|
56
|
+
# 转发阶段不设超时:dsh web 的 WebSocket 可以长时间静默,超时会误杀正常连接
|
|
57
|
+
upstream.settimeout(None)
|
|
58
|
+
client.settimeout(None)
|
|
59
|
+
|
|
60
|
+
finished = threading.Event()
|
|
61
|
+
|
|
62
|
+
def relay(src, dst):
|
|
63
|
+
try:
|
|
64
|
+
while True:
|
|
65
|
+
data = src.recv(BUF)
|
|
66
|
+
if not data:
|
|
67
|
+
break
|
|
68
|
+
dst.sendall(data)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
finally:
|
|
72
|
+
try:
|
|
73
|
+
dst.shutdown(socket.SHUT_WR) # 传播半关闭
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
finished.set()
|
|
77
|
+
|
|
78
|
+
t1 = threading.Thread(target=relay, args=(client, upstream), daemon=True)
|
|
79
|
+
t2 = threading.Thread(target=relay, args=(upstream, client), daemon=True)
|
|
80
|
+
t1.start()
|
|
81
|
+
t2.start()
|
|
82
|
+
|
|
83
|
+
# 健康的长连接两个方向都不会 EOF,这里会一直等 —— 正确,不做超时打扰。
|
|
84
|
+
while not finished.wait(1.0):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# 有一侧结束了:给另一侧 DRAIN_GRACE 秒排空,然后无论如何都关掉两端。
|
|
88
|
+
t1.join(DRAIN_GRACE)
|
|
89
|
+
t2.join(DRAIN_GRACE)
|
|
90
|
+
close_quietly(client)
|
|
91
|
+
close_quietly(upstream)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _local_ipv4(prefix: str) -> list:
|
|
95
|
+
"""本机所有匹配前缀的 IPv4(覆盖网地址/utun 也要能看到)。
|
|
96
|
+
|
|
97
|
+
⚠️ 不能用 `socket.getaddrinfo(gethostname())`:它只返回主机名解析结果,**看不到 utun 这类
|
|
98
|
+
点对点接口**(2026-09-22 实测:EasyTier 的 10.126.126.1 完全查不到 ⇒ auto 会一直判"未就绪")。
|
|
99
|
+
这里直接枚举接口地址:① ioctl(仅 Unix,无子进程) ② 兜底解析 /sbin/ifconfig。
|
|
100
|
+
"""
|
|
101
|
+
found = []
|
|
102
|
+
|
|
103
|
+
def add(ip):
|
|
104
|
+
if ip.startswith(prefix) and ip not in found:
|
|
105
|
+
found.append(ip)
|
|
106
|
+
|
|
107
|
+
# ① ioctl(stdlib):逐接口取 SIOCGIFADDR
|
|
108
|
+
try:
|
|
109
|
+
import fcntl, struct
|
|
110
|
+
SIOCGIFADDR = 0xC0206921 # macOS
|
|
111
|
+
for _, name in socket.if_nameindex():
|
|
112
|
+
try:
|
|
113
|
+
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
114
|
+
packed = fcntl.ioctl(sk.fileno(), SIOCGIFADDR, struct.pack('256s', name.encode()[:15]))
|
|
115
|
+
sk.close()
|
|
116
|
+
add(socket.inet_ntoa(packed[20:24]))
|
|
117
|
+
except OSError:
|
|
118
|
+
continue
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# ② 兜底:解析 ifconfig(ioctl 在个别系统/权限下会失败)
|
|
123
|
+
if not found:
|
|
124
|
+
try:
|
|
125
|
+
import subprocess
|
|
126
|
+
out = subprocess.run(['/sbin/ifconfig'], capture_output=True, text=True, timeout=5).stdout
|
|
127
|
+
for line in out.splitlines():
|
|
128
|
+
line = line.strip()
|
|
129
|
+
if line.startswith('inet '):
|
|
130
|
+
add(line.split()[1])
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return found
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_listen_host(spec: str, prefix: str = '10.126.126.') -> tuple:
|
|
137
|
+
"""把 --listen-host 解析成真正要 bind 的地址 → (ip 或 None, 说明)。
|
|
138
|
+
|
|
139
|
+
· 具体 IP:原样返回(并提示它当前是否真的在本机 —— 这一条就是 2026-09-22 事故的判据)
|
|
140
|
+
· 'auto':按前缀自动挑一个本机覆盖网地址(地址变了也能自愈)
|
|
141
|
+
"""
|
|
142
|
+
if spec and spec != 'auto':
|
|
143
|
+
# 明确给了地址就照用(不擅自改),但**必须能判断它是否存在** —— 这正是 2026-09-22
|
|
144
|
+
# 事故的判据:plist 写死 .2,而重启后本机只有 .1 ⇒ bind 永久失败却看不出原因。
|
|
145
|
+
same_subnet = _local_ipv4(spec.rsplit('.', 1)[0] + '.')
|
|
146
|
+
if spec in same_subnet:
|
|
147
|
+
return spec, 'explicit(本机存在)'
|
|
148
|
+
return spec, 'explicit(⚠️ 该地址当前不在本机接口上,bind 会失败;建议改用 auto)'
|
|
149
|
+
found = _local_ipv4(prefix)
|
|
150
|
+
if not found:
|
|
151
|
+
return None, f'未找到 {prefix}x 的本机地址(EasyTier 未就绪?)'
|
|
152
|
+
return found[0], f'auto(选中 {found[0]})'
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def raise_fd_limit(target=8192):
|
|
156
|
+
"""抬高本进程的 fd 软上限。
|
|
157
|
+
|
|
158
|
+
launchd 给 LaunchAgent 的 maxfiles 默认是 256(实测 `launchctl limit maxfiles`
|
|
159
|
+
= 256 unlimited)。远程访问全靠这个转发器,一旦连接堆积撞上 256,
|
|
160
|
+
accept() 会 EMFILE,远程会彻底进不来。硬上限是 unlimited,进程内可自行抬高。
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
import resource
|
|
164
|
+
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
165
|
+
if hard != resource.RLIM_INFINITY and hard < target:
|
|
166
|
+
target = hard
|
|
167
|
+
if soft < target:
|
|
168
|
+
resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard))
|
|
169
|
+
print(f"[portforward] fd 上限 {soft} → {target}", flush=True)
|
|
170
|
+
except Exception as e: # 抬不动不算致命,继续跑
|
|
171
|
+
print(f"[portforward] 抬高 fd 上限失败(忽略):{e}", flush=True)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main():
|
|
175
|
+
raise_fd_limit()
|
|
176
|
+
ap = argparse.ArgumentParser()
|
|
177
|
+
ap.add_argument('--listen-host', default='auto',
|
|
178
|
+
help="监听地址:'auto'(默认,自动探测 EasyTier 覆盖网 IP)或具体 IP。"
|
|
179
|
+
"⚠️ 2026-09-22 事故:EasyTier 用 -d(DHCP) 拿到的地址**重启后会变**"
|
|
180
|
+
"(.2 → .1),而这里与 dsh-web 的 --trusted-host 都写死了 .2 ⇒ "
|
|
181
|
+
"转发器 bind 失败、界面报连不上(日志还在 /tmp,重启即清,无迹可查)")
|
|
182
|
+
ap.add_argument('--auto-cidr', default='10.126.126.',
|
|
183
|
+
help='auto 模式下用于识别覆盖网地址的前缀(默认 10.126.126.)')
|
|
184
|
+
ap.add_argument('--listen-port', type=int, default=3080,
|
|
185
|
+
help='监听端口(转发入口)')
|
|
186
|
+
ap.add_argument('--target-host', default='127.0.0.1', help='目标地址')
|
|
187
|
+
ap.add_argument('--target-port', type=int, default=3080, help='目标端口')
|
|
188
|
+
args = ap.parse_args()
|
|
189
|
+
|
|
190
|
+
target = (args.target_host, args.target_port)
|
|
191
|
+
|
|
192
|
+
while True: # bind 失败自动重试(EasyTier 接口未就绪时自愈)
|
|
193
|
+
host, why = resolve_listen_host(args.listen_host, args.auto_cidr)
|
|
194
|
+
if host is None:
|
|
195
|
+
print(f"[portforward] 暂无可监听地址({why}),{RETRY_INTERVAL}s 后重试", flush=True)
|
|
196
|
+
time.sleep(RETRY_INTERVAL)
|
|
197
|
+
continue
|
|
198
|
+
listen = (host, args.listen_port)
|
|
199
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
200
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
201
|
+
try:
|
|
202
|
+
srv.bind(listen)
|
|
203
|
+
srv.listen(64)
|
|
204
|
+
break
|
|
205
|
+
except OSError as e:
|
|
206
|
+
srv.close()
|
|
207
|
+
print(f"[portforward] bind {listen} 失败({e}),{RETRY_INTERVAL}s 后重试", flush=True)
|
|
208
|
+
time.sleep(RETRY_INTERVAL)
|
|
209
|
+
|
|
210
|
+
print(f"[portforward] {listen[0]}:{listen[1]} → {target[0]}:{target[1]} 已就绪"
|
|
211
|
+
f"(listen-host={args.listen_host})", flush=True)
|
|
212
|
+
while True:
|
|
213
|
+
try:
|
|
214
|
+
conn, addr = srv.accept()
|
|
215
|
+
except OSError as e:
|
|
216
|
+
print(f"[portforward] accept 异常: {e},2s 后继续", flush=True)
|
|
217
|
+
time.sleep(2)
|
|
218
|
+
continue
|
|
219
|
+
threading.Thread(target=handle, args=(conn, target), daemon=True).start()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == '__main__':
|
|
223
|
+
main()
|