@danceiny/gotry 0.0.1-rc.10 → 0.0.1-rc.11

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.
@@ -0,0 +1,105 @@
1
+ import { flyaiSearch } from '../capabilities/flyai.js';
2
+ const IATA_TO_CITY = {
3
+ HKG: '香港',
4
+ HKT: '普吉',
5
+ BKK: '曼谷',
6
+ KMG: '昆明',
7
+ SZX: '深圳',
8
+ DXB: '迪拜',
9
+ LJG: '丽江',
10
+ DLU: '大理',
11
+ SHA: '上海',
12
+ PVG: '上海',
13
+ PEK: '北京',
14
+ CTU: '成都'
15
+ };
16
+ export async function overlayRealtimeFlightPrices(spec, opts = {}) {
17
+ const out = {
18
+ spec,
19
+ matched: [],
20
+ notes: []
21
+ };
22
+ const flights = [];
23
+ for (const seg of spec.segments){
24
+ if (!seg.date || !/^\d{4}-\d{2}-\d{2}$/.test(seg.date)) continue;
25
+ const route = seg.route;
26
+ if (!route || !route.includes('->')) continue;
27
+ const [a, b] = route.split('->').map((s)=>IATA_TO_CITY[s.trim()]);
28
+ if (!a || !b) continue;
29
+ if (seg.options.some((o)=>!o.move)) continue;
30
+ flights.push({
31
+ seg,
32
+ date: seg.date,
33
+ origin: a,
34
+ destination: b
35
+ });
36
+ }
37
+ if (flights.length === 0) return out;
38
+ const query = opts.query ?? defaultQuery;
39
+ for (const f of flights){
40
+ let result;
41
+ try {
42
+ result = await query({
43
+ kind: 'flight',
44
+ origin: f.origin,
45
+ destination: f.destination,
46
+ depDate: f.date
47
+ });
48
+ } catch (e) {
49
+ out.notes.push(`${f.seg.id}: 实时票价不可用(降级静态包):${e.message?.slice(0, 80)} [实时API:flyai@error]`);
50
+ continue;
51
+ }
52
+ if (!result.ok || result.verdict !== 'hit' || !result.options?.length) {
53
+ out.notes.push(`${f.seg.id}: 实时票价未取回(${result.verdict},降级静态包) ${result.evidence}`);
54
+ continue;
55
+ }
56
+ const ts = /@(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\]/.exec(result.evidence)?.[1] ?? new Date().toISOString();
57
+ const byNo = new Map();
58
+ for (const o of result.options){
59
+ if (Number.isFinite(o.price) && o.price > 0) byNo.set(o.no.toUpperCase(), o);
60
+ }
61
+ for (const opt of f.seg.options){
62
+ const svc = opt.move.services[0];
63
+ const rt = byNo.get(svc.id.toUpperCase());
64
+ if (!rt) continue;
65
+ const evidence = `[实时API:flyai@${ts}] ${rt.no} ¥${rt.price}(静态包原价 ¥${svc.priceCny})`;
66
+ out.matched.push({
67
+ segment: f.seg.id,
68
+ option: opt.id,
69
+ staticCny: svc.priceCny,
70
+ realtimeCny: rt.price,
71
+ evidence
72
+ });
73
+ out.notes.push(evidence);
74
+ svc.priceCny = rt.price;
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ const defaultQuery = (q)=>flyaiSearch({
80
+ kind: q.kind,
81
+ origin: q.origin,
82
+ destination: q.destination,
83
+ depDate: q.depDate
84
+ });
85
+ export function realtimeSolvePort(base, opts = {}) {
86
+ return async (spec)=>{
87
+ const enabled = opts.enabled ?? process.env['GOTRY_REALTIME_PRICING'] === '1';
88
+ if (!enabled) return base(spec);
89
+ try {
90
+ const ov = await overlayRealtimeFlightPrices(spec, opts);
91
+ if (ov.matched.length === 0 && ov.notes.length === 0) return base(ov.spec);
92
+ const res = await base(ov.spec);
93
+ res.skeleton_notes = [
94
+ ...res.skeleton_notes ?? [],
95
+ ...ov.notes
96
+ ];
97
+ return res;
98
+ } catch {
99
+ return base(spec);
100
+ }
101
+ };
102
+ }
103
+
104
+
105
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/realtime-pricing.ts
@@ -1,13 +1,7 @@
1
1
  import { checkConnectivity } from '../scripts/skeleton-check.js';
2
2
  import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
3
- let z3Promise = null;
4
- async function getZ3() {
5
- if (!z3Promise) {
6
- const { init } = await import('z3-solver');
7
- z3Promise = (async ()=>(await init()).Context('main'))();
8
- }
9
- return z3Promise;
10
- }
3
+ import { withZ3 } from './z3-shared.js';
4
+ import { t as i18nT } from './i18n.js';
11
5
  const SEGMENT_ROUTES = {
12
6
  f1: 'HKG->HKT',
13
7
  f2: 'HKT->BKK',
@@ -165,7 +159,12 @@ function workWindowBlocks(spec, option) {
165
159
  const start = ww.startMin + shift, end = ww.endMin + shift;
166
160
  if (start <= dep && dep <= end) {
167
161
  const hhmm = (m)=>`${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
168
- return `周${WEEKDAY_CN[option.depWeekday]} ${hhmm(dep)} 起飞落在工作窗口(当地 ${hhmm(start)}-${hhmm(end)})内`;
162
+ return i18nT('un.workwindow_reason', {
163
+ wd: WEEKDAY_CN[option.depWeekday] ?? '',
164
+ dep: hhmm(dep),
165
+ start: hhmm(start),
166
+ end: hhmm(end)
167
+ });
169
168
  }
170
169
  return null;
171
170
  }
@@ -250,91 +249,95 @@ async function solveUnifiedInner(spec) {
250
249
  };
251
250
  }
252
251
  }
253
- const z3 = await getZ3();
254
- const { Bool, If, Int, Solver, Sum } = z3;
255
- const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
256
- const allSels = {};
257
- const assertions = {};
258
- const svcOf = {};
259
- for (const seg of spec.segments){
260
- const sels = seg.options.map((_, i)=>Bool.const(`${seg.id}_o${i}`));
261
- allSels[seg.id] = sels;
262
- const svcs = seg.options.map((o)=>o.move.services[0]);
263
- svcOf[seg.id] = svcs;
264
- const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(svcs[i][attr]), Int.val(0))));
265
- const dep = pick('depMin'), arr = pick('arrMin');
266
- const o0 = seg.options[0].move;
267
- const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
268
- const arriveStay = arr.add(Int.val(o0.destTransferMin));
269
- if (seg.anchors?.arriveByMin !== undefined) assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
270
- if (seg.anchors?.departAfterMin !== undefined) assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
271
- if (!seg.options.some((o)=>o.move?.redEye)) assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
272
- }
273
- const total = Sum(...spec.segments.flatMap((seg)=>allSels[seg.id].map((s, i)=>If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
274
- if (spec.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
275
- const exactlyOne = (sels)=>Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
276
- const coreOf = (s)=>{
277
- const v = s.unsatCore();
278
- const out = [];
279
- for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
280
- return out;
281
- };
282
- const s = new Solver();
283
- for (const sel of Object.values(allSels))s.add(exactlyOne(sel));
284
- for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
285
- const report = async (model)=>{
286
- const out = [];
252
+ return withZ3('unified.solveUnifiedInner', async (z3)=>{
253
+ const { Bool, If, Int, Solver, Sum } = z3;
254
+ const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
255
+ const allSels = {};
256
+ const assertions = {};
257
+ const svcOf = {};
287
258
  for (const seg of spec.segments){
288
- for(let i = 0; i < allSels[seg.id].length; i++){
289
- const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
290
- if (String(v) !== 'true') continue;
291
- const o = seg.options[i];
292
- out.push({
293
- leg: seg.id,
294
- ...evaluateOptionMove(seg.id, o.move)
259
+ const sels = seg.options.map((_, i)=>Bool.const(`${seg.id}_o${i}`));
260
+ allSels[seg.id] = sels;
261
+ const svcs = seg.options.map((o)=>o.move.services[0]);
262
+ svcOf[seg.id] = svcs;
263
+ const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(svcs[i][attr]), Int.val(0))));
264
+ const dep = pick('depMin'), arr = pick('arrMin');
265
+ const o0 = seg.options[0].move;
266
+ const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
267
+ const arriveStay = arr.add(Int.val(o0.destTransferMin));
268
+ if (seg.anchors?.arriveByMin !== undefined) assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
269
+ if (seg.anchors?.departAfterMin !== undefined) assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
270
+ if (!seg.options.some((o)=>o.move?.redEye)) assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
271
+ }
272
+ const total = Sum(...spec.segments.flatMap((seg)=>allSels[seg.id].map((s, i)=>If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
273
+ if (spec.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
274
+ const exactlyOne = (sels)=>Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
275
+ const coreOf = (s)=>{
276
+ const v = s.unsatCore();
277
+ const out = [];
278
+ for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
279
+ return out;
280
+ };
281
+ const s = new Solver();
282
+ for (const sel of Object.values(allSels))s.add(exactlyOne(sel));
283
+ for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
284
+ const report = async (model)=>{
285
+ const out = [];
286
+ for (const seg of spec.segments){
287
+ for(let i = 0; i < allSels[seg.id].length; i++){
288
+ const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
289
+ if (String(v) !== 'true') continue;
290
+ const o = seg.options[i];
291
+ out.push({
292
+ leg: seg.id,
293
+ ...evaluateOptionMove(seg.id, o.move)
294
+ });
295
+ break;
296
+ }
297
+ }
298
+ return out;
299
+ };
300
+ async function maybeAwait(v) {
301
+ return v instanceof Promise ? await v : v;
302
+ }
303
+ if (String(await s.check()) !== 'unsat') {
304
+ const legs = await report(s.model());
305
+ const money = legs.reduce((a, l)=>a + l.price_cny, 0);
306
+ const redFlags = legs.filter((l)=>spec.segments.find((sg)=>sg.id === l.leg)?.options.some((o)=>o.move?.redEye) && l.energy_pct < 50).map((l)=>i18nT('un.redflag_redeye', {
307
+ leg: l.leg,
308
+ pct: l.energy_pct
309
+ }));
310
+ return {
311
+ feasible: true,
312
+ money_cny: money,
313
+ legs,
314
+ red_flags: redFlags,
315
+ work_window_exclusions: exclusions,
316
+ skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined
317
+ };
318
+ }
319
+ const core = coreOf(s).sort();
320
+ const suggestions = [];
321
+ for (const name of core){
322
+ const s2 = new Solver();
323
+ for (const sel of Object.values(allSels))s2.add(exactlyOne(sel));
324
+ for (const [n2, expr] of Object.entries(assertions)){
325
+ if (n2 !== name) s2.addAndTrack(expr, n2);
326
+ }
327
+ if (String(await s2.check()) !== 'unsat') {
328
+ const legs = await report(s2.model());
329
+ suggestions.push({
330
+ relax: name,
331
+ money_cny: legs.reduce((a, l)=>a + l.price_cny, 0)
295
332
  });
296
- break;
297
333
  }
298
334
  }
299
- return out;
300
- };
301
- async function maybeAwait(v) {
302
- return v instanceof Promise ? await v : v;
303
- }
304
- if (String(await s.check()) !== 'unsat') {
305
- const legs = await report(s.model());
306
- const money = legs.reduce((a, l)=>a + l.price_cny, 0);
307
- const redFlags = legs.filter((l)=>spec.segments.find((sg)=>sg.id === l.leg)?.options.some((o)=>o.move?.redEye) && l.energy_pct < 50).map((l)=>`${l.leg} 落地精力仅 ${l.energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)`);
308
335
  return {
309
- feasible: true,
310
- money_cny: money,
311
- legs,
312
- red_flags: redFlags,
313
- work_window_exclusions: exclusions,
314
- skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined
336
+ feasible: false,
337
+ unsat_core: core,
338
+ suggestions
315
339
  };
316
- }
317
- const core = coreOf(s).sort();
318
- const suggestions = [];
319
- for (const name of core){
320
- const s2 = new Solver();
321
- for (const sel of Object.values(allSels))s2.add(exactlyOne(sel));
322
- for (const [n2, expr] of Object.entries(assertions)){
323
- if (n2 !== name) s2.addAndTrack(expr, n2);
324
- }
325
- if (String(await s2.check()) !== 'unsat') {
326
- const legs = await report(s2.model());
327
- suggestions.push({
328
- relax: name,
329
- money_cny: legs.reduce((a, l)=>a + l.price_cny, 0)
330
- });
331
- }
332
- }
333
- return {
334
- feasible: false,
335
- unsat_core: core,
336
- suggestions
337
- };
340
+ });
338
341
  }
339
342
  function checkTrueCost(t, spec) {
340
343
  const fails = [];
@@ -459,7 +462,10 @@ export function solveChoiceSegment(spec, req) {
459
462
  wish = {
460
463
  name: opt.label,
461
464
  conditions: conds,
462
- reason: `${windowDays} 天窗口装不下(目的需 ${minDays} 天)`
465
+ reason: i18nT('sg.wish_reason_window', {
466
+ days: windowDays,
467
+ minDays
468
+ })
463
469
  };
464
470
  }
465
471
  verdicts.push({
@@ -483,52 +489,99 @@ function renderCandidateMarkdown(req, verdicts, recommended) {
483
489
  const feasible = verdicts.filter((v)=>v['feasible']).sort((a, b)=>b['imagery_match'] - a['imagery_match']);
484
490
  const parked = verdicts.filter((v)=>!v['feasible']);
485
491
  const lines = [];
486
- lines.push(`> 憧憬:${req.note}`);
487
- lines.push(`> 已识别约束:窗口 ${req.windowDays} 天 | 预算 ¥${req.budgetCny} | ` + `动机(休整改写需求 ${requiredUsableHours(req.motivation).toFixed(1)}h 有效休整)`);
492
+ lines.push(i18nT('md.header', {
493
+ note: req.note
494
+ }));
495
+ lines.push(i18nT('md.constraints', {
496
+ days: req.windowDays,
497
+ budget: req.budgetCny,
498
+ hours: requiredUsableHours(req.motivation).toFixed(1)
499
+ }));
488
500
  lines.push('');
489
501
  for (const v of parked){
490
502
  const core = v['unsat_core'] ?? [];
491
- lines.push(`**${v['name']}:现在不行**——冲突约束:${core.join('、')}。`);
503
+ lines.push(i18nT('md.infeasible', {
504
+ name: String(v['name']),
505
+ core: core.join('、')
506
+ }));
492
507
  const sug = (v['suggestions'] ?? [])[0];
493
- if (sug) lines.push(`- 放宽 ${sug['relax']}:约 ¥${sug['resulting_money_cny']}`);
508
+ if (sug) lines.push(i18nT('md.relax_one', {
509
+ relax: String(sug['relax']),
510
+ money: String(sug['resulting_money_cny'] ?? '')
511
+ }));
494
512
  const wish = v['wish_pool'];
495
513
  if (wish) {
496
514
  const conds = wish['conditions'];
497
- const budgetNote = conds['budget_cny'] ? `、约 ¥${conds['budget_cny']}` : '';
515
+ const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', {
516
+ cny: String(conds['budget_cny'])
517
+ }) : '';
498
518
  const months = conds['best_months'];
499
- const season = months ? `,${months} 月最佳` : '';
500
- lines.push(`- 已放入「下一次出发」清单:需要 ${conds['days']} 天${budgetNote}${season}`);
519
+ const season = months ? i18nT('md.wish_season', {
520
+ months: String(months)
521
+ }) : '';
522
+ lines.push(i18nT('md.wish_pool', {
523
+ days: String(conds['days']),
524
+ budgetNote,
525
+ season
526
+ }));
501
527
  }
502
528
  }
503
529
  lines.push('');
504
530
  for (const v of feasible){
505
531
  const ch = v['chosen'];
506
- const t = v['true_cost'];
507
- lines.push(`**${v['name']}:可行**` + `(${ch['out_service']} 出发,${ch['out_transfer']} 接驳,` + `起床 ${t['wake']},到达精力 ${t['energy_arrival_pct']}%,` + `门到门 ${t['door_to_door_out']},有效休整 ${t['usable_hours']}h,共 ¥${t['money_cny']})`);
532
+ const tc = v['true_cost'];
533
+ lines.push(i18nT('md.feasible_unified', {
534
+ name: String(v['name']),
535
+ svc: String(ch['out_service']),
536
+ mode: String(ch['out_transfer']),
537
+ wake: String(tc['wake']),
538
+ energy: String(tc['energy_arrival_pct']),
539
+ d2d: String(tc['door_to_door_out']),
540
+ hours: String(tc['usable_hours']),
541
+ money: String(tc['money_cny'])
542
+ }));
508
543
  }
509
544
  if (feasible.length) {
510
545
  lines.push('');
511
546
  const best = feasible[0];
512
547
  const alt = feasible[1];
513
- lines.push(`**建议:${best['name']}**(意象匹配 ${(best['imagery_match'] * 100).toFixed(0)}%)。` + (alt ? `备选:${alt['name']}(¥${alt['true_cost']['money_cny']},匹配 ${(alt['imagery_match'] * 100).toFixed(0)}%)。` : ''));
548
+ lines.push(i18nT('md.recommend', {
549
+ best: String(best['name']),
550
+ match: (best['imagery_match'] * 100).toFixed(0)
551
+ }) + (alt ? i18nT('md.alt', {
552
+ name: String(alt['name']),
553
+ money: String(alt['true_cost']['money_cny']),
554
+ match: (alt['imagery_match'] * 100).toFixed(0)
555
+ }) : ''));
514
556
  }
515
557
  lines.push('');
516
- lines.push('**待你决定的两个问题**:');
558
+ lines.push(i18nT('md.decide_two'));
517
559
  if (feasible.length >= 2) {
518
- lines.push(`1. ${feasible[0]['name']} 还是 ${feasible[1]['name']}?(前者更贴意象,后者更省)`);
560
+ lines.push(i18nT('md.q_choice', {
561
+ a: String(feasible[0]['name']),
562
+ b: String(feasible[1]['name'])
563
+ }));
519
564
  } else if (feasible.length === 1) {
520
- lines.push(`1. 就去 ${feasible[0]['name']} 吗?`);
565
+ lines.push(i18nT('md.q_single', {
566
+ name: String(feasible[0]['name'])
567
+ }));
521
568
  } else {
522
- lines.push('1. 所有候选都不可行——考虑放宽哪条约束?');
569
+ lines.push(i18nT('md.q_none'));
523
570
  }
524
571
  const p0 = parked[0];
525
572
  const p0wish = p0?.['wish_pool'];
526
573
  if (p0wish) {
527
574
  const conds = p0wish['conditions'];
528
- lines.push(`2. 把 ${p0['name']} 留给「下一次出发」(${conds['days']} 天起),这次先去可行的?`);
575
+ lines.push(i18nT('md.q_wish', {
576
+ name: String(p0['name']),
577
+ days: String(conds['days'])
578
+ }));
529
579
  } else if (feasible.length) {
530
580
  const ch = feasible[0]['chosen'];
531
- lines.push(`2. 出发班次选 ${ch['out_service']} 还是更晚的?`);
581
+ lines.push(i18nT('md.q_depart', {
582
+ id: String(ch['out_service']),
583
+ dep: ''
584
+ }));
532
585
  }
533
586
  return lines.join('\n');
534
587
  }
@@ -0,0 +1,18 @@
1
+ let z3Promise = null;
2
+ let chain = Promise.resolve();
3
+ export async function getZ3() {
4
+ if (!z3Promise) {
5
+ const { init } = await import('z3-solver');
6
+ z3Promise = (async ()=>(await init()).Context('main'))();
7
+ }
8
+ return z3Promise;
9
+ }
10
+ export async function withZ3(label, session) {
11
+ const z3 = await getZ3();
12
+ const turn = chain.then(()=>session(z3));
13
+ chain = turn.then(()=>undefined, ()=>undefined);
14
+ return turn;
15
+ }
16
+
17
+
18
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/z3-shared.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danceiny/gotry",
3
- "version": "0.0.1-rc.10",
3
+ "version": "0.0.1-rc.11",
4
4
  "description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm 包入口 + vendored dsh runtime + 5 行 README 安装路径。",
5
5
  "type": "module",
6
6
  "main": "ts/src/index.ts",
@@ -81,4 +81,4 @@
81
81
  "deepseek-harness"
82
82
  ],
83
83
  "license": "MIT"
84
- }
84
+ }
package/ts/src/unified.ts CHANGED
@@ -9,6 +9,8 @@ import { checkConnectivity } from '../scripts/skeleton-check.ts'
9
9
  import type { Candidate, Choice, MotivationProfile, Service, TransferMode, TravelRequest, TrueCost } from './model.ts'
10
10
  import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.ts'
11
11
  import type { LegReport } from './journey.ts'
12
+ import { withZ3 } from './z3-shared.ts'
13
+ import { t as i18nT } from './i18n.ts'
12
14
 
13
15
  export interface AnchorsSpec {
14
16
  arriveByMin?: number
@@ -85,18 +87,9 @@ export interface JourneySpecTS {
85
87
  skeletonHub?: boolean
86
88
  }
87
89
 
90
+ // Z3 运行时收敛到 z3-shared(单一 WASM 实例 + 单一 Context + 会话级互斥)——
91
+ // 此前模块级 z3Promise 三份并存是 WASM race/OOM 的根因(见 z3-shared.ts 头注)。
88
92
  /* eslint-disable @typescript-eslint/no-explicit-any */
89
- let z3Promise: Promise<any> | null = null
90
-
91
- async function getZ3(): Promise<any> {
92
- // 延迟到首次实际调用 solveUnified 才加载:避免 dsh 加载 GoTry 模块时启动
93
- // WASM worker,引起 worker 线程内存冲突(z3-built.wasm 多线程 unsafe)。
94
- if (!z3Promise) {
95
- const { init } = await import('z3-solver')
96
- z3Promise = (async () => (await init()).Context('main'))()
97
- }
98
- return z3Promise
99
- }
100
93
 
101
94
  /** 航班数据包(data/flights_2026.json 形态)→ 段链 */
102
95
  const SEGMENT_ROUTES: Record<string, string> = {
@@ -211,7 +204,9 @@ function workWindowBlocks(spec: JourneySpecTS, option: SegmentOptionTS): string
211
204
  const start = ww.startMin + shift, end = ww.endMin + shift
212
205
  if (start <= dep && dep <= end) {
213
206
  const hhmm = (m: number) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`
214
- return `周${WEEKDAY_CN[option.depWeekday]} ${hhmm(dep)} 起飞落在工作窗口(当地 ${hhmm(start)}-${hhmm(end)})内`
207
+ return i18nT('un.workwindow_reason', {
208
+ wd: WEEKDAY_CN[option.depWeekday] ?? '', dep: hhmm(dep), start: hhmm(start), end: hhmm(end),
209
+ })
215
210
  }
216
211
  return null
217
212
  }
@@ -310,7 +305,9 @@ async function solveUnifiedInner(spec: JourneySpecTS): Promise<{
310
305
  }
311
306
  }
312
307
 
313
- const z3 = await getZ3()
308
+ // Z3 会话进互斥门(骨架/预过滤等网络段留在门外):solveUnifiedInner 的判定段
309
+ // 从 here 到 return 全部独占共享实例。
310
+ return withZ3('unified.solveUnifiedInner', async z3 => {
314
311
  const { Bool, If, Int, Solver, Sum } = z3
315
312
  const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00')
316
313
 
@@ -374,7 +371,7 @@ async function solveUnifiedInner(spec: JourneySpecTS): Promise<{
374
371
  const money = legs.reduce((a, l) => a + l.price_cny, 0)
375
372
  const redFlags = legs
376
373
  .filter(l => spec.segments.find(sg => sg.id === l.leg)?.options.some(o => o.move?.redEye) && l.energy_pct < 50)
377
- .map(l => `${l.leg} 落地精力仅 ${l.energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)`)
374
+ .map(l => i18nT('un.redflag_redeye', { leg: l.leg, pct: l.energy_pct }))
378
375
  return { feasible: true, money_cny: money, legs, red_flags: redFlags, work_window_exclusions: exclusions, skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined }
379
376
  }
380
377
 
@@ -392,6 +389,7 @@ async function solveUnifiedInner(spec: JourneySpecTS): Promise<{
392
389
  }
393
390
  }
394
391
  return { feasible: false, unsat_core: core, suggestions }
392
+ })
395
393
  }
396
394
 
397
395
  // ---- 候选形态求解(枚举过滤,与 py solve_choice_segment 对齐) --------------------
@@ -504,7 +502,7 @@ export function solveChoiceSegment(spec: JourneySpecTS, req: TravelRequest): Rec
504
502
  conds['budget_cny'] = Math.min(...budgetDropped.map(c => c.cost.moneyCny))
505
503
  }
