@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.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/bin/gotry-inner.js +214 -0
- package/bin/gotry-stdio-ask.js +59 -0
- package/bin/gotry.js +28 -0
- package/cordis.gotry-patch.yml +86 -0
- package/data/flights_2026.json +218 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +151 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +202 -0
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +143 -0
- package/dist/capabilities/incident-log.js +100 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +99 -0
- package/dist/capabilities/session/transport.js +101 -0
- package/dist/capabilities/session-search.js +93 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +37 -0
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +101 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/ledger-tests.js +365 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +88 -0
- package/dist/scripts/publish-preverify.js +68 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +116 -0
- package/dist/scripts/session-attach-diagnose.js +27 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +37 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +328 -0
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +248 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/companions.js +112 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +174 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +1239 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +360 -0
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +103 -0
- package/dist/src/model.js +134 -0
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +901 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +538 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +84 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +158 -0
- package/ts/capabilities/anything.ts +201 -0
- package/ts/capabilities/flyai.ts +151 -0
- package/ts/capabilities/hbcli.ts +152 -0
- package/ts/capabilities/incident-log.ts +148 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/transport.ts +114 -0
- package/ts/capabilities/session-search.ts +129 -0
- package/ts/capabilities/weather.ts +176 -0
- package/ts/package.json +27 -0
- package/ts/scripts/async-collect.ts +50 -0
- package/ts/scripts/skeleton-check.ts +49 -0
- package/ts/scripts/skeleton-integration-test.ts +23 -0
- package/ts/scripts/state-cli.ts +201 -0
- package/ts/src/bridge.ts +44 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +165 -0
- package/ts/src/dsh-llm.ts +155 -0
- package/ts/src/index.ts +968 -0
- package/ts/src/loop.ts +411 -0
- package/ts/src/memory-capture.ts +59 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +100 -0
- package/ts/src/model.ts +220 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +849 -0
- package/ts/src/time-anchor.ts +134 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/unified.ts +582 -0
- package/ts/src/wish-pool.ts +66 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open-Meteo 天气能力层(免费无 key):预报 + 历史气候回退。
|
|
3
|
+
*
|
|
4
|
+
* 产品定位:替代 LLM 的「雨季常识」——季节性建议、窗口内降雨概率、
|
|
5
|
+
* 体感温度范围,全部走真实数据并带证据标注 [实时API:open-meteo@ts]。
|
|
6
|
+
*
|
|
7
|
+
* 契约(与 hbcli.ts 同构,能力层不变量):
|
|
8
|
+
* - 永不抛错:网络失败/超时/解析失败一律降级返回,不穿透到调用方;
|
|
9
|
+
* - 证据链标注:成功带 [实时API:open-meteo@ts],失败带 [实时API:open-meteo@error@ts];
|
|
10
|
+
* - 免费源无配额概念,但仍记录 latency(供 L5 成本工程度量)。
|
|
11
|
+
*
|
|
12
|
+
* 端点(全部免费,无 key):
|
|
13
|
+
* 预报: https://api.open-meteo.com/v1/forecast (≤16 天)
|
|
14
|
+
* 历史气候: https://archive-api.open-meteo.com/v1/archive (1940→,做季节性)
|
|
15
|
+
* 地理编码: https://geocoding-api.open-meteo.com/v1/search (地名→坐标)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface WeatherPoint {
|
|
19
|
+
latitude: number
|
|
20
|
+
longitude: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WeatherDaily {
|
|
24
|
+
date: string
|
|
25
|
+
tempMaxC: number
|
|
26
|
+
tempMinC: number
|
|
27
|
+
precipProbMaxPct: number | null
|
|
28
|
+
weatherCode: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WeatherResult {
|
|
32
|
+
ok: boolean
|
|
33
|
+
via: 'open-meteo' | 'open-meteo-error'
|
|
34
|
+
evidence: string
|
|
35
|
+
latencyMs: number
|
|
36
|
+
timezone?: string
|
|
37
|
+
daily?: WeatherDaily[]
|
|
38
|
+
error?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FORECAST_BASE = 'https://api.open-meteo.com/v1/forecast'
|
|
42
|
+
const ARCHIVE_BASE = 'https://archive-api.open-meteo.com/v1/archive'
|
|
43
|
+
const GEOCODE_BASE = 'https://geocoding-api.open-meteo.com/v1/search'
|
|
44
|
+
|
|
45
|
+
/** WMO weather code → 中文描述(节选最常用的码) */
|
|
46
|
+
const WMO_ZH: Record<number, string> = {
|
|
47
|
+
0: '晴', 1: '大致晴', 2: '多云', 3: '阴',
|
|
48
|
+
45: '雾', 48: '冻雾',
|
|
49
|
+
51: '毛毛雨', 53: '毛毛雨', 55: '密集毛毛雨',
|
|
50
|
+
61: '小雨', 63: '中雨', 65: '大雨',
|
|
51
|
+
66: '冻雨', 67: '强冻雨',
|
|
52
|
+
71: '小雪', 73: '中雪', 75: '大雪', 77: '雪粒',
|
|
53
|
+
80: '阵雨', 81: '强阵雨', 82: '暴雨',
|
|
54
|
+
85: '阵雪', 86: '强阵雪',
|
|
55
|
+
95: '雷暴', 96: '雷暴伴小冰雹', 99: '雷暴伴大冰雹',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function wmoLabel(code: number): string {
|
|
59
|
+
return WMO_ZH[code] ?? `天气码${code}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function fetchJson(url: string, timeoutMs: number): Promise<{ ok: boolean; data?: unknown; error?: string }> {
|
|
63
|
+
const ctrl = new AbortController()
|
|
64
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(url, { signal: ctrl.signal, headers: { Accept: 'application/json' } })
|
|
67
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
|
|
68
|
+
return { ok: true, data: await res.json() }
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return { ok: false, error: (e as Error).message.slice(0, 200) }
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timer)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 地名 → 坐标(地理编码,供 dsh 工具把「大理」转成经纬度) */
|
|
77
|
+
export async function geocodePlace(
|
|
78
|
+
name: string,
|
|
79
|
+
opts: { timeoutMs?: number; count?: number } = {},
|
|
80
|
+
): Promise<{ ok: boolean; evidence: string; results: Array<{ name: string; latitude: number; longitude: number; country?: string; admin1?: string }>; error?: string }> {
|
|
81
|
+
const ts = new Date().toISOString()
|
|
82
|
+
const count = opts.count ?? 5
|
|
83
|
+
const url = `${GEOCODE_BASE}?name=${encodeURIComponent(name)}&count=${count}&language=zh&format=json`
|
|
84
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000)
|
|
85
|
+
if (!r.ok) {
|
|
86
|
+
return { ok: false, evidence: `[实时API:open-meteo-geo@error@${ts}]`, results: [], error: r.error }
|
|
87
|
+
}
|
|
88
|
+
const results = ((r.data as { results?: unknown[] })?.results ?? []).map((item) => {
|
|
89
|
+
const it = item as Record<string, unknown>
|
|
90
|
+
return {
|
|
91
|
+
name: String(it['name'] ?? ''),
|
|
92
|
+
latitude: Number(it['latitude']),
|
|
93
|
+
longitude: Number(it['longitude']),
|
|
94
|
+
country: it['country'] ? String(it['country']) : undefined,
|
|
95
|
+
admin1: it['admin1'] ? String(it['admin1']) : undefined,
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
return { ok: true, evidence: `[实时API:open-meteo-geo@${ts}]`, results }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 未来天气预报(≤16 天) */
|
|
102
|
+
export async function getForecast(
|
|
103
|
+
point: WeatherPoint,
|
|
104
|
+
opts: { days?: number; timeoutMs?: number } = {},
|
|
105
|
+
): Promise<WeatherResult> {
|
|
106
|
+
const started = Date.now()
|
|
107
|
+
const ts = new Date().toISOString()
|
|
108
|
+
const days = Math.min(opts.days ?? 7, 16)
|
|
109
|
+
const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
|
|
110
|
+
+ `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode`
|
|
111
|
+
+ `&forecast_days=${days}&timezone=auto`
|
|
112
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000)
|
|
113
|
+
const latencyMs = Date.now() - started
|
|
114
|
+
if (!r.ok) {
|
|
115
|
+
return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: r.error }
|
|
116
|
+
}
|
|
117
|
+
const daily = (r.data as { daily?: { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; precipitation_probability_max?: (number | null)[]; weathercode: number[] }; timezone?: string }).daily
|
|
118
|
+
if (!daily) {
|
|
119
|
+
return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo@error@${ts}]`, latencyMs, error: 'empty daily payload' }
|
|
120
|
+
}
|
|
121
|
+
const rows: WeatherDaily[] = daily.time.map((date, i) => ({
|
|
122
|
+
date,
|
|
123
|
+
tempMaxC: daily.temperature_2m_max[i] ?? NaN,
|
|
124
|
+
tempMinC: daily.temperature_2m_min[i] ?? NaN,
|
|
125
|
+
precipProbMaxPct: daily.precipitation_probability_max?.[i] ?? null,
|
|
126
|
+
weatherCode: daily.weathercode[i] ?? -1,
|
|
127
|
+
}))
|
|
128
|
+
return {
|
|
129
|
+
ok: true, via: 'open-meteo',
|
|
130
|
+
evidence: `[实时API:open-meteo@${ts}]`,
|
|
131
|
+
latencyMs,
|
|
132
|
+
timezone: (r.data as { timezone?: string }).timezone,
|
|
133
|
+
daily: rows,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 历史气候(季节性推荐底座:去年同期日均温/降雨概率) */
|
|
138
|
+
export async function getClimate(
|
|
139
|
+
point: WeatherPoint,
|
|
140
|
+
month: number,
|
|
141
|
+
opts: { year?: number; timeoutMs?: number } = {},
|
|
142
|
+
): Promise<WeatherResult> {
|
|
143
|
+
const started = Date.now()
|
|
144
|
+
const ts = new Date().toISOString()
|
|
145
|
+
// 默认取去年同月做气候基线
|
|
146
|
+
const year = opts.year ?? (new Date().getFullYear() - 1)
|
|
147
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate()
|
|
148
|
+
const start = `${year}-${String(month).padStart(2, '0')}-01`
|
|
149
|
+
const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`
|
|
150
|
+
const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}`
|
|
151
|
+
+ `&start_date=${start}&end_date=${end}`
|
|
152
|
+
+ `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`
|
|
153
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000)
|
|
154
|
+
const latencyMs = Date.now() - started
|
|
155
|
+
if (!r.ok) {
|
|
156
|
+
return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: r.error }
|
|
157
|
+
}
|
|
158
|
+
const daily = (r.data as { daily?: { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; weathercode: number[] } }).daily
|
|
159
|
+
if (!daily) {
|
|
160
|
+
return { ok: false, via: 'open-meteo-error', evidence: `[实时API:open-meteo-climate@error@${ts}]`, latencyMs, error: 'empty daily payload' }
|
|
161
|
+
}
|
|
162
|
+
const rows: WeatherDaily[] = daily.time.map((date, i) => ({
|
|
163
|
+
date,
|
|
164
|
+
tempMaxC: daily.temperature_2m_max[i] ?? NaN,
|
|
165
|
+
tempMinC: daily.temperature_2m_min[i] ?? NaN,
|
|
166
|
+
precipProbMaxPct: null, // archive 端点没有概率,用降水量另行估算
|
|
167
|
+
weatherCode: daily.weathercode[i] ?? -1,
|
|
168
|
+
}))
|
|
169
|
+
return {
|
|
170
|
+
ok: true, via: 'open-meteo',
|
|
171
|
+
evidence: `[实时API:open-meteo-climate@${ts}]`,
|
|
172
|
+
latencyMs,
|
|
173
|
+
timezone: (r.data as { timezone?: string }).timezone,
|
|
174
|
+
daily: rows,
|
|
175
|
+
}
|
|
176
|
+
}
|
package/ts/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gotry/plugin",
|
|
3
|
+
"version": "0.0.1-rc.2",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "GoTry dsh plugin: motivation profile, feasibility (door-to-door true cost via unified segment model), skeleton connectivity, hotel search, wish pool — all pure TS, no Python runtime required",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"smoke": "tsx scripts/smoke.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@deepseek-ai/cordis": "*",
|
|
14
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
15
|
+
"@deepseek-ai/schemastery": "*",
|
|
16
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
17
|
+
"better-sqlite3": "^13.0.3",
|
|
18
|
+
"z3-solver": "^5.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"playwright-core": "^1.62.1",
|
|
23
|
+
"puppeteer-core": "^25.9.0",
|
|
24
|
+
"tsx": "^4.19.0",
|
|
25
|
+
"typescript": "^5.6.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 异步工单回收(S5 编排半段 → ADR-15 durable 恢复语义):加载持久化工单,
|
|
3
|
+
* 经可日志化 solve 端口执行深度规划(done 步骤复用账本结果,不重执行),
|
|
4
|
+
* 交付物落账本 + .deliverable.md 视图。可由任何后续进程执行(loopx 驱动的
|
|
5
|
+
* tick、state-cli tick、人工、未来的通知系统)——「一小时后回来」跨进程成立,
|
|
6
|
+
* 且崩溃恢复 exactly-once:重放不重复花钱。
|
|
7
|
+
* 运行(在 ts/ 下):npx tsx scripts/async-collect.ts <ticketId> [stateRoot]
|
|
8
|
+
* 环境变量 GOTRY_SOLVE_COUNT_FILE:每次真实求解追加一行(回归用计数探针)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync } from 'node:fs'
|
|
12
|
+
import { collectDeepPlanning, loadAsyncTicket, makeJournaledSolvePort, settleAsyncTicket } from '../src/loop.ts'
|
|
13
|
+
import { solveUnified } from '../src/unified.ts'
|
|
14
|
+
import { openLedgerIfExists } from '../src/state-ledger.ts'
|
|
15
|
+
|
|
16
|
+
const ticketId = process.argv[2]
|
|
17
|
+
const stateRoot = process.argv[3] ?? '.'
|
|
18
|
+
if (!ticketId) {
|
|
19
|
+
console.error('用法:npx tsx scripts/async-collect.ts <ticketId> [stateRoot](权威在账本 workflow_runs,视图在 gotry-state/async/)')
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const loaded = await loadAsyncTicket(ticketId, stateRoot)
|
|
24
|
+
if (!loaded) {
|
|
25
|
+
console.error(`工单 ${ticketId} 不存在(账本 workflow_runs / gotry-state/async/${ticketId}.json)`)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 终态幂等:已 settled 的工单复诵账本交付物,零重算
|
|
30
|
+
const ledger = openLedgerIfExists(stateRoot)
|
|
31
|
+
const run = ledger?.getWorkflowRun(ticketId)
|
|
32
|
+
if (run?.status === 'settled' && run.deliverable) {
|
|
33
|
+
console.log(`工单 ${ticketId} 已交付(账本终态,复诵不重算):\n\n${run.deliverable}`)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
if (!ledger) {
|
|
37
|
+
console.error(`工单 ${ticketId}:stateRoot(${stateRoot})无账本——持久化先于回收,不应发生`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const solve = makeJournaledSolvePort(ledger, ticketId, solveUnified as never, {
|
|
42
|
+
onRealSolve: () => {
|
|
43
|
+
const f = process.env['GOTRY_SOLVE_COUNT_FILE']
|
|
44
|
+
if (f) appendFileSync(f, 'solve\n')
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const { reply } = await collectDeepPlanning(loaded.state, loaded.ticket, solve)
|
|
49
|
+
const out = await settleAsyncTicket(ticketId, reply, stateRoot)
|
|
50
|
+
console.log(`交付物已落盘:${out}\n\n${reply}`)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFlights 骨架校验(M2 段 3,§7-1 骨架层):引擎候选集的通航性合法性校验。
|
|
3
|
+
* 数据源:data/openflights-skeleton.json(OpenFlights ODbL,枢纽间 168 对)。
|
|
4
|
+
* 语义:「查不到≠不可达」(骨架只含枢纽间),但「查到」= 候选集强证据。
|
|
5
|
+
* 运行:cd ts && npx tsx scripts/skeleton-check.ts HKG HKT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
interface Skeleton {
|
|
12
|
+
meta: { source: string; note: string; hub_filter: string[] }
|
|
13
|
+
pairs: Record<string, string[]>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cached: Skeleton | null = null
|
|
17
|
+
|
|
18
|
+
async function load(): Promise<Skeleton> {
|
|
19
|
+
if (!cached) {
|
|
20
|
+
cached = JSON.parse(await readFile(join(import.meta.dirname, '..', '..', 'data', 'openflights-skeleton.json'), 'utf-8')) as Skeleton
|
|
21
|
+
}
|
|
22
|
+
return cached
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function checkConnectivity(a: string, b: string): Promise<{ connected: boolean; airlines?: string[]; evidence: string }> {
|
|
26
|
+
const skeleton = await load()
|
|
27
|
+
const key = [a.toUpperCase(), b.toUpperCase()].sort().join('-')
|
|
28
|
+
const airlines = skeleton.pairs[key]
|
|
29
|
+
if (airlines) {
|
|
30
|
+
return { connected: true, airlines, evidence: `[骨架:openflights] ✅ ${a}↔${b} 直飞(${airlines.length}+ 航司:${airlines.join(',')})` }
|
|
31
|
+
}
|
|
32
|
+
const inHub = (skeleton.meta.hub_filter as string[]).includes(a.toUpperCase())
|
|
33
|
+
&& (skeleton.meta.hub_filter as string[]).includes(b.toUpperCase())
|
|
34
|
+
return {
|
|
35
|
+
connected: false,
|
|
36
|
+
evidence: inHub
|
|
37
|
+
? `[骨架:openflights] ❌ ${a}↔${b} 枢纽间无直飞记录——引擎应将此候选降权或要求中转`
|
|
38
|
+
: `[骨架:openflights] ○ ${a}或${b}不在枢纽集,骨架不覆盖(不作否定结论)`,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// CLI 直跑
|
|
43
|
+
// 仅作为直接 CLI 入口时执行(被 import 时不触发:此前 dsh 子进程的
|
|
44
|
+
// argv[2]/[3](--profile headless)恰好真值,每次启动白跑一次探测还污染 stdout)
|
|
45
|
+
import { pathToFileURL } from 'node:url'
|
|
46
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
47
|
+
if (isMain && process.argv[2] && process.argv[3]) {
|
|
48
|
+
console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence)
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 骨架层集成验证(§7-1 消费面):开 skeletonHub + 带 route 的段 → 结果带三值标注;
|
|
3
|
+
* 枢纽间否定只标注不排除——EK329(数据集滞后的新航线)必须存活。
|
|
4
|
+
* 运行(在 ts/ 下):npx tsx scripts/skeleton-integration-test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { parseFlightPackToSpec, solveUnified } from '../src/unified.ts'
|
|
11
|
+
|
|
12
|
+
const pack = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'))
|
|
13
|
+
const spec = parseFlightPackToSpec(pack)
|
|
14
|
+
spec.budgetCny = 9000
|
|
15
|
+
spec.skeletonHub = true
|
|
16
|
+
spec.segments.forEach(s => { if (s.id === 'f5') s.route = 'SZX->DXB'; if (s.id === 'f1') s.route = 'HKG->HKT' })
|
|
17
|
+
const r = await solveUnified(spec)
|
|
18
|
+
assert.equal(r.feasible, true, '骨架否定不排除——EK329 必须仍在')
|
|
19
|
+
const f5 = r.legs!.find(l => l.leg === 'f5')
|
|
20
|
+
assert.equal(f5?.service, 'EK329', 'EK329 存活(骨架滞后容错)')
|
|
21
|
+
assert.ok((r.skeleton_notes ?? []).some(n => n.includes('SZX') && n.includes('❌')), `枢纽否定标注在: ${r.skeleton_notes}`)
|
|
22
|
+
assert.ok((r.skeleton_notes ?? []).some(n => n.includes('HKG') && n.includes('✅')), '正向标注在')
|
|
23
|
+
console.log(`骨架集成 OK:EK329 在枢纽否定下存活,标注=[${r.skeleton_notes?.join(' | ')}]`)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账本 CLI(ADR-15 事务化状态基座的操作面;D3 决策:仓内极简命令,恢复语义
|
|
3
|
+
* 已与驱动器解耦,随时可换 loopx tick):
|
|
4
|
+
* migrate [root] one-shot 迁移:旧 JSON/JSONL → 账本 events
|
|
5
|
+
* (导入前自动快照到 gotry-state/pre-ledger-backup/)
|
|
6
|
+
* export [root] 账本 → 旧文件名视图(红线 6:可见可导出;单向,不回流)
|
|
7
|
+
* log [root] [--limit N] 事件账本尾部(append-only 审计面)
|
|
8
|
+
* stats [root] 各面计数
|
|
9
|
+
* rebuild [root] [toSeq] DROP 投影 → fold 重放(可截到 toSeq;账本/投影
|
|
10
|
+
* 永不分叉的可验证性)
|
|
11
|
+
* rewind [root] <seq> = rebuild toSeq(投影回到历史时点;events 不动,
|
|
12
|
+
* rebuild 无参即回到最新)
|
|
13
|
+
* forget [root] wish|companion|motivation <id>
|
|
14
|
+
* 物理硬删该主体全部事件 + 审计一行 + 重建投影
|
|
15
|
+
* (红线 6「可删除」;D5 默认物理删)
|
|
16
|
+
* tick [root] 回收全部 pending 工单(durable 恢复语义)
|
|
17
|
+
* whatif [root] <dest.db> VACUUM INTO 分叉副本(WriteGate what-if 预演,不触正本)
|
|
18
|
+
* pw-list [root] pending_writes 清单
|
|
19
|
+
* pw-request [root] <idemKey> <seam> <payloadJson> 登记待确认外部写(L2)
|
|
20
|
+
* pw-confirm [root] <idemKey> <receipt> 具名 seam 确认(L3,携 receipt)
|
|
21
|
+
* pw-compensate [root] <idemKey> <note> saga 补偿
|
|
22
|
+
* root 默认 '.';运行:cd ts && npx tsx scripts/state-cli.ts <cmd> ...
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.ts'
|
|
28
|
+
import { collectDeepPlanning, makeJournaledSolvePort, settleAsyncTicket, type AsyncTicket } from '../src/loop.ts'
|
|
29
|
+
import { solveUnified } from '../src/unified.ts'
|
|
30
|
+
import type { TripState } from '../src/contracts.ts'
|
|
31
|
+
|
|
32
|
+
const [cmd, ...rest] = process.argv.slice(2)
|
|
33
|
+
|
|
34
|
+
function rootOf(args: string[]): string {
|
|
35
|
+
const i = args.indexOf('--state-root')
|
|
36
|
+
if (i >= 0 && args[i + 1]) return args[i + 1]
|
|
37
|
+
const first = args.find(a => !a.startsWith('--') && !/^[\d]+$/.test(a))
|
|
38
|
+
return first ?? '.'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tenantOf(args: string[]): string {
|
|
42
|
+
const i = args.indexOf('--tenant')
|
|
43
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : 'local'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function arg(name: string, fallback?: string): string | undefined {
|
|
47
|
+
const i = process.argv.indexOf(name)
|
|
48
|
+
return i >= 0 ? process.argv[i + 1] : fallback
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function atomicWrite(path: string, text: string): void {
|
|
52
|
+
const tmp = `${path}.tmp`
|
|
53
|
+
writeFileSync(tmp, text, 'utf-8')
|
|
54
|
+
renameSync(tmp, path)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const HELP = '用法见文件头注释(npx tsx scripts/state-cli.ts <cmd> [root] ...)'
|
|
58
|
+
|
|
59
|
+
switch (cmd) {
|
|
60
|
+
case 'migrate': {
|
|
61
|
+
const root = rootOf(rest)
|
|
62
|
+
const tenant = tenantOf(rest)
|
|
63
|
+
const before = openLedgerIfExists(root, tenant)?.countEvents() ?? 0
|
|
64
|
+
const ledger = ensureLedger(root, tenant)
|
|
65
|
+
const backupDir = join(root, 'gotry-state', 'pre-ledger-backup')
|
|
66
|
+
console.log(`账本就绪:${ledger.dbPath}(events ${before} → ${ledger.countEvents()};存在旧文件时已快照至 ${backupDir}/)`)
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
case 'export': {
|
|
70
|
+
const root = rootOf(rest)
|
|
71
|
+
const tenant = tenantOf(rest)
|
|
72
|
+
const ledger = ensureLedger(root, tenant)
|
|
73
|
+
const dir = join(root === '.' ? process.cwd() : root, 'gotry-state')
|
|
74
|
+
mkdirSync(dir, { recursive: true })
|
|
75
|
+
const motivation = ledger.readMotivation()
|
|
76
|
+
if (motivation) atomicWrite(join(dir, 'motivation-profile.json'), JSON.stringify(motivation, null, 2))
|
|
77
|
+
const pool = ledger.readWishPool()
|
|
78
|
+
if (pool.length) atomicWrite(join(dir, 'wish-pool.json'), JSON.stringify(pool, null, 2))
|
|
79
|
+
const companions = ledger.readCompanions()
|
|
80
|
+
if (companions.length) atomicWrite(join(dir, 'companions.json'), JSON.stringify(companions, null, 2))
|
|
81
|
+
const utility = ledger.readUtilityEvents()
|
|
82
|
+
if (utility.length) atomicWrite(join(dir, 'memory-utility.jsonl'), utility.map(e => JSON.stringify(e)).join('\n') + '\n')
|
|
83
|
+
const trips = ledger.readTrips()
|
|
84
|
+
if (trips.length) atomicWrite(join(dir, 'trips.jsonl'), trips.map(e => JSON.stringify(e)).join('\n') + '\n')
|
|
85
|
+
console.log(`视图已导出(单向,DB→文件;红线 6):${dir}/`)
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
case 'log': {
|
|
89
|
+
const root = rootOf(rest)
|
|
90
|
+
const tenant = tenantOf(rest)
|
|
91
|
+
const limit = Number(arg('--limit', '20'))
|
|
92
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
93
|
+
if (!ledger) { console.log('(无账本——未迁移 root,旧文件形态)'); break }
|
|
94
|
+
for (const e of ledger.readEvents(undefined, limit)) {
|
|
95
|
+
console.log(`${String(e.seq).padStart(5)} ${e.ts} ${e.actor.padEnd(28)} ${e.kind.padEnd(24)} ${e.subject_id}${e.idem_key ? ` #${e.idem_key}` : ''}`)
|
|
96
|
+
}
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case 'stats': {
|
|
100
|
+
const root = rootOf(rest)
|
|
101
|
+
const tenant = tenantOf(rest)
|
|
102
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
103
|
+
if (!ledger) { console.log('(无账本——未迁移 root)'); break }
|
|
104
|
+
console.log(`events=${ledger.countEvents()} wishes=${ledger.readWishPool().length} companions=${ledger.readCompanions().length} trips=${ledger.readTrips().length} utility=${ledger.readUtilityEvents().length} pendingRuns=${ledger.pendingWorkflowRuns().length} pendingWrites=${ledger.listPendingWrites().length}`)
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
case 'rebuild':
|
|
108
|
+
case 'rewind': {
|
|
109
|
+
const root = rootOf(rest)
|
|
110
|
+
const tenant = tenantOf(rest)
|
|
111
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
112
|
+
if (!ledger) { console.error('无账本'); process.exit(1) }
|
|
113
|
+
const toSeqRaw = rest.find(a => /^\d+$/.test(a))
|
|
114
|
+
const r = ledger.rebuildProjections(toSeqRaw ? Number(toSeqRaw) : undefined)
|
|
115
|
+
console.log(`fold 重建完成${toSeqRaw ? `(至 seq ${toSeqRaw};rebuild 无参即回最新)` : '(全量)'}:重放 ${r.events} 事件 → 愿望 ${r.wishes} / 同行人 ${r.companions}`)
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
case 'forget': {
|
|
119
|
+
const root = rootOf(rest)
|
|
120
|
+
const tenant = tenantOf(rest)
|
|
121
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
122
|
+
if (!ledger) { console.error('无账本'); process.exit(1) }
|
|
123
|
+
const subject = rest.find(a => !a.startsWith('--') && a !== root)
|
|
124
|
+
const id = rest[rest.indexOf(subject!) + 1]
|
|
125
|
+
if (!subject || id === undefined) { console.error(HELP); process.exit(1) }
|
|
126
|
+
const map: Record<string, { kinds: string[]; subjectId: string }> = {
|
|
127
|
+
wish: { kinds: ['wish.imported', 'wish.added', 'wish.updated', 'memory_utility.event'], subjectId: id },
|
|
128
|
+
companion: { kinds: ['companion.imported', 'companion.saved'], subjectId: id },
|
|
129
|
+
motivation: { kinds: ['motivation.imported', 'motivation.patch'], subjectId: 'motivation' },
|
|
130
|
+
}
|
|
131
|
+
const spec = map[subject]
|
|
132
|
+
if (!spec) { console.error(`未知主体 ${subject}(wish|companion|motivation)`); process.exit(1) }
|
|
133
|
+
const r = ledger.forgetSubject([spec])
|
|
134
|
+
console.log(`已物理硬删 ${r.deleted} 条事件并重建投影(审计一行留痕;红线 6「可删除」)`)
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
case 'tick': {
|
|
138
|
+
const root = rootOf(rest)
|
|
139
|
+
const tenant = tenantOf(rest)
|
|
140
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
141
|
+
if (!ledger) { console.log('(无账本,无待办)'); break }
|
|
142
|
+
const pending = ledger.pendingWorkflowRuns()
|
|
143
|
+
if (pending.length === 0) { console.log('(无 pending 工单)'); break }
|
|
144
|
+
for (const p of pending) {
|
|
145
|
+
const run = ledger.getWorkflowRun(p.id)!
|
|
146
|
+
const ticket = JSON.parse(run.ticket_json) as AsyncTicket
|
|
147
|
+
const state = JSON.parse(run.state_json) as TripState
|
|
148
|
+
const solve = makeJournaledSolvePort(ledger, p.id, solveUnified as never)
|
|
149
|
+
const { reply } = await collectDeepPlanning(state, ticket, solve)
|
|
150
|
+
await settleAsyncTicket(p.id, reply, root)
|
|
151
|
+
console.log(`工单 ${p.id} 已回收(durable 恢复语义:done 步骤零重执行)`)
|
|
152
|
+
}
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
case 'whatif': {
|
|
156
|
+
const root = rootOf(rest)
|
|
157
|
+
const tenant = tenantOf(rest)
|
|
158
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
159
|
+
if (!ledger) { console.error('无账本'); process.exit(1) }
|
|
160
|
+
const dest = rest.find(a => a !== root && !a.startsWith('--'))
|
|
161
|
+
if (!dest) { console.error(HELP); process.exit(1) }
|
|
162
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
163
|
+
ledger.forkWhatIf(dest)
|
|
164
|
+
console.log(`what-if 分叉已生成:${dest}(预演在副本,正本零改动)`)
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
case 'pw-list': {
|
|
168
|
+
const root = rootOf(rest)
|
|
169
|
+
const tenant = tenantOf(rest)
|
|
170
|
+
const ledger = openLedgerIfExists(root, tenant)
|
|
171
|
+
if (!ledger) { console.log('(无账本)'); break }
|
|
172
|
+
for (const w of ledger.listPendingWrites()) {
|
|
173
|
+
console.log(`${w.status.padEnd(12)} ${w.idem_key} seam=${w.seam}${w.receipt ? ` receipt=${w.receipt}` : ''}`)
|
|
174
|
+
}
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
case 'pw-request':
|
|
178
|
+
case 'pw-confirm':
|
|
179
|
+
case 'pw-compensate': {
|
|
180
|
+
const root = rootOf(rest)
|
|
181
|
+
const tenant = tenantOf(rest)
|
|
182
|
+
const ledger = ensureLedger(root, tenant)
|
|
183
|
+
const positional = rest.filter(a => a !== root && !a.startsWith('--'))
|
|
184
|
+
if (positional.length < (cmd === 'pw-request' ? 3 : 2)) { console.error(HELP); process.exit(1) }
|
|
185
|
+
if (cmd === 'pw-request') {
|
|
186
|
+
const r = ledger.requestPendingWrite({ idemKey: positional[0]!, seam: positional[1]!, payload: JSON.parse(positional[2]!) as unknown })
|
|
187
|
+
console.log(`登记:${JSON.stringify(r)}(L2:只登记不执行)`)
|
|
188
|
+
} else if (cmd === 'pw-confirm') {
|
|
189
|
+
const r = ledger.confirmPendingWrite(positional[0]!, positional[1]!)
|
|
190
|
+
console.log(`确认:${JSON.stringify(r)}`)
|
|
191
|
+
} else {
|
|
192
|
+
const r = ledger.compensatePendingWrite(positional[0]!, positional[1]!)
|
|
193
|
+
console.log(`补偿:${JSON.stringify(r)}`)
|
|
194
|
+
}
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
console.error(cmd ? `未知命令 ${cmd}` : HELP)
|
|
199
|
+
console.error(HELP)
|
|
200
|
+
process.exit(1)
|
|
201
|
+
}
|
package/ts/src/bridge.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoTry 桥接工具集(纯 Node 版,无 Python 依赖):
|
|
3
|
+
* - ensureStateDir: 状态目录创建
|
|
4
|
+
* - recordLatency: 延迟日志追加
|
|
5
|
+
* - readJson / writeJson: JSON 文件安全读写
|
|
6
|
+
*
|
|
7
|
+
* 历史:曾有 callFeasibilityEngine 用 spawn(.venv/bin/python) 起 Python CLI,
|
|
8
|
+
* 作为 TS 求解的回退路径。D-7 切轨后 unified solveChoiceSegment 是唯一求解入口,
|
|
9
|
+
* Python oracle 路径于 v0.0.1-rc.2 移除——npm 一键分发不再需要 Python 运行时依赖。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
/** 状态目录:动机画像 / wish pool 的落盘位置(红线 6:用户数据可见、可编辑、可删除) */
|
|
16
|
+
export async function ensureStateDir(root: string): Promise<string> {
|
|
17
|
+
const dir = join(root, 'gotry-state')
|
|
18
|
+
await mkdir(dir, { recursive: true })
|
|
19
|
+
return dir
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** JSON 安全读取:文件不存在返回 fallback,解析失败抛错 */
|
|
23
|
+
export async function readJson<T>(path: string, fallback: T): Promise<T> {
|
|
24
|
+
try {
|
|
25
|
+
const text = await readFile(path, 'utf-8')
|
|
26
|
+
return JSON.parse(text) as T
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return fallback
|
|
29
|
+
throw e
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** JSON 原子写入 */
|
|
34
|
+
export async function writeJson(path: string, value: unknown): Promise<void> {
|
|
35
|
+
const tmp = `${path}.tmp`
|
|
36
|
+
await writeFile(tmp, JSON.stringify(value, null, 2), 'utf-8')
|
|
37
|
+
const { rename } = await import('node:fs/promises')
|
|
38
|
+
await rename(tmp, path)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 桥接延迟日志(追加式;T5 成本工程的度量数据源之一) */
|
|
42
|
+
export async function recordLatency(logPath: string, latencyMs: number, kind: string): Promise<void> {
|
|
43
|
+
await appendFile(logPath, JSON.stringify({ ts: new Date().toISOString(), kind, latencyMs }) + '\n')
|
|
44
|
+
}
|