@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.
- package/README.md +18 -19
- package/dist/scripts/i18n-tests.js +34 -0
- package/dist/scripts/realtime-pricing-tests.js +115 -0
- package/dist/scripts/replay-real.js +3 -1
- package/dist/scripts/session-tests.js +4 -0
- package/dist/scripts/z3-race-tests.js +35 -0
- package/dist/src/engine.js +185 -116
- package/dist/src/i18n.js +77 -0
- package/dist/src/journey.js +78 -82
- package/dist/src/realtime-pricing.js +105 -0
- package/dist/src/unified.js +158 -105
- package/dist/src/z3-shared.js +18 -0
- package/package.json +2 -2
- package/ts/src/unified.ts +40 -36
|
@@ -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
|
package/dist/src/unified.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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:
|
|
310
|
-
|
|
311
|
-
|
|
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:
|
|
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(
|
|
487
|
-
|
|
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(
|
|
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(
|
|
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'] ?
|
|
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 ?
|
|
500
|
-
|
|
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
|
|
507
|
-
lines.push(
|
|
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(
|
|
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(
|
|
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(
|
|
565
|
+
lines.push(i18nT('md.q_single', {
|
|
566
|
+
name: String(feasible[0]['name'])
|
|
567
|
+
}));
|
|
521
568
|
} else {
|
|
522
|
-
lines.push('
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =>
|
|
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:
|
|
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(
|
|
528
|
-
lines.push(
|
|
529
|
-
|
|
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(
|
|
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(
|
|
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'] ?
|
|
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 ?
|
|
542
|
-
lines.push(
|
|
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
|
|
549
|
-
lines.push(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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(
|
|
559
|
-
+ (alt ?
|
|
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(
|
|
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(
|
|
570
|
+
lines.push(i18nT('md.q_single', { name: String(feasible[0]['name']) }))
|
|
567
571
|
} else {
|
|
568
|
-
lines.push('
|
|
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(
|
|
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(
|
|
581
|
+
lines.push(i18nT('md.q_depart', { id: String(ch['out_service']), dep: '' }))
|
|
578
582
|
}
|
|
579
583
|
return lines.join('\n')
|
|
580
584
|
}
|