@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20

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.
Files changed (176) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +309 -30
  4. package/bin/gotry-inner.js +74 -23
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +22 -8
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/anything.js +62 -29
  11. package/dist/capabilities/channel-health.js +103 -0
  12. package/dist/capabilities/channel-registry.js +213 -0
  13. package/dist/capabilities/doctor.js +307 -0
  14. package/dist/capabilities/effect.js +178 -5
  15. package/dist/capabilities/flyai.js +13 -0
  16. package/dist/capabilities/hbcli.js +28 -2
  17. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  18. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  19. package/dist/capabilities/session/extension-bridge.js +14 -1
  20. package/dist/capabilities/session/extension-channel.js +4 -2
  21. package/dist/capabilities/session/extension-distribution.js +1 -1
  22. package/dist/capabilities/session-search.js +311 -0
  23. package/dist/capabilities/visa-policy.js +126 -0
  24. package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
  25. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  26. package/dist/scripts/anything-tests.js +70 -21
  27. package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
  28. package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
  29. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  30. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  31. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  32. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  33. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  34. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  35. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  36. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  37. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  38. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  39. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  40. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  41. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  42. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  43. package/dist/scripts/bootstrap-tests.js +113 -2
  44. package/dist/scripts/build-metrics-report.js +339 -0
  45. package/dist/scripts/channel-probe-tests.js +148 -0
  46. package/dist/scripts/channel-probe.js +252 -0
  47. package/dist/scripts/channel-registry-tests.js +262 -0
  48. package/dist/scripts/doctor-tests.js +175 -0
  49. package/dist/scripts/effect-tests.js +274 -1
  50. package/dist/scripts/extension-tests.js +129 -11
  51. package/dist/scripts/fact-gate-tests.js +91 -2
  52. package/dist/scripts/flyai-tests.js +17 -1
  53. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  54. package/dist/scripts/hbcli-tests.js +38 -1
  55. package/dist/scripts/metrics-report-tests.js +288 -0
  56. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  57. package/dist/scripts/session-tests.js +401 -1
  58. package/dist/scripts/smoke.js +100 -83
  59. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  60. package/dist/scripts/turn-handoff-collect.js +189 -0
  61. package/dist/scripts/turn-policy-tests.js +56 -0
  62. package/dist/scripts/typed-contract-canary.js +261 -0
  63. package/dist/scripts/visa-policy-tests.js +75 -0
  64. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  65. package/dist/src/artifact-gate.js +91 -3
  66. package/dist/src/benchmark-agent-conformance.js +39 -16
  67. package/dist/src/benchmark-environment-bridge.js +316 -52
  68. package/dist/src/bookable-facts.js +62 -3
  69. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  70. package/dist/src/booking-surface/canonical-schema.js +8 -22
  71. package/dist/src/booking-surface/contracts.js +70 -19
  72. package/dist/src/booking-surface/dsh-planner.js +310 -177
  73. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  74. package/dist/src/booking-surface/index.js +1 -5
  75. package/dist/src/booking-surface/profile.js +1 -1
  76. package/dist/src/booking-surface/runtime.js +1682 -247
  77. package/dist/src/booking-surface/server.js +371 -185
  78. package/dist/src/booking-surface/startup.js +17 -27
  79. package/dist/src/booking-surface/validation.js +270 -760
  80. package/dist/src/index.js +739 -150
  81. package/dist/src/turn-deadline.js +292 -0
  82. package/dist/src/turn-policy.js +146 -0
  83. package/dist/src/wish-pool.js +5 -0
  84. package/extension/background.js +23 -4
  85. package/extension/content-main.js +47 -9
  86. package/extension/manifest.json +27 -9
  87. package/package.json +7 -26
  88. package/schemas/booking.surface.schema.json +2593 -0
  89. package/ts/capabilities/agent-reach.ts +4 -4
  90. package/ts/capabilities/anything.ts +89 -38
  91. package/ts/capabilities/flyai.ts +21 -3
  92. package/ts/capabilities/hbcli.ts +53 -4
  93. package/ts/capabilities/session/action-cache.ts +1 -1
  94. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  95. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  96. package/ts/capabilities/session/extension-bridge.ts +32 -2
  97. package/ts/capabilities/session/extension-channel.ts +9 -8
  98. package/ts/capabilities/session/extension-distribution.ts +3 -1
  99. package/ts/capabilities/session/health-watch.ts +1 -1
  100. package/ts/capabilities/session/read-guard.ts +1 -1
  101. package/ts/capabilities/session/wizard.ts +1 -1
  102. package/ts/capabilities/session-search.ts +323 -1
  103. package/ts/dsh-runtime/vendor/README.md +60 -0
  104. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  105. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  106. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  107. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  108. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  109. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  110. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  111. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  112. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  113. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  114. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  115. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  116. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  117. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  118. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  119. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  120. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  121. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  122. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  123. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  124. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  125. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  126. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  127. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  128. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  129. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  130. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  131. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  132. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  133. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  134. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  135. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  136. package/ts/package.json +9 -0
  137. package/ts/scripts/turn-handoff-collect.ts +178 -0
  138. package/ts/src/artifact-gate.ts +82 -2
  139. package/ts/src/benchmark-agent-conformance.ts +54 -15
  140. package/ts/src/benchmark-environment-bridge.ts +234 -46
  141. package/ts/src/bookable-facts.ts +109 -5
  142. package/ts/src/booking-saga.ts +1 -1
  143. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  144. package/ts/src/booking-surface/canonical-schema.js +8 -22
  145. package/ts/src/booking-surface/contracts.ts +278 -239
  146. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  147. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  148. package/ts/src/booking-surface/index.ts +1 -5
  149. package/ts/src/booking-surface/profile.ts +11 -11
  150. package/ts/src/booking-surface/runtime.ts +1351 -351
  151. package/ts/src/booking-surface/server.ts +282 -175
  152. package/ts/src/booking-surface/startup.ts +36 -48
  153. package/ts/src/booking-surface/validation.ts +194 -442
  154. package/ts/src/contracts.ts +1 -1
  155. package/ts/src/index.ts +492 -172
  156. package/ts/src/loop.ts +1 -1
  157. package/ts/src/state-ledger.ts +1 -1
  158. package/ts/src/turn-deadline.ts +365 -0
  159. package/ts/src/turn-policy.ts +154 -0
  160. package/ts/src/wish-pool.ts +17 -4
  161. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  162. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  163. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  164. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  165. package/dist/src/booking-surface/contracts-v2.js +0 -89
  166. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  167. package/dist/src/booking-surface/server-v2.js +0 -334
  168. package/dist/src/booking-surface/validation-v2.js +0 -319
  169. package/dist/src/tool-budget.js +0 -136
  170. package/schemas/booking.surface.v1.schema.json +0 -927
  171. package/schemas/booking.surface.v2.schema.json +0 -61
  172. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  173. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  174. package/ts/src/booking-surface/server-v2.ts +0 -247
  175. package/ts/src/booking-surface/validation-v2.ts +0 -205
  176. package/ts/src/tool-budget.ts +0 -165