506
504
  if (opt.bestMonths?.length) conds['best_months'] = opt.bestMonths
507
- wish = { name: opt.label, conditions: conds, reason: `${windowDays} 天窗口装不下(目的需 ${minDays} 天)` }
505
+ wish = { name: opt.label, conditions: conds, reason: i18nT('sg.wish_reason_window', { days: windowDays, minDays }) }
508
506
  }
509
507
  verdicts.push({
510
508
  candidate_id: opt.id, name: opt.label, feasible: false, imagery_match: opt.score ?? 0,
@@ -524,57 +522,63 @@ function renderCandidateMarkdown(req: TravelRequest, verdicts: Array<Record<stri
524
522
  const feasible = verdicts.filter(v => v['feasible']).sort((a, b) => (b['imagery_match'] as number) - (a['imagery_match'] as number))
525
523
  const parked = verdicts.filter(v => !v['feasible'])
526
524
  const lines: string[] = []
527
- lines.push(`> 憧憬:${req.note}`)
528
- lines.push(`> 已识别约束:窗口 ${req.windowDays} 天 | 预算 ¥${req.budgetCny} | `
529
- + `动机(休整改写需求 ${requiredUsableHours(req.motivation).toFixed(1)}h 有效休整)`)
525
+ lines.push(i18nT('md.header', { note: req.note }))
526
+ lines.push(i18nT('md.constraints', {
527
+ days: req.windowDays, budget: req.budgetCny,
528
+ hours: requiredUsableHours(req.motivation).toFixed(1),
529
+ }))
530
530
  lines.push('')
531
531
  for (const v of parked) {
532
532
  const core = (v['unsat_core'] as string[]) ?? []
533
- lines.push(`**${v['name']}:现在不行**——冲突约束:${core.join('、')}。`)
533
+ lines.push(i18nT('md.infeasible', { name: String(v['name']), core: core.join('、') }))
534
534
  const sug = ((v['suggestions'] as Array<Record<string, unknown>>) ?? [])[0]
535
- if (sug) lines.push(`- 放宽 ${sug['relax']}:约 ¥${sug['resulting_money_cny']}`)
535
+ if (sug) lines.push(i18nT('md.relax_one', { relax: String(sug['relax']), money: String(sug['resulting_money_cny'] ?? '') }))
536
536
  const wish = v['wish_pool'] as Record<string, unknown> | null
537
537
  if (wish) {
538
538
  const conds = wish['conditions'] as Record<string, unknown>
539
- const budgetNote = conds['budget_cny'] ? `、约 ¥${conds['budget_cny']}` : ''
539
+ const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', { cny: String(conds['budget_cny']) }) : ''
540
540
  const months = conds['best_months'] as number[] | undefined
541
- const season = months ? `,${months} 月最佳` : ''
542
- lines.push(`- 已放入「下一次出发」清单:需要 ${conds['days']} 天${budgetNote}${season}`)
541
+ const season = months ? i18nT('md.wish_season', { months: String(months) }) : ''
542
+ lines.push(i18nT('md.wish_pool', { days: String(conds['days']), budgetNote, season }))
543
543
  }
544
544
  }
545
545
  lines.push('')
546
546
  for (const v of feasible) {
547
547
  const ch = v['chosen'] as Record<string, unknown>
548
- const t = v['true_cost'] as Record<string, unknown>
549
- lines.push(`**${v['name']}:可行**`
550
- + `(${ch['out_service']} 出发,${ch['out_transfer']} 接驳,`
551
- + `起床 ${t['wake']},到达精力 ${t['energy_arrival_pct']}%,`
552
- + `门到门 ${t['door_to_door_out']},有效休整 ${t['usable_hours']}h,共 ¥${t['money_cny']})`)
548
+ const tc = v['true_cost'] as Record<string, unknown>
549
+ lines.push(i18nT('md.feasible_unified', {
550
+ name: String(v['name']), svc: String(ch['out_service']), mode: String(ch['out_transfer']),
551
+ wake: String(tc['wake']), energy: String(tc['energy_arrival_pct']), d2d: String(tc['door_to_door_out']),
552
+ hours: String(tc['usable_hours']), money: String(tc['money_cny']),
553
+ }))
553
554
  }
554
555
  if (feasible.length) {
555
556
  lines.push('')
556
557
  const best = feasible[0]
557
558
  const alt = feasible[1]
558
- lines.push(`**建议:${best['name']}**(意象匹配 ${((best['imagery_match'] as number) * 100).toFixed(0)}%)。`
559
- + (alt ? `备选:${alt['name']}(¥${(alt['true_cost'] as Record<string, unknown>)['money_cny']},匹配 ${((alt['imagery_match'] as number) * 100).toFixed(0)}%)。` : ''))
559
+ lines.push(i18nT('md.recommend', { best: String(best['name']), match: ((best['imagery_match'] as number) * 100).toFixed(0) })
560
+ + (alt ? i18nT('md.alt', {
561
+ name: String(alt['name']), money: String((alt['true_cost'] as Record<string, unknown>)['money_cny']),
562
+ match: ((alt['imagery_match'] as number) * 100).toFixed(0),
563
+ }) : ''))
560
564
  }
561
565
  lines.push('')
562
- lines.push('**待你决定的两个问题**:')
566
+ lines.push(i18nT('md.decide_two'))
563
567
  if (feasible.length >= 2) {
564
- lines.push(`1. ${feasible[0]['name']} 还是 ${feasible[1]['name']}?(前者更贴意象,后者更省)`)
568
+ lines.push(i18nT('md.q_choice', { a: String(feasible[0]['name']), b: String(feasible[1]['name']) }))
565
569
  } else if (feasible.length === 1) {
566
- lines.push(`1. 就去 ${feasible[0]['name']} 吗?`)
570
+ lines.push(i18nT('md.q_single', { name: String(feasible[0]['name']) }))
567
571
  } else {
568
- lines.push('1. 所有候选都不可行——考虑放宽哪条约束?')
572
+ lines.push(i18nT('md.q_none'))
569
573
  }
570
574
  const p0 = parked[0]
571
575
  const p0wish = p0?.['wish_pool'] as Record<string, unknown> | null
572
576
  if (p0wish) {
573
577
  const conds = p0wish['conditions'] as Record<string, unknown>
574
- lines.push(`2. 把 ${p0!['name']} 留给「下一次出发」(${conds['days']} 天起),这次先去可行的?`)
578
+ lines.push(i18nT('md.q_wish', { name: String(p0!['name']), days: String(conds['days']) }))
575
579
  } else if (feasible.length) {
576
580
  const ch = feasible[0]['chosen'] as Record<string, unknown>
577
- lines.push(`2. 出发班次选 ${ch['out_service']} 还是更晚的?`)
581
+ lines.push(i18nT('md.q_depart', { id: String(ch['out_service']), dep: '' }))
578
582
  }
579
583
  return lines.join('\n')
580
584
  }