@danceiny/gotry 0.0.1-rc.14 → 0.0.1-rc.16
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 +26 -20
- package/README.zh-CN.md +27 -21
- package/bin/gotry-bootstrap.js +331 -3
- package/cordis.gotry-patch.yml +10 -0
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +217 -0
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +1 -1
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +345 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +377 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +77 -1
- package/dist/capabilities/session-search.js +69 -2
- package/dist/capabilities/weather.js +1 -1
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/booking-saga-tests.js +187 -0
- package/dist/scripts/bootstrap-tests.js +49 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/extension-tests.js +468 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/nightly-evidence-tests.js +186 -0
- package/dist/scripts/nightly-evidence.js +289 -0
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +218 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +1 -1
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +1 -1
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke.js +159 -1
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +1 -1
- package/dist/scripts/wizard-bootstrap.js +32 -0
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +153 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +31 -7
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +322 -46
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +1 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +26 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +8 -2
- package/ts/capabilities/artifacts.ts +235 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +354 -0
- package/ts/capabilities/session/extension-channel.ts +92 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/wizard.ts +282 -0
- package/ts/capabilities/session-login.ts +53 -2
- package/ts/capabilities/session-search.ts +68 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +230 -28
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { openLedgerIfExists } from '../src/state-ledger.js';
|
|
5
|
+
const MAX_LIST = 50;
|
|
6
|
+
const MAX_WINDOW = 400;
|
|
7
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const TEXT_EXT_LANG = {
|
|
9
|
+
md: 'markdown',
|
|
10
|
+
txt: 'text',
|
|
11
|
+
json: 'json',
|
|
12
|
+
jsonl: 'json',
|
|
13
|
+
csv: 'csv',
|
|
14
|
+
log: 'text',
|
|
15
|
+
yaml: 'yaml',
|
|
16
|
+
yml: 'yaml'
|
|
17
|
+
};
|
|
18
|
+
const DIR_DENY = [
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.git'
|
|
21
|
+
];
|
|
22
|
+
function rootOf(stateRoot) {
|
|
23
|
+
return stateRoot === '.' ? process.cwd() : resolve(stateRoot);
|
|
24
|
+
}
|
|
25
|
+
function underRoot(p, root) {
|
|
26
|
+
const r = resolve(root);
|
|
27
|
+
return p === r || p.startsWith(r + sep);
|
|
28
|
+
}
|
|
29
|
+
function hasDeniedSegment(p) {
|
|
30
|
+
return p.split(sep).some((seg)=>DIR_DENY.includes(seg));
|
|
31
|
+
}
|
|
32
|
+
function asyncDeliverablePath(root, id) {
|
|
33
|
+
return join(root, 'gotry-state', 'async', `${id}.deliverable.md`);
|
|
34
|
+
}
|
|
35
|
+
function listRunsFromLedger(root, tenant, limit) {
|
|
36
|
+
const ledger = openLedgerIfExists(root, tenant);
|
|
37
|
+
if (!ledger) return [];
|
|
38
|
+
const rows = ledger.db.prepare('SELECT id, goal, status, deliverable, updated FROM workflow_runs WHERE tenant_id = ? ORDER BY updated DESC LIMIT ?').all(ledger.tenant, limit);
|
|
39
|
+
return rows.map((r)=>{
|
|
40
|
+
const file = asyncDeliverablePath(root, r.id);
|
|
41
|
+
return {
|
|
42
|
+
source: 'async-run',
|
|
43
|
+
id: r.id,
|
|
44
|
+
title: r.goal,
|
|
45
|
+
path: file,
|
|
46
|
+
status: r.status,
|
|
47
|
+
updated: r.updated,
|
|
48
|
+
bytes: r.deliverable?.length
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function listDeliverableFiles(root, limit) {
|
|
53
|
+
const dir = join(root, 'gotry-state', 'async');
|
|
54
|
+
let names = [];
|
|
55
|
+
try {
|
|
56
|
+
names = (await readdir(dir)).filter((n)=>n.endsWith('.deliverable.md'));
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const entries = [];
|
|
61
|
+
for (const n of names.slice(0, limit * 2)){
|
|
62
|
+
const p = join(dir, n);
|
|
63
|
+
const st = await stat(p).catch(()=>null);
|
|
64
|
+
if (!st?.isFile()) continue;
|
|
65
|
+
entries.push({
|
|
66
|
+
source: 'async-run',
|
|
67
|
+
id: n.replace(/\.deliverable\.md$/, ''),
|
|
68
|
+
title: n.replace(/\.deliverable\.md$/, ''),
|
|
69
|
+
path: p,
|
|
70
|
+
status: existsSync(p.replace(/\.deliverable\.md$/, '.json')) ? 'pending-view' : 'legacy',
|
|
71
|
+
updated: new Date(st.mtimeMs).toISOString(),
|
|
72
|
+
bytes: st.size
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return entries.sort((a, b)=>String(b.updated).localeCompare(String(a.updated)));
|
|
76
|
+
}
|
|
77
|
+
async function listCwdMarkdown(cwd, limit) {
|
|
78
|
+
let dirents;
|
|
79
|
+
try {
|
|
80
|
+
dirents = await readdir(cwd, {
|
|
81
|
+
withFileTypes: true
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const d of dirents){
|
|
88
|
+
if (!d.isFile() || !/\.md$/i.test(d.name) || d.name.startsWith('.')) continue;
|
|
89
|
+
const p = join(cwd, d.name);
|
|
90
|
+
const st = await stat(p).catch(()=>null);
|
|
91
|
+
if (!st) continue;
|
|
92
|
+
entries.push({
|
|
93
|
+
source: 'cwd-file',
|
|
94
|
+
id: d.name,
|
|
95
|
+
title: d.name.replace(/\.md$/i, ''),
|
|
96
|
+
path: p,
|
|
97
|
+
updated: new Date(st.mtimeMs).toISOString(),
|
|
98
|
+
bytes: st.size
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return entries.sort((a, b)=>String(b.updated).localeCompare(String(a.updated))).slice(0, limit);
|
|
102
|
+
}
|
|
103
|
+
export async function listArtifacts(opts) {
|
|
104
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 20, MAX_LIST));
|
|
105
|
+
const root = rootOf(opts.stateRoot);
|
|
106
|
+
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
107
|
+
const seenPath = new Set();
|
|
108
|
+
const merged = [];
|
|
109
|
+
for (const e of [
|
|
110
|
+
...listRunsFromLedger(root, 'local', limit),
|
|
111
|
+
...await listDeliverableFiles(root, limit),
|
|
112
|
+
...await listCwdMarkdown(cwd, limit)
|
|
113
|
+
]){
|
|
114
|
+
if (seenPath.has(e.path)) continue;
|
|
115
|
+
seenPath.add(e.path);
|
|
116
|
+
merged.push(e);
|
|
117
|
+
}
|
|
118
|
+
merged.sort((a, b)=>String(b.updated ?? '').localeCompare(String(a.updated ?? '')));
|
|
119
|
+
const total = merged.length;
|
|
120
|
+
return {
|
|
121
|
+
artifacts: merged.slice(0, limit),
|
|
122
|
+
total,
|
|
123
|
+
truncated: total > limit,
|
|
124
|
+
roots: [
|
|
125
|
+
root,
|
|
126
|
+
cwd
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function readArtifact(opts) {
|
|
131
|
+
const root = rootOf(opts.stateRoot);
|
|
132
|
+
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
133
|
+
const raw = String(opts.path ?? '').trim();
|
|
134
|
+
if (!raw) return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: 'path 必填(来自 gotry_artifacts_list 的 path,或异步工单 id)'
|
|
137
|
+
};
|
|
138
|
+
let text = null;
|
|
139
|
+
let filePath = '';
|
|
140
|
+
if (!raw.includes('/') && !raw.includes('\\') && !raw.includes('.')) {
|
|
141
|
+
const ledger = openLedgerIfExists(root, 'local');
|
|
142
|
+
const run = ledger?.db.prepare('SELECT id, goal, status, deliverable FROM workflow_runs WHERE id = ? AND tenant_id = ?').get(raw, ledger.tenant);
|
|
143
|
+
if (run?.deliverable) {
|
|
144
|
+
text = run.deliverable;
|
|
145
|
+
filePath = asyncDeliverablePath(root, raw);
|
|
146
|
+
} else {
|
|
147
|
+
const p = asyncDeliverablePath(root, raw);
|
|
148
|
+
if (existsSync(p)) {
|
|
149
|
+
filePath = p;
|
|
150
|
+
text = await readFile(p, 'utf-8');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (text === null) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: `工单 ${raw} 无 deliverable(未交付或不存在)`,
|
|
156
|
+
hint: '先 gotry_artifacts_list 看在册产物'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (text === null) {
|
|
160
|
+
const candidates = isAbsolute(raw) ? [
|
|
161
|
+
resolve(raw)
|
|
162
|
+
] : [
|
|
163
|
+
resolve(cwd, raw),
|
|
164
|
+
resolve(root, raw),
|
|
165
|
+
resolve(root, 'gotry-state', 'async', raw)
|
|
166
|
+
];
|
|
167
|
+
const allowed = candidates.find((p)=>(underRoot(p, cwd) || underRoot(p, root)) && !hasDeniedSegment(p));
|
|
168
|
+
if (!allowed) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: `路径越界:${raw}`,
|
|
172
|
+
hint: `只读 ${root} 与 dsh 工作目录内的文本产物`
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const ext = allowed.slice(allowed.lastIndexOf('.') + 1).toLowerCase();
|
|
176
|
+
if (!TEXT_EXT_LANG[ext]) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: `不支持的文件类型 .${ext}`,
|
|
180
|
+
hint: `白名单:${Object.keys(TEXT_EXT_LANG).join('/')}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const st = await stat(allowed).catch(()=>null);
|
|
184
|
+
if (!st?.isFile()) return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: `文件不存在:${raw}`,
|
|
187
|
+
hint: '先 gotry_artifacts_list 看在册产物'
|
|
188
|
+
};
|
|
189
|
+
if (st.size > MAX_BYTES) return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: `文件过大(${st.size} bytes > ${MAX_BYTES})`
|
|
192
|
+
};
|
|
193
|
+
filePath = allowed;
|
|
194
|
+
text = await readFile(allowed, 'utf-8');
|
|
195
|
+
}
|
|
196
|
+
const allLines = text.split('\n');
|
|
197
|
+
const offset = Math.max(1, Math.min(opts.offset ?? 1, allLines.length));
|
|
198
|
+
const limit = Math.max(1, Math.min(opts.limit ?? MAX_WINDOW, MAX_WINDOW));
|
|
199
|
+
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
200
|
+
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase();
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
path: filePath,
|
|
204
|
+
offset,
|
|
205
|
+
lines: slice.map((t, i)=>({
|
|
206
|
+
number: offset + i,
|
|
207
|
+
text: t
|
|
208
|
+
})),
|
|
209
|
+
totalLines: allLines.length,
|
|
210
|
+
lang: TEXT_EXT_LANG[ext] ?? 'text',
|
|
211
|
+
content: slice.join('\n'),
|
|
212
|
+
windowed: allLines.length > offset - 1 + slice.length
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/artifacts.ts
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { CircuitBreaker, withRetry } from './resilience.js';
|
|
2
|
+
import { flyaiSearch } from './flyai.js';
|
|
3
|
+
import { checkAvail, hotelRates, searchHotels } from './hbcli.js';
|
|
4
|
+
import { sessionFlightSearch } from './session-search.js';
|
|
5
|
+
import { geocodePlace, getClimate, getForecast } from './weather.js';
|
|
6
|
+
import { verifyFlight } from './opensky.js';
|
|
7
|
+
const DEFAULT_HANDLERS = {
|
|
8
|
+
FLYAI_SEARCH: (p)=>flyaiSearch(p),
|
|
9
|
+
HBCLI_HOTEL_SEARCH: (p)=>searchHotels({
|
|
10
|
+
destination: p.destination,
|
|
11
|
+
checkIn: p.checkIn,
|
|
12
|
+
checkOut: p.checkOut,
|
|
13
|
+
adults: p.adults
|
|
14
|
+
}, {
|
|
15
|
+
hbcliBin: p.hbcliBin,
|
|
16
|
+
timeoutMs: p.timeoutMs,
|
|
17
|
+
fallbackPath: p.fallbackPath
|
|
18
|
+
}),
|
|
19
|
+
HBCLI_HOTEL_RATES: (p)=>hotelRates({
|
|
20
|
+
hotelId: p.hotelId,
|
|
21
|
+
checkIn: p.checkIn,
|
|
22
|
+
checkOut: p.checkOut,
|
|
23
|
+
adults: p.adults,
|
|
24
|
+
countryCode: p.countryCode,
|
|
25
|
+
nationalityCode: p.nationalityCode,
|
|
26
|
+
residencyCode: p.residencyCode
|
|
27
|
+
}, {
|
|
28
|
+
hbcliBin: p.hbcliBin,
|
|
29
|
+
timeoutMs: p.timeoutMs
|
|
30
|
+
}),
|
|
31
|
+
HBCLI_CHECK_AVAIL: (p)=>checkAvail({
|
|
32
|
+
ratePkgId: p.ratePkgId
|
|
33
|
+
}, {
|
|
34
|
+
hbcliBin: p.hbcliBin,
|
|
35
|
+
timeoutMs: p.timeoutMs
|
|
36
|
+
}),
|
|
37
|
+
SESSION_FLIGHT_SEARCH: (p)=>sessionFlightSearch(p),
|
|
38
|
+
WEATHER_GEOCODE: (p)=>geocodePlace(p.name, {
|
|
39
|
+
count: p.count,
|
|
40
|
+
timeoutMs: p.timeoutMs
|
|
41
|
+
}),
|
|
42
|
+
WEATHER_FORECAST: (p)=>getForecast({
|
|
43
|
+
latitude: p.latitude,
|
|
44
|
+
longitude: p.longitude
|
|
45
|
+
}, {
|
|
46
|
+
days: p.days,
|
|
47
|
+
timeoutMs: p.timeoutMs
|
|
48
|
+
}),
|
|
49
|
+
WEATHER_CLIMATE: (p)=>getClimate({
|
|
50
|
+
latitude: p.latitude,
|
|
51
|
+
longitude: p.longitude
|
|
52
|
+
}, p.month, {
|
|
53
|
+
timeoutMs: p.timeoutMs
|
|
54
|
+
}),
|
|
55
|
+
OPENSKY_FLIGHT_VERIFY: (p)=>verifyFlight(p)
|
|
56
|
+
};
|
|
57
|
+
const defaultIsFailure = (r)=>r?.ok === false;
|
|
58
|
+
const flyaiTransient = (msg)=>msg.length > 0 && !/Sentinel/i.test(msg) && /timeout|timed out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket|HTTP 5/i.test(msg);
|
|
59
|
+
const API_RETRY = {
|
|
60
|
+
maxAttempts: 2,
|
|
61
|
+
baseDelayMs: 400,
|
|
62
|
+
maxDelayMs: 1_600
|
|
63
|
+
};
|
|
64
|
+
const API_BREAKER = {
|
|
65
|
+
failureThreshold: 3,
|
|
66
|
+
openMs: 30_000
|
|
67
|
+
};
|
|
68
|
+
const SPECS = {
|
|
69
|
+
FLYAI_SEARCH: {
|
|
70
|
+
channel: 'cli',
|
|
71
|
+
retry: {
|
|
72
|
+
maxAttempts: 2,
|
|
73
|
+
baseDelayMs: 500,
|
|
74
|
+
maxDelayMs: 2_000
|
|
75
|
+
},
|
|
76
|
+
breaker: {
|
|
77
|
+
failureThreshold: 3,
|
|
78
|
+
openMs: 60_000
|
|
79
|
+
},
|
|
80
|
+
isRetryable: (_r, e)=>{
|
|
81
|
+
const msg = e != null ? String(e.message ?? e) : String((_r ?? {}).error ?? '');
|
|
82
|
+
return flyaiTransient(msg);
|
|
83
|
+
},
|
|
84
|
+
isFailure: defaultIsFailure
|
|
85
|
+
},
|
|
86
|
+
HBCLI_HOTEL_SEARCH: {
|
|
87
|
+
channel: 'cli',
|
|
88
|
+
retry: null,
|
|
89
|
+
breaker: {
|
|
90
|
+
failureThreshold: 3,
|
|
91
|
+
openMs: 60_000
|
|
92
|
+
},
|
|
93
|
+
isFailure: (r)=>r.via === 'hbcli-error'
|
|
94
|
+
},
|
|
95
|
+
HBCLI_HOTEL_RATES: {
|
|
96
|
+
channel: 'cli',
|
|
97
|
+
retry: null,
|
|
98
|
+
breaker: {
|
|
99
|
+
failureThreshold: 3,
|
|
100
|
+
openMs: 60_000
|
|
101
|
+
},
|
|
102
|
+
isFailure: (r)=>r.via === 'hbcli-error'
|
|
103
|
+
},
|
|
104
|
+
HBCLI_CHECK_AVAIL: {
|
|
105
|
+
channel: 'cli',
|
|
106
|
+
retry: null,
|
|
107
|
+
breaker: {
|
|
108
|
+
failureThreshold: 3,
|
|
109
|
+
openMs: 60_000
|
|
110
|
+
},
|
|
111
|
+
isFailure: (r)=>r.via === 'hbcli-error'
|
|
112
|
+
},
|
|
113
|
+
SESSION_FLIGHT_SEARCH: {
|
|
114
|
+
channel: 'browser',
|
|
115
|
+
retry: null,
|
|
116
|
+
breaker: null,
|
|
117
|
+
isFailure: defaultIsFailure
|
|
118
|
+
},
|
|
119
|
+
WEATHER_GEOCODE: {
|
|
120
|
+
channel: 'api',
|
|
121
|
+
retry: API_RETRY,
|
|
122
|
+
breaker: API_BREAKER,
|
|
123
|
+
isFailure: defaultIsFailure
|
|
124
|
+
},
|
|
125
|
+
WEATHER_FORECAST: {
|
|
126
|
+
channel: 'api',
|
|
127
|
+
retry: API_RETRY,
|
|
128
|
+
breaker: API_BREAKER,
|
|
129
|
+
isFailure: defaultIsFailure
|
|
130
|
+
},
|
|
131
|
+
WEATHER_CLIMATE: {
|
|
132
|
+
channel: 'api',
|
|
133
|
+
retry: API_RETRY,
|
|
134
|
+
breaker: API_BREAKER,
|
|
135
|
+
isFailure: defaultIsFailure
|
|
136
|
+
},
|
|
137
|
+
OPENSKY_FLIGHT_VERIFY: {
|
|
138
|
+
channel: 'api',
|
|
139
|
+
retry: API_RETRY,
|
|
140
|
+
breaker: API_BREAKER,
|
|
141
|
+
isRetryable: (_r, e)=>e != null || (_r ?? {}).verdict === 'unavailable',
|
|
142
|
+
isFailure: (r)=>r.via === 'opensky-error'
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
export function declinedObservation(effect, trace) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
verdict: 'error',
|
|
149
|
+
summary: trace.declined === 'circuit-open' ? `${effect} 通道被断路器开启保护(连续失败达到阈值,冷却中)——不要立即重试,换其他工具或稍后再试;原因见 trace 证据链` : `${effect} 未登记效应(effect_interpreter.v1 注册表外,生产解译器拒绝)`,
|
|
150
|
+
evidence: trace.evidence.join(';')
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const defaultBreakers = new Map();
|
|
154
|
+
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
155
|
+
export function __resetEffectBreakersForTest() {
|
|
156
|
+
defaultBreakers.clear();
|
|
157
|
+
}
|
|
158
|
+
export function makeProductionInterpreter(opts = {}) {
|
|
159
|
+
const breakers = opts.breakers ?? defaultBreakers;
|
|
160
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
161
|
+
return async (fx)=>{
|
|
162
|
+
const ts = new Date().toISOString();
|
|
163
|
+
const name = fx.effect;
|
|
164
|
+
const spec = SPECS[name];
|
|
165
|
+
const handler = opts.handlers?.[fx.effect] ?? DEFAULT_HANDLERS[name];
|
|
166
|
+
if (!spec || !handler) {
|
|
167
|
+
return {
|
|
168
|
+
result: null,
|
|
169
|
+
trace: {
|
|
170
|
+
effect: fx.effect,
|
|
171
|
+
channel: 'api',
|
|
172
|
+
attempts: 0,
|
|
173
|
+
backoffMs: 0,
|
|
174
|
+
breaker: 'off',
|
|
175
|
+
declined: 'unknown-effect',
|
|
176
|
+
evidence: [
|
|
177
|
+
`[效应:${fx.effect}@${ts}] unknown effect`
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const br = (()=>{
|
|
183
|
+
if (!spec.breaker) return null;
|
|
184
|
+
const existing = breakers.get(fx.effect);
|
|
185
|
+
if (existing) return existing;
|
|
186
|
+
const created = new CircuitBreaker({
|
|
187
|
+
...spec.breaker,
|
|
188
|
+
...opts.now ? {
|
|
189
|
+
now: opts.now
|
|
190
|
+
} : {}
|
|
191
|
+
});
|
|
192
|
+
breakers.set(fx.effect, created);
|
|
193
|
+
return created;
|
|
194
|
+
})();
|
|
195
|
+
const gate = br?.canAttempt() ?? {
|
|
196
|
+
allowed: true,
|
|
197
|
+
state: 'closed'
|
|
198
|
+
};
|
|
199
|
+
if (!gate.allowed) {
|
|
200
|
+
const trace = {
|
|
201
|
+
effect: fx.effect,
|
|
202
|
+
channel: spec.channel,
|
|
203
|
+
attempts: 0,
|
|
204
|
+
backoffMs: 0,
|
|
205
|
+
breaker: gate.state,
|
|
206
|
+
declined: 'circuit-open',
|
|
207
|
+
evidence: [
|
|
208
|
+
`[效应:${fx.effect}@${ts}] breaker=${gate.state} 拒绝(冷却中,不重试)`
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
result: null,
|
|
213
|
+
trace
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const policy = spec.retry ? {
|
|
217
|
+
maxAttempts: spec.retry.maxAttempts,
|
|
218
|
+
baseDelayMs: spec.retry.baseDelayMs,
|
|
219
|
+
maxDelayMs: spec.retry.maxDelayMs,
|
|
220
|
+
isRetryable: spec.isRetryable,
|
|
221
|
+
sleep
|
|
222
|
+
} : {
|
|
223
|
+
maxAttempts: 1,
|
|
224
|
+
baseDelayMs: 0,
|
|
225
|
+
maxDelayMs: 0,
|
|
226
|
+
sleep
|
|
227
|
+
};
|
|
228
|
+
const dispatch = handler;
|
|
229
|
+
const outcome = await withRetry(()=>dispatch(fx.params), policy);
|
|
230
|
+
const failed = outcome.error != null || spec.isFailure(outcome.result);
|
|
231
|
+
if (br) failed ? br.onFailure() : br.onSuccess();
|
|
232
|
+
const breakerState = br?.state() ?? 'off';
|
|
233
|
+
const evidence = [
|
|
234
|
+
`[效应:${fx.effect}@${ts}] attempts=${outcome.attempts} backoff=${outcome.backoffMs}ms breaker=${breakerState}`
|
|
235
|
+
];
|
|
236
|
+
return {
|
|
237
|
+
result: outcome.error != null ? null : outcome.result,
|
|
238
|
+
trace: {
|
|
239
|
+
effect: fx.effect,
|
|
240
|
+
channel: spec.channel,
|
|
241
|
+
attempts: outcome.attempts,
|
|
242
|
+
backoffMs: outcome.backoffMs,
|
|
243
|
+
breaker: breakerState,
|
|
244
|
+
evidence
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export const productionInterpreter = makeProductionInterpreter();
|
|
250
|
+
export async function interpretEffect(fx) {
|
|
251
|
+
return productionInterpreter(fx);
|
|
252
|
+
}
|
|
253
|
+
export function makeMockInterpreter(fixtures) {
|
|
254
|
+
return async (fx)=>{
|
|
255
|
+
const ts = new Date().toISOString();
|
|
256
|
+
const registered = Object.prototype.hasOwnProperty.call(fixtures, fx.effect);
|
|
257
|
+
if (!registered) {
|
|
258
|
+
return {
|
|
259
|
+
result: null,
|
|
260
|
+
trace: {
|
|
261
|
+
effect: fx.effect,
|
|
262
|
+
channel: 'mock',
|
|
263
|
+
attempts: 0,
|
|
264
|
+
backoffMs: 0,
|
|
265
|
+
breaker: 'off',
|
|
266
|
+
declined: 'unknown-effect',
|
|
267
|
+
evidence: [
|
|
268
|
+
`[效应:mock@${ts}] ${fx.effect} 未登记夹具`
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
result: fixtures[fx.effect],
|
|
275
|
+
trace: {
|
|
276
|
+
effect: fx.effect,
|
|
277
|
+
channel: 'mock',
|
|
278
|
+
attempts: 1,
|
|
279
|
+
backoffMs: 0,
|
|
280
|
+
breaker: 'off',
|
|
281
|
+
evidence: [
|
|
282
|
+
`[效应:mock@${ts}] ${fx.effect} 夹具回放(确定性,无网络)`
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export function selectInterpreter(env, opts = {}) {
|
|
289
|
+
return env === 'mock' ? makeMockInterpreter(opts.fixtures ?? {}) : makeProductionInterpreter(opts);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/effect.ts
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { dedupeFacts } from '../src/bookable-facts.js';
|
|
4
|
+
export const FACT_LOG_NAME = 'bookable-facts.jsonl';
|
|
5
|
+
export function factLogPath(stateRoot) {
|
|
6
|
+
const root = stateRoot === '.' ? process.cwd() : stateRoot;
|
|
7
|
+
return join(root, 'gotry-state', FACT_LOG_NAME);
|
|
8
|
+
}
|
|
9
|
+
export async function appendFacts(stateRoot, facts) {
|
|
10
|
+
if (facts.length === 0) return true;
|
|
11
|
+
try {
|
|
12
|
+
const p = factLogPath(stateRoot);
|
|
13
|
+
await mkdir(join(p, '..'), {
|
|
14
|
+
recursive: true
|
|
15
|
+
});
|
|
16
|
+
await appendFile(p, facts.map((f)=>JSON.stringify(f)).join('\n') + '\n', 'utf-8');
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function loadFactRegistry(stateRoot) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await readFile(factLogPath(stateRoot), 'utf-8');
|
|
25
|
+
const facts = [];
|
|
26
|
+
for (const line of raw.split('\n')){
|
|
27
|
+
const t = line.trim();
|
|
28
|
+
if (!t) continue;
|
|
29
|
+
try {
|
|
30
|
+
const f = JSON.parse(t);
|
|
31
|
+
if (f.schema === 'gotry_bookable_fact.v1' && f.fact_id) facts.push(f);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return dedupeFacts(facts);
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/fact-log.ts
|
|
@@ -177,6 +177,48 @@ export async function listDestinations(opts = {}) {
|
|
|
177
177
|
destinations: r.result
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
+
export async function hotelRates(query, opts = {}) {
|
|
181
|
+
const hbArgs = [
|
|
182
|
+
'search',
|
|
183
|
+
'hotel-rates',
|
|
184
|
+
'--json',
|
|
185
|
+
'--hotel-id',
|
|
186
|
+
query.hotelId
|
|
187
|
+
];
|
|
188
|
+
if (query.checkIn) hbArgs.push('--check-in', query.checkIn);
|
|
189
|
+
if (query.checkOut) hbArgs.push('--check-out', query.checkOut);
|
|
190
|
+
if (query.countryCode) hbArgs.push('--country-code', query.countryCode);
|
|
191
|
+
if (query.nationalityCode) hbArgs.push('--nationality-code', query.nationalityCode);
|
|
192
|
+
if (query.residencyCode) hbArgs.push('--residency-code', query.residencyCode);
|
|
193
|
+
if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([
|
|
194
|
+
{
|
|
195
|
+
adultCount: query.adults,
|
|
196
|
+
childrenAges: []
|
|
197
|
+
}
|
|
198
|
+
]));
|
|
199
|
+
const live = await callHbcliJson(hbArgs, opts);
|
|
200
|
+
const summary = live.via === 'hbcli-realtime' ? `hotel ${query.hotelId}:hbcli 实时报价${query.checkIn ? `(${query.checkIn} → ${query.checkOut ?? '?'})` : ''}` : `hotel ${query.hotelId}:hbcli 实时报价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed 不估算`;
|
|
201
|
+
return {
|
|
202
|
+
...live,
|
|
203
|
+
rates: live.result,
|
|
204
|
+
summary
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export async function checkAvail(query, opts = {}) {
|
|
208
|
+
const live = await callHbcliJson([
|
|
209
|
+
'search',
|
|
210
|
+
'check-avail',
|
|
211
|
+
'--json',
|
|
212
|
+
'--rate-pkg-id',
|
|
213
|
+
query.ratePkgId
|
|
214
|
+
], opts);
|
|
215
|
+
const summary = live.via === 'hbcli-realtime' ? `ratePkg ${query.ratePkgId}:hbcli 实时验价(库存与价格以后端为准)` : `ratePkg ${query.ratePkgId}:hbcli 实时验价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed`;
|
|
216
|
+
return {
|
|
217
|
+
...live,
|
|
218
|
+
avail: live.result,
|
|
219
|
+
summary
|
|
220
|
+
};
|
|
221
|
+
}
|
|
180
222
|
|
|
181
223
|
|
|
182
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/hbcli.ts
|
|
224
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/hbcli.ts
|