@@ -0,0 +1,293 @@
1
+ /**
2
+ * 12306 火车站点适配器(2026-09-03,继 ctrip-hotel 后会话面第三通道):
3
+ * - entry:https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=<城市名,电报码>&ts=<...>&date=YYYY-MM-DD&flag=N,N,Y
4
+ * (电报码表官方站表全量校准 2026-09-03,129 城;曾纠出南宁 NIZ→NNZ 错码)
5
+ * - networkHints:kyfw leftTicket/query(负载均衡变体 queryG/queryZ/queryA 等全部命中)
6
+ * - 解析:leftTicket/query 返回 data.result[] 管道行(12306 经典 | 拼接格式)。
7
+ * **第一方校准(2026-09-03)**:字段索引与站名映射(data.map[电报码])逐一
8
+ * 取自官方前端 queryLeftTicket_end_js.js 的 cN(result,map) 转换函数——旧版
9
+ * 公开常识索引与官方现行映射不符(站名/座位桶全错),真会话前即纠;行级
10
+ * 签名校验(车次码/时刻形态)继续兜住未来漂移:失配整行跳过,fail-visible
11
+ *
12
+ * 与机/酒通道的两点刻意差异:
13
+ * - 无登录闸:12306 余票查询是公开面(登录只关系下单),无账号数据过手;
14
+ * 扩展照样只读被动嗅探(零写行为),证据链标注「公开查询面」
15
+ * - 列表接口不含票价:如实只呈现 车次/时刻/历时/余票,票价以 12306 落地页为准
16
+ */
17
+
18
+ /** 城市电报码表(**官方口径校准 2026-09-03**:全部条目逐一核对自
19
+ * kyfw.12306.cn/otn/resources/js/framework/station_name.js 官方全量站表
20
+ * ——曾借此抓出南宁 NIZ→NNZ 错码;代表站规则=城市同名站,fs/ts 用城市码
21
+ * 服务端扩到市内全部站。词表外 unresolved 逐字保留,调用方可显式传电报码
22
+ * 覆盖——kyfw 查询页 URL 里的 fs=城市,XXX 三位码;核对快照见
23
+ * data/stations-12306-verify.json) */
24
+ export const STATION_TELECODES: Record<string, string> = {
25
+ "三亚": 'SEQ',
26
+ "上海": 'SHH',
27
+ "上饶": 'SRG',
28
+ "东莞": 'RTQ',
29
+ "中卫": 'ZWJ',
30
+ "中山": 'ZSQ',
31
+ "丽江": 'LHM',
32
+ "乌鲁木齐": 'WAR',
33
+ "乐山": 'IVW',
34
+ "九江": 'JJG',
35
+ "伊宁": 'YMR',
36
+ "佛山": 'FSQ',
37
+ "保山": 'BAM',
38
+ "六盘水": 'UMW',
39
+ "兰州": 'LZJ',
40
+ "包头": 'BTC',
41
+ "北京": 'BJP',
42
+ "北海": 'BHZ',
43
+ "十堰": 'SNN',
44
+ "南京": 'NJH',
45
+ "南充": 'NCW',
46
+ "南宁": 'NNZ',
47
+ "南昌": 'NCG',
48
+ "南通": 'NUH',
49
+ "南阳": 'NFF',
50
+ "厦门": 'XMS',
51
+ "台州": 'TEU',
52
+ "合肥": 'HFH',
53
+ "吉林": 'JLL',
54
+ "吐鲁番": 'TFR',
55
+ "呼和浩特": 'HHC',
56
+ "咸阳": 'XYY',
57
+ "哈密": 'HMR',
58
+ "哈尔滨": 'HBB',
59
+ "喀什": 'KSR',
60
+ "嘉兴": 'JXH',
61
+ "嘉峪关": 'JGJ',
62
+ "大同": 'DTV',
63
+ "大庆": 'DZX',
64
+ "大理": 'DKM',
65
+ "大连": 'DLT',
66
+ "天水": 'TSJ',
67
+ "天津": 'TJP',
68
+ "太原": 'TYV',
69
+ "宁波": 'NGH',
70
+ "安顺": 'ASW',
71
+ "宜宾": 'YKE',
72
+ "宜昌": 'YCN',
73
+ "宝鸡": 'BJY',
74
+ "岳阳": 'YYQ',
75
+ "常州": 'CZH',
76
+ "常德": 'VGQ',
77
+ "广州": 'GZQ',
78
+ "库尔勒": 'KLR',
79
+ "延安": 'YWY',
80
+ "开封": 'KFF',
81
+ "张掖": 'ZYJ',
82
+ "徐州": 'XCH',
83
+ "德阳": 'DYW',
84
+ "惠州": 'HCQ',
85
+ "成都": 'CDW',
86
+ "扬州": 'YLH',
87
+ "承德": 'CDP',
88
+ "抚顺": 'FET',
89
+ "拉萨": 'LSO',
90
+ "揭阳": 'JYA',
91
+ "新乡": 'XXF',
92
+ "无锡": 'WGH',
93
+ "日喀则": 'RKO',
94
+ "昆明": 'KMM',
95
+ "普洱": 'PEM',
96
+ "景德镇": 'JCG',
97
+ "曲靖": 'QJM',
98
+ "杭州": 'HZH',
99
+ "林芝": 'LZO',
100
+ "柳州": 'LZZ',
101
+ "株洲": 'ZZQ',
102
+ "格尔木": 'GRO',
103
+ "桂林": 'GLZ',
104
+ "梧州": 'WZZ',
105
+ "武汉": 'WHN',
106
+ "汉中": 'HOY',
107
+ "汕头": 'OTQ',
108
+ "沈阳": 'SYT',
109
+ "泉州": 'QYS',
110
+ "泸州": 'LUE',
111
+ "洛阳": 'LYF',
112
+ "济南": 'JNK',
113
+ "深圳": 'SZQ',
114
+ "温州": 'RZH',
115
+ "湛江": 'ZJZ',
116
+ "漳州": 'ZUS',
117
+ "潮州": 'CKQ',
118
+ "牡丹江": 'MDB',
119
+ "珠海": 'ZHQ',
120
+ "盐城": 'AFH',
121
+ "石家庄": 'SJP',
122
+ "福州": 'FZS',
123
+ "秦皇岛": 'QTP',
124
+ "绍兴": 'SOH',
125
+ "绵阳": 'MYW',
126
+ "芜湖": 'WHH',
127
+ "苏州": 'SZH',
128
+ "茂名": 'MDQ',
129
+ "莆田": 'PTS',
130
+ "衡阳": 'HYQ',
131
+ "襄阳": 'XFN',
132
+ "西宁": 'XNO',
133
+ "西安": 'XAY',
134
+ "西昌": 'ECW',
135
+ "贵阳": 'GIW',
136
+ "赣州": 'GZG',
137
+ "达州": 'RXW',
138
+ "遵义": 'ZYE',
139
+ "郑州": 'ZZF',
140
+ "重庆": 'CUW',
141
+ "金华": 'JBH',
142
+ "银川": 'YIJ',
143
+ "镇江": 'ZJH',
144
+ "长春": 'CCT',
145
+ "长沙": 'CSQ',
146
+ "阜阳": 'FYH',
147
+ "青岛": 'QDK',
148
+ "鞍山": 'AST',
149
+ "韶关": 'SNQ',
150
+ "香格里拉": 'EUM',
151
+ "香港": 'XJA',
152
+ "齐齐哈尔": 'QHX',
153
+ "龙岩": 'LYS',
154
+ }
155
+
156
+ export interface TrainEntryQuery {
157
+ from: string
158
+ to: string
159
+ /** YYYY-MM-DD */
160
+ date: string
161
+ /** 显式城市电报码(覆盖码表;kyfw 查询页 URL fs=城市,XXX 的三位码) */
162
+ fromStationTelecode?: string
163
+ toStationTelecode?: string
164
+ }
165
+
166
+ export interface AdapterEntry {
167
+ ok: boolean
168
+ url?: string
169
+ unresolved?: string[]
170
+ }
171
+
172
+ export function buildTrainEntryUrl(q: TrainEntryQuery): AdapterEntry {
173
+ const unresolved: string[] = []
174
+ const fromTc = (q.fromStationTelecode ?? '').trim().toUpperCase() || STATION_TELECODES[q.from.trim()]
175
+ const toTc = (q.toStationTelecode ?? '').trim().toUpperCase() || STATION_TELECODES[q.to.trim()]
176
+ if (!fromTc) unresolved.push(q.from)
177
+ if (!toTc) unresolved.push(q.to)
178
+ if (unresolved.length > 0 || !/^\d{4}-\d{2}-\d{2}$/.test(q.date)) {
179
+ return { ok: false, unresolved: unresolved.length > 0 ? unresolved : [q.date] }
180
+ }
181
+ const fs = encodeURIComponent(`${q.from.trim()},${fromTc}`)
182
+ const ts = encodeURIComponent(`${q.to.trim()},${toTc}`)
183
+ return { ok: true, url: `https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=${fs}&ts=${ts}&date=${q.date}&flag=N,N,Y` }
184
+ }
185
+
186
+ /** networkHints:余票查询 XHR(queryG/Z/A/U 等负载均衡变体全命中) */
187
+ export const TRAIN_NETWORK_HINTS = [/leftTicket\/query/i]
188
+
189
+ /** 12306 页域(content_scripts 注入面与 background 白名单的对账源) */
190
+ export const TRAIN_SITE_HOST = 'kyfw.12306.cn'
191
+
192
+ export interface SessionTrainOption {
193
+ /** 车次(G1375 / K79 等) */
194
+ trainCode: string
195
+ fromStation: string
196
+ toStation: string
197
+ /** 出发/到达时刻(HH:MM;跨日由到达时刻本身表达,官方 DTO 无隔日字段) */
198
+ depTime: string
199
+ arrTime: string
200
+ /** 历时(分钟) */
201
+ durationMin: number
202
+ /** Y=可预订 / N=不可 / 其他上游原话 */
203
+ canWebBuy: string
204
+ /** 余票分桶(第一方校准索引,见 SEAT_BUCKETS;值原样:数字 / 有 / 无 / --) */
205
+ seats: Record<string, string>
206
+ /** 查询落地页(由人选车完成预订;gotry 不碰) */
207
+ jumpUrl?: string
208
+ }
209
+
210
+ /** 座位桶索引表(**第一方校准 2026-09-03**:逐字段取自 12306 官方前端
211
+ * queryLeftTicket_end_js.js 的 cN(result,map) 转换函数——旧版公开常识索引
212
+ * (19-28 段)与官方现行映射不符,首查前即纠);label=页面席别口径,未知席别
213
+ * 保留上游字段 id 原样 */
214
+ const SEAT_BUCKETS: Array<{ index: number; field: string; label: string }> = [
215
+ { index: 20, field: 'gg_num', label: '其他(通勤)' },
216
+ { index: 21, field: 'gr_num', label: '高级软卧' },
217
+ { index: 22, field: 'qt_num', label: '其他' },
218
+ { index: 23, field: 'rw_num', label: '软卧(一等卧)' },
219
+ { index: 24, field: 'rz_num', label: '软座' },
220
+ { index: 25, field: 'tz_num', label: '特等座' },
221
+ { index: 26, field: 'wz_num', label: '无座' },
222
+ { index: 27, field: 'yb_num', label: 'yp_b(上游席别)' },
223
+ { index: 28, field: 'yw_num', label: '硬卧(二等卧)' },
224
+ { index: 29, field: 'yz_num', label: '硬座' },
225
+ { index: 30, field: 'ze_num', label: '二等座' },
226
+ { index: 31, field: 'zy_num', label: '一等座' },
227
+ { index: 32, field: 'swz_num', label: '商务座' },
228
+ { index: 33, field: 'srrb_num', label: '动卧' },
229
+ ]
230
+
231
+ const TRAIN_CODE_RE = /^[GDCZTKYLSF]\d{1,5}[A-Z]?$/i
232
+ const HHMM_RE = /^\d{2}:\d{2}$/
233
+
234
+ /** 历时 "07:52" → 分钟 */
235
+ function lishiToMin(s: string | undefined): number {
236
+ const m = /^(\d{1,3}):(\d{2})$/.exec((s ?? '').trim())
237
+ if (!m) return 0
238
+ return Number(m[1]) * 60 + Number(m[2])
239
+ }
240
+
241
+ /** 行级签名:车次码形态 + 时刻形态不符即整行跳过(索引漂移 fail-visible,不造数) */
242
+ function rowToOption(row: string, stationMap: Record<string, string> | undefined, entryUrl: string): SessionTrainOption | null {
243
+ const f = row.split('|')
244
+ if (f.length < 34) return null
245
+ const trainCode = (f[3] ?? '').trim()
246
+ const depTime = (f[8] ?? '').trim()
247
+ const arrTime = (f[9] ?? '').trim()
248
+ if (!TRAIN_CODE_RE.test(trainCode) || !HHMM_RE.test(depTime) || !HHMM_RE.test(arrTime)) return null
249
+ // 官方 cN 口径:站名 = data.map[电报码](map 缺失/缺键时退电报码原样,不猜名)
250
+ const fromStation = stationMap?.[f[6] ?? ''] ?? (f[6] ?? '')
251
+ const toStation = stationMap?.[f[7] ?? ''] ?? (f[7] ?? '')
252
+ const seats: Record<string, string> = {}
253
+ for (const b of SEAT_BUCKETS) {
254
+ const v = (f[b.index] ?? '').trim()
255
+ if (v && v !== '--') seats[b.label] = v
256
+ }
257
+ return {
258
+ trainCode,
259
+ fromStation,
260
+ toStation,
261
+ depTime,
262
+ arrTime,
263
+ durationMin: lishiToMin(f[10]),
264
+ canWebBuy: (f[11] ?? '').trim(),
265
+ seats,
266
+ jumpUrl: entryUrl,
267
+ }
268
+ }
269
+
270
+ /** 解析 leftTicket/query 响应(纯函数,fixture 测试锚点;字段索引与站名映射
271
+ * 全部对齐 12306 官方前端 cN(result,map)——见 SEAT_BUCKETS 注释);
272
+ * malformed 一律返空,不抛错 */
273
+ export function parseLeftTicketQuery(body: string, entryUrl: string, opts: { maxItems?: number } = {}): SessionTrainOption[] {
274
+ let raw: { data?: { result?: unknown; map?: Record<string, string> } }
275
+ try {
276
+ raw = JSON.parse(body) as typeof raw
277
+ } catch {
278
+ return []
279
+ }
280
+ const rows = raw.data?.result
281
+ if (!Array.isArray(rows)) return []
282
+ const map = (raw.data?.map && typeof raw.data.map === 'object') ? raw.data.map : undefined
283
+ const out: SessionTrainOption[] = []
284
+ for (const r of rows) {
285
+ if (typeof r !== 'string') continue
286
+ const opt = rowToOption(r, map, entryUrl)
287
+ if (opt) {
288
+ out.push(opt)
289
+ if (out.length >= (opts.maxItems ?? 30)) break
290
+ }
291
+ }
292
+ return out
293
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 会话扩展桥(extension-bridge,RFC user-session-data-rfc.md §2.2 通道 C——2026-08-29 founder 定案为 PRIMARY 传输)。
2
+ * 会话扩展桥(extension-bridge,RFC docs/rfc/user-session-data-rfc.md §2.2 通道 C——2026-08-29 founder 定案为 PRIMARY 传输)。
3
3
  *
4
4
  * 形态:Node http 服务,懒启动单例,只绑 127.0.0.1,端口池 8791-8795(与 extension/manifest.json
5
5
  * host_permissions 一一对应,防漂移测试守住);**零新依赖**(node:http 手写,对齐已退役 shell 薄壳
@@ -22,6 +22,9 @@
22
22
  */
23
23
 
24
24
  import { randomUUID } from 'node:crypto'
25
+ import { existsSync } from 'node:fs'
26
+ import { homedir } from 'node:os'
27
+ import { join } from 'node:path'
25
28
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
26
29
 
27
30
  /** 桥端口池(extension/manifest.json host_permissions 必须同面;§38 断言) */
@@ -41,6 +44,32 @@ export const EXTENSION_ORIGIN_STORE = `chrome-extension://${EXTENSION_ID_STORE}`
41
44
  export const EXTENSION_STORE_URL = `https://chromewebstore.google.com/detail/gotry-session-bridge/${EXTENSION_ID_STORE}`
42
45
  /** 桥信任的扩展 Origin 白名单:unpacked(固定 key)+ 商店版,双通道同一扩展同源信任 */
43
46
  export const EXTENSION_ORIGINS: readonly string[] = [EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE]
47
+
48
+ /**
49
+ * 本地 unpacked 通道是否已落位(~/.gotry/extension/manifest.json)。
50
+ * 商店版没有本地文件面(装在 Chrome 里),离线不可探测——本函数只回答「本地通道在不在」,
51
+ * 商店版用户在这里恒 false,这正是 D-24 自适应文案的判定输入。
52
+ */
53
+ export function localExtensionInstalled(home = homedir()): boolean {
54
+ return existsSync(join(home, '.gotry', 'extension', 'manifest.json'))
55
+ }
56
+
57
+ /**
58
+ * needs-extension 自适应摘要(issue #117,D-24 残量):
59
+ * - 本地 unpacked 已落位 → 完整双通道指引(商店 + GitHub/开发者模式本地通道);
60
+ * - 未落位(商店版用户,或完全未装)→ 只推商店一键装,并给「已装商店版?打开 Chrome 即可」
61
+ * 提示——**不再出现开发者模式/本地通道文案**(已装商店版的用户不该再被指导 load unpacked)。
62
+ * 纯函数(默认 HOME 可注入)。
63
+ */
64
+ export function needsExtensionSummary(opts: { localInstalled?: boolean; home?: string } = {}): string {
65
+ const local = opts.localInstalled ?? localExtensionInstalled(opts.home)
66
+ if (local) {
67
+ return 'GoTry Session Bridge 扩展未连接(未安装或已停用)。推荐 Chrome 应用商店一键装(自动更新) '
68
+ + `${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`
69
+ }
70
+ return 'GoTry Session Bridge 扩展未连接。若你已从 Chrome 商店安装(自动更新):打开 Chrome 并确认扩展已启用即可自动连接,无需再装。'
71
+ + `若尚未安装:应用商店一键装 ${EXTENSION_STORE_URL} (装完零弹窗,浏览器自己当安装器)`
72
+ }
44
73
  /** 扩展在线判定:/health 心跳 ≤30s 一次 + 轮询重连,1.5 倍容差 */
45
74
  export const EXTENSION_CONNECTED_WINDOW_MS = 45_000
46
75
  /** /jobs 长轮询 hold 上限(必须 < MV3 SW 30s 存活窗口) */
@@ -292,7 +321,8 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
292
321
  settle({
293
322
  ok: false,
294
323
  reason: 'extension-not-connected',
295
- summary: `GoTry Session Bridge 扩展未连接(未安装或已停用)。一次性安装(装完零弹窗):推荐 Chrome 应用商店一键装(自动更新) ${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`,
324
+ // D-24 自适应文案(issue #117):按本地通道是否落位决定是否提及开发者模式
325
+ summary: needsExtensionSummary(),
296
326
  })
297
327
  }
298
328
  }, 250)
