@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
package/dist/src/engine.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseCandidate, parseRequest } from './model.js';
|
|
2
2
|
import { evaluateChoice, minToHhmm, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!z3Promise) {
|
|
6
|
-
z3Promise = (async ()=>(await init()).Context('main'))();
|
|
7
|
-
}
|
|
8
|
-
return z3Promise;
|
|
9
|
-
}
|
|
3
|
+
import { withZ3 } from './z3-shared.js';
|
|
4
|
+
import { t } from './i18n.js';
|
|
10
5
|
const CONSTRAINT_LABEL_KEYS = [
|
|
11
6
|
'wake_floor',
|
|
12
7
|
'energy_floor',
|
|
@@ -111,8 +106,7 @@ async function unsatCoreOf(s) {
|
|
|
111
106
|
for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)));
|
|
112
107
|
return out;
|
|
113
108
|
}
|
|
114
|
-
async function cheapestPlan(enc, skip) {
|
|
115
|
-
const z3 = await getZ3();
|
|
109
|
+
async function cheapestPlan(enc, skip, z3) {
|
|
116
110
|
const opt = new z3.Optimize();
|
|
117
111
|
opt.add(enc.structure);
|
|
118
112
|
for (const name of CONSTRAINT_LABEL_KEYS){
|
|
@@ -133,109 +127,138 @@ async function cheapestPlan(enc, skip) {
|
|
|
133
127
|
function suggestText(relax, cand, req, choice, cost) {
|
|
134
128
|
const mot = req.motivation;
|
|
135
129
|
const parts = [];
|
|
136
|
-
if (relax.includes('duration')) parts.push(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (relax.includes('
|
|
141
|
-
|
|
142
|
-
|
|
130
|
+
if (relax.includes('duration')) parts.push(t('sg.duration', {
|
|
131
|
+
days: choice.days,
|
|
132
|
+
name: cand.name
|
|
133
|
+
}));
|
|
134
|
+
if (relax.includes('budget')) parts.push(t('sg.budget', {
|
|
135
|
+
cost: cost.moneyCny,
|
|
136
|
+
budget: req.budgetCny
|
|
137
|
+
}));
|
|
138
|
+
if (relax.includes('wake_floor')) parts.push(t('sg.wake_floor', {
|
|
139
|
+
wake: minToHhmm(cost.wakeMin)
|
|
140
|
+
}));
|
|
141
|
+
if (relax.includes('energy_floor')) parts.push(t('sg.energy_floor', {
|
|
142
|
+
pct: cost.energyArrivalPct,
|
|
143
|
+
min: mot.minArrivalEnergyPct
|
|
144
|
+
}));
|
|
145
|
+
if (relax.includes('usable_hours')) parts.push(t('sg.usable_hours', {
|
|
146
|
+
hours: cost.usableHours.toFixed(1),
|
|
147
|
+
need: requiredUsableHours(mot).toFixed(1)
|
|
148
|
+
}));
|
|
149
|
+
if (relax.includes('arrival_before_evening')) parts.push(t('sg.arrival', {
|
|
150
|
+
time: minToHhmm(cost.arriveStayMin)
|
|
151
|
+
}));
|
|
152
|
+
const plan = t('sg.plan', {
|
|
153
|
+
id: choice.outService.id,
|
|
154
|
+
dep: minToHhmm(choice.outService.depMin),
|
|
155
|
+
mode: choice.outTransfer.mode,
|
|
156
|
+
money: cost.moneyCny,
|
|
157
|
+
wake: minToHhmm(cost.wakeMin),
|
|
158
|
+
arrive: minToHhmm(cost.arriveStayMin),
|
|
159
|
+
percent: cost.energyArrivalPct
|
|
160
|
+
});
|
|
143
161
|
return parts.join(' + ') + `;${plan}`;
|
|
144
162
|
}
|
|
145
163
|
export async function solveCandidate(cand, req) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
const core = (await unsatCoreOf(s)).sort();
|
|
164
|
-
verdict.unsatCore = core;
|
|
165
|
-
for (const name of core){
|
|
166
|
-
const skip = new Set([
|
|
167
|
-
name
|
|
168
|
-
]);
|
|
169
|
-
const enc2 = name === 'duration' ? new Encoding(z3, cand, req, cand.minDaysForPurpose) : enc;
|
|
170
|
-
const s2 = await enc2.solverWith(skip);
|
|
171
|
-
if (!await isUnsat(s2)) {
|
|
172
|
-
const choice = await enc2.extract(s2.model());
|
|
173
|
-
const cost = evaluateChoice(cand, req, choice);
|
|
174
|
-
verdict.suggestions.push({
|
|
175
|
-
relax: [
|
|
176
|
-
name
|
|
177
|
-
],
|
|
178
|
-
text: suggestText([
|
|
179
|
-
name
|
|
180
|
-
], cand, req, choice, cost),
|
|
181
|
-
resulting: {
|
|
182
|
-
days: choice.days,
|
|
183
|
-
...trueCostToDict(cost)
|
|
184
|
-
}
|
|
185
|
-
});
|
|
164
|
+
return withZ3('engine.solveCandidate', async (z3)=>{
|
|
165
|
+
const enc = new Encoding(z3, cand, req);
|
|
166
|
+
const verdict = {
|
|
167
|
+
candidateId: cand.id,
|
|
168
|
+
name: cand.name,
|
|
169
|
+
feasible: false,
|
|
170
|
+
imageryMatch: cand.imageryMatch,
|
|
171
|
+
unsatCore: [],
|
|
172
|
+
suggestions: []
|
|
173
|
+
};
|
|
174
|
+
const s = await enc.solverWith();
|
|
175
|
+
if (!await isUnsat(s)) {
|
|
176
|
+
verdict.feasible = true;
|
|
177
|
+
verdict.choice = await enc.extract(s.model());
|
|
178
|
+
verdict.trueCost = evaluateChoice(cand, req, verdict.choice);
|
|
179
|
+
return verdict;
|
|
186
180
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
];
|
|
199
|
-
for (const name of joint.filter((n)=>n !== 'duration')){
|
|
200
|
-
const trial = new Set(joint.filter((n)=>n !== name));
|
|
201
|
-
if (!await isUnsat(await longEnc.solverWith(trial))) joint = [
|
|
202
|
-
...trial
|
|
203
|
-
];
|
|
204
|
-
}
|
|
205
|
-
const plan = await cheapestPlan(longEnc, new Set(joint));
|
|
206
|
-
if (plan) {
|
|
181
|
+
const core = (await unsatCoreOf(s)).sort();
|
|
182
|
+
verdict.unsatCore = core;
|
|
183
|
+
for (const name of core){
|
|
184
|
+
const skip = new Set([
|
|
185
|
+
name
|
|
186
|
+
]);
|
|
187
|
+
const enc2 = name === 'duration' ? new Encoding(z3, cand, req, cand.minDaysForPurpose) : enc;
|
|
188
|
+
const s2 = await enc2.solverWith(skip);
|
|
189
|
+
if (!await isUnsat(s2)) {
|
|
190
|
+
const choice = await enc2.extract(s2.model());
|
|
191
|
+
const cost = evaluateChoice(cand, req, choice);
|
|
207
192
|
verdict.suggestions.push({
|
|
208
|
-
relax:
|
|
209
|
-
|
|
193
|
+
relax: [
|
|
194
|
+
name
|
|
195
|
+
],
|
|
196
|
+
text: suggestText([
|
|
197
|
+
name
|
|
198
|
+
], cand, req, choice, cost),
|
|
210
199
|
resulting: {
|
|
211
|
-
days:
|
|
212
|
-
...trueCostToDict(
|
|
200
|
+
days: choice.days,
|
|
201
|
+
...trueCostToDict(cost)
|
|
213
202
|
}
|
|
214
203
|
});
|
|
215
204
|
}
|
|
216
205
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
206
|
+
if (core.includes('duration')) {
|
|
207
|
+
const longEnc = new Encoding(z3, cand, req, cand.minDaysForPurpose);
|
|
208
|
+
const s2 = await longEnc.solverWith(new Set([
|
|
209
|
+
'duration'
|
|
210
|
+
]));
|
|
211
|
+
if (await isUnsat(s2)) {
|
|
212
|
+
const core2 = await unsatCoreOf(s2);
|
|
213
|
+
let joint = [
|
|
214
|
+
'duration',
|
|
215
|
+
...core2.filter((n)=>n !== 'duration')
|
|
216
|
+
];
|
|
217
|
+
for (const name of joint.filter((n)=>n !== 'duration')){
|
|
218
|
+
const trial = new Set(joint.filter((n)=>n !== name));
|
|
219
|
+
if (!await isUnsat(await longEnc.solverWith(trial))) joint = [
|
|
220
|
+
...trial
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
const plan = await cheapestPlan(longEnc, new Set(joint), z3);
|
|
224
|
+
if (plan) {
|
|
225
|
+
verdict.suggestions.push({
|
|
226
|
+
relax: joint,
|
|
227
|
+
text: suggestText(joint, cand, req, plan.choice, plan.cost),
|
|
228
|
+
resulting: {
|
|
229
|
+
days: plan.choice.days,
|
|
230
|
+
...trueCostToDict(plan.cost)
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (verdict.suggestions.some((sg)=>sg.relax.includes('duration'))) {
|
|
237
|
+
const longEnc = new Encoding(z3, cand, req, cand.minDaysForPurpose);
|
|
238
|
+
const plan = await cheapestPlan(longEnc, new Set([
|
|
239
|
+
'duration',
|
|
240
|
+
'budget'
|
|
241
|
+
]), z3);
|
|
242
|
+
const conditions = {
|
|
243
|
+
days: cand.minDaysForPurpose
|
|
244
|
+
};
|
|
245
|
+
if (plan) conditions['budget_cny'] = plan.cost.moneyCny;
|
|
246
|
+
if (cand.bestMonths.length) conditions['best_months'] = cand.bestMonths;
|
|
247
|
+
verdict.wishPool = {
|
|
248
|
+
name: cand.name,
|
|
249
|
+
conditions,
|
|
250
|
+
reason: t('sg.wish_reason_engine', {
|
|
251
|
+
weights: JSON.stringify(req.motivation.weights),
|
|
252
|
+
days: req.windowDays
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return verdict;
|
|
257
|
+
});
|
|
236
258
|
}
|
|
237
259
|
export async function solve(req, candidates) {
|
|
238
|
-
const verdicts =
|
|
260
|
+
const verdicts = [];
|
|
261
|
+
for (const c of candidates)verdicts.push(await solveCandidate(c, req));
|
|
239
262
|
const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imageryMatch - a.imageryMatch);
|
|
240
263
|
const recommended = feasible[0]?.candidateId ?? null;
|
|
241
264
|
return {
|
|
@@ -284,45 +307,91 @@ function renderMarkdown(req, verdicts, recommended) {
|
|
|
284
307
|
const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imageryMatch - a.imageryMatch);
|
|
285
308
|
const parked = verdicts.filter((v)=>!v.feasible);
|
|
286
309
|
const lines = [];
|
|
287
|
-
lines.push(
|
|
288
|
-
|
|
310
|
+
lines.push(t('md.header', {
|
|
311
|
+
note: req.note
|
|
312
|
+
}));
|
|
313
|
+
lines.push(t('md.constraints', {
|
|
314
|
+
days: req.windowDays,
|
|
315
|
+
budget: req.budgetCny,
|
|
316
|
+
hours: requiredUsableHours(req.motivation).toFixed(1)
|
|
317
|
+
}));
|
|
289
318
|
lines.push('');
|
|
290
319
|
for (const v of parked){
|
|
291
|
-
lines.push(
|
|
320
|
+
lines.push(t('md.infeasible', {
|
|
321
|
+
name: v.name,
|
|
322
|
+
core: v.unsatCore.join('、')
|
|
323
|
+
}));
|
|
292
324
|
for (const sg of v.suggestions.slice(0, 1))lines.push(`- ${sg.text}`);
|
|
293
325
|
if (v.wishPool) {
|
|
294
326
|
const conds = v.wishPool.conditions;
|
|
295
|
-
const budgetNote = conds['budget_cny'] ?
|
|
327
|
+
const budgetNote = conds['budget_cny'] ? t('md.wish_budget', {
|
|
328
|
+
cny: String(conds['budget_cny'])
|
|
329
|
+
}) : '';
|
|
296
330
|
const months = conds['best_months'];
|
|
297
|
-
const season = months ?
|
|
298
|
-
|
|
331
|
+
const season = months ? t('md.wish_season', {
|
|
332
|
+
months: months.join(',')
|
|
333
|
+
}) : '';
|
|
334
|
+
lines.push(t('md.wish_pool', {
|
|
335
|
+
days: String(conds['days']),
|
|
336
|
+
budgetNote,
|
|
337
|
+
season
|
|
338
|
+
}));
|
|
299
339
|
}
|
|
300
340
|
}
|
|
301
341
|
lines.push('');
|
|
302
342
|
for (const v of feasible){
|
|
303
|
-
const c = v.choice,
|
|
304
|
-
lines.push(
|
|
343
|
+
const c = v.choice, cost = v.trueCost;
|
|
344
|
+
lines.push(t('md.feasible_engine', {
|
|
345
|
+
name: v.name,
|
|
346
|
+
id: c.outService.id,
|
|
347
|
+
dep: minToHhmm(c.outService.depMin),
|
|
348
|
+
mode: c.outTransfer.mode,
|
|
349
|
+
arrive: minToHhmm(cost.arriveStayMin),
|
|
350
|
+
wake: minToHhmm(cost.wakeMin),
|
|
351
|
+
energy: cost.energyArrivalPct,
|
|
352
|
+
d2d: `${Math.floor(cost.doorToDoorOutMin / 60)}h${String(cost.doorToDoorOutMin % 60).padStart(2, '0')}m`,
|
|
353
|
+
hours: cost.usableHours.toFixed(1),
|
|
354
|
+
money: cost.moneyCny
|
|
355
|
+
}));
|
|
305
356
|
}
|
|
306
357
|
if (feasible.length) {
|
|
307
358
|
lines.push('');
|
|
308
359
|
const best = feasible[0];
|
|
309
360
|
const alt = feasible[1];
|
|
310
|
-
lines.push(
|
|
361
|
+
lines.push(t('md.recommend', {
|
|
362
|
+
best: best.name,
|
|
363
|
+
match: (best.imageryMatch * 100).toFixed(0)
|
|
364
|
+
}) + (alt ? t('md.alt', {
|
|
365
|
+
name: alt.name,
|
|
366
|
+
money: alt.trueCost.moneyCny,
|
|
367
|
+
match: (alt.imageryMatch * 100).toFixed(0)
|
|
368
|
+
}) : ''));
|
|
311
369
|
}
|
|
312
370
|
lines.push('');
|
|
313
|
-
lines.push('
|
|
371
|
+
lines.push(t('md.decide_two'));
|
|
314
372
|
if (feasible.length >= 2) {
|
|
315
|
-
lines.push(
|
|
373
|
+
lines.push(t('md.q_choice', {
|
|
374
|
+
a: feasible[0].name,
|
|
375
|
+
b: feasible[1].name
|
|
376
|
+
}));
|
|
316
377
|
} else if (feasible.length === 1) {
|
|
317
|
-
lines.push(
|
|
378
|
+
lines.push(t('md.q_single', {
|
|
379
|
+
name: feasible[0].name
|
|
380
|
+
}));
|
|
318
381
|
} else {
|
|
319
|
-
lines.push('
|
|
382
|
+
lines.push(t('md.q_none'));
|
|
320
383
|
}
|
|
321
384
|
const p0 = parked[0];
|
|
322
385
|
if (p0?.wishPool) {
|
|
323
|
-
lines.push(
|
|
386
|
+
lines.push(t('md.q_wish', {
|
|
387
|
+
name: p0.name,
|
|
388
|
+
days: String(p0.wishPool.conditions['days'])
|
|
389
|
+
}));
|
|
324
390
|
} else if (feasible.length) {
|
|
325
|
-
lines.push(
|
|
391
|
+
lines.push(t('md.q_depart', {
|
|
392
|
+
id: feasible[0].choice.outService.id,
|
|
393
|
+
dep: minToHhmm(feasible[0].choice.outService.depMin)
|
|
394
|
+
}));
|
|
326
395
|
}
|
|
327
396
|
return lines.join('\n');
|
|
328
397
|
}
|
package/dist/src/i18n.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const ZH = {
|
|
2
|
+
'md.header': '> 憧憬:{note}',
|
|
3
|
+
'md.constraints': '> 已识别约束:窗口 {days} 天 | 预算 ¥{budget} | 动机(休整改写需求 {hours}h 有效休整)',
|
|
4
|
+
'md.infeasible': '**{name}:现在不行**——冲突约束:{core}。',
|
|
5
|
+
'md.relax_one': '- 放宽 {relax}:约 ¥{money}',
|
|
6
|
+
'md.wish_pool': '- 已放入「下一次出发」清单:需要 {days} 天{budgetNote}{season}',
|
|
7
|
+
'md.wish_budget': '、约 ¥{cny}',
|
|
8
|
+
'md.wish_season': ',{months} 月最佳',
|
|
9
|
+
'md.feasible_engine': '**{name}:可行**({id} {dep} 出发,{mode} 接驳,{arrive} 到住处,起床 {wake},到达精力 {energy}%,门到门 {d2d},有效休整 {hours}h,共 ¥{money})',
|
|
10
|
+
'md.feasible_unified': '**{name}:可行**({svc} 出发,{mode} 接驳,起床 {wake},到达精力 {energy}%,门到门 {d2d},有效休整 {hours}h,共 ¥{money})',
|
|
11
|
+
'md.recommend': '**建议:{best}**(意象匹配 {match}%)。',
|
|
12
|
+
'md.alt': '备选:{name}(¥{money},匹配 {match}%)。',
|
|
13
|
+
'md.decide_two': '**待你决定的两个问题**:',
|
|
14
|
+
'md.q_choice': '1. {a} 还是 {b}?(前者更贴意象,后者更省)',
|
|
15
|
+
'md.q_single': '1. 就去 {name} 吗?',
|
|
16
|
+
'md.q_none': '1. 所有候选都不可行——考虑放宽哪条约束?',
|
|
17
|
+
'md.q_wish': '2. 把 {name} 留给「下一次出发」({days} 天起),这次先去可行的?',
|
|
18
|
+
'md.q_depart': '2. 出发班次选 {id}({dep})还是更晚的?',
|
|
19
|
+
'sg.duration': '把行程延长到 {days} 天({name} 值得这个窗口)',
|
|
20
|
+
'sg.budget': '预算提高到 ¥{cost}(原 ¥{budget})',
|
|
21
|
+
'sg.wake_floor': '接受 {wake} 起床——破坏生物钟,与你的休整动机冲突,不推荐',
|
|
22
|
+
'sg.energy_floor': '接受到达精力 {pct}%(低于你要求的 {min}%)',
|
|
23
|
+
'sg.usable_hours': '接受有效休整 {hours}h(低于动机所需的 {need}h)',
|
|
24
|
+
'sg.arrival': '接受 {time} 才到住处',
|
|
25
|
+
'sg.plan': '可行方案:{id} {dep} 出发、{mode} 接驳,¥{money},{wake} 起床,{arrive} 到住处,到达精力 {percent}%',
|
|
26
|
+
'sg.wish_reason_engine': '动机谱系 {weights} 下,{days} 天窗口装不下这个目的地',
|
|
27
|
+
'sg.wish_reason_window': '{days} 天窗口装不下(目的需 {minDays} 天)',
|
|
28
|
+
'un.workwindow_reason': '周{wd} {dep} 起飞落在工作窗口(当地 {start}-{end})内',
|
|
29
|
+
'un.redflag_redeye': '{leg} 落地精力仅 {pct}%(红眼后直奔事务,当日不宜安排重要会议)'
|
|
30
|
+
};
|
|
31
|
+
const EN = {
|
|
32
|
+
'md.header': '> Aspiration: {note}',
|
|
33
|
+
'md.constraints': '> Constraints captured: {days}-day window | budget ¥{budget} | motivation (needs {hours}h of usable rest)',
|
|
34
|
+
'md.infeasible': '**{name}: not yet** — conflicting constraints: {core}.',
|
|
35
|
+
'md.relax_one': '- Relax {relax}: about ¥{money}',
|
|
36
|
+
'md.wish_pool': '- Added to the "next departure" list: needs {days} days{budgetNote}{season}',
|
|
37
|
+
'md.wish_budget': ', about ¥{cny}',
|
|
38
|
+
'md.wish_season': ', best in months {months}',
|
|
39
|
+
'md.feasible_engine': '**{name}: feasible** ({id} departs {dep}, {mode} transfer, at your stay by {arrive}, wake {wake}, arrival energy {energy}%, door-to-door {d2d}, {hours}h of real rest, total ¥{money})',
|
|
40
|
+
'md.feasible_unified': '**{name}: feasible** ({svc} departs, {mode} transfer, wake {wake}, arrival energy {energy}%, door-to-door {d2d}, {hours}h of real rest, total ¥{money})',
|
|
41
|
+
'md.recommend': '**Suggestion: {best}** (imagery match {match}%).',
|
|
42
|
+
'md.alt': 'Alternative: {name} (¥{money}, match {match}%).',
|
|
43
|
+
'md.decide_two': '**Two decisions for you**:',
|
|
44
|
+
'md.q_choice': '1. {a} or {b}? (former fits the vision, latter saves more)',
|
|
45
|
+
'md.q_single': '1. Go with {name}?',
|
|
46
|
+
'md.q_none': '1. All candidates are infeasible — which constraint to relax?',
|
|
47
|
+
'md.q_wish': '2. Keep {name} for the "next departure" ({days}+ days) and go feasible this time?',
|
|
48
|
+
'md.q_depart': '2. Departure: {id} ({dep}) or later?',
|
|
49
|
+
'sg.duration': 'Extend the trip to {days} days ({name} deserves this window)',
|
|
50
|
+
'sg.budget': 'Raise budget to ¥{cost} (was ¥{budget})',
|
|
51
|
+
'sg.wake_floor': 'Accept a {wake} wake-up — wrecks your body clock, conflicts with your rest motivation, not recommended',
|
|
52
|
+
'sg.energy_floor': 'Accept arrival energy {pct}% (below your required {min}%)',
|
|
53
|
+
'sg.usable_hours': 'Accept {hours}h of usable rest (below the {need}h your motivation needs)',
|
|
54
|
+
'sg.arrival': 'Accept reaching your stay only at {time}',
|
|
55
|
+
'sg.plan': 'Feasible plan: {id} departs {dep}, {mode} transfer, ¥{money}, wake {wake}, at your stay {arrive}, arrival energy {percent}%',
|
|
56
|
+
'sg.wish_reason_engine': 'under motivation weights {weights}, a {days}-day window cannot fit this destination',
|
|
57
|
+
'sg.wish_reason_window': '{days}-day window cannot fit (the purpose needs {minDays} days)',
|
|
58
|
+
'un.workwindow_reason': 'Departs {dep} on weekday {wd}, inside your work window (local {start}-{end})',
|
|
59
|
+
'un.redflag_redeye': '{leg} lands with only {pct}% energy (red-eye then straight to business — no big meetings that day)'
|
|
60
|
+
};
|
|
61
|
+
let locale = process.env['GOTRY_LOCALE'] === 'en' ? 'en' : 'zh-CN';
|
|
62
|
+
export function getLocale() {
|
|
63
|
+
return locale;
|
|
64
|
+
}
|
|
65
|
+
export function setLocale(l) {
|
|
66
|
+
locale = l;
|
|
67
|
+
}
|
|
68
|
+
export function t(key, vars = {}) {
|
|
69
|
+
const tpl = (locale === 'en' ? EN[key] : undefined) ?? ZH[key] ?? String(key);
|
|
70
|
+
return tpl.replace(/\{(\w+)\}/g, (_, k)=>k in vars ? String(vars[k]) : `{${k}}`);
|
|
71
|
+
}
|
|
72
|
+
export function enCoverage() {
|
|
73
|
+
return Object.keys(ZH).filter((k)=>EN[k] === undefined);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/i18n.ts
|
package/dist/src/journey.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { init } from 'z3-solver';
|
|
2
1
|
import { hhmmToMin, minToHhmm } from './model.js';
|
|
3
|
-
|
|
4
|
-
async function getZ3() {
|
|
5
|
-
if (!z3Promise) z3Promise = (async ()=>(await init()).Context('main'))();
|
|
6
|
-
return z3Promise;
|
|
7
|
-
}
|
|
2
|
+
import { withZ3 } from './z3-shared.js';
|
|
8
3
|
export function evaluateLeg(leg, svc) {
|
|
9
4
|
const wake = svc.depMin - leg.bufferMin - leg.originTransferMin;
|
|
10
5
|
const wakeDisplay = wake < 0 ? `${minToHhmm(wake + 1440)}(前一日)` : minToHhmm(wake);
|
|
@@ -38,87 +33,88 @@ function exactlyOne(z3, sels) {
|
|
|
38
33
|
return Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
|
|
39
34
|
}
|
|
40
35
|
export async function solveJourney(req) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const leg of legSpecs){
|
|
48
|
-
const sels = leg.services.map((_, i)=>Bool.const(`${leg.id}_s${i}`));
|
|
49
|
-
allSels[leg.id] = sels;
|
|
50
|
-
const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(leg.services[i][attr]), Int.val(0))));
|
|
51
|
-
const dep = pick('depMin'), arr = pick('arrMin'), price = pick('priceCny');
|
|
52
|
-
const wake = dep.sub(Int.val(leg.bufferMin + leg.originTransferMin));
|
|
53
|
-
const arriveStay = arr.add(Int.val(leg.destTransferMin));
|
|
54
|
-
if (leg.arriveByMin !== undefined) assertions[`${leg.id}:arrive_by`] = arriveStay.le(Int.val(leg.arriveByMin));
|
|
55
|
-
if (leg.departAfterMin !== undefined) assertions[`${leg.id}:depart_after`] = dep.ge(Int.val(leg.departAfterMin));
|
|
56
|
-
if (!leg.redEye) assertions[`${leg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
|
|
57
|
-
}
|
|
58
|
-
const total = Sum(...legSpecs.flatMap((leg)=>allSels[leg.id].map((s, i)=>If(s, Int.val(leg.services[i].priceCny), Int.val(0)))));
|
|
59
|
-
if (req.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(req.budgetCny));
|
|
60
|
-
const coreOf = (s)=>{
|
|
61
|
-
const v = s.unsatCore();
|
|
62
|
-
const out = [];
|
|
63
|
-
for(let i = 0; i < v.length(); i++){
|
|
64
|
-
out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
};
|
|
68
|
-
const extract = async (model)=>{
|
|
69
|
-
const chosen = new Map();
|
|
36
|
+
return withZ3('journey.solveJourney', async (z3)=>{
|
|
37
|
+
const { Bool, If, Sum, Int, Solver } = z3;
|
|
38
|
+
const wakeFloor = req.wakeFloorMin ?? hhmmToMin('06:00');
|
|
39
|
+
const allSels = {};
|
|
40
|
+
const assertions = {};
|
|
41
|
+
const legSpecs = req.legs;
|
|
70
42
|
for (const leg of legSpecs){
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
43
|
+
const sels = leg.services.map((_, i)=>Bool.const(`${leg.id}_s${i}`));
|
|
44
|
+
allSels[leg.id] = sels;
|
|
45
|
+
const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(leg.services[i][attr]), Int.val(0))));
|
|
46
|
+
const dep = pick('depMin'), arr = pick('arrMin'), price = pick('priceCny');
|
|
47
|
+
const wake = dep.sub(Int.val(leg.bufferMin + leg.originTransferMin));
|
|
48
|
+
const arriveStay = arr.add(Int.val(leg.destTransferMin));
|
|
49
|
+
if (leg.arriveByMin !== undefined) assertions[`${leg.id}:arrive_by`] = arriveStay.le(Int.val(leg.arriveByMin));
|
|
50
|
+
if (leg.departAfterMin !== undefined) assertions[`${leg.id}:depart_after`] = dep.ge(Int.val(leg.departAfterMin));
|
|
51
|
+
if (!leg.redEye) assertions[`${leg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
|
|
75
52
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const redFlags = [
|
|
86
|
-
...legSpecs.map((leg, i)=>leg.redEye && legReports[i].energy_pct < 50 ? `${leg.id} 落地精力仅 ${legReports[i].energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)` : '').filter(Boolean),
|
|
87
|
-
...legSpecs.map((leg, i)=>!leg.redEye && legReports[i].wakeMin >= 0 && legReports[i].wakeMin < 6 * 60 ? `${leg.id} 起床 ${legReports[i].wake}(早于 6:00,生物钟代价)` : '').filter(Boolean)
|
|
88
|
-
];
|
|
89
|
-
return {
|
|
90
|
-
feasible: true,
|
|
91
|
-
money_cny: money,
|
|
92
|
-
legs: legReports,
|
|
93
|
-
red_flags: redFlags
|
|
53
|
+
const total = Sum(...legSpecs.flatMap((leg)=>allSels[leg.id].map((s, i)=>If(s, Int.val(leg.services[i].priceCny), Int.val(0)))));
|
|
54
|
+
if (req.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(req.budgetCny));
|
|
55
|
+
const coreOf = (s)=>{
|
|
56
|
+
const v = s.unsatCore();
|
|
57
|
+
const out = [];
|
|
58
|
+
for(let i = 0; i < v.length(); i++){
|
|
59
|
+
out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
94
62
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
63
|
+
const extract = async (model)=>{
|
|
64
|
+
const chosen = new Map();
|
|
65
|
+
for (const leg of legSpecs){
|
|
66
|
+
for(let i = 0; i < allSels[leg.id].length; i++){
|
|
67
|
+
const v = await maybeAwait(model.eval(allSels[leg.id][i], true));
|
|
68
|
+
if (String(v) === 'true') chosen.set(leg.id, leg.services[i]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return chosen;
|
|
72
|
+
};
|
|
73
|
+
const s = new Solver();
|
|
74
|
+
for (const sel of Object.values(allSels))s.add(exactlyOne(z3, sel));
|
|
75
|
+
for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
|
|
76
|
+
if (String(await s.check()) !== 'unsat') {
|
|
77
|
+
const chosen = await extract(s.model());
|
|
78
|
+
const legReports = legSpecs.map((leg)=>evaluateLeg(leg, chosen.get(leg.id)));
|
|
79
|
+
const money = legReports.reduce((a, r)=>a + r.price_cny, 0);
|
|
80
|
+
const redFlags = [
|
|
81
|
+
...legSpecs.map((leg, i)=>leg.redEye && legReports[i].energy_pct < 50 ? `${leg.id} 落地精力仅 ${legReports[i].energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)` : '').filter(Boolean),
|
|
82
|
+
...legSpecs.map((leg, i)=>!leg.redEye && legReports[i].wakeMin >= 0 && legReports[i].wakeMin < 6 * 60 ? `${leg.id} 起床 ${legReports[i].wake}(早于 6:00,生物钟代价)` : '').filter(Boolean)
|
|
83
|
+
];
|
|
84
|
+
return {
|
|
85
|
+
feasible: true,
|
|
86
|
+
money_cny: money,
|
|
87
|
+
legs: legReports,
|
|
88
|
+
red_flags: redFlags
|
|
89
|
+
};
|
|
103
90
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
91
|
+
const core = coreOf(s).sort();
|
|
92
|
+
const suggestions = [];
|
|
93
|
+
for (const name of core){
|
|
94
|
+
const s2 = new Solver();
|
|
95
|
+
for (const sel of Object.values(allSels))s2.add(exactlyOne(z3, sel));
|
|
96
|
+
for (const [n2, expr] of Object.entries(assertions)){
|
|
97
|
+
if (n2 !== name) s2.addAndTrack(expr, n2);
|
|
98
|
+
}
|
|
99
|
+
if (String(await s2.check()) !== 'unsat') {
|
|
100
|
+
const chosen = await extract(s2.model());
|
|
101
|
+
const rep = legSpecs.map((leg)=>evaluateLeg(leg, chosen.get(leg.id)));
|
|
102
|
+
suggestions.push({
|
|
103
|
+
relax: name,
|
|
104
|
+
money_cny: rep.reduce((a, r)=>a + r.price_cny, 0),
|
|
105
|
+
legs: rep.map((r, i)=>({
|
|
106
|
+
...r,
|
|
107
|
+
leg: legSpecs[i].id
|
|
108
|
+
}))
|
|
109
|
+
});
|
|
110
|
+
}
|
|
115
111
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
112
|
+
return {
|
|
113
|
+
feasible: false,
|
|
114
|
+
unsat_core: core,
|
|
115
|
+
suggestions
|
|
116
|
+
};
|
|
117
|
+
});
|
|
122
118
|
}
|
|
123
119
|
async function maybeAwait(v) {
|
|
124
120
|
return v instanceof Promise ? await v : v;
|