@danceiny/gotry 0.0.1-rc.10

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/bin/gotry-inner.js +214 -0
  4. package/bin/gotry-stdio-ask.js +59 -0
  5. package/bin/gotry.js +28 -0
  6. package/cordis.gotry-patch.yml +86 -0
  7. package/data/flights_2026.json +218 -0
  8. package/data/golden_erhai.json +92 -0
  9. package/data/golden_trip_2026.json +75 -0
  10. package/data/hotels_2026.json +151 -0
  11. package/data/openflights-skeleton.json +895 -0
  12. package/data/time-slot-eval.json +384 -0
  13. package/data/yunnan-pack.json +202 -0
  14. package/dist/capabilities/agent-reach-bridge.py +90 -0
  15. package/dist/capabilities/agent-reach-deep.js +172 -0
  16. package/dist/capabilities/agent-reach.js +204 -0
  17. package/dist/capabilities/anything.js +139 -0
  18. package/dist/capabilities/flyai.js +131 -0
  19. package/dist/capabilities/hbcli.js +143 -0
  20. package/dist/capabilities/incident-log.js +100 -0
  21. package/dist/capabilities/opensky.js +74 -0
  22. package/dist/capabilities/session/action-cache.js +120 -0
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  24. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  25. package/dist/capabilities/session/extract.js +40 -0
  26. package/dist/capabilities/session/read-guard.js +99 -0
  27. package/dist/capabilities/session/transport.js +101 -0
  28. package/dist/capabilities/session-search.js +93 -0
  29. package/dist/capabilities/weather.js +180 -0
  30. package/dist/scripts/action-cache-tests.js +107 -0
  31. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  32. package/dist/scripts/agent-reach-tests.js +38 -0
  33. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  34. package/dist/scripts/anything-tests.js +95 -0
  35. package/dist/scripts/async-collect.js +37 -0
  36. package/dist/scripts/companion-tests.js +112 -0
  37. package/dist/scripts/diff-test.js +39 -0
  38. package/dist/scripts/engine-run.js +14 -0
  39. package/dist/scripts/engine-tests.js +45 -0
  40. package/dist/scripts/hbcli-tests.js +101 -0
  41. package/dist/scripts/incident-tests.js +96 -0
  42. package/dist/scripts/journey-tests.js +45 -0
  43. package/dist/scripts/ledger-tests.js +365 -0
  44. package/dist/scripts/ledger-workflow-crash.js +42 -0
  45. package/dist/scripts/memory-capture-tests.js +77 -0
  46. package/dist/scripts/memory-decay-tests.js +83 -0
  47. package/dist/scripts/memory-metrics.js +24 -0
  48. package/dist/scripts/nudge-digest.js +83 -0
  49. package/dist/scripts/opensky-check.js +20 -0
  50. package/dist/scripts/opensky-tests.js +52 -0
  51. package/dist/scripts/probe-poi-tests.js +88 -0
  52. package/dist/scripts/publish-preverify.js +68 -0
  53. package/dist/scripts/replay-async.js +44 -0
  54. package/dist/scripts/replay-real.js +38 -0
  55. package/dist/scripts/replay.js +116 -0
  56. package/dist/scripts/session-attach-diagnose.js +27 -0
  57. package/dist/scripts/session-attach-poc.js +65 -0
  58. package/dist/scripts/session-extract-tests.js +81 -0
  59. package/dist/scripts/session-tests.js +162 -0
  60. package/dist/scripts/skeleton-check.js +37 -0
  61. package/dist/scripts/skeleton-integration-test.js +22 -0
  62. package/dist/scripts/skills-contract-tests.js +85 -0
  63. package/dist/scripts/smoke.js +328 -0
  64. package/dist/scripts/state-cli-tests.js +136 -0
  65. package/dist/scripts/state-cli.js +248 -0
  66. package/dist/scripts/time-eval-tests.js +318 -0
  67. package/dist/scripts/travel-timeline-tests.js +123 -0
  68. package/dist/scripts/unified-tests.js +49 -0
  69. package/dist/scripts/weather-tests.js +48 -0
  70. package/dist/src/bridge.js +34 -0
  71. package/dist/src/companions.js +112 -0
  72. package/dist/src/contracts.js +64 -0
  73. package/dist/src/dsh-llm.js +174 -0
  74. package/dist/src/engine.js +331 -0
  75. package/dist/src/index.js +1239 -0
  76. package/dist/src/journey.js +147 -0
  77. package/dist/src/loop.js +360 -0
  78. package/dist/src/memory-capture.js +40 -0
  79. package/dist/src/memory-decay.js +31 -0
  80. package/dist/src/memory-utility.js +55 -0
  81. package/dist/src/mock-llm.js +103 -0
  82. package/dist/src/model.js +134 -0
  83. package/dist/src/slot-spec.js +165 -0
  84. package/dist/src/state-ledger.js +901 -0
  85. package/dist/src/time-anchor.js +100 -0
  86. package/dist/src/tool-packet.js +12 -0
  87. package/dist/src/travel-slots.js +144 -0
  88. package/dist/src/travel-timeline.js +78 -0
  89. package/dist/src/unified.js +538 -0
  90. package/dist/src/wish-pool.js +27 -0
  91. package/package.json +84 -0
  92. package/ts/capabilities/agent-reach-deep.ts +114 -0
  93. package/ts/capabilities/agent-reach.ts +158 -0
  94. package/ts/capabilities/anything.ts +201 -0
  95. package/ts/capabilities/flyai.ts +151 -0
  96. package/ts/capabilities/hbcli.ts +152 -0
  97. package/ts/capabilities/incident-log.ts +148 -0
  98. package/ts/capabilities/opensky.ts +121 -0
  99. package/ts/capabilities/session/action-cache.ts +148 -0
  100. package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
  101. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  102. package/ts/capabilities/session/extract.ts +54 -0
  103. package/ts/capabilities/session/read-guard.ts +141 -0
  104. package/ts/capabilities/session/transport.ts +114 -0
  105. package/ts/capabilities/session-search.ts +129 -0
  106. package/ts/capabilities/weather.ts +176 -0
  107. package/ts/package.json +27 -0
  108. package/ts/scripts/async-collect.ts +50 -0
  109. package/ts/scripts/skeleton-check.ts +49 -0
  110. package/ts/scripts/skeleton-integration-test.ts +23 -0
  111. package/ts/scripts/state-cli.ts +201 -0
  112. package/ts/src/bridge.ts +44 -0
  113. package/ts/src/companions.ts +113 -0
  114. package/ts/src/contracts.ts +165 -0
  115. package/ts/src/dsh-llm.ts +155 -0
  116. package/ts/src/index.ts +968 -0
  117. package/ts/src/loop.ts +411 -0
  118. package/ts/src/memory-capture.ts +59 -0
  119. package/ts/src/memory-decay.ts +55 -0
  120. package/ts/src/memory-utility.ts +70 -0
  121. package/ts/src/mock-llm.ts +100 -0
  122. package/ts/src/model.ts +220 -0
  123. package/ts/src/slot-spec.ts +172 -0
  124. package/ts/src/state-ledger.ts +849 -0
  125. package/ts/src/time-anchor.ts +134 -0
  126. package/ts/src/tool-packet.ts +32 -0
  127. package/ts/src/travel-slots.ts +219 -0
  128. package/ts/src/travel-timeline.ts +83 -0
  129. package/ts/src/unified.ts +582 -0
  130. package/ts/src/wish-pool.ts +66 -0