@@ -10,15 +10,16 @@
10
10
  * 检索页无回包不是车道失败:与 CDP 车道同语义(到点无 hints 命中 ⇒ miss,页标题命挑战 ⇒ challenged)。
11
11
  */
12
12
 
13
- import { getOrCreateSessionBridge, EXTENSION_STORE_URL, type SessionJobHandle } from './extension-bridge.ts'
13
+ import { getOrCreateSessionBridge, needsExtensionSummary, type SessionJobHandle } from './extension-bridge.ts'
14
14
 
15
- /** 一次性用户门文案(needs-extension 统一指引,与 bootstrap setupExtension 输出同口径) */
16
- export const NEEDS_EXTENSION_HINT =
17
- '需要一次性安装 GoTry Session Bridge 浏览器扩展(装完零弹窗):'
18
- + `推荐 Chrome 应用商店一键安装(自动更新) ${EXTENSION_STORE_URL} ;`
19
- + '免审核、版本更新更快的 GitHub 通道:npx gotry setup --extension-from=github(自动下载落位 ~/.gotry/extension;'
20
- + '手动下载 github.com/Danceiny/gotry/releases 标签 ext-* 的 gotry-session-bridge.tar.gz),'
21
- + '再在 chrome://extensions 开发者模式「加载已解压的扩展程序」指向解压目录'
15
+ /**
16
+ * needs-extension 用户门文案(D-24 自适应,issue #117):按本地通道是否落位自动跳过
17
+ * 开发者模式/本地通道指引——商店版用户只看到商店一键装与「已装即可」提示。
18
+ * 旧静态常量已退役:静态文案无法感知安装状态,正是 D-24 残量的病灶。
19
+ */
20
+ export function needsExtensionHint(): string {
21
+ return needsExtensionSummary()
22
+ }
22
23
 
