@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sponsor 插件契约面(issue #235,M6 备产;B2B principal/sponsor 插件化与同内核端到端复用证明)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠ 非运行时激活:本模块是纯函数契约层——不 spawn 子进程、不发起网络请求、不读取任何凭据、
|
|
5
|
+
* 不接真实供应商/交易调用;运行激活默认关闭(SPONSOR_RUNTIME_ACTIVATION_DEFAULT=false,
|
|
6
|
+
* 未来真实运行时接线必须挂显式 opt-in 门,形态同 #318 GOTRY_HBCLI_LIVE,且在 #137 M6 Entry
|
|
7
|
+
* 之后)。一切行为由 fixture 合成数据证明(sponsor-reuse-tests,run-all §61)。
|
|
8
|
+
*
|
|
9
|
+
* 定位(milestones/m6-b2b-reuse-walkthrough.md,P6 draft;issue #137 总控):
|
|
10
|
+
* - 三主体分离:traveler principal(动机主体)/sponsor(商业与库存主体)/BFF principal
|
|
11
|
+
* (鉴权主体,ADR-23)三个词位互不混用——sponsor 配置里三者缺一即拒,sponsorId
|
|
12
|
+
* 冒充 traveler principal 结构性拒绝;
|
|
13
|
+
* - sponsor 只包三个插件槽:入口(路由授权)、库存池(候选)、配置/披露(佣金+偏置声明),
|
|
14
|
+
* 不写 MotivationProfile,不改旅行者约束;
|
|
15
|
+
* - 同内核复用证明面:本模块只 import 内核函数(src/booking-saga.ts 的
|
|
16
|
+
* booking_saga_fsm.v1 状态机 + src/wish-pool.ts 的条件评分),零拷贝零重声明;
|
|
17
|
+
* B2C 与 B2B 两种形态由同一 runner(runBookingScenario)以"是否携带 sponsor 插件"
|
|
18
|
+
* 参数化,saga 边序列逐格相同——内核 diff 为零即本件证明目标
|
|
19
|
+
* (PR 内核文件 diff 必须为空,内核漂移即本套件红);
|
|
20
|
+
* - 隔离:C 端默认路径不 import 本模块(sponsor-reuse-tests 4c 机械闸),
|
|
21
|
+
* 无插件参数即 B2C 形态,trace 经 sponsorLeakFindings 检查零 sponsor 字段。
|
|
22
|
+
*
|
|
23
|
+
* 红线(结构性编码,全部 fixture 证明):
|
|
24
|
+
* - 排序不能以赞助收益伪造用户效用:候选必须先过内核条件评分(全命中强制),
|
|
25
|
+
* 佣金不可赎回未命中的条件;同分 tie-break 只按 sponsor 申报顺位,
|
|
26
|
+
* 且赞助受益位必须显式标记 sponsorTieBreakApplied 并携偏置声明;
|
|
27
|
+
* - sponsor 佣金/收益明确披露并进入确认指纹:disclosureDigestSha256 参与
|
|
28
|
+
* buildConfirmationFingerprint——披露口径变化则确认指纹变化;
|
|
29
|
+
* - 相同 WriteGate 与未知态/取消退款规则:确认必携外部回执(空 receipt 两种形态同拒,
|
|
30
|
+
* 无 B2B 豁免);取消退款走同一内核边表(propose→confirm→compensate),回放零违例;
|
|
31
|
+
* - 路由授权:供应商 route 必须在 sponsor 声明的授权闭集内(selector)+ session/order
|
|
32
|
+
* 绑定——禁止复制 Buyer/credentials 规避(本模块全链无凭据字段)。
|
|
33
|
+
*/
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
35
|
+
import { resolveSagaTrigger, sagaTraceViolations, } from '../src/booking-saga.js';
|
|
36
|
+
import { scoreWishMatch } from '../src/wish-pool.js';
|
|
37
|
+
export const SPONSOR_PLUGIN_SCHEMA = 'sponsor.plugin.v1';
|
|
38
|
+
/** 运行激活默认态:关闭。真实运行时激活属 M6 Entry(#137)后的显式 opt-in 面。 */
|
|
39
|
+
export const SPONSOR_RUNTIME_ACTIVATION_DEFAULT = false;
|
|
40
|
+
function sha256Canon(value) {
|
|
41
|
+
const canon = (v) => {
|
|
42
|
+
if (Array.isArray(v))
|
|
43
|
+
return v.map(canon);
|
|
44
|
+
if (v && typeof v === 'object') {
|
|
45
|
+
return Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : 1)).map(([k, x]) => [k, canon(x)]));
|
|
46
|
+
}
|
|
47
|
+
return v;
|
|
48
|
+
};
|
|
49
|
+
return createHash('sha256').update(JSON.stringify(canon(value))).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
function requireNonEmpty(field, value) {
|
|
52
|
+
const v = typeof value === 'string' ? value.trim() : '';
|
|
53
|
+
if (!v)
|
|
54
|
+
throw new Error(`sponsor-plugin: 缺必填字段 ${field}(fail-closed;三主体与披露口径缺一即拒)`);
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 构造 sponsor 插件(fail-closed):三主体标识、授权路由闭集、披露口径缺一即拒;
|
|
59
|
+
* sponsorId 与 travelerPrincipalId 混同(三主体边界破坏)即拒。
|
|
60
|
+
*/
|
|
61
|
+
export function createSponsorPlugin(config) {
|
|
62
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
63
|
+
throw new Error('sponsor-plugin: 配置必须是对象(fail-closed)');
|
|
64
|
+
}
|
|
65
|
+
const sponsorId = requireNonEmpty('sponsorId', config.sponsorId);
|
|
66
|
+
const travelerPrincipalId = requireNonEmpty('travelerPrincipalId', config.travelerPrincipalId);
|
|
67
|
+
const bffPrincipalId = requireNonEmpty('bffPrincipalId', config.bffPrincipalId);
|
|
68
|
+
if (sponsorId === travelerPrincipalId) {
|
|
69
|
+
throw new Error('sponsor-plugin: sponsorId 不得与 travelerPrincipalId 相同(traveler/sponsor/BFF 三主体分离,walkthrough §1)');
|
|
70
|
+
}
|
|
71
|
+
const routes = config.allowedRoutes;
|
|
72
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
73
|
+
throw new Error('sponsor-plugin: allowedRoutes 必须是非空数组(授权路由闭集;空闭集 = 不可提出任何候选)');
|
|
74
|
+
}
|
|
75
|
+
const cleaned = routes.map((r) => requireNonEmpty('allowedRoutes[]', r));
|
|
76
|
+
if (new Set(cleaned).size !== cleaned.length) {
|
|
77
|
+
throw new Error('sponsor-plugin: allowedRoutes 存在重复(授权闭集必须无歧义)');
|
|
78
|
+
}
|
|
79
|
+
const d = (config.disclosure ?? {});
|
|
80
|
+
const statement = requireNonEmpty('disclosure.statement', d.statement);
|
|
81
|
+
const rankingBiasNotice = requireNonEmpty('disclosure.rankingBiasNotice', d.rankingBiasNotice);
|
|
82
|
+
if (typeof d.commissionBps !== 'number' || !Number.isInteger(d.commissionBps) || d.commissionBps < 0) {
|
|
83
|
+
throw new Error('sponsor-plugin: disclosure.commissionBps 必须是非负整数基点(佣金必须显式申报)');
|
|
84
|
+
}
|
|
85
|
+
const frozen = Object.freeze({
|
|
86
|
+
sponsorId,
|
|
87
|
+
travelerPrincipalId,
|
|
88
|
+
bffPrincipalId,
|
|
89
|
+
allowedRoutes: Object.freeze(cleaned),
|
|
90
|
+
disclosure: Object.freeze({ commissionBps: d.commissionBps, statement, rankingBiasNotice }),
|
|
91
|
+
});
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
schemaVersion: SPONSOR_PLUGIN_SCHEMA,
|
|
94
|
+
config: frozen,
|
|
95
|
+
disclosureDigestSha256: sha256Canon({ sponsorId, ...frozen.disclosure }),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 运行激活门(默认关闭):除显式 true 外一律抛错。
|
|
100
|
+
* 本件(#235)是结构性证明,不是运行时激活——真实交易路径在 #137 M6 Entry 之后。
|
|
101
|
+
*/
|
|
102
|
+
export function requireRuntimeActivation(enabled) {
|
|
103
|
+
if (enabled !== true) {
|
|
104
|
+
throw new Error('sponsor-plugin: 运行激活默认关闭(SPONSOR_RUNTIME_ACTIVATION_DEFAULT=false);真实运行时接线须显式 opt-in 且在 #137 M6 Entry 之后');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 供应商路由授权:路由必须在 sponsor 声明的授权闭集内(selector),且必须携带
|
|
109
|
+
* session 绑定(order 可选)——缺绑定即拒,禁止以复制 Buyer/credentials 方式规避
|
|
110
|
+
* (本模块全链无凭据字段;授权只认 principal 标识 + 会话/订单引用)。
|
|
111
|
+
*/
|
|
112
|
+
export function authorizeSponsorRoute(plugin, route, binding) {
|
|
113
|
+
if (!plugin.config.allowedRoutes.includes(route)) {
|
|
114
|
+
return { ok: false, reason: 'route-unauthorized', detail: `路由 ${route} 不在 sponsor ${plugin.config.sponsorId} 的授权闭集内(越权路由拒绝)` };
|
|
115
|
+
}
|
|
116
|
+
const sessionRef = typeof binding.sessionRef === 'string' ? binding.sessionRef.trim() : '';
|
|
117
|
+
if (!sessionRef) {
|
|
118
|
+
return { ok: false, reason: 'missing-session-binding', detail: '授权路由缺 session/order 绑定(禁止复制 Buyer/credentials 规避)' };
|
|
119
|
+
}
|
|
120
|
+
const orderRef = typeof binding.orderRef === 'string' && binding.orderRef.trim() ? binding.orderRef.trim() : null;
|
|
121
|
+
return { ok: true, route, sessionRef, orderRef };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 内核条件评分包装(两形态共用):候选映射为 wish 条目后调用内核 scoreWishMatch,
|
|
125
|
+
* 并强制「申报条件全命中才可预订」——部分命中不算(sponsor 不可放宽旅行者约束,
|
|
126
|
+
* wish conditions 强制红线)。内核不满足 → null。
|
|
127
|
+
*/
|
|
128
|
+
export function scoreCandidateAgainstWish(candidate, wish) {
|
|
129
|
+
const c = candidate.conditions;
|
|
130
|
+
const declared = [typeof c.days === 'number', typeof c.budget_cny === 'number', Array.isArray(c.best_months)].filter(Boolean).length;
|
|
131
|
+
const match = scoreWishMatch({ wish_id: candidate.candidateRef, name: candidate.name, conditions: c }, wish);
|
|
132
|
+
if (!match || match.hits.length < declared)
|
|
133
|
+
return null;
|
|
134
|
+
return { candidate, hits: match.hits };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* sponsor 库存排序:①路由越权 → 排除(不进入评分);②内核条件评分非全命中 → 排除
|
|
138
|
+
* (佣金不可赎回——排序不能以赞助收益伪造用户效用);③排序键 = 内核评分降序,
|
|
139
|
+
* 同分 tie 按 sponsor 申报顺位(入参顺序;佣金数值从不进入排序键),
|
|
140
|
+
* tie 中申报顺位受益位(其佣金高于组内最低)显式标记 sponsorTieBreakApplied。
|
|
141
|
+
*/
|
|
142
|
+
export function rankSponsorCandidates(plugin, candidates, wish) {
|
|
143
|
+
const ranked = [];
|
|
144
|
+
const excluded = [];
|
|
145
|
+
const scored = [];
|
|
146
|
+
for (const cand of candidates) {
|
|
147
|
+
if (!plugin.config.allowedRoutes.includes(cand.route)) {
|
|
148
|
+
excluded.push({ candidateRef: cand.candidateRef, why: 'route-unauthorized' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const match = scoreCandidateAgainstWish(cand, wish);
|
|
152
|
+
if (!match) {
|
|
153
|
+
excluded.push({ candidateRef: cand.candidateRef, why: 'conditions-not-met' });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
scored.push({ cand, hits: match.hits });
|
|
157
|
+
}
|
|
158
|
+
// 内核评分降序;同分保持申报顺位(稳定排序)——申报顺位即 sponsor 声明的偏好序
|
|
159
|
+
const ordered = [...scored].sort((a, b) => b.hits.length - a.hits.length);
|
|
160
|
+
// 逐同分组判定 tie 受益位
|
|
161
|
+
let i = 0;
|
|
162
|
+
while (i < ordered.length) {
|
|
163
|
+
let j = i;
|
|
164
|
+
while (j + 1 < ordered.length && ordered[j + 1].hits.length === ordered[i].hits.length)
|
|
165
|
+
j++;
|
|
166
|
+
const group = ordered.slice(i, j + 1);
|
|
167
|
+
const minCommission = Math.min(...group.map((g) => g.cand.commissionBps ?? 0));
|
|
168
|
+
for (const g of group) {
|
|
169
|
+
ranked.push({
|
|
170
|
+
candidate: g.cand,
|
|
171
|
+
kernelScore: g.hits.length,
|
|
172
|
+
kernelHits: g.hits,
|
|
173
|
+
disclosure: {
|
|
174
|
+
sponsorId: plugin.config.sponsorId,
|
|
175
|
+
statement: plugin.config.disclosure.statement,
|
|
176
|
+
rankingBiasNotice: plugin.config.disclosure.rankingBiasNotice,
|
|
177
|
+
commissionBps: plugin.config.disclosure.commissionBps,
|
|
178
|
+
disclosureDigestSha256: plugin.disclosureDigestSha256,
|
|
179
|
+
},
|
|
180
|
+
sponsorTieBreakApplied: group.length > 1 && (g.cand.commissionBps ?? 0) > minCommission,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
i = j + 1;
|
|
184
|
+
}
|
|
185
|
+
return { ranked, excluded, biasNotice: plugin.config.disclosure.rankingBiasNotice };
|
|
186
|
+
}
|
|
187
|
+
/** 确认指纹:规范 JSON(键排序)后 sha256;披露摘要参与,披露口径变化 → 指纹变化。 */
|
|
188
|
+
export function buildConfirmationFingerprint(parts) {
|
|
189
|
+
return sha256Canon({
|
|
190
|
+
schema: SPONSOR_PLUGIN_SCHEMA,
|
|
191
|
+
businessId: parts.businessId,
|
|
192
|
+
travelerPrincipalId: parts.travelerPrincipalId,
|
|
193
|
+
sponsorId: parts.sponsorId,
|
|
194
|
+
candidateRef: parts.candidateRef,
|
|
195
|
+
route: parts.route,
|
|
196
|
+
priceCny: parts.priceCny,
|
|
197
|
+
disclosureDigestSha256: parts.disclosureDigestSha256,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* approval 绑定:必携非空外部回执(L3 具名确认红线,与内核 write.confirmed 空 receipt
|
|
202
|
+
* 违例同源;B2B 无豁免)。
|
|
203
|
+
*/
|
|
204
|
+
export function bindApproval(input) {
|
|
205
|
+
const receipt = typeof input.receipt === 'string' ? input.receipt.trim() : '';
|
|
206
|
+
if (!receipt)
|
|
207
|
+
throw new Error('sponsor-plugin: 确认必携外部回执(空 receipt 红线;B2B 与 C 端同一 WriteGate 规则)');
|
|
208
|
+
if (!input.fingerprintSha256)
|
|
209
|
+
throw new Error('sponsor-plugin: approval 必须绑定确认指纹');
|
|
210
|
+
return { receipt, fingerprintSha256: input.fingerprintSha256, travelerPrincipalId: input.travelerPrincipalId, sponsorId: input.sponsorId };
|
|
211
|
+
}
|
|
212
|
+
/** approval 绑定校验:指纹不一致(含跨 sponsor/跨租户重放)一律失败。 */
|
|
213
|
+
export function verifyApprovalBinding(expectedFingerprintSha256, approval) {
|
|
214
|
+
return approval.fingerprintSha256 === expectedFingerprintSha256;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 端到端预订场景(fixture 合成数据;纯函数,零真实调用):
|
|
218
|
+
* 同一段代码驱动 B2C 与 B2B——内核调用(resolveSagaTrigger/scoreWishMatch/
|
|
219
|
+
* sagaTraceViolations)在两种形态下逐字相同,差异只在 sponsor 插件槽
|
|
220
|
+
* (路由授权/披露标注)。任何一步不合法即 fail-closed 抛错,不静默降级。
|
|
221
|
+
*/
|
|
222
|
+
export function runBookingScenario(input) {
|
|
223
|
+
const businessId = requireNonEmpty('businessId', input.businessId);
|
|
224
|
+
const travelerPrincipalId = requireNonEmpty('travelerPrincipalId', input.travelerPrincipalId);
|
|
225
|
+
const plugin = input.plugin ?? null;
|
|
226
|
+
const form = plugin ? 'b2b-sponsor' : 'b2c';
|
|
227
|
+
const scope = {
|
|
228
|
+
businessId,
|
|
229
|
+
travelerPrincipalId,
|
|
230
|
+
sponsorId: plugin ? plugin.config.sponsorId : null,
|
|
231
|
+
key: `${businessId}|${travelerPrincipalId}|${plugin ? plugin.config.sponsorId : 'c-side'}`,
|
|
232
|
+
};
|
|
233
|
+
const receipt = typeof input.receipt === 'string' ? input.receipt.trim() : '';
|
|
234
|
+
if (!receipt)
|
|
235
|
+
throw new Error('sponsor-plugin: 确认必携外部回执(空 receipt 红线;B2B 与 C 端同一 WriteGate 规则)');
|
|
236
|
+
let sessionRef = null;
|
|
237
|
+
if (plugin) {
|
|
238
|
+
sessionRef = typeof input.sessionRef === 'string' ? input.sessionRef.trim() : '';
|
|
239
|
+
if (!sessionRef)
|
|
240
|
+
throw new Error('sponsor-plugin: B2B 场景缺 session/order 绑定(路由授权前置条件)');
|
|
241
|
+
}
|
|
242
|
+
// 库存/规划:同一内核评分包装,两形态共用;B2B 附加路由授权闭集过滤
|
|
243
|
+
const eligible = [];
|
|
244
|
+
const events = [];
|
|
245
|
+
let seq = 0;
|
|
246
|
+
const push = (e) => {
|
|
247
|
+
events.push({ ...e, seq: seq++, scope });
|
|
248
|
+
};
|
|
249
|
+
for (const cand of input.candidates) {
|
|
250
|
+
if (plugin) {
|
|
251
|
+
const auth = authorizeSponsorRoute(plugin, cand.route, { sessionRef, orderRef: null });
|
|
252
|
+
if (!auth.ok) {
|
|
253
|
+
throw new Error(`sponsor-plugin: 端到端 ${auth.reason}(${auth.detail};candidate=${cand.candidateRef})`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const match = scoreCandidateAgainstWish(cand, input.wish);
|
|
257
|
+
if (match)
|
|
258
|
+
eligible.push({ cand, hits: match.hits });
|
|
259
|
+
}
|
|
260
|
+
if (eligible.length === 0) {
|
|
261
|
+
throw new Error('sponsor-plugin: 无可预订候选(条件全命中强制;赞助权重不可伪造用户效用)');
|
|
262
|
+
}
|
|
263
|
+
eligible.sort((a, b) => b.hits.length - a.hits.length);
|
|
264
|
+
const top = eligible[0];
|
|
265
|
+
const fingerprint = buildConfirmationFingerprint({
|
|
266
|
+
businessId,
|
|
267
|
+
travelerPrincipalId,
|
|
268
|
+
sponsorId: scope.sponsorId,
|
|
269
|
+
candidateRef: top.cand.candidateRef,
|
|
270
|
+
route: top.cand.route,
|
|
271
|
+
priceCny: top.cand.priceCny,
|
|
272
|
+
disclosureDigestSha256: plugin ? plugin.disclosureDigestSha256 : null,
|
|
273
|
+
});
|
|
274
|
+
if (plugin) {
|
|
275
|
+
push({
|
|
276
|
+
kind: 'route.authorized', sagaEvent: null, from: null, to: null, guard: null,
|
|
277
|
+
detail: `route=${top.cand.route} selector=sponsor-allowed-routes session=${sessionRef} sponsor=${plugin.config.sponsorId}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
push({
|
|
281
|
+
kind: 'candidate.ranked', sagaEvent: null, from: null, to: null, guard: null,
|
|
282
|
+
fingerprintSha256: fingerprint,
|
|
283
|
+
detail: `candidate=${top.cand.candidateRef} score=${top.hits.length} hits=[${top.hits.join(',')}]`
|
|
284
|
+
+ (plugin ? ` bias=${plugin.config.disclosure.rankingBiasNotice}` : ' bias=null'),
|
|
285
|
+
});
|
|
286
|
+
// 确认闸:唯一状态推进词汇 = 内核 booking_saga_fsm.v1 边表(两形态同一张表)
|
|
287
|
+
let stage = 'none';
|
|
288
|
+
/** propose 必先执行(否则 fire 内即抛);settled 后为终态 */
|
|
289
|
+
let settled = null;
|
|
290
|
+
const approval = bindApproval({ fingerprintSha256: fingerprint, receipt, travelerPrincipalId, sponsorId: scope.sponsorId });
|
|
291
|
+
const fire = (trigger, extra = {}) => {
|
|
292
|
+
const verdict = resolveSagaTrigger(stage, trigger);
|
|
293
|
+
if (!verdict.ok)
|
|
294
|
+
throw new Error(`sponsor-plugin: saga 拒绝(${stage} --${trigger}--> ${verdict.reason};内核边表唯一归宿)`);
|
|
295
|
+
stage = verdict.to;
|
|
296
|
+
settled = verdict.to;
|
|
297
|
+
push({
|
|
298
|
+
kind: verdict.event, sagaEvent: verdict.event, from: verdict.from, to: verdict.to, guard: verdict.guard,
|
|
299
|
+
...(trigger === 'confirm' ? { receipt: approval.receipt } : {}),
|
|
300
|
+
...extra,
|
|
301
|
+
detail: `business=${businessId} candidate=${top.cand.candidateRef} route=${top.cand.route} priceCny=${top.cand.priceCny} fp=${fingerprint.slice(0, 12)}`,
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
fire('propose', { fingerprintSha256: fingerprint });
|
|
305
|
+
fire('confirm', { fingerprintSha256: fingerprint });
|
|
306
|
+
if (input.action === 'cancel')
|
|
307
|
+
fire('compensate');
|
|
308
|
+
// 审计/回放:内核校验器对 write.* 事件链逐段校验(必携回执/吸收态/拒绝闭包)
|
|
309
|
+
const audit = events
|
|
310
|
+
.filter((e) => e.sagaEvent !== null)
|
|
311
|
+
.map((e) => ({ seq: e.seq, kind: e.sagaEvent, receipt: e.receipt ?? null }));
|
|
312
|
+
const violations = sagaTraceViolations(audit);
|
|
313
|
+
if (settled === null)
|
|
314
|
+
throw new Error('sponsor-plugin: saga 未推进(不可达:propose 必先执行)');
|
|
315
|
+
return { form, scope, selectedCandidateRef: top.cand.candidateRef, sagaStatus: settled, events, sagaViolations: violations };
|
|
316
|
+
}
|
|
317
|
+
// ── 隔离检查器(C 端 trace 零 sponsor 渗入) ─────────────────────────────────
|
|
318
|
+
/**
|
|
319
|
+
* sponsor 字段泄漏检查:递归扫描 trace,凡键名含 sponsor 且值非空(或 scope.sponsorId
|
|
320
|
+
* 非 null)即记一条发现。C 端默认形态 trace 必须零发现;B2B trace 必有发现
|
|
321
|
+
* (双向断言保证检查器自身有效)。
|
|
322
|
+
*/
|
|
323
|
+
export function sponsorLeakFindings(trace) {
|
|
324
|
+
const findings = [];
|
|
325
|
+
const walk = (node, path) => {
|
|
326
|
+
if (Array.isArray(node)) {
|
|
327
|
+
node.forEach((v, i) => walk(v, `${path}[${i}]`));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (node && typeof node === 'object') {
|
|
331
|
+
for (const [k, v] of Object.entries(node)) {
|
|
332
|
+
const empty = v === null || v === undefined || v === '';
|
|
333
|
+
if (/sponsor/i.test(k) && !empty)
|
|
334
|
+
findings.push(`${path}.${k}`);
|
|
335
|
+
walk(v, `${path}.${k}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
walk(trace, 'trace');
|
|
340
|
+
return findings;
|
|
341
|
+
}
|
|
@@ -1,126 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 政策事实生产端 v1(issue #141,D-26 收口切片)——C 档数据源:中国领事服务网
|
|
3
|
+
* (cs.mfa.gov.cn,外交部一手口径,免费权威)的「了解目的地」国家指南树。
|
|
4
|
+
*
|
|
5
|
+
* 形态:VISA_POLICY_FETCH 效应(effect 注册表行,礼貌抓取:UA 标识 + 超时 +
|
|
6
|
+
* 永不重试 + 断路器护站)→ 国家页 HTML → 「签证入境」章节文本 → PolicyFact[]
|
|
7
|
+
* (as_of + review_by + cs.mfa.gov.cn 来源证据链)。
|
|
8
|
+
*
|
|
9
|
+
* 纪律(ADR-19 + 抓取红线):
|
|
10
|
+
* - 永不抛错(永不抛错能力层契约);传输失败 → {ok:false, via:'visa-policy-error'},
|
|
11
|
+
* **不落负事实**(无结论不是证据);
|
|
12
|
+
* - 抓取失败/页面缺失 → 结构化降级,不猜不填;
|
|
13
|
+
* - 政策事实恒带 review_by 复核 gate(默认 D-30);
|
|
14
|
+
* - 礼貌抓取:单站点串行 + 调用方控制频率;站点不可达不重试(策略表 null)。
|
|
15
|
+
*
|
|
16
|
+
* 升级路径:D-31 后可切 B 档 Sherpa° 商业源(effect 注册表行可插拔替换)。
|
|
17
|
+
*
|
|
18
|
+
* @module capabilities/visa-policy
|
|
19
|
+
*/
|
|
1
20
|
import { readFileSync } from 'node:fs';
|
|
2
21
|
import { BOOKABLE_FACT_SCHEMA, makeFactId } from '../src/bookable-facts.js';
|
|
3
22
|
const SOURCE_HOST = 'https://cs.mfa.gov.cn';
|
|
4
23
|
const SOURCE_ID = 'cs-mfa';
|
|
5
24
|
const USER_AGENT = 'GoTry/0.0.1 (+https://github.com/Danceiny/gotry; polite policy reader)';
|
|
6
25
|
const CONTINENTS = [
|
|
7
|
-
'yz_645708',
|
|
8
|
-
'feizhou_645714',
|
|
9
|
-
'ouzhou_645711',
|
|
10
|
-
'bz_645718',
|
|
11
|
-
'dyz_645717'
|
|
26
|
+
'yz_645708', // 亚洲
|
|
27
|
+
'feizhou_645714', // 非洲
|
|
28
|
+
'ouzhou_645711', // 欧洲
|
|
29
|
+
'bz_645718', // 美洲(北/南)
|
|
30
|
+
'dyz_645717', // 大洋洲
|
|
12
31
|
];
|
|
32
|
+
/** 国家路径 → 政策页 URL(抓取纪律:仅此两种 URL 形态,不拼接任意输入) */
|
|
13
33
|
export function countryPageUrl(continent, country) {
|
|
14
|
-
if (!/^[a-z0-9_]+$/.test(continent) || !/^[a-z0-9_]+$/.test(country))
|
|
34
|
+
if (!/^[a-z0-9_]+$/.test(continent) || !/^[a-z0-9_]+$/.test(country))
|
|
35
|
+
return null;
|
|
15
36
|
return `${SOURCE_HOST}/zggmcg/ljmdd/${continent}/${country}/`;
|
|
16
37
|
}
|
|
38
|
+
/** 剥 HTML 标签 → 纯文本(保守:不执行任何内嵌脚本/样式) */
|
|
17
39
|
function stripTags(html) {
|
|
18
|
-
return html
|
|
40
|
+
return html
|
|
41
|
+
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ')
|
|
42
|
+
.replace(/<[^>]+>/g, ' ')
|
|
43
|
+
.replace(/ /g, ' ')
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/\s+/g, ' ');
|
|
19
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* 从国家页文本抽取「签证入境」章节(到下一个同级标题为止)。
|
|
49
|
+
* 页面为 MFA 编辑器产出的散文(如中泰互免协定生效日期/停留期限条款),逐段保留原句。
|
|
50
|
+
*/
|
|
20
51
|
export function extractVisaSection(pageText) {
|
|
21
52
|
const text = stripTags(pageText);
|
|
22
53
|
const start = text.indexOf('签证入境');
|
|
23
|
-
if (start < 0)
|
|
54
|
+
if (start < 0)
|
|
55
|
+
return null;
|
|
24
56
|
const rest = text.slice(start + '签证入境'.length);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'领事保护',
|
|
32
|
-
'国家安全',
|
|
33
|
-
'特别提醒'
|
|
34
|
-
].map((kw)=>rest.indexOf(kw)).filter((i)=>i > 0);
|
|
35
|
-
const numHeaders = Array.from(rest.matchAll(/[一二三四五六七八九十]+、(?!免签|落地签|签证|入境规定)/g)).map((m)=>m.index).filter((i)=>i > 0);
|
|
36
|
-
const allEnds = [
|
|
37
|
-
...sectionEnds,
|
|
38
|
-
...numHeaders
|
|
39
|
-
].sort((a, b)=>a - b);
|
|
57
|
+
// 下一大节标题探测:中文数字编号(非签证词)+已知下一节名(不用正文高频词做边界)
|
|
58
|
+
const sectionEnds = ['海关检查', '海关申报', '医疗条件', '交通出行', '实用信息', '领事保护', '国家安全', '特别提醒']
|
|
59
|
+
.map(kw => rest.indexOf(kw))
|
|
60
|
+
.filter(i => i > 0);
|
|
61
|
+
const numHeaders = Array.from(rest.matchAll(/[一二三四五六七八九十]+、(?!免签|落地签|签证|入境规定)/g)).map(m => m.index).filter(i => i > 0);
|
|
62
|
+
const allEnds = [...sectionEnds, ...numHeaders].sort((a, b) => a - b);
|
|
40
63
|
const end = allEnds.length > 0 ? allEnds[0] : Math.min(rest.length, 4_000);
|
|
41
64
|
const section = rest.slice(0, end).trim();
|
|
42
65
|
return section.length > 20 ? section : null;
|
|
43
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 章节 → 政策陈述条目:按中文序号段落切分(一、二、…),每段一条 PolicyFact 原料。
|
|
69
|
+
* 免签/落地签/签证要求各成一条,供闸侧逐条回溯。
|
|
70
|
+
*/
|
|
44
71
|
export function splitPolicyEntries(section) {
|
|
45
|
-
const parts = section.split(/(?:[一二三四五六七八九十]+、|[((]\s*[一二三四五六七八九十]+\s*[))])\s*/).map(
|
|
46
|
-
return parts.filter(
|
|
72
|
+
const parts = section.split(/(?:[一二三四五六七八九十]+、|[((]\s*[一二三四五六七八九十]+\s*[))])\s*/).map(p => p.trim());
|
|
73
|
+
return parts.filter(p => p.length > 30);
|
|
47
74
|
}
|
|
48
75
|
export function policyFactsFromMfa(input) {
|
|
49
76
|
const entries = splitPolicyEntries(input.section);
|
|
50
77
|
const asOf = input.fetchedAt.slice(0, 10);
|
|
51
78
|
const queryId = `visa-policy:${SOURCE_ID}:${input.countryPath}:${asOf}`;
|
|
52
|
-
return entries.map((entry, i)=>({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
query_id: queryId,
|
|
65
|
-
fetched_at: input.fetchedAt,
|
|
66
|
-
as_of: asOf,
|
|
67
|
-
review_by: defaultReviewBy(input.fetchedAt)
|
|
68
|
-
}));
|
|
79
|
+
return entries.map((entry, i) => ({
|
|
80
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
81
|
+
fact_id: makeFactId([input.countryPath, String(i), entry.slice(0, 64), queryId]),
|
|
82
|
+
kind: 'policy',
|
|
83
|
+
subject: `${input.countryLabel} 签证入境(条款 ${i + 1})`,
|
|
84
|
+
statement: entry,
|
|
85
|
+
source: `${SOURCE_ID}@${input.fetchedAt}`,
|
|
86
|
+
query_id: queryId,
|
|
87
|
+
fetched_at: input.fetchedAt,
|
|
88
|
+
as_of: asOf,
|
|
89
|
+
review_by: defaultReviewBy(input.fetchedAt),
|
|
90
|
+
}));
|
|
69
91
|
}
|
|
92
|
+
/** 政策复核 gate:抓取日 +30 天(D-30 族) */
|
|
70
93
|
export function defaultReviewBy(fetchedAt) {
|
|
71
94
|
const d = new Date(fetchedAt);
|
|
72
95
|
d.setDate(d.getDate() + 30);
|
|
73
96
|
return d.toISOString().slice(0, 10);
|
|
74
97
|
}
|
|
98
|
+
/** 从洲列表页 HTML 提取国家路径段(相对形态 ./tg_647570/ → tg_647570;仅收本站链接) */
|
|
75
99
|
export function listCountryPaths(continentListHtml, continent) {
|
|
76
100
|
const hits = continentListHtml.matchAll(new RegExp(`href="(\\./([a-z0-9_]+)/)"[^>]*>([^<]{2,20})<`, 'g'));
|
|
77
101
|
const out = [];
|
|
78
|
-
for (const m of hits){
|
|
79
|
-
if (!m[2] || !m[3])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
label: m[3].trim()
|
|
83
|
-
});
|
|
102
|
+
for (const m of hits) {
|
|
103
|
+
if (!m[2] || !m[3])
|
|
104
|
+
continue;
|
|
105
|
+
out.push({ path: m[2], label: m[3].trim() });
|
|
84
106
|
}
|
|
85
|
-
return out.map(
|
|
107
|
+
return out.map(x => x.path).filter(p => /^[a-z0-9_]+$/.test(p));
|
|
86
108
|
}
|
|
109
|
+
/** 洲路径段 → 洲列表页 URL(形态受控,不拼接任意输入) */
|
|
87
110
|
export function continentListUrl(continent) {
|
|
88
|
-
if (!/^[a-z0-9_]+$/.test(continent))
|
|
111
|
+
if (!/^[a-z0-9_]+$/.test(continent))
|
|
112
|
+
return null;
|
|
89
113
|
return `${SOURCE_HOST}/zggmcg/ljmdd/${continent}/`;
|
|
90
114
|
}
|
|
115
|
+
/** 从本地 fixture 读页面(测试确定性;生产走 fetchVisaPolicyPage) */
|
|
91
116
|
export function readPageFixture(path) {
|
|
92
117
|
return readFileSync(path, 'utf-8');
|
|
93
118
|
}
|
|
119
|
+
/** 礼貌抓取国家页(永不抛错;失败 = 结构化降级,不落负事实) */
|
|
94
120
|
export async function fetchVisaPolicyPage(url, timeoutMs = 20_000) {
|
|
95
121
|
const controller = new AbortController();
|
|
96
|
-
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
123
|
try {
|
|
98
124
|
const res = await fetch(url, {
|
|
99
|
-
headers: {
|
|
100
|
-
'user-agent': USER_AGENT,
|
|
101
|
-
'accept-language': 'zh-CN,zh'
|
|
102
|
-
},
|
|
125
|
+
headers: { 'user-agent': USER_AGENT, 'accept-language': 'zh-CN,zh' },
|
|
103
126
|
signal: controller.signal,
|
|
104
|
-
redirect: 'follow'
|
|
127
|
+
redirect: 'follow',
|
|
105
128
|
});
|
|
106
|
-
if (!res.ok)
|
|
107
|
-
ok: false,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
ok: true,
|
|
112
|
-
html: await res.text()
|
|
113
|
-
};
|
|
114
|
-
} catch (e) {
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
131
|
+
return { ok: true, html: await res.text() };
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
115
134
|
const msg = controller.signal.aborted ? `timeout after ${timeoutMs}ms` : String(e.message ?? e);
|
|
116
|
-
return {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
120
|
-
} finally{
|
|
135
|
+
return { ok: false, error: msg };
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
121
138
|
clearTimeout(timer);
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//# sourceURL=ts/capabilities/visa-policy.ts
|