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

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 (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  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 +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -0,0 +1,292 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { classifyTurn, TURN_HANDOFF_ETA_LABEL, turnPolicyFor } from './turn-policy.js';
5
+ export const TURN_DEADLINE_SOFT = 'TURN_DEADLINE_SOFT';
6
+ export const TURN_DEADLINE_EXHAUSTED = 'TURN_DEADLINE_EXHAUSTED';
7
+ export const TURN_DEADLINE_HANDOFF = 'TURN_DEADLINE_HANDOFF';
8
+ export const TURN_HANDOFF_SCHEMA = 'gotry_turn_handoff.v1';
9
+ export async function writeTurnHandoffTicket(stateRoot, userMessage) {
10
+ const ticket = {
11
+ schema: TURN_HANDOFF_SCHEMA,
12
+ id: `th-${Date.now().toString(36)}-${randomUUID().slice(0, 6)}`,
13
+ objective: userMessage.length > 120 ? `${userMessage.slice(0, 120)}…` : userMessage,
14
+ userMessage,
15
+ requestedAt: new Date().toISOString(),
16
+ etaLabel: TURN_HANDOFF_ETA_LABEL,
17
+ status: 'open'
18
+ };
19
+ await writeTicketJson(stateRoot, ticket);
20
+ return ticket;
21
+ }
22
+ function handoffDir(stateRoot) {
23
+ const root = stateRoot === '.' ? process.cwd() : stateRoot;
24
+ return join(root, 'gotry-state', 'turn-handoffs');
25
+ }
26
+ async function writeTicketJson(stateRoot, ticket) {
27
+ const dir = handoffDir(stateRoot);
28
+ await mkdir(dir, {
29
+ recursive: true
30
+ });
31
+ const path = join(dir, `${ticket.id}.json`);
32
+ const tmp = `${path}.tmp`;
33
+ await writeFile(tmp, JSON.stringify(ticket, null, 2), 'utf-8');
34
+ await rename(tmp, path);
35
+ return path;
36
+ }
37
+ export async function settleTurnHandoffTicket(stateRoot, ticket, status, deliverable, error) {
38
+ const dir = handoffDir(stateRoot);
39
+ await mkdir(dir, {
40
+ recursive: true
41
+ });
42
+ const deliverablePath = join(dir, `${ticket.id}.deliverable.md`);
43
+ const deliverableTmp = `${deliverablePath}.tmp`;
44
+ await writeFile(deliverableTmp, deliverable, 'utf-8');
45
+ await rename(deliverableTmp, deliverablePath);
46
+ const settled = {
47
+ ...ticket,
48
+ status,
49
+ settledAt: new Date().toISOString(),
50
+ deliverableFile: `${ticket.id}.deliverable.md`,
51
+ ...error ? {
52
+ error
53
+ } : {}
54
+ };
55
+ const ticketPath = await writeTicketJson(stateRoot, settled);
56
+ return {
57
+ ticketPath,
58
+ deliverablePath
59
+ };
60
+ }
61
+ export async function listTurnHandoffTickets(stateRoot) {
62
+ const dir = handoffDir(stateRoot);
63
+ let names;
64
+ try {
65
+ names = await readdir(dir);
66
+ } catch {
67
+ return [];
68
+ }
69
+ const views = [];
70
+ for (const name of names){
71
+ if (!name.endsWith('.json')) continue;
72
+ try {
73
+ const raw = JSON.parse(await readFile(join(dir, name), 'utf-8'));
74
+ if (raw?.schema !== TURN_HANDOFF_SCHEMA) continue;
75
+ const view = {
76
+ id: raw.id,
77
+ status: raw.status,
78
+ objective: raw.objective,
79
+ requestedAt: raw.requestedAt,
80
+ etaLabel: raw.etaLabel,
81
+ ...raw.settledAt ? {
82
+ settledAt: raw.settledAt
83
+ } : {},
84
+ ...raw.error ? {
85
+ error: raw.error
86
+ } : {}
87
+ };
88
+ if (raw.status !== 'open' && raw.deliverableFile) {
89
+ try {
90
+ const text = await readFile(join(dir, raw.deliverableFile), 'utf-8');
91
+ view.deliverableExcerpt = text.length > 600 ? `${text.slice(0, 600)}…` : text;
92
+ } catch {}
93
+ }
94
+ views.push(view);
95
+ } catch {}
96
+ }
97
+ views.sort((a, b)=>a.requestedAt < b.requestedAt ? 1 : -1);
98
+ return views;
99
+ }
100
+ function resolveEnvPin(name) {
101
+ const raw = process.env[name];
102
+ if (!raw) return null;
103
+ const parsed = Number.parseInt(raw, 10);
104
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
105
+ }
106
+ function convergenceContext(code, text) {
107
+ return {
108
+ id: randomUUID(),
109
+ role: 'user',
110
+ content: [
111
+ {
112
+ type: 'text',
113
+ text: `${code}: ${text}`
114
+ }
115
+ ],
116
+ source: {
117
+ kind: 'plugin',
118
+ plugin: 'gotry-turn-deadline'
119
+ }
120
+ };
121
+ }
122
+ function exhaustionResult(reason) {
123
+ return {
124
+ isError: true,
125
+ error: {
126
+ message: reason,
127
+ info: {
128
+ name: 'TurnDeadlineError',
129
+ code: TURN_DEADLINE_EXHAUSTED
130
+ }
131
+ },
132
+ content: [
133
+ {
134
+ type: 'text',
135
+ text: reason
136
+ }
137
+ ],
138
+ additionalContexts: [
139
+ convergenceContext(TURN_DEADLINE_EXHAUSTED, 'No more tool dispatches are permitted in this turn. Produce the final answer from existing results.')
140
+ ]
141
+ };
142
+ }
143
+ function handoffResult(ticket) {
144
+ const instruction = `Turn handed off to background planning. Async ticket ${ticket.id} created (ETA ${ticket.etaLabel}). Stop dispatching tools and tell the user in one short message: the deep planning continues in the background, the expected time, and to check back later (mention ticket ${ticket.id}).`;
145
+ return {
146
+ isError: true,
147
+ error: {
148
+ message: `${TURN_DEADLINE_HANDOFF}: sync window exhausted for a deep-planning turn; ticket ${ticket.id}`,
149
+ info: {
150
+ name: 'TurnDeadlineHandoff',
151
+ code: TURN_DEADLINE_HANDOFF
152
+ }
153
+ },
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: `${TURN_DEADLINE_HANDOFF}: ${instruction}`
158
+ }
159
+ ],
160
+ additionalContexts: [
161
+ convergenceContext(TURN_DEADLINE_HANDOFF, instruction)
162
+ ]
163
+ };
164
+ }
165
+ export function installTurnDeadline(ctx, options = {}) {
166
+ if (options.enabled === false) return;
167
+ if (typeof ctx.on !== 'function') return;
168
+ const now = options.now ?? (()=>Date.now());
169
+ const softPin = resolveEnvPin('GOTRY_TURN_DEADLINE_SOFT_MS');
170
+ const hardPin = resolveEnvPin('GOTRY_TURN_DEADLINE_HARD_MS');
171
+ const defaultPolicy = options.fixedPolicy ?? turnPolicyFor('sync-planning');
172
+ const routePolicy = (message)=>{
173
+ if (options.fixedPolicy) return {
174
+ ...options.fixedPolicy
175
+ };
176
+ const policy = turnPolicyFor(classifyTurn(message));
177
+ return {
178
+ ...policy,
179
+ softMs: softPin ?? policy.softMs,
180
+ hardMs: hardPin ?? policy.hardMs
181
+ };
182
+ };
183
+ const stateByAgent = new Map();
184
+ const softExecutions = new Map();
185
+ const finalOnlyDisposers = new Map();
186
+ const clearAgent = (agentId)=>{
187
+ stateByAgent.delete(agentId);
188
+ for (const [token, owner] of softExecutions){
189
+ if (owner.agentId === agentId) softExecutions.delete(token);
190
+ }
191
+ finalOnlyDisposers.get(agentId)?.();
192
+ finalOnlyDisposers.delete(agentId);
193
+ };
194
+ const enterFinalOnly = (agent)=>{
195
+ const agentId = String(agent.id);
196
+ if (finalOnlyDisposers.has(agentId)) return;
197
+ const globalTools = ctx.tools;
198
+ const scopedTools = agent.ctx?.tools;
199
+ if (typeof globalTools.schemas !== 'function' || typeof scopedTools?.restrict !== 'function') {
200
+ return;
201
+ }
202
+ const inheritedNames = globalTools.schemas().map((schema)=>schema.name).filter((name)=>name !== 'run_code');
203
+ if (inheritedNames.length === 0) return;
204
+ finalOnlyDisposers.set(agentId, scopedTools.restrict({
205
+ deny: inheritedNames
206
+ }));
207
+ };
208
+ const keyOfAgent = (agent)=>{
209
+ const session = agent.session;
210
+ return String(session?.id ?? agent.id);
211
+ };
212
+ ctx.on('session/event', (subject, event)=>{
213
+ const agentId = String(subject.id);
214
+ if (event.type === 'turn/end') {
215
+ clearAgent(agentId);
216
+ return;
217
+ }
218
+ if (event.type === 'turn/start') {
219
+ finalOnlyDisposers.get(agentId)?.();
220
+ finalOnlyDisposers.delete(agentId);
221
+ if (!stateByAgent.has(agentId)) {
222
+ stateByAgent.set(agentId, {
223
+ startedAt: now(),
224
+ policy: {
225
+ ...defaultPolicy,
226
+ softMs: softPin ?? defaultPolicy.softMs,
227
+ hardMs: hardPin ?? defaultPolicy.hardMs
228
+ }
229
+ });
230
+ }
231
+ return;
232
+ }
233
+ if (event.type === 'user/message') {
234
+ const message = event.data;
235
+ if (message?.source?.kind !== 'user') return;
236
+ const text = (message.content ?? []).find((block)=>block.type === 'text')?.text ?? '';
237
+ if (!text.trim()) return;
238
+ stateByAgent.set(agentId, {
239
+ startedAt: now(),
240
+ policy: routePolicy(text),
241
+ userMessage: text
242
+ });
243
+ }
244
+ });
245
+ ctx.on('session/disposed', (subject)=>clearAgent(String(subject.id)));
246
+ ctx.on('tools/execute', (exec, next)=>{
247
+ if (!exec.agent) return next();
248
+ const agentId = keyOfAgent(exec.agent);
249
+ const state = stateByAgent.get(agentId) ?? {
250
+ startedAt: now(),
251
+ policy: {
252
+ ...defaultPolicy,
253
+ softMs: softPin ?? defaultPolicy.softMs,
254
+ hardMs: hardPin ?? defaultPolicy.hardMs
255
+ }
256
+ };
257
+ stateByAgent.set(agentId, state);
258
+ const elapsed = now() - state.startedAt;
259
+ if (elapsed > state.policy.hardMs) {
260
+ enterFinalOnly(exec.agent);
261
+ if (state.policy.exit === 'handoff' && state.userMessage && options.stateRoot) {
262
+ return writeTurnHandoffTicket(options.stateRoot, state.userMessage).then((ticket)=>handoffResult(ticket)).catch(()=>exhaustionResult(`${TURN_DEADLINE_EXHAUSTED}: hard deadline ${state.policy.hardMs}ms reached (ticket persistence failed; degraded to converge)`));
263
+ }
264
+ return Promise.resolve(exhaustionResult(`${TURN_DEADLINE_EXHAUSTED}: turn exceeded hard deadline of ${state.policy.hardMs}ms (elapsed ${elapsed}ms); provide a final answer using existing results`));
265
+ }
266
+ if (elapsed > state.policy.softMs) {
267
+ softExecutions.set(exec.token, {
268
+ agentId,
269
+ exit: state.policy.exit
270
+ });
271
+ }
272
+ return next();
273
+ });
274
+ ctx.on('tools/post-execute', (exec, _result, next)=>{
275
+ return next().then((decision)=>{
276
+ const owner = softExecutions.get(exec.token);
277
+ if (!owner) return decision;
278
+ softExecutions.delete(exec.token);
279
+ const text = owner.exit === 'handoff' ? 'Turn has crossed the soft deadline of a deep-planning turn. Wrap up context gathering now; the turn will hand off to background planning at the hard deadline.' : 'Turn has crossed the soft deadline. Stop dispatching new tools and produce the final answer from gathered evidence.';
280
+ return {
281
+ ...decision,
282
+ additionalContexts: [
283
+ ...decision.additionalContexts ?? [],
284
+ convergenceContext(TURN_DEADLINE_SOFT, text)
285
+ ]
286
+ };
287
+ });
288
+ });
289
+ }
290
+
291
+
292
+ //# sourceURL=ts/src/turn-deadline.ts
@@ -0,0 +1,146 @@
1
+ export const TURN_POLICIES = {
2
+ quick: {
3
+ softMs: 120_000,
4
+ hardMs: 180_000,
5
+ exit: 'converge'
6
+ },
7
+ 'sync-planning': {
8
+ softMs: 300_000,
9
+ hardMs: 600_000,
10
+ exit: 'converge'
11
+ },
12
+ 'deep-planning': {
13
+ softMs: 120_000,
14
+ hardMs: 240_000,
15
+ exit: 'handoff'
16
+ }
17
+ };
18
+ export const TURN_HANDOFF_ETA_LABEL = '约 1 小时';
19
+ export const TURN_CONSTRAINT_LEXICON = [
20
+ '婚礼',
21
+ '婚宴',
22
+ '回门',
23
+ '订婚',
24
+ 'IRW',
25
+ 'irw',
26
+ '请假',
27
+ '年假',
28
+ '调休',
29
+ '额度',
30
+ '签证',
31
+ '同行',
32
+ '伴侣',
33
+ '亲子',
34
+ '带娃',
35
+ '父母',
36
+ '团建',
37
+ '出差',
38
+ '红眼',
39
+ '中转',
40
+ '联程',
41
+ '多城'
42
+ ];
43
+ const CN_DIGITS = {
44
+ 一: 1,
45
+ 二: 2,
46
+ 两: 2,
47
+ 三: 3,
48
+ 四: 4,
49
+ 五: 5,
50
+ 六: 6,
51
+ 七: 7,
52
+ 八: 8,
53
+ 九: 9,
54
+ 十: 10
55
+ };
56
+ function dayCount(year, month, day) {
57
+ const y = year ?? 1970;
58
+ return Date.UTC(y, month - 1, day) / 86_400_000;
59
+ }
60
+ function spanWordDays(message) {
61
+ if (/十几天/.test(message)) return 12;
62
+ if (/半个月/.test(message)) return 15;
63
+ if (/一个月/.test(message)) return 30;
64
+ const week = message.match(/[一两]个?(?:星期|周)/);
65
+ if (week) return week[0].startsWith('两') ? 14 : 7;
66
+ const dayMatch = message.match(/([0-9一二两三四五六七八九十]+)\s*天/);
67
+ if (dayMatch) {
68
+ const raw = dayMatch[1];
69
+ if (/^[0-9]+$/.test(raw)) return Math.min(Number.parseInt(raw, 10), 60);
70
+ if (CN_DIGITS[raw] !== undefined) return Math.min(CN_DIGITS[raw], 60);
71
+ }
72
+ return null;
73
+ }
74
+ export function extractTurnSignals(message) {
75
+ const absoluteDates = [];
76
+ let rest = message.replace(/(?<!\d)(\d{4})[年\-.\/](\d{1,2})[月\-.\/](\d{1,2})日?(?!\d)/g, (_all, y, m, d)=>{
77
+ const month = Number.parseInt(m, 10);
78
+ const day = Number.parseInt(d, 10);
79
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
80
+ absoluteDates.push({
81
+ year: Number.parseInt(y, 10),
82
+ month,
83
+ day
84
+ });
85
+ }
86
+ return ' ';
87
+ });
88
+ rest = rest.replace(/(?<!\d)(\d{1,2})月(\d{1,2})日?(?!\d)/g, (_all, m, d)=>{
89
+ const month = Number.parseInt(m, 10);
90
+ const day = Number.parseInt(d, 10);
91
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
92
+ absoluteDates.push({
93
+ year: null,
94
+ month,
95
+ day
96
+ });
97
+ }
98
+ return ' ';
99
+ });
100
+ rest = rest.replace(/(?<!\d)(\d{1,2})[.\/](\d{1,2})(?!\d)/g, (_all, m, d)=>{
101
+ const month = Number.parseInt(m, 10);
102
+ const day = Number.parseInt(d, 10);
103
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
104
+ absoluteDates.push({
105
+ year: null,
106
+ month,
107
+ day
108
+ });
109
+ }
110
+ return ' ';
111
+ });
112
+ const constraintHits = TURN_CONSTRAINT_LEXICON.filter((word)=>message.includes(word));
113
+ let spanDays = null;
114
+ if (absoluteDates.length >= 2) {
115
+ const counts = absoluteDates.map((d)=>dayCount(d.year, d.month, d.day));
116
+ spanDays = Math.max(...counts) - Math.min(...counts);
117
+ } else {
118
+ spanDays = spanWordDays(message);
119
+ }
120
+ return {
121
+ messageLength: message.length,
122
+ absoluteDates,
123
+ spanDays,
124
+ constraintHits
125
+ };
126
+ }
127
+ export function classifyTurn(message) {
128
+ const s = extractTurnSignals(message);
129
+ if (s.spanDays !== null && s.spanDays >= 7) return 'deep-planning';
130
+ if (s.absoluteDates.length >= 2 && s.constraintHits.length >= 1) return 'deep-planning';
131
+ if (s.constraintHits.length >= 2 && (s.absoluteDates.length >= 1 || s.spanDays !== null)) {
132
+ return 'deep-planning';
133
+ }
134
+ if (s.absoluteDates.length === 0 && s.constraintHits.length === 0 && s.spanDays === null && s.messageLength <= 60) {
135
+ return 'quick';
136
+ }
137
+ return 'sync-planning';
138
+ }
139
+ export function turnPolicyFor(taskClass) {
140
+ return {
141
+ ...TURN_POLICIES[taskClass]
142
+ };
143
+ }
144
+
145
+
146
+ //# sourceURL=ts/src/turn-policy.ts
@@ -1,5 +1,10 @@
1
1
  export function scoreWishMatch(entry, ctx) {
2
2
  const c = entry.conditions ?? {};
3
+ if (ctx.channelDown && Array.isArray(c.channels)) {
4
+ for (const ch of c.channels){
5
+ if (typeof ch === 'string' && ctx.channelDown.has(ch)) return null;
6
+ }
7
+ }
3
8
  const hits = [];
4
9
  if (typeof ctx.days === 'number' && typeof c.days === 'number' && ctx.days >= c.days) hits.push(`days≥${c.days}`);
5
10
  if (typeof ctx.budgetCny === 'number' && typeof c.budget_cny === 'number' && ctx.budgetCny >= c.budget_cny) hits.push(`budget≥${c.budget_cny}`);
@@ -1,11 +1,11 @@
1
1
  /**
2
- * GoTry Session Bridge — MV3 Service Worker(RFC user-session-data-rfc.md §2.2 通道 C,2026-08-29 定案为 PRIMARY 传输)。
2
+ * GoTry Session Bridge — MV3 Service Worker(RFC docs/rfc/user-session-data-rfc.md §2.2 通道 C,2026-08-29 定案为 PRIMARY 传输)。
3
3
  *
4
4
  * 职责(与 Node 侧 session/extension-bridge.ts 配对,零构建纯 JS):
5
5
  * - 长轮询取活:POST /jobs(桥最多 hold 20s;每次响应/失败都重置 SW 30s 生命周期,任务秒级触达);
6
6
  * 断线每 5s 重试,chrome.alarms(30s)兜底唤醒防 SW 悬挂。
7
7
  * - 三种 job:
8
- * search → 后台标签打开 job.url(仅 flights.ctrip.com 白名单),等 content hook 嗅探回包,收尾关自己的标签;
8
+ * search → 后台标签打开 job.url(per-site 白名单:flights/hotels.ctrip.com),等 content hook 嗅探回包,收尾关自己的标签;
9
9
  * open-login → 置前台打开登录入口页(登录页纪律 #34),标签留给用户,绝不代关;
10
10
  * cookie-names → chrome.cookies 只读票据 cookie **名字**(值即取即弃,永不离开扩展——红线)。
11
11
  * - 物理只读(ReadGuard 扩展车道形态):本 SW 绝不向站点发任何请求——检索请求由站点自己的
@@ -23,6 +23,24 @@ const SITES = {
23
23
  domain: 'ctrip.com',
24
24
  ticketNames: ['cticket', 'uid', 'uname', 'passport'],
25
25
  },
26
+ // 酒店(2026-09-03 实装):同一携程账号体系;检索走 hotels.ctrip.com 后台标签 + 被动嗅探
27
+ 'ctrip-hotel': {
28
+ domain: 'ctrip.com',
29
+ ticketNames: ['cticket', 'uid', 'uname', 'passport'],
30
+ },
31
+ // 火车(2026-09-03 实装):12306 余票查询是公开面(登录只关系下单),无票据名可检;
32
+ // search job 照常经 per-site 白名单,扩展只读被动嗅探
33
+ 'train-12306': {
34
+ domain: '12306.cn',
35
+ ticketNames: [],
36
+ },
37
+ }
38
+
39
+ /** 检索 URL 白名单(per-site;search job 只允许开各自站点域,其余一律拒) */
40
+ const SITE_SEARCH_PREFIXES = {
41
+ 'ctrip-flight': 'https://flights.ctrip.com/',
42
+ 'ctrip-hotel': 'https://hotels.ctrip.com/',
43
+ 'train-12306': 'https://kyfw.12306.cn/',
26
44
  }
27
45
 
28
46
  let activePort = null
@@ -112,8 +130,9 @@ async function handleJob(job) {
112
130
  return
113
131
  }
114
132
  if (job.kind === 'search') {
115
- if (typeof job.url !== 'string' || !job.url.startsWith('https://flights.ctrip.com/')) {
116
- await postResult(jobId, { ok: false, error: `search 只允许 flights.ctrip.com 域(收到 ${String(job.url).slice(0, 80)})` })
133
+ const allowedPrefix = SITE_SEARCH_PREFIXES[site]
134
+ if (!allowedPrefix || typeof job.url !== 'string' || !job.url.startsWith(allowedPrefix)) {
135
+ await postResult(jobId, { ok: false, error: `search 只允许 ${allowedPrefix ?? '已注册站点域'}(收到 ${String(job.url).slice(0, 80)})` })
117
136
  return
118
137
  }
119
138
  const tab = await chrome.tabs.create({ url: job.url, active: false })
@@ -2,8 +2,16 @@
2
2
  * GoTry Session Bridge — MAIN-world 网络嗅探(manifest world:"MAIN",document_start)。
3
3
  *
4
4
  * MAIN world 没有 chrome.runtime,只负责:hook 站点自己的 fetch/XHR,把命中
5
- * NETWORK_HINTS(batchSearch)的响应文本以 CustomEvent 派发到页面里;
6
- * ISOLATED 侧的 content-bridge.js 监听事件并经 chrome.runtime 转发给 Service Worker。
5
+ * 嗅探面的响应文本以 CustomEvent 派发到页面里;ISOLATED 侧的 content-bridge.js
6
+ * 监听事件并经 chrome.runtime 转发给 Service Worker。
7
+ *
8
+ * 嗅探面(与 Node 侧 adapters/ctrip-flight.ts NETWORK_HINTS、adapters/ctrip-hotel.ts
9
+ * HOTEL_NETWORK_HINTS、adapters/rail-12306.ts TRAIN_NETWORK_HINTS 对账,run-all §38
10
+ * 防漂移断言守住):
11
+ * - 机票:search/api/search/batchSearch(精确接口)
12
+ * - 酒店(2026-09-03 实装):URL hint 多版接口名 + **形状嗅探兜底**——响应体
13
+ * JSON 含酒店清单签名即转发,对接口改名免疫(接口名公开资料多版并存,首个
14
+ * 真会话后校准,D-13 同款边界)
7
15
  *
8
16
  * 只读被动转发:不修改请求、不重放、不伪造(§2.3 反模式红线)——响应由站点自己的
9
17
  * 代码发出,我们只听。
@@ -12,8 +20,15 @@
12
20
  if (window.__gotrySniffInstalled) return
13
21
  window.__gotrySniffInstalled = true
14
22
 
15
- /** 与 Node 侧 adapters/ctrip-flight.ts NETWORK_HINTS 对账(run-all §38 防漂移断言) */
16
- var HINT_RE = /search\/api\/search\/batchSearch/
23
+ var FLIGHT_HINT_RE = /search\/api\/search\/batchSearch/
24
+ var HOTEL_HINT_RE = /hotels\.ctrip\.com\/(hotels\/api|domestic\/pc\/api)|GetHotelListBySOA|GetHotelListByCity|HotelSearch|hotelsearch|restapi\/soa2\/\d+\/fetchHotelList/i
25
+ /** 火车(2026-09-03 实装):12306 余票查询 XHR(负载均衡变体 queryG/Z/A/U 全命中) */
26
+ var TRAIN_HINT_RE = /leftTicket\/query/i
27
+ /** 形状嗅探(酒店页兜底;与 Node 侧 looksLikeHotelListBody 签名一致) */
28
+ var HOTEL_BODY_SIG_RE = /"hotelList"|"hotelMatchInfos"|"hotelName"/
29
+ var HOTEL_BODY_MAX = 2_000_000
30
+ var isHotelPage = /(^|\.)hotels\.ctrip\.com$/.test(location.hostname)
31
+ var isTrainPage = /(^|\.)12306\.cn$/.test(location.hostname)
17
32
 
18
33
  function dispatch(url, body) {
19
34
  try {
@@ -21,6 +36,13 @@
21
36
  } catch { /* 页面环境异常不抛 */ }
22
37
  }
23
38
 
39
+ function hotelPageWants(url) {
40
+ return isHotelPage && (HOTEL_HINT_RE.test(url) || FLIGHT_HINT_RE.test(url))
41
+ }
42
+ function trainPageWants(url) {
43
+ return isTrainPage && TRAIN_HINT_RE.test(url)
44
+ }
45
+
24
46
  var origFetch = window.fetch
25
47
  if (typeof origFetch === 'function') {
26
48
  window.fetch = function (input, init) {
@@ -29,9 +51,21 @@
29
51
  var url = ''
30
52
  if (typeof input === 'string') url = input
31
53
  else if (input && typeof input.url === 'string') url = input.url
32
- if (HINT_RE.test(url)) {
54
+ var urlHint = FLIGHT_HINT_RE.test(url) || hotelPageWants(url) || trainPageWants(url)
55
+ var isPost = false
56
+ try { isPost = String((init && init.method) || (input && input.method) || 'GET').toUpperCase() === 'POST' } catch { /* ignore */ }
57
+ var capOk = true
58
+ try {
59
+ var len = Number(res.headers && res.headers.get('content-length'))
60
+ if (isHotelPage && !urlHint && len > HOTEL_BODY_MAX) capOk = false
61
+ } catch { /* 头不可读则放行小体兜底 */ }
62
+ if (urlHint || (isHotelPage && isPost && capOk)) {
33
63
  var clone = res.clone()
34
- clone.text().then(function (t) { dispatch(url, t) }).catch(function () { /* 流不可读则跳过 */ })
64
+ clone.text().then(function (t) {
65
+ try {
66
+ if (urlHint || (t.length <= HOTEL_BODY_MAX && HOTEL_BODY_SIG_RE.test(t))) dispatch(url, t)
67
+ } catch { /* 嗅探失败不影响站点自身 */ }
68
+ }).catch(function () { /* 流不可读则跳过 */ })
35
69
  }
36
70
  } catch { /* 嗅探失败不影响站点自身 */ }
37
71
  return res
@@ -43,20 +77,24 @@
43
77
  var origSend = XMLHttpRequest.prototype.send
44
78
  XMLHttpRequest.prototype.open = function (method, url) {
45
79
  this.__gotryUrl = typeof url === 'string' ? url : String(url ?? '')
80
+ this.__gotryMethod = typeof method === 'string' ? method.toUpperCase() : 'GET'
46
81
  return origOpen.apply(this, arguments)
47
82
  }
48
83
  XMLHttpRequest.prototype.send = function () {
49
84
  var xhr = this
50
85
  xhr.addEventListener('load', function () {
51
86
  try {
52
- if (!xhr.__gotryUrl || !HINT_RE.test(xhr.__gotryUrl)) return
87
+ if (!xhr.__gotryUrl) return
88
+ var urlHint = FLIGHT_HINT_RE.test(xhr.__gotryUrl) || hotelPageWants(xhr.__gotryUrl) || trainPageWants(xhr.__gotryUrl)
89
+ if (!urlHint && !(isHotelPage && xhr.__gotryMethod === 'POST')) return
53
90
  var body = ''
54
91
  if (xhr.responseType === '' || xhr.responseType === 'text') body = xhr.responseText
55
92
  else if (xhr.responseType === 'json') body = JSON.stringify(xhr.response)
56
93
  else return
57
- if (body) dispatch(xhr.__gotryUrl, body)
94
+ if (!body) return
95
+ if (urlHint || (body.length <= HOTEL_BODY_MAX && HOTEL_BODY_SIG_RE.test(body))) dispatch(xhr.__gotryUrl, body)
58
96
  } catch { /* 嗅探失败不影响站点 */ }
59
97
  })
60
98
  return origSend.apply(this, arguments)
61
99
  }
62
- })()
100
+ })()
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "GoTry Session Bridge",
4
- "version": "0.1.0",
5
- "description": "GoTry 会话检索桥(issue #21):在你自己的登录态里只读嗅探检索回包与登录票据 cookie 名;零凭证经手,登录在官网由你完成,检索只读不写。gotry 侧不再需要开启 Remote Debugging,也没有任何系统级权限弹窗。",
4
+ "version": "0.0.1.19",
5
+ "description": "GoTry 会话检索桥(issue #21):在你自己的登录态里只读嗅探检索回包(机票/酒店/火车)与登录票据 cookie 名;零凭证经手,登录在官网由你完成,检索只读不写。",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnuzBX8rT8+RZKEUip5VRA0cY056pExy9PNdN9YCiuY3dgglRbcIBhjuFJoR6EX3/MaUv82vJVyItRVrowXLTwDk4WZof0vwFgKL0zBK304PC46pUaAKTSu1PgXeL1+j5KTAGz/9a9+RnPdbj7jND5rM/MOmBHNPVPpp1NlwxRGIgF9OxHSOfXORcH4Iyte1aJSFUJbFtNS/DaE5D+KjyupPLyIqVN/dCiKnNsTCEHQB9ngeli665PFp76hkccHQQZ4sVuMLS4zBPcazUsp1kcnW+xsQoUATkODTCY+Pzk3zUVF1QOESTjRCVnbNi8tkO43RMRs2q63gX/3inFivcEwIDAQAB",
7
- "permissions": ["cookies", "alarms"],
7
+ "permissions": [
8
+ "cookies",
9
+ "alarms"
10
+ ],
8
11
  "host_permissions": [
9
12
  "http://127.0.0.1:8791/*",
10
13
  "http://127.0.0.1:8792/*",
@@ -18,15 +21,30 @@
18
21
  },
19
22
  "content_scripts": [
20
23
  {
21
- "matches": ["https://flights.ctrip.com/*"],
22
- "js": ["content-main.js"],
24
+ "matches": [
25
+ "https://flights.ctrip.com/*",
26
+ "https://hotels.ctrip.com/*",
27
+ "https://kyfw.12306.cn/*",
28
+ "https://www.12306.cn/*"
29
+ ],
30
+ "js": [
31
+ "content-main.js"
32
+ ],
23
33
  "run_at": "document_start",
24
34
  "world": "MAIN"
25
35
  },
26
36
  {
27
- "matches": ["https://flights.ctrip.com/*"],
28
- "js": ["content-bridge.js"],
37
+ "matches": [
38
+ "https://flights.ctrip.com/*",
39
+ "https://hotels.ctrip.com/*",
40
+ "https://kyfw.12306.cn/*",
41
+ "https://www.12306.cn/*"
42
+ ],
43
+ "js": [
44
+ "content-bridge.js"
45
+ ],
29
46
  "run_at": "document_start"
30
47
  }
31
- ]
32
- }
48
+ ],
49
+ "version_name": "0.0.1-rc.19"
50
+ }