23
24
  export type BridgeFailureKind = 'bridge-unavailable' | 'extension-not-connected' | 'timeout' | 'job-error'
24
25
 
@@ -104,7 +104,9 @@ export function parseDistManifest(raw: string): ExtensionDistManifest {
104
104
  const tarballSha256 = obj.tarballSha256
105
105
  const zip = obj.zip
106
106
  const zipSha256 = obj.zipSha256
107
- if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error('dist-manifest.version 缺失或非 x.y.z')
107
+ // Chrome manifest version 形态:1-4 段点分整数(扩展版本跟随 gotry 主版本
108
+ // 0.0.1-rc.N → 四段投影 0.0.1.N,展示名走 version_name 原样)
109
+ if (typeof version !== 'string' || !/^\d+(\.\d+){1,3}$/.test(version)) throw new Error('dist-manifest.version 缺失或非 1-4 段点分整数')
108
110
  if (tarball !== DIST_ASSET_TARBALL) throw new Error(`dist-manifest.tarball 资产名漂移(期望 ${DIST_ASSET_TARBALL})`)
109
111
  if (zip !== DIST_ASSET_STORE_ZIP) throw new Error(`dist-manifest.zip 资产名漂移(期望 ${DIST_ASSET_STORE_ZIP})`)
110
112
  if (typeof tarballSha256 !== 'string' || !SHA256_RE.test(tarballSha256)) throw new Error('dist-manifest.tarballSha256 非 64-hex')
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 扩展健康检查 + 自动重放(issue #21 onboarding UX,P3.6 gotry-session-onboarding-goal,
3
- * RFC user-session-data-rfc.md §3.3)。
3
+ * RFC docs/rfc/user-session-data-rfc.md §3.3)。
4
4
  *
5
5
  * 动机:首次 `sessionFlightSearch` 遇 `needs-extension` 时,旧版要求用户装完扩展后**自己重跑命令**;
6
6
  * 本模块把「用户手工重跑」改成「后台有界轮询 + 扩展一就位自动重放同一 query_id」——
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ReadGuard:会话检索面的只读物理隔离(RFC user-session-data-rfc.md §3.3)。
2
+ * ReadGuard:会话检索面的只读物理隔离(RFC docs/rfc/user-session-data-rfc.md §3.3)。
3
3
  *
4
4
  * 写不是「被禁止的行为」,是「不存在的原语」——WriteGate(L0-L4)的检索态前置:
5
5
  * ① 网络层:route 拦截——命中硬支付模式(任意方法)或写方法+写URL模式 → abort + 审计落盘;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 会话扩展 onboarding wizard(RFC user-session-data-rfc.md §3.3,ADR-21 上架后重设)。
2
+ * 会话扩展 onboarding wizard(RFC docs/rfc/user-session-data-rfc.md §3.3,ADR-21 上架后重设)。
3
3
  *
4
4
  * 单一职责(2026-09-02 重设):**只做 Node 端两件事**——
5
5
  * ① 扩展文件从源落位到 `~/.gotry/extension`(幂等)