@danceiny/gotry 0.0.1-rc.10 → 0.0.1-rc.12
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 +35 -24
- package/cordis.gotry-patch.yml +14 -7
- package/dist/capabilities/flyai.js +135 -30
- package/dist/capabilities/hbcli.js +14 -2
- package/dist/capabilities/session/benchmark.js +252 -0
- package/dist/capabilities/session/transport.js +14 -9
- package/dist/capabilities/session-consent.js +82 -0
- package/dist/capabilities/session-login.js +103 -0
- package/dist/capabilities/session-search.js +5 -3
- package/dist/capabilities/weather.js +72 -17
- package/dist/scripts/agent-reach-wrapper-tests.js +4 -0
- package/dist/scripts/async-collect.js +33 -5
- package/dist/scripts/hbcli-tests.js +25 -4
- package/dist/scripts/i18n-tests.js +34 -0
- package/dist/scripts/ledger-tests.js +134 -4
- package/dist/scripts/memory-value-report.js +379 -0
- package/dist/scripts/product-metrics.js +569 -0
- package/dist/scripts/realtime-pricing-tests.js +115 -0
- package/dist/scripts/replay-real.js +3 -1
- package/dist/scripts/session-benchmark.js +261 -0
- package/dist/scripts/session-login.js +28 -0
- package/dist/scripts/session-tests.js +263 -22
- package/dist/scripts/smoke.js +112 -14
- package/dist/scripts/weather-tests.js +8 -1
- 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/index.js +124 -5
- package/dist/src/journey.js +78 -82
- package/dist/src/loop.js +65 -14
- package/dist/src/realtime-pricing.js +105 -0
- package/dist/src/state-ledger.js +30 -4
- package/dist/src/unified.js +158 -105
- package/dist/src/z3-shared.js +18 -0
- package/package.json +5 -3
- package/ts/capabilities/flyai.ts +177 -22
- package/ts/capabilities/hbcli.ts +16 -4
- package/ts/capabilities/session/benchmark.ts +273 -0
- package/ts/capabilities/session/transport.ts +19 -8
- package/ts/capabilities/session-consent.ts +127 -0
- package/ts/capabilities/session-login.ts +130 -0
- package/ts/capabilities/session-search.ts +11 -4
- package/ts/capabilities/weather.ts +84 -19
- package/ts/scripts/async-collect.ts +48 -7
- package/ts/src/index.ts +107 -11
- package/ts/src/loop.ts +85 -12
- package/ts/src/state-ledger.ts +48 -4
- package/ts/src/unified.ts +40 -36
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/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
|
21
21
|
import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.js';
|
|
22
22
|
import { flyaiSearch } from '../capabilities/flyai.js';
|
|
23
23
|
import { sessionFlightSearch } from '../capabilities/session-search.js';
|
|
24
|
+
import { sessionLogin } from '../capabilities/session-login.js';
|
|
25
|
+
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.js';
|
|
24
26
|
export const name = 'gotry-tools';
|
|
25
27
|
export const inject = [
|
|
26
28
|
'tools',
|
|
@@ -29,7 +31,8 @@ export const inject = [
|
|
|
29
31
|
export const Config = z.object({
|
|
30
32
|
stateRoot: z.string().default('.'),
|
|
31
33
|
timeoutMs: z.number().default(30_000),
|
|
32
|
-
hbcliBin: z.string().default('hbcli')
|
|
34
|
+
hbcliBin: z.string().default('hbcli'),
|
|
35
|
+
sessionAccess: z.string().default('ask')
|
|
33
36
|
});
|
|
34
37
|
const unwrapQuery = interpretArgs;
|
|
35
38
|
function renderMotivationBrief(stateRoot) {
|
|
@@ -95,6 +98,13 @@ export function apply(ctx, config) {
|
|
|
95
98
|
uncaughtException: 'gotry-tools',
|
|
96
99
|
unhandledRejection: 'gotry-tools'
|
|
97
100
|
});
|
|
101
|
+
const ctxOn = ctx.on;
|
|
102
|
+
if (typeof ctxOn === 'function') {
|
|
103
|
+
ctx.on('tools/pre-execute', createConsentGate({
|
|
104
|
+
access: ()=>config.sessionAccess ?? 'ask',
|
|
105
|
+
approval: approvalFromContext(ctx)
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
98
108
|
const registerGuarded = (tool)=>{
|
|
99
109
|
const t = {
|
|
100
110
|
...tool
|
|
@@ -776,12 +786,12 @@ export function apply(ctx, config) {
|
|
|
776
786
|
}));
|
|
777
787
|
registerGuarded(defineTool({
|
|
778
788
|
name: 'gotry_flyai_search',
|
|
779
|
-
description: 'Live
|
|
789
|
+
description: 'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). ' + 'kind="flight"|"train": { kind, from, to, date } (中文城市名, date YYYY-MM-DD) — real schedules & prices, split 直达/中转 in results. ' + 'kind="hotel": { kind:"hotel", to:"大理"(目的地中文), checkIn?, checkOut? (YYYY-MM-DD,成对可选——未定档期可不填先摸底), keyWords? }. ' + 'Hotel prices may be masked upstream (priceRaw like "¥7xx"): always present the mask as a range, and let the human open jumpUrl for the real price. ' + 'Evidence [实时API:flyai@ts]. Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.',
|
|
780
790
|
parameters: {
|
|
781
791
|
query: {
|
|
782
792
|
type: 'json',
|
|
783
793
|
required: true,
|
|
784
|
-
description: '
|
|
794
|
+
description: 'kind=flight|train: { kind, from: "上海", to: "丽江", date: "2026-10-01" }; kind="hotel": { kind:"hotel", to:"大理", checkIn?: "YYYY-MM-DD", checkOut?: "YYYY-MM-DD", keyWords?: "洱海" }'
|
|
785
795
|
}
|
|
786
796
|
},
|
|
787
797
|
output: {
|
|
@@ -797,6 +807,50 @@ export function apply(ctx, config) {
|
|
|
797
807
|
},
|
|
798
808
|
async execute (args, _exec) {
|
|
799
809
|
const q = unwrapQuery(args, 'from');
|
|
810
|
+
if (q.kind === 'hotel') {
|
|
811
|
+
const dest = (q.to ?? '').trim();
|
|
812
|
+
if (!dest) return {
|
|
813
|
+
ok: false,
|
|
814
|
+
summary: 'kind=hotel 需要 to(目的地中文,如 大理)'
|
|
815
|
+
};
|
|
816
|
+
if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn)) {
|
|
817
|
+
return {
|
|
818
|
+
ok: false,
|
|
819
|
+
summary: '酒店 checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不填,先摸底)'
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (q.checkIn) {
|
|
823
|
+
const now = new Date();
|
|
824
|
+
const todayYmd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
825
|
+
if (q.checkIn < todayYmd || (q.checkOut ?? '') < q.checkIn) {
|
|
826
|
+
return JSON.parse(JSON.stringify({
|
|
827
|
+
ok: false,
|
|
828
|
+
verdict: 'error',
|
|
829
|
+
kind: 'hotel',
|
|
830
|
+
summary: `未发起查询:入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${todayYmd})。向用户确认日期后再查。`
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
if ((q.checkOut ?? '').length !== 10) {
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
summary: 'checkOut 需 YYYY-MM-DD(与 checkIn 成对)'
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const r = await flyaiSearch({
|
|
841
|
+
kind: 'hotel',
|
|
842
|
+
destName: dest,
|
|
843
|
+
checkInDate: q.checkIn,
|
|
844
|
+
checkOutDate: q.checkOut,
|
|
845
|
+
keyWords: q.keyWords
|
|
846
|
+
});
|
|
847
|
+
const top = (r.hotels ?? []).slice(0, 8).map((o)=>`${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`);
|
|
848
|
+
const summary = r.verdict === 'hit' ? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}` : `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
849
|
+
return JSON.parse(JSON.stringify({
|
|
850
|
+
...r,
|
|
851
|
+
summary
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
800
854
|
const kind = q.kind === 'train' ? 'train' : 'flight';
|
|
801
855
|
if (!q.from || !q.to || !q.date) {
|
|
802
856
|
return {
|
|
@@ -804,6 +858,16 @@ export function apply(ctx, config) {
|
|
|
804
858
|
summary: '需要 from/to(中文城市名)与 date(YYYY-MM-DD)'
|
|
805
859
|
};
|
|
806
860
|
}
|
|
861
|
+
const now = new Date();
|
|
862
|
+
const todayYmd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
863
|
+
if (q.date < todayYmd) {
|
|
864
|
+
return JSON.parse(JSON.stringify({
|
|
865
|
+
ok: false,
|
|
866
|
+
verdict: 'error',
|
|
867
|
+
kind,
|
|
868
|
+
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayYmd}),过去不存在在售机/火车票。` + `多为用户时间表达未带年份所致——向用户确认年份(或按未来最近的同月日修正)后再查。`
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
807
871
|
const r = await flyaiSearch({
|
|
808
872
|
kind,
|
|
809
873
|
origin: q.from,
|
|
@@ -825,10 +889,65 @@ export function apply(ctx, config) {
|
|
|
825
889
|
rawInput: args.query
|
|
826
890
|
}),
|
|
827
891
|
presentResult: (args, value)=>{
|
|
892
|
+
const isHotel = String(args.query?.kind) === 'hotel';
|
|
893
|
+
const r = value;
|
|
894
|
+
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length);
|
|
895
|
+
return {
|
|
896
|
+
card: 'generic',
|
|
897
|
+
title: `${isHotel ? '飞猪酒店' : '飞猪检索'}:${r.ok && n > 0 ? `${n} 条` : '降级'}`,
|
|
898
|
+
content: [
|
|
899
|
+
{
|
|
900
|
+
type: 'text',
|
|
901
|
+
text: String(value.summary ?? '')
|
|
902
|
+
}
|
|
903
|
+
]
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
}));
|
|
907
|
+
registerGuarded(defineTool({
|
|
908
|
+
name: 'gotry_session_login',
|
|
909
|
+
description: 'Productized login bootstrap for the account session channel (call this when gotry_session_search returns needs-login — the user never needs a terminal). ' + 'OPENS the site login entry in the USER\'S OWN Chrome and waits for the user to finish logging in on the external site. ' + 'GoTry NEVER collects, stores, or transmits credentials: no passwords, no SMS codes, no cookie values — it only checks the boolean fact "already logged in" (reads cookie NAMES only, zero values). ' + 'verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-attach (one-time Chrome remote-debugging switch instructions). ' + 'Evidence [会话:<site>-login@ts].',
|
|
910
|
+
parameters: {
|
|
911
|
+
query: {
|
|
912
|
+
type: 'json',
|
|
913
|
+
required: true,
|
|
914
|
+
description: '可空对象 {}: { waitSeconds?: number }(等待用户完成登录的上限秒数,默认 90,至多 300)'
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
output: {
|
|
918
|
+
schema: {
|
|
919
|
+
type: 'json'
|
|
920
|
+
},
|
|
921
|
+
render: (_a, v)=>[
|
|
922
|
+
{
|
|
923
|
+
type: 'text',
|
|
924
|
+
text: String(v.evidence ?? JSON.stringify(v).slice(0, 600))
|
|
925
|
+
}
|
|
926
|
+
]
|
|
927
|
+
},
|
|
928
|
+
async execute (args, _exec) {
|
|
929
|
+
const q = unwrapQuery(args, 'waitSeconds');
|
|
930
|
+
const r = await sessionLogin({
|
|
931
|
+
waitMs: typeof q?.waitSeconds === 'number' ? q.waitSeconds * 1000 : undefined
|
|
932
|
+
});
|
|
933
|
+
const summary = r.verdict === 'logged-in' ? `${r.site} 登录完成确认(票据 cookie 名已检出,只读名字)。说明:登录是在携程官网、用你自己的浏览器完成的——gotry 全程未接触任何密码/验证码/cookie 值。现在可以继续会话检索了。${r.evidence}` : r.verdict === 'pending' ? `登录入口已在你的 Chrome 打开;请在弹出的标签页里正常登录携程(登录由你在官网完成,不属于 gotry)。完成后说一声"继续",我再确认。gotry 只检查"是否已登录",永不收集你的账号信息。${r.evidence}` : r.verdict === 'needs-attach' ? `需要一次性开启你 Chrome 的远程调试开关:在你的 Chrome 地址栏打开 chrome://inspect/#remote-debugging 并打开开关,然后说一声"重试"。${r.evidence}` : `登录引导未完成:${r.error ?? '未知原因'} ${r.evidence}`;
|
|
934
|
+
return JSON.parse(JSON.stringify({
|
|
935
|
+
...r,
|
|
936
|
+
summary
|
|
937
|
+
}));
|
|
938
|
+
},
|
|
939
|
+
presentCall: (_args)=>({
|
|
940
|
+
card: 'generic',
|
|
941
|
+
title: '登录携程(用你自己的浏览器,不在 gotry 输入)',
|
|
942
|
+
kind: 'fetch',
|
|
943
|
+
rawInput: {}
|
|
944
|
+
}),
|
|
945
|
+
presentResult: (_args, value)=>{
|
|
828
946
|
const r = value;
|
|
947
|
+
const label = r.verdict === 'logged-in' ? `已登录(${(r.tickets ?? []).length} 票据)` : r.verdict === 'pending' ? '等待你在携程页面完成登录' : r.verdict ?? '降级';
|
|
829
948
|
return {
|
|
830
949
|
card: 'generic',
|
|
831
|
-
title:
|
|
950
|
+
title: `账号登录:${label}`,
|
|
832
951
|
content: [
|
|
833
952
|
{
|
|
834
953
|
type: 'text',
|
|
@@ -840,7 +959,7 @@ export function apply(ctx, config) {
|
|
|
840
959
|
}));
|
|
841
960
|
registerGuarded(defineTool({
|
|
842
961
|
name: 'gotry_session_search',
|
|
843
|
-
description: '
|
|
962
|
+
description: 'Search on the USER\'S OWN logged-in browser session (Ctrip flights today; the account channel, not an anonymous instance). ' + 'Consent gate: the FIRST call in a session asks the user via the runtime approval card; once granted it holds for the session, a refusal revokes it for the session (no repeat prompting). ' + 'ReadGuard = physically read-only (write requests aborted at network layer; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). ' + 'Currently ctrip-flight: sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. ' + 'verdict needs-login = call gotry_session_login (product tool: opens the Ctrip login entry in the user\'s own Chrome and waits for them to log in on the external site — no terminal, no credentials through GoTry); needs-attach = one-time Chrome remote-debugging switch. ' + 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).',
|
|
844
963
|
parameters: {
|
|
845
964
|
query: {
|
|
846
965
|
type: 'json',
|
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;
|
package/dist/src/loop.js
CHANGED
|
@@ -71,6 +71,35 @@ export function validateSpec(spec) {
|
|
|
71
71
|
}
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
|
+
export const ASYNC_TERMINAL_SCHEMA = 'gotry_async_terminal.v1';
|
|
75
|
+
function asyncTerminalOutcome(ticketId, checks) {
|
|
76
|
+
const failedChecks = Object.entries(checks).filter(([, passed])=>!passed).map(([name])=>name);
|
|
77
|
+
return {
|
|
78
|
+
schema: ASYNC_TERMINAL_SCHEMA,
|
|
79
|
+
ticket_id: ticketId,
|
|
80
|
+
status: failedChecks.length === 0 ? 'succeeded' : 'failed',
|
|
81
|
+
passed: Object.values(checks).filter(Boolean).length,
|
|
82
|
+
total: 4,
|
|
83
|
+
checks,
|
|
84
|
+
failed_checks: failedChecks
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function terminalOutcomeFromState(ticketId, state) {
|
|
88
|
+
if (!state.spec || !state.solve) {
|
|
89
|
+
return asyncTerminalOutcome(ticketId, {
|
|
90
|
+
'1_承诺时间后必有明确产物': false,
|
|
91
|
+
'2_产物通过自检清单': false,
|
|
92
|
+
'3_待决问题全部是简单选择题': false,
|
|
93
|
+
'4_做不到的诚实说': false
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return asyncTerminalOutcome(ticketId, {
|
|
97
|
+
'1_承诺时间后必有明确产物': Boolean(state.solve.legs?.length || state.solve.verdicts?.length),
|
|
98
|
+
'2_产物通过自检清单': state.solve.feasible ? state.solve.legs?.every((l)=>l['energy_pct'] !== undefined) ?? false : Boolean(state.solve.unsat_core?.length),
|
|
99
|
+
'3_待决问题全部是简单选择题': state.gates.every((g)=>g.id === 'budget' || g.options.length >= 2),
|
|
100
|
+
'4_做不到的诚实说': state.solve.feasible || Boolean(state.solve.suggestions?.length)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
74
103
|
export function isComplex(state) {
|
|
75
104
|
const p = state.profile;
|
|
76
105
|
return Boolean(p.workWindow && p.bookedResources) && (state.gates.length > 0 || Boolean(state.spec));
|
|
@@ -95,22 +124,22 @@ export async function requestDeepPlanning(state) {
|
|
|
95
124
|
}
|
|
96
125
|
export async function collectDeepPlanning(state, ticket, solve) {
|
|
97
126
|
const spec = state.spec;
|
|
98
|
-
if (!spec)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
if (!spec) {
|
|
128
|
+
const outcome = terminalOutcomeFromState(ticket.id, state);
|
|
129
|
+
return {
|
|
130
|
+
reply: '(内部状态缺失 spec——深度规划未就绪,这是不应发生的路径)',
|
|
131
|
+
state,
|
|
132
|
+
outcome
|
|
133
|
+
};
|
|
134
|
+
}
|
|
102
135
|
state.solve = await solve(spec);
|
|
103
136
|
state.gates = state.gates.filter((g)=>!g.id.startsWith('async-'));
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
'2_产物通过自检清单': state.solve.feasible ? state.solve.legs?.every((l)=>l['energy_pct'] !== undefined) ?? false : Boolean(state.solve.unsat_core?.length),
|
|
107
|
-
'3_待决问题全部是简单选择题': state.gates.every((g)=>g.id === 'budget' || g.options.length >= 2),
|
|
108
|
-
'4_做不到的诚实说': state.solve.feasible || Boolean(state.solve.suggestions?.length)
|
|
109
|
-
};
|
|
110
|
-
const head = `# 回访交付:${ticket.objective}\n(工单 ${ticket.id},不失望四条:${Object.values(checks).every(Boolean) ? '4/4 ✅' : '有未达项 ❌'})`;
|
|
137
|
+
const outcome = terminalOutcomeFromState(ticket.id, state);
|
|
138
|
+
const head = `# 回访交付:${ticket.objective}\n(工单 ${ticket.id},不失望四条:${outcome.status === 'succeeded' ? '4/4 ✅' : '有未达项 ❌'})`;
|
|
111
139
|
return {
|
|
112
140
|
reply: `${head}\n\n${renderSolve(state)}`,
|
|
113
|
-
state
|
|
141
|
+
state,
|
|
142
|
+
outcome
|
|
114
143
|
};
|
|
115
144
|
}
|
|
116
145
|
export function renderSolve(state) {
|
|
@@ -311,13 +340,35 @@ export async function loadAsyncTicket(ticketId, stateRoot = '.') {
|
|
|
311
340
|
return null;
|
|
312
341
|
}
|
|
313
342
|
}
|
|
314
|
-
export async function settleAsyncTicket(ticketId, reply, stateRoot = '.') {
|
|
315
|
-
openLedgerIfExists(stateRoot)
|
|
343
|
+
export async function settleAsyncTicket(ticketId, reply, stateRoot = '.', outcome) {
|
|
344
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
345
|
+
const terminalOutcome = outcome ?? recoverAsyncTerminalOutcome(ledger, ticketId);
|
|
346
|
+
if (ledger && !terminalOutcome) {
|
|
347
|
+
throw new Error(`workflow ${ticketId} 缺少可恢复的 ${ASYNC_TERMINAL_SCHEMA},拒绝误结算`);
|
|
348
|
+
}
|
|
349
|
+
if (terminalOutcome?.status === 'failed') ledger?.failWorkflowRun(ticketId, reply, terminalOutcome);
|
|
350
|
+
else ledger?.settleWorkflowRun(ticketId, reply, terminalOutcome);
|
|
316
351
|
const dir = await asyncDir(stateRoot);
|
|
317
352
|
const p = join(dir, `${ticketId}.deliverable.md`);
|
|
318
353
|
await atomicWrite(p, reply);
|
|
319
354
|
return p;
|
|
320
355
|
}
|
|
356
|
+
function recoverAsyncTerminalOutcome(ledger, ticketId) {
|
|
357
|
+
if (!ledger) return undefined;
|
|
358
|
+
const run = ledger.getWorkflowRun(ticketId);
|
|
359
|
+
if (!run) return undefined;
|
|
360
|
+
try {
|
|
361
|
+
const state = JSON.parse(run.state_json);
|
|
362
|
+
if (!state.spec) return terminalOutcomeFromState(ticketId, state);
|
|
363
|
+
const solveStep = ledger.getWorkflowStep(ticketId, 'solve');
|
|
364
|
+
if (solveStep?.status !== 'done' || !solveStep.result) return undefined;
|
|
365
|
+
state.solve = JSON.parse(solveStep.result);
|
|
366
|
+
state.gates = state.gates.filter((g)=>!g.id.startsWith('async-'));
|
|
367
|
+
return terminalOutcomeFromState(ticketId, state);
|
|
368
|
+
} catch {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
321
372
|
export function makeJournaledSolvePort(ledger, runId, solve, opts) {
|
|
322
373
|
return async (spec)=>{
|
|
323
374
|
const step = ledger.getWorkflowStep(runId, 'solve');
|