@danceiny/gotry 0.0.1-rc.13 → 0.0.1-rc.15
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 +121 -140
- package/README.zh-CN.md +235 -0
- package/bin/gotry-bootstrap.js +165 -0
- package/bin/gotry-inner.js +10 -3
- package/dist/capabilities/artifacts.js +217 -0
- package/dist/capabilities/hbcli.js +42 -15
- package/dist/scripts/booking-saga-tests.js +187 -0
- package/dist/scripts/bootstrap-tests.js +66 -0
- package/dist/scripts/flyai-tests.js +95 -0
- package/dist/scripts/hbcli-tests.js +28 -3
- package/dist/scripts/nightly-evidence-tests.js +123 -0
- package/dist/scripts/nightly-evidence.js +233 -0
- package/dist/scripts/smoke.js +83 -0
- package/dist/src/booking-saga.js +153 -0
- package/dist/src/dsh-llm.js +30 -6
- package/dist/src/index.js +141 -3
- package/package.json +5 -2
- package/ts/capabilities/artifacts.ts +235 -0
- package/ts/capabilities/hbcli.ts +51 -19
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +145 -25
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gotry 外部依赖自举(founder 2026-08-29 指令:安装 gotry 时按上游本身的方式装好外部依赖):
|
|
4
|
+
* hbcli(hotelbyte-cli) → 官方 install.sh(原生二进制,~/.local/bin/hbcli)
|
|
5
|
+
* agent-reach → 官方 pip 安装 git+ upstream(包内 .venv,与 z3-solver 同址原则)
|
|
6
|
+
* flyai → 无需安装(npx 每次自拉 @fly-ai/flyai-cli)
|
|
7
|
+
* dsh-better-sidebar → dsh 宿主层插件市场组件(dshmarket.com #1 UI,18.9 万周装):
|
|
8
|
+
* dsh web 侧栏工作台(文件浏览/Markdown/Mermaid/PDF 预览)——
|
|
9
|
+
* gotry 产物(工单交付 md/行程 md)的成熟查看面(issue #25)。
|
|
10
|
+
* 宿主层安装(dsh plugin → ~/.dsh/profiles/web),不进 gotry 依赖。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node bin/gotry-bootstrap.js # 显式安装(缺啥装啥;失败 exit 1)
|
|
14
|
+
* node bin/gotry-bootstrap.js --auto # postinstall 模式:CI/跳过开关检测,任何失败不挡安装(exit 0)
|
|
15
|
+
* node bin/gotry-bootstrap.js --check-only # 只探测报告,不安装(测试钩子)
|
|
16
|
+
*
|
|
17
|
+
* 环境开关:
|
|
18
|
+
* GOTRY_SETUP_SKIP=1 全部跳过
|
|
19
|
+
* GOTRY_SETUP_HBCLI=0 跳过 hbcli
|
|
20
|
+
* GOTRY_SETUP_REACH=0 跳过 agent-reach
|
|
21
|
+
* GOTRY_SETUP_SIDEBAR=0 跳过 dsh-better-sidebar
|
|
22
|
+
*
|
|
23
|
+
* 契约:安装外部依赖永远不挡 gotry 本体——能力层各有降级路径(静态包/not-installed
|
|
24
|
+
* verdict),自举失败只降级体验,不产生故障。凭证(hbcli auth / agent-reach 渠道
|
|
25
|
+
* cookie)属用户资产,不自动配置,装完二进制后给指引。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawn } from 'node:child_process'
|
|
29
|
+
import { existsSync } from 'node:fs'
|
|
30
|
+
import { homedir } from 'node:os'
|
|
31
|
+
import { join, dirname } from 'node:path'
|
|
32
|
+
import { fileURLToPath } from 'node:url'
|
|
33
|
+
|
|
34
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
35
|
+
const AUTO = process.argv.includes('--auto')
|
|
36
|
+
const CHECK_ONLY = process.argv.includes('--check-only')
|
|
37
|
+
|
|
38
|
+
const HBCLI_INSTALL_CMD = 'curl -fsSL https://github.com/hotelbyte-com/docs/releases/latest/download/install.sh | bash'
|
|
39
|
+
const REACH_INSTALL_URL = 'git+https://github.com/Panniantong/Agent-Reach.git'
|
|
40
|
+
|
|
41
|
+
/** 带超时的子进程(inherit stdio 让用户看见上游安装进度) */
|
|
42
|
+
function run(cmd, args, { timeoutMs, cwd } = {}) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const child = spawn(cmd, args, { stdio: 'inherit', cwd, env: process.env })
|
|
45
|
+
let done = false
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } }
|
|
48
|
+
}, timeoutMs)
|
|
49
|
+
child.on('error', (e) => { if (!done) { done = true; clearTimeout(timer); resolve({ ok: false, error: e.message }) } })
|
|
50
|
+
child.on('exit', (code) => { if (!done) { done = true; clearTimeout(timer); resolve({ ok: code === 0, error: code === 0 ? undefined : `exit ${code}` }) } })
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 静默探测命令是否可执行(带超时) */
|
|
55
|
+
function probe(cmd, args, timeoutMs = 10_000) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const child = spawn(cmd, args, { stdio: 'ignore' })
|
|
58
|
+
let done = false
|
|
59
|
+
const timer = setTimeout(() => { if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } resolve(false) } }, timeoutMs)
|
|
60
|
+
child.on('error', () => { if (!done) { done = true; clearTimeout(timer); resolve(false) } })
|
|
61
|
+
child.on('exit', (code) => { if (!done) { done = true; clearTimeout(timer); resolve(code === 0) } })
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const say = (s) => console.log(s)
|
|
66
|
+
|
|
67
|
+
async function setupHbcli() {
|
|
68
|
+
say('[gotry-setup] hbcli(hotelbyte-cli,可选酒店实时源)')
|
|
69
|
+
const candidates = ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]
|
|
70
|
+
const present = candidates.some((p) => existsSync(p)) && (await probe(candidates[0], ['version']) || await probe(candidates[1], ['version']) || await probe(candidates[2], ['version']))
|
|
71
|
+
if (present) { say(' ✓ 已安装'); return { ok: true } }
|
|
72
|
+
if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
|
|
73
|
+
say(` 安装中(官方脚本): ${HBCLI_INSTALL_CMD}`)
|
|
74
|
+
const r = await run('bash', ['-c', HBCLI_INSTALL_CMD], { timeoutMs: 120_000 })
|
|
75
|
+
if (!r.ok) { say(` ✗ 安装失败(${r.error})——不影响 gotry,酒店检索将用内置静态包;可稍后重试: npx gotry setup`); return { ok: false } }
|
|
76
|
+
const binDir = join(homedir(), '.local/bin')
|
|
77
|
+
if (!process.env.PATH.split(':').includes(binDir)) {
|
|
78
|
+
say(` ⚠ ${binDir} 不在当前 PATH —— gotry 工具已内建候选路径回退,无需手动处理;其他程序可用: export PATH="${binDir}:$PATH"`)
|
|
79
|
+
}
|
|
80
|
+
say(' ✓ 安装完成(凭证选配: hbcli auth set-credentials --app-key ... --app-secret ...,或 HOTELBYTE_TOKEN;未配时酒店检索自动用静态包)')
|
|
81
|
+
return { ok: true }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function setupReach() {
|
|
85
|
+
say('[gotry-setup] agent-reach(可选网页/社媒读取源,pip 装入包内 .venv)')
|
|
86
|
+
const venvBin = join(repoRoot, '.venv/bin/agent-reach')
|
|
87
|
+
if (existsSync(venvBin)) { say(' ✓ 已安装(.venv)'); return { ok: true } }
|
|
88
|
+
if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
|
|
89
|
+
const hasPy = await probe('python3', ['--version'], 10_000)
|
|
90
|
+
if (!hasPy) { say(' ✗ 跳过:未找到 python3(agent-reach 需 Python 3;装好后重跑 npx gotry setup)'); return { ok: false } }
|
|
91
|
+
say(` 创建 .venv 并安装上游(${REACH_INSTALL_URL})`)
|
|
92
|
+
const venv = await run('python3', ['-m', 'venv', join(repoRoot, '.venv')], { timeoutMs: 120_000 })
|
|
93
|
+
if (!venv.ok) { say(` ✗ venv 创建失败(${venv.error})——不影响 gotry,gotry_agent_reach 将返回 not-installed 指引`); return { ok: false } }
|
|
94
|
+
const pip = join(repoRoot, '.venv/bin/pip')
|
|
95
|
+
const inst = await run(pip, ['install', '-q', REACH_INSTALL_URL], { timeoutMs: 300_000 })
|
|
96
|
+
if (!inst.ok) { say(` ✗ pip 安装失败(${inst.error})——可稍后重试: npx gotry setup`); return { ok: false } }
|
|
97
|
+
say(' ✓ 安装完成(渠道凭证选配见 docs/tokens.md: .venv/bin/agent-reach configure --from-browser chrome --platform <渠道>)')
|
|
98
|
+
return { ok: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function setupSidebar() {
|
|
102
|
+
say('[gotry-setup] dsh-better-sidebar(dsh web 侧栏工作台,产物查看面 issue #25)')
|
|
103
|
+
const installed = existsSync(join(homedir(), '.dsh/profiles/web/node_modules/dsh-better-sidebar/package.json'))
|
|
104
|
+
if (installed) { say(' ✓ 已安装(~/.dsh/profiles/web)'); return { ok: true } }
|
|
105
|
+
if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
|
|
106
|
+
const SIDEBAR_PKG = 'dsh-better-sidebar@latest'
|
|
107
|
+
// dsh CLI:本包依赖里的 @deepseek-ai/dsh 优先;解析不到走 npx 自拉(官方安装途径同款)
|
|
108
|
+
const { createRequire } = await import('node:module')
|
|
109
|
+
const require_ = createRequire(join(repoRoot, 'package.json'))
|
|
110
|
+
let launched = false
|
|
111
|
+
try {
|
|
112
|
+
const dshBin = require_.resolve('@deepseek-ai/dsh/lib/bin.js')
|
|
113
|
+
say(` 安装中(dsh plugin → web profile): ${SIDEBAR_PKG}`)
|
|
114
|
+
const r = await run(process.execPath, [dshBin, 'plugin', '--profile', 'web', 'add', SIDEBAR_PKG], { timeoutMs: 300_000 })
|
|
115
|
+
launched = true
|
|
116
|
+
if (r.ok) {
|
|
117
|
+
say(' ✓ 安装完成(gotry web 刷新浏览器即见右侧工作台;工作区里的产物 md 可直接预览)')
|
|
118
|
+
return { ok: true }
|
|
119
|
+
}
|
|
120
|
+
say(` ✗ dsh plugin 安装失败(${r.error})——尝试 npx 途径`)
|
|
121
|
+
} catch { /* 本包未携带 @deepseek-ai/dsh */ }
|
|
122
|
+
if (!launched) say(' 本包未携带 @deepseek-ai/dsh,走 npx 途径安装')
|
|
123
|
+
const r2 = await run('npx', ['-y', '--package', '@deepseek-ai/dsh', 'dsh', 'plugin', '--profile', 'web', 'add', SIDEBAR_PKG], { timeoutMs: 300_000 })
|
|
124
|
+
if (!r2.ok) {
|
|
125
|
+
say(' ✗ 安装失败——不影响 gotry:产物仍可在对话里说「看看我生成的行程」经 gotry_artifacts_list/read 查看;可稍后重试: npx gotry setup')
|
|
126
|
+
return { ok: false }
|
|
127
|
+
}
|
|
128
|
+
say(' ✓ 安装完成(gotry web 刷新浏览器即见右侧工作台)')
|
|
129
|
+
return { ok: true }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
if (process.platform === 'win32') {
|
|
134
|
+
say('[gotry-setup] Windows 暂不支持自动安装(hbcli 上游仅 darwin/linux)。手动指引:')
|
|
135
|
+
say(` hbcli: ${HBCLI_INSTALL_CMD}(WSL);agent-reach: python -m venv .venv && .venv/Scripts/pip install ${REACH_INSTALL_URL}`)
|
|
136
|
+
say(' dsh-better-sidebar: npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar@latest')
|
|
137
|
+
process.exit(AUTO ? 0 : 1)
|
|
138
|
+
}
|
|
139
|
+
if (AUTO && (process.env.CI || process.env.GOTRY_SETUP_SKIP === '1')) {
|
|
140
|
+
say('[gotry-setup] CI/GOTRY_SETUP_SKIP 检测——跳过外部依赖自举(可随时手动: npx gotry setup)')
|
|
141
|
+
say('GoTry installed. Run: npx gotry web (dsh Web UI on :3080)')
|
|
142
|
+
process.exit(0)
|
|
143
|
+
}
|
|
144
|
+
if (!AUTO && process.env.GOTRY_SETUP_SKIP === '1') { say('[gotry-setup] GOTRY_SETUP_SKIP=1——跳过'); process.exit(0) }
|
|
145
|
+
const results = []
|
|
146
|
+
if (process.env.GOTRY_SETUP_HBCLI !== '0') results.push(await setupHbcli())
|
|
147
|
+
else say('[gotry-setup] hbcli:GOTRY_SETUP_HBCLI=0 跳过')
|
|
148
|
+
if (process.env.GOTRY_SETUP_REACH !== '0') results.push(await setupReach())
|
|
149
|
+
else say('[gotry-setup] agent-reach:GOTRY_SETUP_REACH=0 跳过')
|
|
150
|
+
if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
|
|
151
|
+
else say('[gotry-setup] dsh-better-sidebar:GOTRY_SETUP_SIDEBAR=0 跳过')
|
|
152
|
+
say('[gotry-setup] flyai:无需安装(npx 每次自拉 @fly-ai/flyai-cli,免 key)')
|
|
153
|
+
const failed = results.filter((r) => !r.ok).length
|
|
154
|
+
if (failed > 0) {
|
|
155
|
+
say(`[gotry-setup] ${failed} 项未就绪——gotry 本体不受影响(各能力均有降级路径);可稍后重跑: npx gotry setup`)
|
|
156
|
+
process.exit(AUTO ? 0 : 1)
|
|
157
|
+
}
|
|
158
|
+
say('[gotry-setup] 全部就绪。Run: npx gotry web')
|
|
159
|
+
process.exit(0)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch((e) => {
|
|
163
|
+
say(`[gotry-setup] 异常:${e.message}(不影响 gotry 本体;可重试 npx gotry setup)`)
|
|
164
|
+
process.exit(AUTO ? 0 : 1)
|
|
165
|
+
})
|
package/bin/gotry-inner.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 占位(本机绝对路径),随 tarball 分发后对其他机器必错。
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { spawn } from 'node:child_process'
|
|
19
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { fileURLToPath } from 'node:url'
|
|
22
22
|
import { dirname, join } from 'node:path'
|
|
@@ -56,6 +56,7 @@ if (help) {
|
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
58
|
gotry web # dsh Web UI on http://127.0.0.1:3080
|
|
59
|
+
gotry setup # 安装可选外部依赖(hbcli 官方脚本 / agent-reach pip;装时已自动跑过)
|
|
59
60
|
gotry "一段完整任务..." # headless 一问一答
|
|
60
61
|
gotry help # this help
|
|
61
62
|
|
|
@@ -68,12 +69,18 @@ Detail: https://github.com/Danceiny/gotry — README
|
|
|
68
69
|
process.exit(0)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
// mode 决定路径: 'web'/'
|
|
72
|
-
const literal = new Set(['web', 'help', '-h', '--help'])
|
|
72
|
+
// mode 决定路径: 'web'/'setup'/'help' 是字面命令;否则第一段 args[0] 是任务本身的一部分
|
|
73
|
+
const literal = new Set(['web', 'setup', 'help', '-h', '--help'])
|
|
73
74
|
const isLiteral = literal.has(args[0])
|
|
74
75
|
const mode = isLiteral ? args[0] : 'headless'
|
|
75
76
|
const rest = isLiteral ? args.slice(1) : args
|
|
76
77
|
|
|
78
|
+
// setup:外部依赖自举(hbcli/agent-reach),不需要 dsh runtime 与 LLM key,同步分发后即退
|
|
79
|
+
if (mode === 'setup') {
|
|
80
|
+
const r = spawnSync(process.execPath, [join(here, 'gotry-bootstrap.js'), ...rest], { stdio: 'inherit' })
|
|
81
|
+
process.exit(r.status ?? (r.error ? 1 : 0))
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// --- dsh runtime 定位:repo checkout(vendored)优先,npm 安装走依赖解析 ---
|
|
78
85
|
const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
|
|
79
86
|
let dshBin = ''
|
|
@@ -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/ts/capabilities/artifacts.ts
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function hbcliBinCandidates(bin, homeDir = homedir()) {
|
|
5
|
+
if (bin !== 'hbcli') return [
|
|
6
|
+
bin
|
|
7
|
+
];
|
|
8
|
+
return [
|
|
9
|
+
bin,
|
|
10
|
+
join(homeDir, '.local/bin/hbcli'),
|
|
11
|
+
join(homeDir, '.staicli/current/hbcli')
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function attemptHbcli(bin, args, opts) {
|
|
3
15
|
const started = Date.now();
|
|
4
|
-
const bin = opts.hbcliBin ?? 'hbcli';
|
|
5
|
-
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
6
|
-
const env = opts.env ?? 'uat';
|
|
7
|
-
const envVars = {
|
|
8
|
-
HOTELBYTE_ENV: env
|
|
9
|
-
};
|
|
10
|
-
if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
11
16
|
return new Promise((resolve)=>{
|
|
12
17
|
let stdout = '';
|
|
13
18
|
let stderr = '';
|
|
@@ -15,7 +20,7 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
15
20
|
const child = spawn(bin, args, {
|
|
16
21
|
env: {
|
|
17
22
|
...process.env,
|
|
18
|
-
...envVars
|
|
23
|
+
...opts.envVars
|
|
19
24
|
}
|
|
20
25
|
});
|
|
21
26
|
const timer = setTimeout(()=>{
|
|
@@ -28,10 +33,10 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
28
33
|
result: null,
|
|
29
34
|
evidence: `[实时API:hbcli@timeout@${new Date().toISOString()}]`,
|
|
30
35
|
latencyMs: Date.now() - started,
|
|
31
|
-
error: `timeout after ${timeoutMs}ms`
|
|
36
|
+
error: `timeout after ${opts.timeoutMs}ms`
|
|
32
37
|
});
|
|
33
38
|
}
|
|
34
|
-
}, timeoutMs);
|
|
39
|
+
}, opts.timeoutMs);
|
|
35
40
|
child.stdout.on('data', (d)=>{
|
|
36
41
|
stdout += d.toString();
|
|
37
42
|
});
|
|
@@ -82,11 +87,31 @@ export async function callHbcliJson(args, opts = {}) {
|
|
|
82
87
|
result: null,
|
|
83
88
|
evidence: `[实时API:hbcli@spawn_error@${new Date().toISOString()}]`,
|
|
84
89
|
latencyMs: Date.now() - started,
|
|
85
|
-
error: e.message
|
|
90
|
+
error: e.message,
|
|
91
|
+
spawnError: true
|
|
86
92
|
});
|
|
87
93
|
});
|
|
88
94
|
});
|
|
89
95
|
}
|
|
96
|
+
export async function callHbcliJson(args, opts = {}) {
|
|
97
|
+
const env = opts.env ?? 'uat';
|
|
98
|
+
const envVars = {
|
|
99
|
+
HOTELBYTE_ENV: env
|
|
100
|
+
};
|
|
101
|
+
if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
102
|
+
const callOpts = {
|
|
103
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
104
|
+
env,
|
|
105
|
+
envVars
|
|
106
|
+
};
|
|
107
|
+
const candidates = hbcliBinCandidates(opts.hbcliBin ?? 'hbcli');
|
|
108
|
+
let last;
|
|
109
|
+
for (const bin of candidates){
|
|
110
|
+
last = await attemptHbcli(bin, args, callOpts);
|
|
111
|
+
if (!(last.spawnError && candidates.indexOf(bin) < candidates.length - 1)) return last;
|
|
112
|
+
}
|
|
113
|
+
return last;
|
|
114
|
+
}
|
|
90
115
|
export async function searchHotels(query, opts = {}) {
|
|
91
116
|
const hbArgs = [
|
|
92
117
|
'search',
|
|
@@ -110,6 +135,8 @@ export async function searchHotels(query, opts = {}) {
|
|
|
110
135
|
summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}`
|
|
111
136
|
};
|
|
112
137
|
}
|
|
138
|
+
const rawReason = live.error ?? live.via;
|
|
139
|
+
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason;
|
|
113
140
|
const fallback = opts.fallbackPath;
|
|
114
141
|
if (fallback) {
|
|
115
142
|
try {
|
|
@@ -124,19 +151,19 @@ export async function searchHotels(query, opts = {}) {
|
|
|
124
151
|
hotels: {
|
|
125
152
|
stays: matched
|
|
126
153
|
},
|
|
127
|
-
summary: `${query.destination}:hbcli
|
|
154
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块`
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
return {
|
|
131
158
|
...live,
|
|
132
159
|
hotels: null,
|
|
133
|
-
summary: `${query.destination}:hbcli
|
|
160
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`
|
|
134
161
|
};
|
|
135
162
|
} catch {}
|
|
136
163
|
}
|
|
137
164
|
return {
|
|
138
165
|
...live,
|
|
139
|
-
summary: `${query.destination}:hbcli
|
|
166
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)`
|
|
140
167
|
};
|
|
141
168
|
}
|
|
142
169
|
export async function listDestinations(opts = {}) {
|