@@ -0,0 +1,64 @@
1
+ export const TOOL_CONTRACTS = {
2
+ 'gotry_interview_next': {},
3
+ 'gotry_spec_extract': {},
4
+ 'gotry_solve': {},
5
+ 'gotry_render': {},
6
+ 'gotry_wish_pool_add': {}
7
+ };
8
+ export const TRIP_STATE_JSON_SCHEMA = {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ properties: {
12
+ calendar: {
13
+ type: 'object',
14
+ additionalProperties: true,
15
+ description: '年份与已断言的星期映射;一次断言终身使用,冲突必须显式指出'
16
+ },
17
+ profile: {
18
+ type: 'object',
19
+ additionalProperties: true,
20
+ description: 'workWindow(带 evidence)/companions/budgetTier/bookedResources/motivation'
21
+ },
22
+ spec: {
23
+ type: 'json',
24
+ description: '统一行程模型 JourneySpec(见 unified.ts)'
25
+ },
26
+ solve: {
27
+ type: 'json',
28
+ description: '求解结果(锚点/排除/红旗/建议)'
29
+ },
30
+ gates: {
31
+ type: 'array',
32
+ description: '待决问题,只能是选择题',
33
+ items: {
34
+ type: 'object',
35
+ additionalProperties: true,
36
+ properties: {
37
+ id: {
38
+ type: 'string'
39
+ },
40
+ question: {
41
+ type: 'string'
42
+ }
43
+ }
44
+ }
45
+ },
46
+ wishes: {
47
+ type: 'array',
48
+ description: '「下一次出发」清单',
49
+ items: {
50
+ type: 'object',
51
+ additionalProperties: true
52
+ }
53
+ }
54
+ },
55
+ required: [
56
+ 'calendar',
57
+ 'profile',
58
+ 'gates',
59
+ 'wishes'
60
+ ]
61
+ };
62
+
63
+
64
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/contracts.ts
@@ -0,0 +1,174 @@
1
+ import { parseFlightPackToSpec } from './unified.js';
2
+ import { buildTimeAnchor } from './time-anchor.js';
3
+ import { buildSlotSystem, flagExpiredSlots, normalizeExtraction } from './travel-slots.js';
4
+ const model = ()=>process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
5
+ const base = ()=>(process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '');
6
+ async function chat(messages, json) {
7
+ const key = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
8
+ if (!key) throw new Error('LLM_API_KEY 未设置(兼容 DEEPSEEK_API_KEY 别名)——真 LLM 路径不可用,请回退 mock(ADR-8)');
9
+ const res = await fetch(`${base()}/chat/completions`, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ Authorization: `Bearer ${key}`
14
+ },
15
+ body: JSON.stringify({
16
+ model: model(),
17
+ messages,
18
+ ...json ? {
19
+ response_format: {
20
+ type: 'json_object'
21
+ }
22
+ } : {},
23
+ temperature: json ? 0 : 0.7
24
+ })
25
+ });
26
+ if (!res.ok) throw new Error(`llm ${res.status}: ${(await res.text()).slice(0, 300)}`);
27
+ const data = await res.json();
28
+ const raw = data.choices[0]?.message?.content ?? '';
29
+ return raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim() || raw;
30
+ }
31
+ function parseJsonBlock(text) {
32
+ const m = text.match(/\{[\s\S]*\}/);
33
+ if (!m) return null;
34
+ try {
35
+ return JSON.parse(m[0]);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ const FACTS_SYSTEM = `你是旅行规划的事实抽取器。从对话中抽取两类事实并以 JSON 返回:
41
+ {"calendar": {"year": 数字, "assertedWeekdays": {"YYYY-MM-DD": "mon|tue|wed|thu|fri|sat|sun"}},
42
+ "profile": {"workWindow": {"homeTzOffsetMin": 数字, "startMin": 数字, "endMin": 数字, "workdays": [0,1,2,3,4], "evidence": "用户原话"},
43
+ "companions": ["..."], "budgetTier": "economy|comfort|convenience",
44
+ "bookedResources": [{"kind": "flight|hotel", "ref": "...", "window": "..."}]}}
45
+ 只放用户明确说过的事实;没有的字段省略。分钟数从 HH:MM 换算;UTC+4 → homeTzOffsetMin=240。
46
+ **休假语义(关键)**:用户说「请假/年假/不用办公/休假」→ workWindow 输出 {"vacation": true}(不是省略!省略会触发重复追问);只有用户明确给了工作时间才输出完整 workWindow 对象。只输出 JSON。`;
47
+ const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程的**骨架**——段(移动)与锚点,不包含任何班次数据(班次来自数据层,你不要编造时刻/价格/航班号)。
48
+ 输出 JSON:{"scenario":"erhai|workation|yunnan|generic","segments":[{"id":"f1","role":"choice|fixed","route":"HKG->HKT","dateHint":"2026-07-18","anchors":{"arriveByMin":885}}]}
49
+ 规则:每个跨城移动一段;锚点只放用户明说或必然的(如"当天到"→arriveByMin 23:59=1439);时刻用当日分钟。
50
+ scenario 判定:「洱海/大理/千岛湖/太湖+选目的地」→erhai(候选集);「普吉/workation/远程办公+多城链」→workation(五段链);「云南/大理丽江」→yunnan;不确定→generic。只输出 JSON。`;
51
+ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
52
+ const pack = flightPackPath;
53
+ const historyText = (h)=>h.map((t)=>`${t.role === 'user' ? '用户' : '助手'}: ${t.text}`).join('\n');
54
+ const anchorContext = ()=>`时间锚点卡:\n${buildTimeAnchor(clock()).card}`;
55
+ return {
56
+ async extractFacts (history) {
57
+ const out = await chat([
58
+ {
59
+ role: 'system',
60
+ content: FACTS_SYSTEM
61
+ },
62
+ {
63
+ role: 'user',
64
+ content: `${anchorContext()}\n\n${historyText(history)}`
65
+ }
66
+ ], true);
67
+ const obj = parseJsonBlock(out);
68
+ if (!obj) return {
69
+ assumptions: []
70
+ };
71
+ const calendar = obj['calendar'];
72
+ const profile = obj['profile'];
73
+ const assumptions = Object.keys(profile ?? {}).map((f)=>({
74
+ field: f,
75
+ source: 'user-verbatim'
76
+ }));
77
+ return {
78
+ calendar,
79
+ profile,
80
+ assumptions
81
+ };
82
+ },
83
+ async extractSpec (history, state) {
84
+ const context = `${anchorContext()}\n已断言日历:${JSON.stringify(state.calendar.assertedWeekdays)}\nprofile:${JSON.stringify(state.profile)}`;
85
+ const out = await chat([
86
+ {
87
+ role: 'system',
88
+ content: SKELETON_SYSTEM
89
+ },
90
+ {
91
+ role: 'user',
92
+ content: `${context}\n\n${historyText(history)}`
93
+ }
94
+ ], true);
95
+ const skeleton = parseJsonBlock(out);
96
+ if (!skeleton || !Array.isArray(skeleton['segments']) || skeleton['segments'].length === 0) return null;
97
+ if (!pack) return null;
98
+ const { readFile } = await import('node:fs/promises');
99
+ const scenario = String(skeleton['scenario'] ?? 'generic');
100
+ if (scenario === 'generic') return null;
101
+ const packByScenario = {
102
+ erhai: pack.replace('flights_2026.json', 'golden_erhai.json'),
103
+ workation: pack,
104
+ yunnan: pack.replace('flights_2026.json', 'yunnan-pack.json')
105
+ };
106
+ const packPath = packByScenario[scenario];
107
+ if (!packPath) return null;
108
+ let packSpec;
109
+ try {
110
+ if (scenario === 'erhai') {
111
+ return {
112
+ segments: [],
113
+ note: 'erhai-candidates',
114
+ budgetCny: 3000
115
+ };
116
+ }
117
+ packSpec = parseFlightPackToSpec(JSON.parse(await readFile(packPath, 'utf-8')));
118
+ } catch {
119
+ return null;
120
+ }
121
+ const anchorsById = new Map(skeleton['segments'].map((s)=>[
122
+ String(s['id'] ?? ''),
123
+ s['anchors'] ?? {}
124
+ ]));
125
+ for (const seg of packSpec.segments){
126
+ const a = anchorsById.get(seg.id);
127
+ if (a?.arriveByMin !== undefined) seg.anchors = {
128
+ arriveByMin: a.arriveByMin
129
+ };
130
+ }
131
+ packSpec.workWindow = state.profile.workWindow ? {
132
+ homeTzOffsetMin: state.profile.workWindow.homeTzOffsetMin,
133
+ startMin: state.profile.workWindow.startMin,
134
+ endMin: state.profile.workWindow.endMin,
135
+ workdays: state.profile.workWindow.workdays
136
+ } : undefined;
137
+ packSpec.budgetCny = 9000;
138
+ return packSpec;
139
+ },
140
+ async extractSlots (history, now) {
141
+ const anchor = buildTimeAnchor(now ?? clock());
142
+ const out = await chat([
143
+ {
144
+ role: 'system',
145
+ content: buildSlotSystem(anchor)
146
+ },
147
+ {
148
+ role: 'user',
149
+ content: historyText(history)
150
+ }
151
+ ], true);
152
+ const obj = parseJsonBlock(out);
153
+ if (!obj) return null;
154
+ const ext = normalizeExtraction(obj, history.map((t)=>t.text).join('\n'));
155
+ return ext ? flagExpiredSlots(ext, anchor) : null;
156
+ },
157
+ async polishQuestion (q) {
158
+ const out = await chat([
159
+ {
160
+ role: 'system',
161
+ content: '把旅行规划的追问润色得更自然,保留全部信息(含为什么问),一两句话,不要加表情。'
162
+ },
163
+ {
164
+ role: 'user',
165
+ content: `【${q.key}】${q.text}(为什么问:${q.why})`
166
+ }
167
+ ], false);
168
+ return out.trim() || `【${q.key}】${q.text}`;
169
+ }
170
+ };
171
+ }
172
+
173
+
174
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/dsh-llm.ts
@@ -0,0 +1,331 @@
1
+ import { init } from 'z3-solver';
2
+ import { evaluateChoice, minToHhmm, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
3
+ let z3Promise = null;
4
+ async function getZ3() {
5
+ if (!z3Promise) {
6
+ z3Promise = (async ()=>(await init()).Context('main'))();
7
+ }
8
+ return z3Promise;
9
+ }
10
+ const CONSTRAINT_LABEL_KEYS = [
11
+ 'wake_floor',
12
+ 'energy_floor',
13
+ 'usable_hours',
14
+ 'budget',
15
+ 'duration',
16
+ 'arrival_before_evening'
17
+ ];
18
+ class Encoding {
19
+ days;
20
+ cand;
21
+ req;
22
+ selO;
23
+ selT;
24
+ selR;
25
+ selTr;
26
+ structure;
27
+ assertions;
28
+ moneyExpr;
29
+ z3;
30
+ constructor(z3, cand, req, days){
31
+ this.z3 = z3;
32
+ this.cand = cand;
33
+ this.req = req;
34
+ this.days = days ?? req.windowDays;
35
+ const { Bool, Int, If, Sum, And } = this.z3;
36
+ const mot = req.motivation;
37
+ this.selO = cand.servicesOut.map((_, i)=>Bool.const(`o${i}`));
38
+ this.selT = cand.destTransfers.map((_, j)=>Bool.const(`t${j}`));
39
+ this.selR = cand.servicesRet.map((_, k)=>Bool.const(`r${k}`));
40
+ this.selTr = cand.destTransfers.map((_, m)=>Bool.const(`tr${m}`));
41
+ const access = req.homeHubAccess[cand.hub];
42
+ const pick = (sels, values)=>Sum(...sels.map((s, i)=>If(s, Int.val(values[i]), Int.val(0))));
43
+ const depOut = pick(this.selO, cand.servicesOut.map((s)=>s.depMin));
44
+ const arrOut = pick(this.selO, cand.servicesOut.map((s)=>s.arrMin));
45
+ const priceOut = pick(this.selO, cand.servicesOut.map((s)=>s.priceCny));
46
+ const transferOut = pick(this.selT, cand.destTransfers.map((t)=>t.minutes));
47
+ const transferOutPrice = pick(this.selT, cand.destTransfers.map((t)=>t.priceCny));
48
+ const depRet = pick(this.selR, cand.servicesRet.map((s)=>s.depMin));
49
+ const priceRet = pick(this.selR, cand.servicesRet.map((s)=>s.priceCny));
50
+ const transferRet = pick(this.selTr, cand.destTransfers.map((t)=>t.minutes));
51
+ const transferRetPrice = pick(this.selTr, cand.destTransfers.map((t)=>t.priceCny));
52
+ const wake = depOut.sub(Int.val(cand.bufferOutMin + access.toHubMin));
53
+ const arriveStay = arrOut.add(transferOut);
54
+ const d2dOut = arriveStay.sub(wake);
55
+ const wakePen = If(wake.lt(Int.val(300)), Int.val(30), If(wake.lt(Int.val(360)), Int.val(25), If(wake.lt(Int.val(390)), Int.val(15), Int.val(0))));
56
+ const energy = Int.val(100).sub(wakePen).sub(Int.val(2 * 8)).sub(If(arriveStay.gt(Int.val(21 * 60)), Int.val(10), Int.val(0))).sub(If(d2dOut.gt(Int.val(6 * 60)), Int.val(10), Int.val(0)));
57
+ const dayEnd = Int.val(LATEST_ARRIVE_STAY_MIN + 3 * 60);
58
+ const day1Raw = If(arriveStay.lt(dayEnd), dayEnd.sub(arriveStay), Int.val(0));
59
+ const leaveStayRet = depRet.sub(Int.val(cand.bufferRetMin)).sub(transferRet);
60
+ const day2Raw = If(leaveStayRet.gt(Int.val(9 * 60)), leaveStayRet.sub(Int.val(9 * 60)), Int.val(0));
61
+ const midDays = Math.max(0, this.days - 2);
62
+ const usable200 = day1Raw.mul(energy.add(Int.val(100))).add(day2Raw.mul(Int.val(180))).add(Int.val(midDays * 86400));
63
+ const money = priceOut.add(priceRet).add(transferOutPrice).add(transferRetPrice).add(Int.val(cand.stayCnyPerNight * (this.days - 1) + cand.localDailyCny * this.days));
64
+ const exactlyOne = (sels)=>Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
65
+ const requiredMin = Math.round(requiredUsableHours(mot) * 60);
66
+ this.assertions = {
67
+ wake_floor: wake.ge(Int.val(mot.wakeFloorMin)),
68
+ energy_floor: energy.ge(Int.val(mot.minArrivalEnergyPct)),
69
+ usable_hours: usable200.ge(Int.val(requiredMin * 200)),
70
+ budget: money.le(Int.val(req.budgetCny)),
71
+ duration: Bool.val(this.days >= cand.minDaysForPurpose),
72
+ arrival_before_evening: arriveStay.le(Int.val(LATEST_ARRIVE_STAY_MIN))
73
+ };
74
+ this.structure = And(exactlyOne(this.selO), exactlyOne(this.selT), exactlyOne(this.selR), exactlyOne(this.selTr));
75
+ this.moneyExpr = money;
76
+ }
77
+ async solverWith(skip = new Set()) {
78
+ const s = new this.z3.Solver();
79
+ s.add(this.structure);
80
+ for (const name of CONSTRAINT_LABEL_KEYS){
81
+ if (!skip.has(name)) s.addAndTrack(this.assertions[name], name);
82
+ }
83
+ return s;
84
+ }
85
+ async extract(model) {
86
+ const chosen = async (sels, items)=>{
87
+ for(let i = 0; i < sels.length; i++){
88
+ const v = await maybeAwait(model.eval(sels[i], true));
89
+ if (String(v) === 'true' || String(String(v)) === 'true') return items[i];
90
+ }
91
+ throw new Error('no selection in model');
92
+ };
93
+ return {
94
+ outService: await chosen(this.selO, this.cand.servicesOut),
95
+ outTransfer: await chosen(this.selT, this.cand.destTransfers),
96
+ retService: await chosen(this.selR, this.cand.servicesRet),
97
+ retTransfer: await chosen(this.selTr, this.cand.destTransfers),
98
+ days: this.days
99
+ };
100
+ }
101
+ }
102
+ async function maybeAwait(v) {
103
+ return v instanceof Promise ? await v : v;
104
+ }
105
+ async function isUnsat(s) {
106
+ return String(await s.check()) === 'unsat';
107
+ }
108
+ async function unsatCoreOf(s) {
109
+ const v = s.unsatCore();
110
+ const out = [];
111
+ for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)));
112
+ return out;
113
+ }
114
+ async function cheapestPlan(enc, skip) {
115
+ const z3 = await getZ3();
116
+ const opt = new z3.Optimize();
117
+ opt.add(enc.structure);
118
+ for (const name of CONSTRAINT_LABEL_KEYS){
119
+ if (!skip.has(name)) opt.add(enc.assertions[name]);
120
+ }
121
+ const obj = opt.minimize(enc.moneyExpr);
122
+ if (await isUnsat(opt)) return null;
123
+ const best = Number(String(await maybeAwait(opt.getLower(obj))));
124
+ const s = await enc.solverWith(skip);
125
+ s.add(enc.moneyExpr.eq(z3.Int.val(best)));
126
+ if (await isUnsat(s)) return null;
127
+ const choice = await enc.extract(s.model());
128
+ return {
129
+ choice,
130
+ cost: evaluateChoice(enc.cand, enc.req, choice)
131
+ };
132
+ }
133
+ function suggestText(relax, cand, req, choice, cost) {
134
+ const mot = req.motivation;
135
+ const parts = [];
136
+ if (relax.includes('duration')) parts.push(`把行程延长到 ${choice.days} 天(${cand.name} 值得这个窗口)`);
137
+ if (relax.includes('budget')) parts.push(`预算提高到 ¥${cost.moneyCny}(原 ¥${req.budgetCny})`);
138
+ if (relax.includes('wake_floor')) parts.push(`接受 ${minToHhmm(cost.wakeMin)} 起床——破坏生物钟,与你的休整动机冲突,不推荐`);
139
+ if (relax.includes('energy_floor')) parts.push(`接受到达精力 ${cost.energyArrivalPct}%(低于你要求的 ${mot.minArrivalEnergyPct}%)`);
140
+ if (relax.includes('usable_hours')) parts.push(`接受有效休整 ${cost.usableHours.toFixed(1)}h(低于动机所需的 ${requiredUsableHours(mot).toFixed(1)}h)`);
141
+ if (relax.includes('arrival_before_evening')) parts.push(`接受 ${minToHhmm(cost.arriveStayMin)} 才到住处`);
142
+ const plan = `可行方案:${choice.outService.id} ${minToHhmm(choice.outService.depMin)} 出发、` + `${choice.outTransfer.mode} 接驳,¥${cost.moneyCny},${minToHhmm(cost.wakeMin)} 起床,` + `${minToHhmm(cost.arriveStayMin)} 到住处,到达精力 ${cost.energyArrivalPct}%`;
143
+ return parts.join(' + ') + `;${plan}`;
144
+ }
145
+ export async function solveCandidate(cand, req) {
146
+ const z3 = await getZ3();
147
+ const enc = new Encoding(z3, cand, req);
148
+ const verdict = {
149
+ candidateId: cand.id,
150
+ name: cand.name,
151
+ feasible: false,
152
+ imageryMatch: cand.imageryMatch,
153
+ unsatCore: [],
154
+ suggestions: []
155
+ };
156
+ const s = await enc.solverWith();
157
+ if (!await isUnsat(s)) {
158
+ verdict.feasible = true;
159
+ verdict.choice = await enc.extract(s.model());
160
+ verdict.trueCost = evaluateChoice(cand, req, verdict.choice);
161
+ return verdict;
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
+ });
186
+ }
187
+ }
188
+ if (core.includes('duration')) {
189
+ const longEnc = new Encoding(z3, cand, req, cand.minDaysForPurpose);
190
+ const s2 = await longEnc.solverWith(new Set([
191
+ 'duration'
192
+ ]));
193
+ if (await isUnsat(s2)) {
194
+ const core2 = await unsatCoreOf(s2);
195
+ let joint = [
196
+ 'duration',
197
+ ...core2.filter((n)=>n !== 'duration')
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) {
207
+ verdict.suggestions.push({
208
+ relax: joint,
209
+ text: suggestText(joint, cand, req, plan.choice, plan.cost),
210
+ resulting: {
211
+ days: plan.choice.days,
212
+ ...trueCostToDict(plan.cost)
213
+ }
214
+ });
215
+ }
216
+ }
217
+ }
218
+ if (verdict.suggestions.some((sg)=>sg.relax.includes('duration'))) {
219
+ const longEnc = new Encoding(z3, cand, req, cand.minDaysForPurpose);
220
+ const plan = await cheapestPlan(longEnc, new Set([
221
+ 'duration',
222
+ 'budget'
223
+ ]));
224
+ const conditions = {
225
+ days: cand.minDaysForPurpose
226
+ };
227
+ if (plan) conditions['budget_cny'] = plan.cost.moneyCny;
228
+ if (cand.bestMonths.length) conditions['best_months'] = cand.bestMonths;
229
+ verdict.wishPool = {
230
+ name: cand.name,
231
+ conditions,
232
+ reason: `动机谱系 ${JSON.stringify(req.motivation.weights)} 下,${req.windowDays} 天窗口装不下这个目的地`
233
+ };
234
+ }
235
+ return verdict;
236
+ }
237
+ export async function solve(req, candidates) {
238
+ const verdicts = await Promise.all(candidates.map((c)=>solveCandidate(c, req)));
239
+ const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imageryMatch - a.imageryMatch);
240
+ const recommended = feasible[0]?.candidateId ?? null;
241
+ return {
242
+ request: {
243
+ note: req.note,
244
+ window_days: req.windowDays,
245
+ budget_cny: req.budgetCny,
246
+ required_usable_hours: Math.round(requiredUsableHours(req.motivation) * 10) / 10
247
+ },
248
+ verdicts: verdicts.map(verdictToDict),
249
+ recommended,
250
+ answer_md: renderMarkdown(req, verdicts, recommended)
251
+ };
252
+ }
253
+ function verdictToDict(v) {
254
+ const d = {
255
+ candidate_id: v.candidateId,
256
+ name: v.name,
257
+ feasible: v.feasible,
258
+ imagery_match: v.imageryMatch,
259
+ unsat_core: v.unsatCore,
260
+ suggestions: v.suggestions.map((s)=>({
261
+ relax: s.relax,
262
+ text: s.text,
263
+ resulting: s.resulting
264
+ }))
265
+ };
266
+ if (v.choice) {
267
+ d['chosen'] = {
268
+ out_service: v.choice.outService.id,
269
+ out_transfer: v.choice.outTransfer.mode,
270
+ ret_service: v.choice.retService.id,
271
+ ret_transfer: v.choice.retTransfer.mode,
272
+ days: v.choice.days
273
+ };
274
+ }
275
+ if (v.trueCost) d['true_cost'] = trueCostToDict(v.trueCost);
276
+ if (v.wishPool) d['wish_pool'] = {
277
+ name: v.wishPool.name,
278
+ conditions: v.wishPool.conditions,
279
+ reason: v.wishPool.reason
280
+ };
281
+ return d;
282
+ }
283
+ function renderMarkdown(req, verdicts, recommended) {
284
+ const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imageryMatch - a.imageryMatch);
285
+ const parked = verdicts.filter((v)=>!v.feasible);
286
+ const lines = [];
287
+ lines.push(`> 憧憬:${req.note}`);
288
+ lines.push(`> 已识别约束:窗口 ${req.windowDays} 天 | 预算 ¥${req.budgetCny} | ` + `动机(休整改写需求 ${requiredUsableHours(req.motivation).toFixed(1)}h 有效休整)`);
289
+ lines.push('');
290
+ for (const v of parked){
291
+ lines.push(`**${v.name}:现在不行**——冲突约束:${v.unsatCore.join('、')}。`);
292
+ for (const sg of v.suggestions.slice(0, 1))lines.push(`- ${sg.text}`);
293
+ if (v.wishPool) {
294
+ const conds = v.wishPool.conditions;
295
+ const budgetNote = conds['budget_cny'] ? `、约 ¥${conds['budget_cny']}` : '';
296
+ const months = conds['best_months'];
297
+ const season = months ? `,${months} 月最佳` : '';
298
+ lines.push(`- 已放入「下一次出发」清单:需要 ${conds['days']} 天${budgetNote}${season}`);
299
+ }
300
+ }
301
+ lines.push('');
302
+ for (const v of feasible){
303
+ const c = v.choice, t = v.trueCost;
304
+ lines.push(`**${v.name}:可行**` + `(${c.outService.id} ${minToHhmm(c.outService.depMin)} 出发,` + `${c.outTransfer.mode} 接驳,${minToHhmm(t.arriveStayMin)} 到住处,` + `起床 ${minToHhmm(t.wakeMin)},到达精力 ${t.energyArrivalPct}%,` + `门到门 ${Math.floor(t.doorToDoorOutMin / 60)}h${String(t.doorToDoorOutMin % 60).padStart(2, '0')}m,` + `有效休整 ${t.usableHours.toFixed(1)}h,共 ¥${t.moneyCny})`);
305
+ }
306
+ if (feasible.length) {
307
+ lines.push('');
308
+ const best = feasible[0];
309
+ const alt = feasible[1];
310
+ lines.push(`**建议:${best.name}**(意象匹配 ${(best.imageryMatch * 100).toFixed(0)}%)。` + (alt ? `备选:${alt.name}(¥${alt.trueCost.moneyCny},匹配 ${(alt.imageryMatch * 100).toFixed(0)}%)。` : ''));
311
+ }
312
+ lines.push('');
313
+ lines.push('**待你决定的两个问题**:');
314
+ if (feasible.length >= 2) {
315
+ lines.push(`1. ${feasible[0].name} 还是 ${feasible[1].name}?(前者更贴意象,后者更省)`);
316
+ } else if (feasible.length === 1) {
317
+ lines.push(`1. 就去 ${feasible[0].name} 吗?`);
318
+ } else {
319
+ lines.push('1. 所有候选都不可行——考虑放宽哪条约束?');
320
+ }
321
+ const p0 = parked[0];
322
+ if (p0?.wishPool) {
323
+ lines.push(`2. 把 ${p0.name} 留给「下一次出发」(${p0.wishPool.conditions['days']} 天起),这次先去可行的?`);
324
+ } else if (feasible.length) {
325
+ lines.push(`2. 出发班次选 ${feasible[0].choice.outService.id}(${minToHhmm(feasible[0].choice.outService.depMin)})还是更晚的?`);
326
+ }
327
+ return lines.join('\n');
328
+ }
329
+
330
+
331
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/engine.ts