From f410b123c20711a85c33396d11daab32d6f90fd9 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 17:01:52 +0400 Subject: [PATCH 01/12] =?UTF-8?q?feat(tools):=20flyai=20=E6=A3=80=E7=B4=A2?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20typed=20=E5=8F=82=E6=95=B0=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E7=AC=AC=E4=B8=80=E5=88=80(issue=20#112,D-30)?= =?UTF-8?q?=E2=80=94=E2=80=94query=20json=20blob=20=E8=BF=81=20dsh=20typed?= =?UTF-8?q?=20ParameterSchemaSpec=20=E5=B9=B3=E9=93=BA=E4=B8=83=E5=AD=97?= =?UTF-8?q?=E6=AE=B5(kind=20=E6=9E=9A=E4=B8=BE=20required+=E5=85=AD?= =?UTF-8?q?=E5=8F=AF=E9=80=89),=E6=A8=A1=E5=9E=8B=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E9=80=90=E5=AD=97=E6=AE=B5=20JSON=20Schema+execute=20=E5=89=8D?= =?UTF-8?q?=E5=AE=BF=E4=B8=BB=E6=9D=83=E6=A0=A1=E9=AA=8C(ToolArgsError?= =?UTF-8?q?=E2=86=92guardToolExecute=20=E5=85=9C=20ADR-13=20ToolFailure=20?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E4=B8=8D=E5=8F=98);=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E9=94=81=20smoke=20=C2=A712+channel-registry-tests=20=C2=A78(l?= =?UTF-8?q?egacy=20blob/=E6=9E=9A=E4=B8=BE=E5=A4=96=E5=80=BC=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E5=8D=B3=E7=BB=93=E6=9E=84=E5=8C=96=E6=8B=92=E7=BB=9D?= =?UTF-8?q?);=E6=99=AE=E9=80=9A=E6=A8=A1=E5=9E=8B=E5=AF=B9=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=BD=A2=E7=8A=B6=E4=B8=8D=E5=86=8D=E9=9D=A0=E7=8C=9C?= =?UTF-8?q?=E3=80=82=E4=BD=99=E9=87=8F=E5=B7=A5=E5=85=B7=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E4=B8=8E=20flyai=20=E6=99=AE=E9=80=9A=E6=A8=A1=E5=9E=8B=20cana?= =?UTF-8?q?ry=20=E8=AE=B0=20D-30;=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2?= =?UTF-8?q?=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5(arch=20=C2=A79/?= =?UTF-8?q?=C2=A710=E3=80=81roadmap=20=E6=9E=B6=E6=9E=84=E9=9D=A2=E5=A2=9E?= =?UTF-8?q?=E9=87=8F=E3=80=81stage1=20=E7=8A=B6=E6=80=81=E5=A4=B4)?= =?UTF-8?q?=E3=80=82run-all=20=E5=85=A8=E6=A0=88=E5=9B=9E=E5=BD=92=20ALL?= =?UTF-8?q?=20SUITES=20GREEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 3 ++- docs/roadmap.md | 1 + docs/stage1-top-down-design.md | 1 + ts/scripts/channel-registry-tests.ts | 7 ++++++- ts/scripts/smoke.ts | 21 ++++++++++++++++----- ts/src/index.ts | 26 ++++++++++++++++++-------- 6 files changed, 44 insertions(+), 15 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 25ad519a..d6624e49 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -400,6 +400,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - E2E:`ts/scripts/model-override-e2e.ts` mock 中转四场景(指定模型→请求体 model 一致/指定+预置用户层→env 压过/不设→内置默认 deepseek-v4-flash/不设+用户层→用户选择保留;②④ 同一 settings 文件仅差 env,对照证明覆盖因果),隔离 DSH_HOME、要求 current tarball 的 clean installed-package bin,全绿;不再靠临时移动 vendored 依赖伪装 package mode。smoke §17 单元回归(未设零监听/无事件总线不抛/覆盖语义)。附查发现一项环境债:vendored 仓内形态 Node 兼容窗口断裂,记 D-27。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 +- **typed 参数契约迁移第一刀(2026-09-04,issue #112,D-30)**:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁);条件必填(机/火要 from/to/date,酒店要 to)不在 schema 强制,仍由 execute 结构化报错给修正方向。余量(session/hotel/weather 等其余工具)与普通模型 canary 仍在 D-30。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -428,7 +429,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | | D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | -| D-30 检索工具 typed 参数契约迁移未完成(ADR-25 配套) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具(flyai/session/hotel/weather)参数面仍是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。迁移以独立 PR 推进,每个首批工具带一轮普通模型 canary;`interpretArgs` 留作旧形态容忍层 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | +| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 flyai 已迁,余 session/hotel/weather 等) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**:`gotry_flyai_search` 平铺七字段(模型可见 JSON Schema+宿主权入口拒绝,ToolFailure 形状迁移锁 smoke §12/channel-registry-tests §8)。**余量**:session/hotel/weather 及其余工具迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合主干发布面);`interpretArgs` 留作旧形态容忍层 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | **[D-NEW] dsh 进程保活缺失** diff --git a/docs/roadmap.md b/docs/roadmap.md index e6feed13..ecbaae68 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,6 +26,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **效应解译器(2026-08-29,issue #16,ADR-18)**:`effect_interpreter.v1` 落地 L4 渠道边界——指数退避重试 / 断路器 / mock 解译器(纯离线 CI 面)收敛进解译层;五渠道工具 + realtime-pricing 已接,余下渠道增量迁移(D-23)。run-all §37。OTA 工具面照旧平铺,证据链逐源标注不变。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 +- **typed 参数契约迁移第一刀(2026-09-04,issue #112,D-30)**:最高流量工具 `gotry_flyai_search` 参数面 query blob → dsh typed ParameterSchemaSpec 平铺七字段——模型可见逐字段 JSON Schema + execute 前宿主权校验(ToolArgsError→guard 兜 ADR-13 ToolFailure,迁移锁 smoke §12/channel-registry-tests §8);普通模型对参数形状不再靠猜。余量工具迁移与 flyai canary 记 D-30。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 94eee6c9..bc683d7c 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,typed 参数契约迁移第一刀(issue #112,D-30):最高流量工具 `gotry_flyai_search` 参数面 query blob → dsh typed ParameterSchemaSpec 平铺七字段(kind 枚举 required+六可选),模型可见逐字段 JSON Schema + execute 前宿主权校验,拒绝形状保持 ADR-13 ToolFailure(迁移锁 smoke §12/channel-registry-tests §8);余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 - S2 mock 切片 ✅ / S3 求解挂载 ✅ / S4 真 LLM ✅(MiniMax-M2,`bb880f3`)/ S5 编排+持久化 ✅(详见 `architecture.md` §9,那里是唯一的当前状态源);本文保留设计原文供追溯。**M1/M2 已退出;M3 工程面已就绪但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仍受各自 Entry gate 约束。 diff --git a/ts/scripts/channel-registry-tests.ts b/ts/scripts/channel-registry-tests.ts index 35bea5a1..20c2c529 100644 --- a/ts/scripts/channel-registry-tests.ts +++ b/ts/scripts/channel-registry-tests.ts @@ -152,8 +152,13 @@ apply(ctx, { stateRoot, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessio assert.equal(typeof variables.channel_routing_card, 'function', 'channel_routing_card 变量已注册') assert.match(variables.channel_routing_card!(), /检索通道顺位/, '卡片可渲染') const pastDate = await registered.find(t => t.name === 'gotry_flyai_search')!.execute( - { query: { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' } }, null) as { routing?: unknown } + { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' }, null) as { routing?: unknown } assert.ok(!('routing' in pastDate), '参数级预校验拒绝不带 routing(未发起检索,无改道语义)') +// D-30 迁移锁(issue #112):typed 平铺契约下 legacy blob 包裹在宿主权校验即拒绝, +// 拒绝形状保持 ADR-13 ToolFailure(guardToolExecute 兜底:ok:false + evidence)且不带 routing +const legacyBlob = await registered.find(t => t.name === 'gotry_flyai_search')!.execute( + { query: { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' } } as never, null) as { ok?: boolean; routing?: unknown; evidence?: string } +assert.ok(legacyBlob.ok === false && !('routing' in legacyBlob) && !!legacyBlob.evidence, 'legacy blob → 结构化拒绝(ToolFailure,无 routing)') console.log('8. apply 接线 OK') await rm(stateRoot, { recursive: true, force: true }) diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index 88750e75..6feab7e6 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -198,7 +198,7 @@ async function main() { // 12) 会话数据面工具(P3 切片1):官方通道 live + 会话工具 needs-login 合同(隔离 profile,零导航) { - const fa = await byName('gotry_flyai_search').execute({ query: { kind: 'flight', from: '上海', to: '丽江', date: '2026-10-01' } }, null) as { ok?: boolean; verdict?: string; via?: string; options?: unknown[]; evidence?: string; error?: string; setup?: string } + const fa = await byName('gotry_flyai_search').execute({ kind: 'flight', from: '上海', to: '丽江', date: '2026-10-01' }, null) as { ok?: boolean; verdict?: string; via?: string; options?: unknown[]; evidence?: string; error?: string; setup?: string } const faBlocked = fa.verdict === 'error' && /sentinel|block|trial limit/i.test(fa.error ?? '') // 端点不可达/超时(出口 IP 被拒或网络抖动)→ 工具以带证据链的 error 终态优雅降级,同样合法 const faErrTerminal = fa.ok === false && fa.verdict === 'error' && /^flyai-error$/.test(String(fa.via ?? '')) && /\[实时API:flyai@error@/.test(String(fa.evidence ?? '')) @@ -221,7 +221,7 @@ async function main() { // 指向隔离空目录,确定性验证 needs-attach/no-spend 合同。真实 attach 只走 #21 人在场验收。 const prof = mkdtempSync(join(smokeRoot, 'sess-')) // 过去日期预校验(issue #24):代码层直接拒绝并指明修正方向,不发上游查询、不产生误导性 miss - const faPast = await byName('gotry_flyai_search').execute({ query: { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' } }, null) as { ok?: boolean; summary?: string } + const faPast = await byName('gotry_flyai_search').execute({ kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' }, null) as { ok?: boolean; summary?: string } if (!(faPast.ok === false && /已是过去/.test(String(faPast.summary ?? '')))) { throw new Error(`FAIL: flyai 过去日期应代码层预校验拒绝,实际:${JSON.stringify(faPast).slice(0, 200)}`) } @@ -252,7 +252,7 @@ async function main() { : `live hit(${fa.options?.length ?? 0} 条)` console.log(`session-face tools: flyai ${faOutcome}; session cdp 隔离门禁=needs-attach`) // 酒店平铺接入(2026-08-29):同一 flyai 工具 kind=hotel——live 三合法终态(试用达限 needs-setup / 限流·端点降级 or hit) - const fh = await byName('gotry_flyai_search').execute({ query: { kind: 'hotel', to: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03' } }, null) as { ok?: boolean; verdict?: string; via?: string; hotels?: unknown[]; evidence?: string; error?: string; setup?: string } + const fh = await byName('gotry_flyai_search').execute({ kind: 'hotel', to: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03' }, null) as { ok?: boolean; verdict?: string; via?: string; hotels?: unknown[]; evidence?: string; error?: string; setup?: string } const fhBlocked = fh.verdict === 'error' && /sentinel|block|trial limit/i.test(fh.error ?? '') const fhErrTerminal = fh.ok === false && fh.verdict === 'error' && /^flyai-error$/.test(String(fh.via ?? '')) && /\[实时API:flyai@error@/.test(String(fh.evidence ?? '')) const fhNeedsSetup = fh.ok === false && fh.verdict === 'needs-setup' @@ -266,8 +266,19 @@ async function main() { } else if (fh.ok !== true || fh.verdict !== 'hit' || (fh.hotels?.length ?? 0) < 1 || !/\[实时API:flyai@/.test(fh.evidence ?? '')) { throw new Error(`FAIL: flyai hotel 应 live hit,实际:${JSON.stringify(fh).slice(0, 220)}`) } - const fhDest = await byName('gotry_flyai_search').execute({ query: { kind: 'hotel' } }, null) as { ok?: boolean } + const fhDest = await byName('gotry_flyai_search').execute({ kind: 'hotel' }, null) as { ok?: boolean } if (fhDest.ok !== false) throw new Error('FAIL: hotel 缺目的地应参数闸拒绝') + // D-30 迁移锁:legacy blob 形态({query:{...}} 包裹)在宿主权校验即被结构化拒绝—— + // defineTool validateArgs 抛 ToolArgsError,guardToolExecute 兜成 ADR-13 ToolFailure(ok:false+evidence) + const faLegacy = await byName('gotry_flyai_search').execute({ query: { kind: 'flight', from: '上海', to: '丽江', date: '2026-10-01' } } as never, null) as { ok?: boolean; summary?: string; evidence?: string } + if (!(faLegacy.ok === false && !!faLegacy.evidence && /内部错误|参数|schema|violat/i.test(String(faLegacy.summary ?? '')))) { + throw new Error(`FAIL: flyai legacy blob 形态应被 typed 契约结构化拒绝(ADR-13 ToolFailure),实际:${JSON.stringify(faLegacy).slice(0, 200)}`) + } + const faBadEnum = await byName('gotry_flyai_search').execute({ kind: 'plane' } as never, null) as { ok?: boolean; summary?: string; evidence?: string } + if (!(faBadEnum.ok === false && !!faBadEnum.evidence)) { + throw new Error(`FAIL: flyai kind 枚举外值应被宿主权校验拒绝,实际:${JSON.stringify(faBadEnum).slice(0, 200)}`) + } + console.log(' D-30 typed 契约:legacy blob/枚举外值 → 宿主权结构化拒绝(ToolFailure 形状保持)') // 会话酒店路由(2026-09-03 实装):未收录城市在 transport 前短路 → error + cityId 指引(offline 零桥零浏览器) const sh = await byName('gotry_session_search').execute({ query: { kind: 'hotel', to: '不在码表的城市' } }, null) as { ok?: boolean; verdict?: string; summary?: string } if (!(sh.ok === false && /city=/.test(String(sh.summary ?? '')))) { @@ -280,7 +291,7 @@ async function main() { throw new Error(`FAIL: 会话火车未收录电报码应带发现指引,实际:${JSON.stringify(st).slice(0, 200)}`) } console.log(' train kind 路由:未收录电报码 → error + 电报码指引(transport 前短路)') - const fhPast = await byName('gotry_flyai_search').execute({ query: { kind: 'hotel', to: '大理', checkIn: '2026-01-01', checkOut: '2026-01-03' } }, null) as { ok?: boolean; summary?: string } + const fhPast = await byName('gotry_flyai_search').execute({ kind: 'hotel', to: '大理', checkIn: '2026-01-01', checkOut: '2026-01-03' }, null) as { ok?: boolean; summary?: string } if (!(fhPast.ok === false && /不是未来合法区间/.test(String(fhPast.summary ?? '')))) { throw new Error(`FAIL: 酒店过去入住日应代码层预校验拒绝,实际:${JSON.stringify(fhPast).slice(0, 200)}`) } diff --git a/ts/src/index.ts b/ts/src/index.ts index d8899d2f..7fe7888d 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -811,17 +811,28 @@ export function apply(ctx: Context, config: Config): void { name: 'gotry_flyai_search', 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? }. ' + + 'kind="flight"|"train": from/to 中文城市名 + date YYYY-MM-DD — real schedules & prices, split 直达/中转 in results. ' + + '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]. verdict=needs-setup (anonymous trial quota exhausted, upstream 429) is a CONFIG issue, not a search failure: surface the setup hint once, do NOT retry this tool this session — switch to gotry_session_search or web search. ' + 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.', + // D-30 第一刀(issue #112):query json blob → 平铺 typed 契约。逐字段 schema 由 dsh + // parameterSchemaSpecToJsonSchema 投影为模型可见 JSON Schema,validateArgs 在 execute 前 + // 宿主权校验——畸形参数(缺 kind/枚举外值/类型错/legacy blob 包裹)在入口即被结构化拒绝 + // (ToolArgsError → guardToolExecute 兜成 ADR-13 ToolFailure,形状由迁移测试锁死)。 + // 条件必填(机/火要 from/to/date,酒店要 to)不在 schema 强制,仍由 execute 结构化报错给方向。 parameters: { - query: { type: 'json', required: true, 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?: "洱海" }' }, + kind: { type: 'string', enum: ['flight', 'train', 'hotel'], required: true, description: '检索类型:flight 机票 / train 火车 / hotel 酒店' }, + from: { type: 'string', description: '出发城市中文,如 上海——kind=flight|train 必填' }, + to: { type: 'string', description: '到达城市中文,如 丽江;kind=hotel 时为目的地,如 大理(必填)' }, + date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' }, + checkIn: { type: 'string', description: '入住日期 YYYY-MM-DD,仅 kind=hotel;与 checkOut 成对可选(未定档期可不填先摸底)' }, + checkOut: { type: 'string', description: '退房日期 YYYY-MM-DD,仅 kind=hotel;与 checkIn 成对' }, + keyWords: { type: 'string', description: '酒店关键词,如 洱海——仅 kind=hotel 可选' }, }, output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { summary?: string }).summary ?? JSON.stringify(v).slice(0, 600)) }] }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ kind?: string; from?: string; to?: string; date?: string; checkIn?: string; checkOut?: string; keyWords?: string }>(args, 'from') + async execute(args, _exec) { + const q = args if (q.kind === 'hotel') { const dest = (q.to ?? '').trim() if (!dest) return { ok: false, summary: 'kind=hotel 需要 to(目的地中文,如 大理)' } as const @@ -894,10 +905,9 @@ export function apply(ctx: Context, config: Config): void { ...(r.verdict !== 'hit' ? routingField(kind === 'train' ? 'search-train' : 'search-flight', 'flyai') : {}), })) as Record }, - presentCall: args => ({ card: 'generic', title: `官方检索:${String((args.query as { kind?: string })?.kind ?? 'flight')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `官方检索:${args.kind}`, kind: 'fetch', rawInput: args }), presentResult: (args, value) => { - const kindQ = String((args.query as { kind?: string })?.kind) - const isHotel = kindQ === 'hotel' + const isHotel = args.kind === 'hotel' const r = value as { ok?: boolean; options?: unknown[]; hotels?: unknown[] } const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length) return { card: 'generic', title: `${isHotel ? '飞猪酒店' : '飞猪检索'}:${r.ok && n > 0 ? `${n} 条` : '降级'}`, content: [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }] } From 4b09e1db7bd0c8d0fda230fb38593b55c96c202f Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 17:41:08 +0400 Subject: [PATCH 02/12] =?UTF-8?q?feat(tools):=20D-30=20=E7=AC=AC=E4=BA=8C?= =?UTF-8?q?=E5=88=80=E2=80=94=E2=80=94session/hotel/weather=20=E4=B8=89?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20typed=20=E5=8F=82=E6=95=B0=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E8=BF=81=E7=A7=BB(issue=20#112)=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B9=B3=E9=93=BA=E5=AD=97=E6=AE=B5=E6=A8=A1=E5=9E=8B=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E9=80=90=E5=AD=97=E6=AE=B5=20schema;hotel(destination?= =?UTF-8?q?=20required)=E5=AE=BF=E4=B8=BB=E6=9D=83=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E6=8B=92=E7=95=B8=E5=BD=A2=E5=8F=82=E6=95=B0,session=20?= =?UTF-8?q?=E4=B8=89=E6=84=8F=E5=9B=BE=E6=97=A0=E5=85=AC=E5=85=B1=20requir?= =?UTF-8?q?ed=20=E4=BF=9D=E7=95=99=20interpretArgs=20=E5=AE=B9=E5=BF=8D?= =?UTF-8?q?=E5=B1=82(blob=20=E5=BD=92=E4=B8=80=E7=85=A7=E8=B5=B0=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E9=97=B8,smoke=20=E8=BF=81=E7=A7=BB=E9=94=81=E9=92=89?= =?UTF-8?q?=E6=AD=BB=E9=9B=B6=20transport=20=E7=9F=AD=E8=B7=AF=E8=B7=AF?= =?UTF-8?q?=E5=BE=84);skills-contract=20=E5=A5=91=E7=BA=A6=E5=AF=B9?= =?UTF-8?q?=E9=BD=90(checkOut=20=E5=AD=97=E9=9D=A2=E9=87=8F=E8=BF=9B?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=8F=8F=E8=BF=B0);smoke/hbcli-e2e/channel-r?= =?UTF-8?q?egistry/skills-contract=20=E5=85=A8=E7=BB=BF,run-all=20ALL=20SU?= =?UTF-8?q?ITES=20GREEN;=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5(arch=20=C2=A79/=C2=A710?= =?UTF-8?q?=E3=80=81roadmap=20=E5=A2=9E=E9=87=8F=E3=80=81stage1=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=A4=B4);=E4=BD=99=E9=87=8F=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=B8=8E=E6=99=AE=E9=80=9A=E6=A8=A1=E5=9E=8B=20canary?= =?UTF-8?q?=20=E8=AE=B0=20D-30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 4 +- docs/roadmap.md | 2 +- docs/stage1-top-down-design.md | 2 +- ts/scripts/hbcli-e2e-tests.ts | 2 +- ts/scripts/smoke.ts | 18 ++++++--- ts/src/index.ts | 74 ++++++++++++++++++++-------------- 6 files changed, 62 insertions(+), 40 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index d6624e49..dc1f4d4f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -400,7 +400,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - E2E:`ts/scripts/model-override-e2e.ts` mock 中转四场景(指定模型→请求体 model 一致/指定+预置用户层→env 压过/不设→内置默认 deepseek-v4-flash/不设+用户层→用户选择保留;②④ 同一 settings 文件仅差 env,对照证明覆盖因果),隔离 DSH_HOME、要求 current tarball 的 clean installed-package bin,全绿;不再靠临时移动 vendored 依赖伪装 package mode。smoke §17 单元回归(未设零监听/无事件总线不抛/覆盖语义)。附查发现一项环境债:vendored 仓内形态 Node 兼容窗口断裂,记 D-27。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 -- **typed 参数契约迁移第一刀(2026-09-04,issue #112,D-30)**:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁);条件必填(机/火要 from/to/date,酒店要 to)不在 schema 强制,仍由 execute 结构化报错给修正方向。余量(session/hotel/weather 等其余工具)与普通模型 canary 仍在 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,两刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。条件必填仍由 execute 结构化报错给修正方向。余量工具迁移与普通模型 canary 仍在 D-30。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -429,7 +429,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | | D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | -| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 flyai 已迁,余 session/hotel/weather 等) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**:`gotry_flyai_search` 平铺七字段(模型可见 JSON Schema+宿主权入口拒绝,ToolFailure 形状迁移锁 smoke §12/channel-registry-tests §8)。**余量**:session/hotel/weather 及其余工具迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合主干发布面);`interpretArgs` 留作旧形态容忍层 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | +| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 两刀已迁 flyai/session/hotel/weather 四个高流量工具,余其余工具) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**:flyai(七字段,kind required 宿主权即拒)+ session(十字段;无公共 required,保留 interpretArgs 容忍层)+ hotel(weather)——迁移锁 smoke §12/channel-registry-tests §8。**余量**:其余工具逐刀迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合发布面);`interpretArgs` 留作旧形态容忍层 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | **[D-NEW] dsh 进程保活缺失** diff --git a/docs/roadmap.md b/docs/roadmap.md index ecbaae68..d9994d1e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,7 +26,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **效应解译器(2026-08-29,issue #16,ADR-18)**:`effect_interpreter.v1` 落地 L4 渠道边界——指数退避重试 / 断路器 / mock 解译器(纯离线 CI 面)收敛进解译层;五渠道工具 + realtime-pricing 已接,余下渠道增量迁移(D-23)。run-all §37。OTA 工具面照旧平铺,证据链逐源标注不变。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 -- **typed 参数契约迁移第一刀(2026-09-04,issue #112,D-30)**:最高流量工具 `gotry_flyai_search` 参数面 query blob → dsh typed ParameterSchemaSpec 平铺七字段——模型可见逐字段 JSON Schema + execute 前宿主权校验(ToolArgsError→guard 兜 ADR-13 ToolFailure,迁移锁 smoke §12/channel-registry-tests §8);普通模型对参数形状不再靠猜。余量工具迁移与 flyai canary 记 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,两刀)**:四个高流量工具(flyai/session/hotel/weather)参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段——模型可见逐字段 JSON Schema;flyai(kind required)与 hotel(destination required)宿主权入口即拒畸形参数(ToolFailure 形状不变),session 无公共 required 保留 interpretArgs 容忍层(blob 归一照走业务闸);迁移锁 smoke §12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量工具迁移与 flyai canary 记 D-30。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index bc683d7c..5a2664a4 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,7 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: -- 2026-09-04 起,typed 参数契约迁移第一刀(issue #112,D-30):最高流量工具 `gotry_flyai_search` 参数面 query blob → dsh typed ParameterSchemaSpec 平铺七字段(kind 枚举 required+六可选),模型可见逐字段 JSON Schema + execute 前宿主权校验,拒绝形状保持 ADR-13 ToolFailure(迁移锁 smoke §12/channel-registry-tests §8);余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 +- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,两刀):四个高流量工具 gotry_flyai_search/gotry_session_search/gotry_hotel_search/gotry_weather_check 参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;required 具备处宿主权入口拒畸形参数(ToolFailure 形状不变),session 无公共 required 保留 interpretArgs 容忍层;迁移锁 smoke §12/channel-registry-tests §8;余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 - S2 mock 切片 ✅ / S3 求解挂载 ✅ / S4 真 LLM ✅(MiniMax-M2,`bb880f3`)/ S5 编排+持久化 ✅(详见 `architecture.md` §9,那里是唯一的当前状态源);本文保留设计原文供追溯。**M1/M2 已退出;M3 工程面已就绪但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仍受各自 Entry gate 约束。 diff --git a/ts/scripts/hbcli-e2e-tests.ts b/ts/scripts/hbcli-e2e-tests.ts index c8e4a56b..701f4bc6 100644 --- a/ts/scripts/hbcli-e2e-tests.ts +++ b/ts/scripts/hbcli-e2e-tests.ts @@ -144,7 +144,7 @@ async function main(): Promise { { stateRoot, timeoutMs: 30_000, hbcliBin: 'hbcli', sessionAccess: 'ask' } as never) const tool = registered.find(t => t.name === 'gotry_hotel_search') assert.ok(tool, 'gotry_hotel_search 应已注册') - const obs = await tool.execute({ query: { destination: '深圳' } }, null) as Record + const obs = await tool.execute({ destination: '深圳' }, null) as Record assert.equal(obs.ok, true, '工具面应返回 ok 包络(永不抛错契约)') assert.ok(obs.via === 'hbcli-error' || obs.via === 'hbcli-realtime', `工具面 via 应与渠道一致,实际 ${obs.via}`) const ev = String(obs.evidence) diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index 6feab7e6..ddf43fab 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -141,11 +141,11 @@ async function main() { // 8) D-10 切片 B:hotel 日期槽位接线——逐字表达代码层换算;unresolved 不猜(降级+note) { const hotel = byName('gotry_hotel_search') - const resolved = await hotel.execute({ query: { destination: '大理', checkIn: '2026-9-4', checkOut: '2026-09-06' } } as never, null) as { date_notes?: string[] } + const resolved = await hotel.execute({ destination: '大理', checkIn: '2026-9-4', checkOut: '2026-09-06' } as never, null) as { date_notes?: string[] } if (!resolved.date_notes?.some(n => n.includes('2026-9-4 → 2026-09-04'))) { throw new Error(`FAIL: 非规整 ISO 应产生 slot-resolved note,实际 ${JSON.stringify(resolved.date_notes)}`) } - const unresolved = await hotel.execute({ query: { destination: '大理', checkIn: '近期' } } as never, null) as { date_notes?: string[] } + const unresolved = await hotel.execute({ destination: '大理', checkIn: '近期' } as never, null) as { date_notes?: string[] } if (!unresolved.date_notes?.some(n => n.includes('日期未解析:近期'))) { throw new Error(`FAIL: 词表外表达应产生「日期未解析」note(不猜),实际 ${JSON.stringify(unresolved.date_notes)}`) } @@ -233,7 +233,7 @@ async function main() { const previousChromeUserDataDir = process.env.CHROME_USER_DATA_DIR process.env.CHROME_USER_DATA_DIR = prof try { - ss = await byName('gotry_session_search').execute({ query: { from: '上海', to: '丽江', date: '2026-10-01' } }, null) as typeof ss + ss = await byName('gotry_session_search').execute({ from: '上海', to: '丽江', date: '2026-10-01' }, null) as typeof ss } finally { if (previousChromeUserDataDir === undefined) delete process.env.CHROME_USER_DATA_DIR else process.env.CHROME_USER_DATA_DIR = previousChromeUserDataDir @@ -280,17 +280,25 @@ async function main() { } console.log(' D-30 typed 契约:legacy blob/枚举外值 → 宿主权结构化拒绝(ToolFailure 形状保持)') // 会话酒店路由(2026-09-03 实装):未收录城市在 transport 前短路 → error + cityId 指引(offline 零桥零浏览器) - const sh = await byName('gotry_session_search').execute({ query: { kind: 'hotel', to: '不在码表的城市' } }, null) as { ok?: boolean; verdict?: string; summary?: string } + const sh = await byName('gotry_session_search').execute({ kind: 'hotel', to: '不在码表的城市' }, null) as { ok?: boolean; verdict?: string; summary?: string } if (!(sh.ok === false && /city=/.test(String(sh.summary ?? '')))) { throw new Error(`FAIL: 会话酒店未收录城市应带 cityId 指引,实际:${JSON.stringify(sh).slice(0, 200)}`) } console.log(' hotel kind 路由:未收录城市 → error + cityId 指引(transport 前短路)') // 会话火车路由(2026-09-03 实装):未收录电报码在 transport 前短路 → error + 电报码发现指引(offline 零桥零浏览器) - const st = await byName('gotry_session_search').execute({ query: { kind: 'train', from: '不在码表', to: '也不在', date: '2026-12-01' } }, null) as { ok?: boolean; verdict?: string; summary?: string } + const st = await byName('gotry_session_search').execute({ kind: 'train', from: '不在码表', to: '也不在', date: '2026-12-01' }, null) as { ok?: boolean; verdict?: string; summary?: string } if (!(st.ok === false && /fromStationTelecode/.test(String(st.summary ?? '')))) { throw new Error(`FAIL: 会话火车未收录电报码应带发现指引,实际:${JSON.stringify(st).slice(0, 200)}`) } console.log(' train kind 路由:未收录电报码 → error + 电报码指引(transport 前短路)') + // D-30 第二刀迁移锁:session 工具全字段可选(kind 缺省=flight),根 schema 开放无法宿主权拒 blob + // → interpretArgs 容忍层接住(设计 §4③):blob 归一后照常走业务闸——用 kind=hotel 缺 to 的 + // 参数闸短路出确定性结构化报错(零 transport 零节律闸),证明容忍层归一语义不漂 + const ssLegacy = await byName('gotry_session_search').execute({ query: { kind: 'hotel' } } as never, null) as { ok?: boolean; summary?: string; routing?: unknown } + if (!(ssLegacy.ok === false && !('routing' in ssLegacy) && /需要 to/.test(String(ssLegacy.summary ?? '')))) { + throw new Error(`FAIL: session legacy blob 应经容忍层归一走业务闸(结构化报错,无 routing),实际:${JSON.stringify(ssLegacy).slice(0, 200)}`) + } + console.log(' D-30 typed 契约:session legacy blob → 容忍层归一 + 结构化业务报错(全字段可选无法宿主权拒)') const fhPast = await byName('gotry_flyai_search').execute({ kind: 'hotel', to: '大理', checkIn: '2026-01-01', checkOut: '2026-01-03' }, null) as { ok?: boolean; summary?: string } if (!(fhPast.ok === false && /不是未来合法区间/.test(String(fhPast.summary ?? '')))) { throw new Error(`FAIL: 酒店过去入住日应代码层预校验拒绝,实际:${JSON.stringify(fhPast).slice(0, 200)}`) diff --git a/ts/src/index.ts b/ts/src/index.ts index 7fe7888d..023d6746 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -589,23 +589,23 @@ export function apply(ctx: Context, config: Config): void { name: 'gotry_hotel_search', description: 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). ' - + 'Input: destination city name + optional dates/occupancy. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — ' + + 'Input: destination city name + optional dates/adults. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — ' + 'the code layer resolves them against the time anchor; unresolved expressions degrade to an undated search with an explicit ' + 'date_notes entry instead of guessing. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, ' + 'or [static-pack:estimate]) per the L4 invariant.', + // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。 parameters: { - query: { - type: 'json', - required: true, - description: '{ destination: "<目的地城市>", checkIn?: "YYYY-MM-DD 或自然表达(下周五/8.20)", checkOut?: 同上, occupancy?: { adults: 2 } }', - }, + destination: { type: 'string', required: true, description: '目的地城市,如 大理' }, + checkIn: { type: 'string', description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);解析不了降级无日期检索并记 date_notes' }, + checkOut: { type: 'string', description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn' }, + adults: { type: 'integer', description: '成人数(顶层字段,默认 2)' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 400)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ destination?: string; checkIn?: string; checkOut?: string; adults?: number }>(args, 'destination') + async execute(args, _exec) { + const q = args if (!q.destination) throw new Error('gotry_hotel_search requires destination') const started = Date.now() const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json') @@ -648,7 +648,7 @@ export function apply(ctx: Context, config: Config): void { } as never return JSON.parse(JSON.stringify(payload)) as never }, - presentCall: args => ({ card: 'generic', title: `酒店搜索:${String((args.query as { destination?: string })?.destination ?? '')}`, kind: 'search', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `酒店搜索:${args.destination ?? ''}`, kind: 'search', rawInput: args }), presentResult: (_args, value) => { const r = value as { hotels?: unknown; via?: string; destination?: string; summary?: string } const h = r.hotels @@ -700,18 +700,20 @@ export function apply(ctx: Context, config: Config): void { + 'Returns daily temp range, precipitation probability, weather code — with evidence chain tagging ' + '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.', parameters: { - query: { - type: 'json', - required: true, - description: '{ place: "<城市名>", month?: 8, mode?: "forecast"|"climate", days?: 7 }', - }, + // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。 + place: { type: 'string', description: '城市名(中文可),如 大理;与 lat/lng 二选一,都没给 execute 结构化报错' }, + lat: { type: 'number', description: '纬度(与 lng 成对;给了则跳过地理编码)' }, + lng: { type: 'number', description: '经度(与 lat 成对)' }, + month: { type: 'integer', description: '气候基线月份 1-12(mode=climate 或给了 month 未指 mode 时用)' }, + mode: { type: 'string', enum: ['forecast', 'climate'], description: '默认 forecast' }, + days: { type: 'integer', description: '预报天数(≤16,默认 7)' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ place?: string; lat?: number; lng?: number; month?: number; mode?: string; days?: number }>(args, 'place') + async execute(args, _exec) { + const q = args const started = Date.now() let lat: number | undefined = q.lat, lng: number | undefined = q.lng let placeLabel = q.place ?? `${q.lat},${q.lng}` @@ -746,10 +748,10 @@ export function apply(ctx: Context, config: Config): void { latency_ms: Date.now() - started, })) as Record }, - presentCall: args => ({ card: 'generic', title: `天气:${String((args.query as { place?: string })?.place ?? '')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`, kind: 'fetch', rawInput: args }), presentResult: (args, value) => { const r = value as { summary?: string } - const place = String((args.query as { place?: string })?.place ?? '') + const place = String(args.place ?? '') const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable') return { card: 'generic', @@ -966,18 +968,32 @@ export function apply(ctx: Context, config: Config): void { 'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). ' + '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). ' + 'Transport: GoTry Session Bridge browser extension (one-time install) — the agent side never talks to Chrome debugging, ZERO system dialogs; read-only by construction (the extension never issues requests; it only passively forwards the site\'s own search responses; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). ' - + 'kind="flight": { from, to, date } — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. ' - + 'kind="hotel": { to, cityId?, checkIn?, checkOut?, adults? } — cityId = the numeric city= in a hotels.ctrip.com list URL (web-search it when the destination is outside the built-in city table); hotel prices are the user\'s real logged-in prices. Evidence [会话:ctrip-hotel@ts]. ' - + 'kind="train": { from, to, date, fromStationTelecode?, toStationTelecode? } — 12306 left-ticket query (public face): train codes, times, durations, seat availability; the list API carries NO prices (prices live on the 12306 page). Evidence [会话:train-12306@ts]. ' + + 'kind="flight": from/to 中文城市名 + date YYYY-MM-DD — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. ' + + 'kind="hotel": to=目的地中文, cityId? = the numeric city= in a hotels.ctrip.com list URL (web-search it when the destination is outside the built-in city table), checkIn?/checkOut? (YYYY-MM-DD), adults?; hotel prices are the user\'s real logged-in prices. Evidence [会话:ctrip-hotel@ts]. ' + + 'kind="train": from/to/date(YYYY-MM-DD) + fromStationTelecode?/toStationTelecode? (three-letter codes in the kyfw query URL, for cities outside the built-in table) — 12306 left-ticket query (public face): train codes, times, durations, seat availability; the list API carries NO prices (prices live on the 12306 page). Evidence [会话:train-12306@ts]. ' + 'verdict needs-login = call gotry_session_login (opens the Ctrip login entry in the user\'s own foreground tab — no terminal, no credentials through GoTry); ' + `needs-extension = one-time browser-extension install (Chrome Web Store one-click, installUrl is also surfaced as a clickable link in the verdict field for dsh UI to render) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. ` + 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).', + // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(逐字段 schema 模型可见)。 + // 与 flyai 刀的差异:本工具三意图无公共 required 字段(kind 缺省=flight),根 schema 是 + // 隐式开放对象,宿主权无法用 required 拒 legacy blob——故保留 interpretArgs 容忍层 + // (tool-orchestration-design §4③「interpretArgs 留作旧形态容忍层」),blob 调用在 + // execute 内归一后走原条件闸,结构化报错不崩。flyai 因 kind required 仍在宿主权即拒。 parameters: { - query: { type: 'json', required: true, description: 'kind=flight(默认): { from: "上海", to: "丽江", date: "2026-10-01" }; kind=hotel: { kind: "hotel", to: "迪拜", cityId?: 220, checkIn?: "YYYY-MM-DD", checkOut?: "YYYY-MM-DD", adults?: 2 }; kind=train: { kind: "train", from: "上海", to: "昆明", date: "2026-10-01", fromStationTelecode?: "SHH", toStationTelecode?: "KMM" }' }, + kind: { type: 'string', enum: ['flight', 'hotel', 'train'], description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面)' }, + from: { type: 'string', description: '出发城市中文(词表内),如 上海——kind=flight|train 必填' }, + to: { type: 'string', description: '到达城市中文——kind=flight|train 必填;kind=hotel 时为目的地(必填)' }, + date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' }, + cityId: { type: 'integer', description: '携程酒店 city= 数字,仅 kind=hotel;目的地码表外时 web 搜 hotels.ctrip.com list URL 取 city=' }, + checkIn: { type: 'string', description: '入住日 YYYY-MM-DD,仅 kind=hotel 可选' }, + checkOut: { type: 'string', description: '退房日 YYYY-MM-DD,仅 kind=hotel 可选' }, + adults: { type: 'integer', description: '成人数,仅 kind=hotel 可选' }, + fromStationTelecode: { type: 'string', description: '出发站三位电报码,仅 kind=train;城市码表外时填(kyfw 查询页 URL 里)' }, + toStationTelecode: { type: 'string', description: '到达站三位电报码,仅 kind=train,同上' }, }, output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { summary?: string }).summary ?? JSON.stringify(v).slice(0, 600)) }] }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ kind?: string; from?: string; to?: string; date?: string; cityId?: number | string; checkIn?: string; checkOut?: string; adults?: number; fromStationTelecode?: string; toStationTelecode?: string }>(args, 'from') + async execute(args, _exec) { + const q = unwrapQuery<{ kind?: string; from?: string; to?: string; date?: string; cityId?: number | string; checkIn?: string; checkOut?: string; adults?: number; fromStationTelecode?: string; toStationTelecode?: string }>(args) // ---- 会话酒店(2026-09-03 实装;2026-09-02 迪拜 session:用户要携程找酒店,会话面却只有机票)---- // ---- 会话火车(2026-09-03 实装;12306 公开查询面,无登录闸)---- if (q.kind === 'train') { @@ -1068,14 +1084,12 @@ export function apply(ctx: Context, config: Config): void { })) as Record }, presentCall: args => { - const q = (args.query as { kind?: string; from?: string; to?: string }) ?? {} - const callTitle = q.kind === 'hotel' ? `会话酒店:${q.to ?? ''}` : q.kind === 'train' ? `会话火车:${q.from ?? ''}` : `会话检索:${q.from ?? ''}` - return { card: 'generic', title: callTitle, kind: 'fetch', rawInput: args.query } + const callTitle = args.kind === 'hotel' ? `会话酒店:${args.to ?? ''}` : args.kind === 'train' ? `会话火车:${args.from ?? ''}` : `会话检索:${args.from ?? ''}` + return { card: 'generic', title: callTitle, kind: 'fetch', rawInput: args } }, presentResult: (args, value) => { const r = value as { verdict?: string; options?: unknown[]; hotels?: unknown[]; installUrl?: string } - const kindQ = String((args.query as { kind?: string })?.kind) - const isHotel = kindQ === 'hotel' + const isHotel = args.kind === 'hotel' const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length) const needsExt = r.verdict === 'needs-extension' const label = r.verdict === 'hit' @@ -1087,7 +1101,7 @@ export function apply(ctx: Context, config: Config): void { : r.verdict ?? '降级' const content: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }] if (needsExt && r.installUrl) content.push({ type: 'text', text: `安装链接:${r.installUrl}` }) - const face = kindQ === 'hotel' ? '会话酒店' : kindQ === 'train' ? '会话火车' : '会话检索' + const face = args.kind === 'hotel' ? '会话酒店' : args.kind === 'train' ? '会话火车' : '会话检索' return { card: 'generic', title: `${face}:${label}`, content } }, })) From 7671dc5f8ac70d4eb4a8eb5fb812b1c6f182ce7d Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 17:58:56 +0400 Subject: [PATCH 03/12] =?UTF-8?q?feat(tools):=20D-30=20=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E5=88=80=E2=80=94=E2=80=94=E4=B8=83=E9=AB=98=E9=A2=91=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=20typed=20=E5=8F=82=E6=95=B0=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E8=BF=81=E7=A7=BB(issue=20#112)=E2=80=94=E2=80=94session=5Flog?= =?UTF-8?q?in/doctor/artifacts=5Flist/fact=5Fgate/agent=5Freach/turn=5Fhan?= =?UTF-8?q?doff=5Flist/artifacts=5Fread=20=E5=B9=B3=E9=93=BA=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=A8=A1=E5=9E=8B=E5=8F=AF=E8=A7=81=E9=80=90=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=20schema;=E5=88=80=E6=B3=95=E6=80=BB=E7=BA=B2?= =?UTF-8?q?=E7=A1=AE=E7=AB=8B:=E6=9C=89=20required=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E5=85=B7(artifacts=5Fread=20path)=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB=E6=9D=83=E5=85=A5=E5=8F=A3=E6=8B=92=20blob,=E5=85=A8?= =?UTF-8?q?=E5=8F=AF=E9=80=89=E5=B7=A5=E5=85=B7=E4=BF=9D=E7=95=99=20interp?= =?UTF-8?q?retArgs=20=E5=AE=B9=E5=BF=8D=E5=B1=82(doctor-tests=20blob=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E9=9A=90=E5=BC=8F=E9=94=81);agent=5Freach=20?= =?UTF-8?q?=E7=9A=84=20action=20=E4=B8=8D=E8=AE=BE=20enum=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=8F=8D=E5=B0=84=E6=A1=A5=20D-4a'=20=E9=80=8F?= =?UTF-8?q?=E4=BC=A0=E4=B8=8A=E6=B8=B8=E6=96=B0=E5=A2=9E=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E9=9B=B6=E6=94=B9=E5=8A=A8;fact=5Fgate=20=E7=9A=84=20itinerary?= =?UTF-8?q?=20=E4=BF=9D=20object+additionalProperties:true,=E5=B5=8C?= =?UTF-8?q?=E5=A5=97=E6=94=B6=E7=B4=A7=E5=BD=92=20#118=20D-26;=E7=B4=AF?= =?UTF-8?q?=E8=AE=A1=2011=20=E5=B7=A5=E5=85=B7=E8=BF=81=E7=A7=BB,tsc+smoke?= =?UTF-8?q?=20=E5=85=A8=E7=BB=BF,run-all=20ALL=20SUITES=20GREEN;=E5=85=AD?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C?= =?UTF-8?q?=E6=AD=A5(arch=20=C2=A79/=C2=A710=E3=80=81roadmap=20=E5=A2=9E?= =?UTF-8?q?=E9=87=8F=E3=80=81stage1=20=E7=8A=B6=E6=80=81=E5=A4=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 4 +- docs/roadmap.md | 2 +- docs/stage1-top-down-design.md | 2 +- ts/scripts/smoke.ts | 14 +++--- ts/src/index.ts | 83 ++++++++++++++++------------------ 5 files changed, 51 insertions(+), 54 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index dc1f4d4f..aacaf7bf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -400,7 +400,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - E2E:`ts/scripts/model-override-e2e.ts` mock 中转四场景(指定模型→请求体 model 一致/指定+预置用户层→env 压过/不设→内置默认 deepseek-v4-flash/不设+用户层→用户选择保留;②④ 同一 settings 文件仅差 env,对照证明覆盖因果),隔离 DSH_HOME、要求 current tarball 的 clean installed-package bin,全绿;不再靠临时移动 vendored 依赖伪装 package mode。smoke §17 单元回归(未设零监听/无事件总线不抛/覆盖语义)。附查发现一项环境债:vendored 仓内形态 Node 兼容窗口断裂,记 D-27。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,两刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。条件必填仍由 execute 结构化报错给修正方向。余量工具迁移与普通模型 canary 仍在 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,三刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。第三刀:七个模型高频工具——`gotry_session_login`/`gotry_doctor`/`gotry_artifacts_list`/`gotry_fact_gate`(全字段可选,容忍层;fact_gate 的 itinerary 保 object+additionalProperties:true,嵌套形状归一在闸内,收紧归 #118 D-26)/`gotry_agent_reach`(action 不设 enum——反射桥按 D-4a' 透传上游新增渠道,gotry 零改动)/`gotry_turn_handoff_list`(容忍层)/`gotry_artifacts_read`(path required,宿主权即拒)。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量工具(trip_log/anything/web/github/video/wish/motivation/companion 等)与普通模型 canary 仍在 D-30。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -429,7 +429,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | | D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | -| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 两刀已迁 flyai/session/hotel/weather 四个高流量工具,余其余工具) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**:flyai(七字段,kind required 宿主权即拒)+ session(十字段;无公共 required,保留 interpretArgs 容忍层)+ hotel(weather)——迁移锁 smoke §12/channel-registry-tests §8。**余量**:其余工具逐刀迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合发布面);`interpretArgs` 留作旧形态容忍层 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | +| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 三刀已迁 11 工具,余 trip_log/anything/web/github/video/wish/motivation/companion 等) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层):flyai/session/hotel/weather 四高流量 + session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list 七高频——迁移锁 smoke §12/channel-registry-tests §8。**余量**:其余工具逐刀迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | **[D-NEW] dsh 进程保活缺失** diff --git a/docs/roadmap.md b/docs/roadmap.md index d9994d1e..5d608ee6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,7 +26,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **效应解译器(2026-08-29,issue #16,ADR-18)**:`effect_interpreter.v1` 落地 L4 渠道边界——指数退避重试 / 断路器 / mock 解译器(纯离线 CI 面)收敛进解译层;五渠道工具 + realtime-pricing 已接,余下渠道增量迁移(D-23)。run-all §37。OTA 工具面照旧平铺,证据链逐源标注不变。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,两刀)**:四个高流量工具(flyai/session/hotel/weather)参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段——模型可见逐字段 JSON Schema;flyai(kind required)与 hotel(destination required)宿主权入口即拒畸形参数(ToolFailure 形状不变),session 无公共 required 保留 interpretArgs 容忍层(blob 归一照走业务闸);迁移锁 smoke §12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量工具迁移与 flyai canary 记 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,三刀)**:11 个工具参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段——四高流量(flyai/session/hotel/weather)+ 七高频(session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list);刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量工具迁移与 canary 记 D-30。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 5a2664a4..a4f56fe4 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,7 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: -- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,两刀):四个高流量工具 gotry_flyai_search/gotry_session_search/gotry_hotel_search/gotry_weather_check 参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;required 具备处宿主权入口拒畸形参数(ToolFailure 形状不变),session 无公共 required 保留 interpretArgs 容忍层;迁移锁 smoke §12/channel-registry-tests §8;余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 +- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,三刀):11 个工具(flyai/session/hotel/weather 四高流量 + session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list 七高频)参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §12/channel-registry-tests §8;余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 - S2 mock 切片 ✅ / S3 求解挂载 ✅ / S4 真 LLM ✅(MiniMax-M2,`bb880f3`)/ S5 编排+持久化 ✅(详见 `architecture.md` §9,那里是唯一的当前状态源);本文保留设计原文供追溯。**M1/M2 已退出;M3 工程面已就绪但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仍受各自 Entry gate 约束。 diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index ddf43fab..f4562bf5 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -112,7 +112,7 @@ async function main() { if (typeof byName(n).presentResult !== 'function') throw new Error(`FAIL: ${n} 缺 presentResult`) } const ar = byName('gotry_agent_reach') - const arView = ar.presentResult!({ query: { action: 'reach', channel: 'v2ex', method: 'get_hot_topics' } }, { verdict: 'found', summary: '10 topics' }) + const arView = ar.presentResult!({ action: 'reach', channel: 'v2ex', method: 'get_hot_topics' }, { verdict: 'found', summary: '10 topics' }) if (!arView?.title?.includes('✅') || !arView.title.includes('v2ex.get_hot_topics')) throw new Error(`FAIL: agent_reach 结果卡,实际 ${arView?.title}`) console.log(`result cards on 5 tools; agent_reach card: ${arView.title}`) @@ -362,7 +362,7 @@ async function main() { ledger.settleWorkflowRun('art-probe-1', '# 交付·账本权威\nD1 大理\nD2 洱海') const listTool = byName('gotry_artifacts_list') - const ledList = await listTool.execute({ query: {} }, null) as { ok?: boolean; artifacts?: Array<{ source?: string; id?: string; status?: string }>; total?: number } + const ledList = await listTool.execute({}, null) as { ok?: boolean; artifacts?: Array<{ source?: string; id?: string; status?: string }>; total?: number } const run = ledList.artifacts?.find(a => a.id === 'art-probe-1') if (!ledList.ok || !run || run.source !== 'async-run' || run.status !== 'settled') { throw new Error(`FAIL: artifacts list 应发现账本已交付工单,实际:${JSON.stringify(ledList).slice(0, 200)}`) @@ -373,11 +373,11 @@ async function main() { } const readTool = byName('gotry_artifacts_read') - const r1 = await readTool.execute({ query: { path: 'art-probe-1' } }, null) as { ok?: boolean; path?: string; offset?: number; lines?: Array<{ number: number; text: string }>; totalLines?: number; lang?: string } + const r1 = await readTool.execute({ path: 'art-probe-1' }, null) as { ok?: boolean; path?: string; offset?: number; lines?: Array<{ number: number; text: string }>; totalLines?: number; lang?: string } if (!r1.ok || r1.lines?.[0]?.number !== 1 || !r1.lines?.[0]?.text.includes('交付·账本权威') || r1.lang !== 'markdown') { throw new Error(`FAIL: 裸工单 id 应从账本读出行号视图,实际:${JSON.stringify(r1).slice(0, 200)}`) } - const view = readTool.presentResult?.({ query: { path: 'art-probe-1' } }, r1) as { card?: string; path?: string; offset?: number; lines?: unknown[]; totalLines?: number; lang?: string } + const view = readTool.presentResult?.({ path: 'art-probe-1' }, r1) as { card?: string; path?: string; offset?: number; lines?: unknown[]; totalLines?: number; lang?: string } if (view?.card !== 'read' || view.path !== r1.path || view.offset !== 1 || view.lines?.length !== r1.lines?.length || view.totalLines !== r1.totalLines || view.lang !== 'markdown') { throw new Error(`FAIL: read 卡字段不齐,实际:${JSON.stringify(view).slice(0, 200)}`) } @@ -416,16 +416,16 @@ async function main() { } const gate = byName('gotry_fact_gate') const passMd = ['# 行程片段', '## D3 7.18 香港 → 普吉', '- 国泰航空 CX771 08:05→10:35(已按 exact-date 检索记录)'].join('\n') - const pass = await gate.execute({ query: { markdown: passMd, tripYear: 2027 } }, null) as { verdict?: string; presentation?: string } + const pass = await gate.execute({ markdown: passMd, tripYear: 2027 }, null) as { verdict?: string; presentation?: string } if (pass.verdict !== 'pass' || pass.presentation !== 'verified_itinerary_allowed') { throw new Error(`FAIL: 可回溯产物应 pass,实际:${JSON.stringify(pass).slice(0, 300)}`) } const badMd = ['# 行程片段', '## D1 7.16 深圳 → 普吉', '- 香港快运 UO784 10:05→12:40 直飞 ✓'].join('\n') - const blocked = await gate.execute({ query: { markdown: badMd, tripYear: 2027 } }, null) as { verdict?: string; violations?: Array<{ kind?: string }>; summary?: string } + const blocked = await gate.execute({ markdown: badMd, tripYear: 2027 }, null) as { verdict?: string; violations?: Array<{ kind?: string }>; summary?: string } if (blocked.verdict !== 'blocked' || !blocked.violations?.some(v => v.kind === 'not_in_source')) { throw new Error(`FAIL: exact-date miss 被 UO784 填充必须 blocked(not_in_source),实际:${JSON.stringify(blocked).slice(0, 300)}`) } - const view = gate.presentResult?.({ query: {} }, blocked) as { title?: string } + const view = gate.presentResult?.({}, blocked) as { title?: string } if (!view?.title?.includes('blocked')) throw new Error(`FAIL: blocked 呈现卡标题应含 blocked,实际:${view?.title}`) console.log(`fact gate: registry 1+1(hit/miss);verified 措辞 pass;miss 填充 UO784 blocked(not_in_source) + 呈现卡`) } diff --git a/ts/src/index.ts b/ts/src/index.ts index 023d6746..161a277b 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -476,17 +476,15 @@ export function apply(ctx: Context, config: Config): void { + 'a previously handed-off plan («规划好了吗» / «上次那个行程»): open = still being worked in the ' + 'background (ETA ' + '约 1 小时' + '), settled = deliverable ready (excerpt included, full text in the ' + 'ticket\'s .deliverable.md), failed = honest failure note. Never fabricate a deliverable that is not here.', + // D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层(同 session 刀法) parameters: { - query: { - type: 'json', - description: '{ ticketId?: "th-...(可选,只看这一单)" }', - }, + ticketId: { type: 'string', description: '只看这一单,如 th-...(缺省列全部)' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }], }, - async execute(args: { query?: unknown }) { + async execute(args, _exec: unknown) { const q = interpretArgs<{ ticketId?: string }>(args) const root = process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.' const all = await listTurnHandoffTickets(root) @@ -510,7 +508,7 @@ export function apply(ctx: Context, config: Config): void { summary: `后台规划工单 ${tickets.length} 张:\n${lines.join('\n')}`, } as never }, - presentCall: args => ({ card: 'generic', title: '后台规划工单查询', kind: 'search', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: '后台规划工单查询', kind: 'search', rawInput: args }), })) registerGuarded(defineTool({ @@ -925,11 +923,12 @@ export function apply(ctx: Context, config: Config): void { + 'Transport: the GoTry Session Bridge browser extension (one-time install, ZERO Chrome system dialogs). ' + `verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-extension (one-time extension install: the verdict surfaces the Chrome Web Store installUrl as a clickable link for dsh UI to render — installation is a browser concern, not gotry's). ` + 'Evidence [会话:-login@ts].', + // D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层 parameters: { - query: { type: 'json', required: true, description: '可空对象 {}: { waitSeconds?: number }(等待用户完成登录的上限秒数,默认 90,至多 300)' }, + waitSeconds: { type: 'integer', description: '等待用户完成登录的上限秒数,默认 90,至多 300;不传用默认' }, }, output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { evidence?: string }).evidence ?? JSON.stringify(v).slice(0, 600)) }] }, - async execute(args: { query?: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ waitSeconds?: number }>(args, 'waitSeconds') const r = await sessionLogin({ waitMs: typeof q?.waitSeconds === 'number' ? q.waitSeconds * 1000 : undefined }) const summary = r.verdict === 'logged-in' @@ -1288,18 +1287,20 @@ export function apply(ctx: Context, config: Config): void { 'Action "status" runs the real `agent-reach doctor` (.venv/bin/agent-reach). ' + 'Channels needing cookies/setup return the upstream check() guidance verbatim — do NOT give up there: hand the user the exact configure command, then offer to re-check. ' + 'Evidence chain: [agent-reach:.@ts].', + // D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层。 + // action 不设 enum:反射桥按 D-4a' 透传上游新增渠道/方法,gotry 零改动(生态开放面) parameters: { - query: { - type: 'json', - required: true, - description: '{ action: "status" } 或 { action: "reach", channel: "<上游渠道名,如 web/v2ex/xueqiu>", method: "<上游方法名,如 read/get_hot_topics/get_stock_quote>", args?: "<空格分隔参数>" }', - }, + action: { type: 'string', description: 'status(默认,无 channel 时)或 reach' }, + channel: { type: 'string', description: '上游渠道名,如 web/v2ex/xueqiu(action=reach 时)' }, + method: { type: 'string', description: '上游方法名,如 read/get_hot_topics/get_stock_quote' }, + args: { type: 'string', description: '空格分隔参数' }, + timeoutMs: { type: 'integer', description: '超时毫秒' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ action?: string; channel?: string; method?: string; args?: string; timeoutMs?: number }>(args, 'channel') const started = Date.now() const dir = await ensureStateDir(config.stateRoot) @@ -1353,10 +1354,10 @@ export function apply(ctx: Context, config: Config): void { latency_ms: Date.now() - started, })) as Record }, - presentCall: args => ({ card: 'generic', title: `Agent Reach:${String((args.query as { channel?: string; method?: string })?.channel ?? '')}.${String((args.query as { method?: string })?.method ?? 'status')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`, kind: 'fetch', rawInput: args }), presentResult: (args, value) => { const r = value as { verdict?: string; summary?: string } - const q = (args.query as { channel?: string; method?: string }) ?? {} + const q = (args as { channel?: string; method?: string }) ?? {} const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌' return { card: 'generic', @@ -1377,18 +1378,15 @@ export function apply(ctx: Context, config: Config): void { + 'Returns per-item status (ok/degraded/missing) with exact fix commands. ' + 'Repair = `npx gotry doctor --fix` run BY THE USER in a terminal (this tool never installs anything); LLM keys are the dsh host\'s business and are deliberately out of scope. ' + 'A markdown report is rendered for the workspace (gotry-state/doctor-report.md) so the sidebar workbench can preview it.', + // D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层 parameters: { - query: { - type: 'json', - required: true, - description: '{ },{ 体检 } 或 { writeReport?: boolean(默认 true,把 markdown 报告写进 gotry-state/doctor-report.md,侧栏工作台可预览) }', - }, + writeReport: { type: 'boolean', description: '默认 true,把 markdown 报告写进 gotry-state/doctor-report.md(侧栏工作台可预览)' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 900)) }], }, - async execute(args: { query?: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ writeReport?: boolean }>(args, 'writeReport') const report = await runDoctorChecks({ stateRoot: config.stateRoot ?? '.' }) // 报告落 gotry-state(侧栏工作台预览面);写失败不阻塞体检结论本身 @@ -1426,18 +1424,15 @@ export function apply(ctx: Context, config: Config): void { 'in the working directory (trip plans etc.). READ-ONLY discovery. ' + 'Use when the user asks to see/open/revisit a previously generated artifact ' + '(「看看刚才生成的行程」「上次的规划在哪」「打开那个 md」) — list first, then read with gotry_artifacts_read.', + // D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层 parameters: { - query: { - type: 'json', - required: true, - description: '{ limit?: 20 }', - }, + limit: { type: 'integer', description: '最多返回条数,默认 20' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ limit?: number }>(args, 'limit') const r = await listArtifacts({ stateRoot: config.stateRoot ?? '.', limit: q.limit }) const lines = r.artifacts.map(a => @@ -1465,19 +1460,19 @@ export function apply(ctx: Context, config: Config): void { 'Input: the path from gotry_artifacts_list, or a bare async ticket id (e.g. dp-xxxx). ' + 'Optional offset (1-based) / limit window for paging large files. ' + 'READ-ONLY; text artifacts only (md/txt/json/jsonl/csv/log/yaml).', + // D-30 第三刀(issue #112):query blob → 平铺 typed;path required → 宿主权入口拒畸形参数(同 flyai/hotel 刀法) parameters: { - query: { - type: 'json', - required: true, - description: '{ path: "", offset?: 1, limit?: 400 }', - }, + path: { type: 'string', required: true, description: 'list 返回的路径或工单 id' }, + offset: { type: 'integer', description: '起始行号(1 起)' }, + limit: { type: 'integer', description: '窗口行数,默认 400' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { content?: string }).content ?? JSON.stringify(value).slice(0, 600)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ path?: string; offset?: number; limit?: number }>(args, 'path') + async execute(args, _exec) { + const q = args + if (!q.path) throw new Error('gotry_artifacts_read requires path') if (!q.path) return JSON.parse(JSON.stringify({ ok: false, error: 'path 必填(来自 gotry_artifacts_list)' })) as Record const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', path: q.path, offset: q.offset, limit: q.limit }) if (!r.ok) return JSON.parse(JSON.stringify(r)) as Record @@ -1486,7 +1481,7 @@ export function apply(ctx: Context, config: Config): void { summary: `${r.path}(${r.totalLines} 行)第 ${r.offset}-${r.offset + r.lines.length - 1} 行${r.windowed ? `(共 ${r.totalLines} 行,可翻页)` : ''}`, })) as Record }, - presentCall: args => ({ card: 'generic', title: `读产物:${String((args.query as { path?: string })?.path ?? '')}`, kind: 'read', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `读产物:${args.path ?? ''}`, kind: 'read', rawInput: args }), presentResult: (_args, value) => { const r = value as unknown as { ok?: boolean; path?: string; offset?: number; lines?: Array<{ number: number; text: string }>; totalLines?: number; lang?: string; content?: string; error?: string } if (!r.ok) { @@ -1521,18 +1516,20 @@ export function apply(ctx: Context, config: Config): void { + '「联程」without protected_connection=true; policy claims without 「截至 YYYY-MM-DD」; unconditional ✓ on unverified claims. ' + 'Optional itinerary object enables machine invariants (hotel_nights + onboard_nights = total nights, O&D segments vs flight legs, ' + 'budget floor ≥ sum of item minimums). verdict=blocked ⇒ fix or downgrade wording — never present as a verified plan.', + // D-30 第三刀(issue #112):query blob → 平铺 typed;markdown/path 二选一无公共 required + // → interpretArgs 容忍层;itinerary 保 object+additionalProperties:true(嵌套形状归一在闸内, + // #118 D-26 渲染原语切片再收紧),给模型「这是对象」的可见信号 parameters: { - query: { - type: 'json', - required: true, - description: '{ markdown?: "<产物全文>", path?: "<产物路径(artifacts_list 返回的)>— 二选一", tripYear?: 2027, itinerary?: { trip_start, trip_end, stays: [{place,check_in,check_out}], onboard_nights, od_segments: [{from,to,date,mode,legs}], budget_items: [{label,min_cny,max_cny}], claimed_floor_cny? } }', - }, + markdown: { type: 'string', description: '产物全文;与 path 二选一' }, + path: { type: 'string', description: '产物路径(artifacts_list 返回的);与 markdown 二选一' }, + tripYear: { type: 'integer', description: '行程年份,如 2027(日期归一用)' }, + itinerary: { type: 'object', additionalProperties: true, description: '结构化行程 { trip_start, trip_end, stays:[{place,check_in,check_out}], onboard_nights, od_segments:[{from,to,date,mode,legs}], budget_items:[{label,min_cny,max_cny}], claimed_floor_cny? }' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ markdown?: string; path?: string; tripYear?: number; itinerary?: Record }>(args, 'markdown') const avMap = await loadAirlineAirportMap() if (!avMap) { @@ -1554,7 +1551,7 @@ export function apply(ctx: Context, config: Config): void { : `事实闸 BLOCKED(${report.violations.length} 违例,${report.traceable}/${report.claims_checked} claim 可回溯)——不得宣称「已验证方案」:\n${lines.join('\n')}\n修正路径:逐条改成 exact-date 源返回的事实,或降级为「未确认/当前不可售,到 D-xx 复核」;exact-date miss 的 route+date 不得用历史班期/相邻日期/航线页填充。` return JSON.parse(JSON.stringify({ ok: true, ...report, summary })) as Record }, - presentCall: args => ({ card: 'generic', title: '产物事实闸', kind: 'execute', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: '产物事实闸', kind: 'execute', rawInput: args }), presentResult: (_args, value) => { const r = value as { verdict?: string; violations?: unknown[]; summary?: string } return { From 59be5fb0608363e5014370ee330f5a6c30b312e3 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 18:37:02 +0400 Subject: [PATCH 04/12] =?UTF-8?q?feat(tools):=20D-30=20=E7=AC=AC=E5=9B=9B?= =?UTF-8?q?=E5=88=80=E2=80=94=E2=80=94query=20blob=20=E6=B8=85=E9=9B=B6,wi?= =?UTF-8?q?sh=5Fpool/trip=5Flog/flight=5Fverify/anything/web/video/github?= =?UTF-8?q?=20=E4=B8=83=E5=B7=A5=E5=85=B7=20typed=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E8=BF=81=E7=A7=BB(issue=20#112)=E2=80=94?= =?UTF-8?q?=E2=80=94trip=5Flog=20=E7=9A=84=20destination/start/evidence=20?= =?UTF-8?q?required=20=E8=BF=9B=20schema,evidence=20P0=20=E6=BA=AF?= =?UTF-8?q?=E6=BA=90=E7=BA=A2=E7=BA=BF=E7=94=B1=E5=AE=BF=E4=B8=BB=E6=9D=83?= =?UTF-8?q?=E9=97=B8;web=5Fsearch=20=E5=AD=97=E7=AC=A6=E4=B8=B2=20blob=20?= =?UTF-8?q?=E5=BD=A2=E6=80=81=E9=80=80=E5=BD=B9(smoke=20=C2=A76=20?= =?UTF-8?q?=E6=94=B9=E9=94=81=E5=AE=BF=E4=B8=BB=E6=9D=83=E6=8B=92=E7=BB=9D?= =?UTF-8?q?);github=5Fsearch=20=E9=A1=B6=E5=B1=82=20query=20=E4=BB=8E?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=BD=A2=E6=80=81=E5=8F=98=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=AD=97=E6=AE=B5;wish=5Fpool=20=E5=85=A8=E5=8F=AF?= =?UTF-8?q?=E9=80=89=E4=BF=9D=E7=95=99=E5=AE=B9=E5=BF=8D=E5=B1=82;?= =?UTF-8?q?=E5=88=80=E6=B3=95=E6=80=BB=E7=BA=B2:required=20=E5=85=B7?= =?UTF-8?q?=E5=A4=87=E5=AE=BF=E4=B8=BB=E6=9D=83=E5=8D=B3=E6=8B=92,?= =?UTF-8?q?=E5=85=A8=E5=8F=AF=E9=80=89=E5=AE=B9=E5=BF=8D=E5=B1=82;?= =?UTF-8?q?=E7=B4=AF=E8=AE=A1=2018=20=E5=B7=A5=E5=85=B7=E8=BF=81=E7=A7=BB,?= =?UTF-8?q?=E4=BD=99=20feasibility/motivation/companion=20=E4=B8=89?= =?UTF-8?q?=E4=B8=AA=20payload=20=E5=BD=A2=E6=80=81=E8=AE=B0=20D-30;tsc+sm?= =?UTF-8?q?oke=20=E5=85=A8=E7=BB=BF,run-all=20ALL=20SUITES=20GREEN;?= =?UTF-8?q?=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=90=8C=E6=AD=A5(arch=20=C2=A79/=C2=A710=E3=80=81roadmap=20?= =?UTF-8?q?=E5=A2=9E=E9=87=8F=E3=80=81stage1=20=E7=8A=B6=E6=80=81=E5=A4=B4?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 4 +- docs/roadmap.md | 2 +- docs/stage1-top-down-design.md | 2 +- ts/scripts/smoke.ts | 12 ++-- ts/src/index.ts | 110 ++++++++++++++++----------------- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index aacaf7bf..a399144a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -400,7 +400,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - E2E:`ts/scripts/model-override-e2e.ts` mock 中转四场景(指定模型→请求体 model 一致/指定+预置用户层→env 压过/不设→内置默认 deepseek-v4-flash/不设+用户层→用户选择保留;②④ 同一 settings 文件仅差 env,对照证明覆盖因果),隔离 DSH_HOME、要求 current tarball 的 clean installed-package bin,全绿;不再靠临时移动 vendored 依赖伪装 package mode。smoke §17 单元回归(未设零监听/无事件总线不抛/覆盖语义)。附查发现一项环境债:vendored 仓内形态 Node 兼容窗口断裂,记 D-27。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,三刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。第三刀:七个模型高频工具——`gotry_session_login`/`gotry_doctor`/`gotry_artifacts_list`/`gotry_fact_gate`(全字段可选,容忍层;fact_gate 的 itinerary 保 object+additionalProperties:true,嵌套形状归一在闸内,收紧归 #118 D-26)/`gotry_agent_reach`(action 不设 enum——反射桥按 D-4a' 透传上游新增渠道,gotry 零改动)/`gotry_turn_handoff_list`(容忍层)/`gotry_artifacts_read`(path required,宿主权即拒)。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量工具(trip_log/anything/web/github/video/wish/motivation/companion 等)与普通模型 canary 仍在 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,四刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。第三刀:七个模型高频工具——`gotry_session_login`/`gotry_doctor`/`gotry_artifacts_list`/`gotry_fact_gate`(全字段可选,容忍层;fact_gate 的 itinerary 保 object+additionalProperties:true,嵌套形状归一在闸内,收紧归 #118 D-26)/`gotry_agent_reach`(action 不设 enum——反射桥按 D-4a' 透传上游新增渠道,gotry 零改动)/`gotry_turn_handoff_list`(容忍层)/`gotry_artifacts_read`(path required,宿主权即拒)。第四刀:余下全部 `query:` blob 工具清零——`gotry_wish_pool_list`(容忍层)/`gotry_trip_log`(destination/start/evidence required,**evidence 是 P0 溯源红线,进 schema 由宿主权闸**)/`gotry_flight_verify`(callsign)/`gotry_anything_search`(keyword+contentType enum)/`gotry_web_search`(url;字符串 blob 形态退役,smoke §6 改锁宿主权拒绝)/`gotry_video_subtitle`(url)/`gotry_github_search`(顶层 query 字符串)。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:`gotry_feasibility_check`(payload)/`gotry_motivation_save`(profile)/`gotry_companion_save`(companion)三个引擎/记忆面 blob 与普通模型 canary 仍在 D-30。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -429,7 +429,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | | D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | -| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 三刀已迁 11 工具,余 trip_log/anything/web/github/video/wish/motivation/companion 等) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层):flyai/session/hotel/weather 四高流量 + session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list 七高频——迁移锁 smoke §12/channel-registry-tests §8。**余量**:其余工具逐刀迁移;每个首批工具一轮普通模型 canary(flyai canary 待跑,跑前不合发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | +| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 四刀已迁 18 工具,`query:` blob 清零,余 feasibility/motivation/companion 三个 payload 形态) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层):四高流量 + 七高频 + wish_pool/trip_log(evidence 红线进 schema)/flight_verify/anything/web/video/github——迁移锁 smoke §12/§6/channel-registry-tests §8。**余量**:feasibility_check(payload)/motivation_save(profile)/companion_save(companion)三个引擎与记忆面 blob;普通模型 canary(flyai 待跑,跑前不合发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | **[D-NEW] dsh 进程保活缺失** diff --git a/docs/roadmap.md b/docs/roadmap.md index 5d608ee6..fc5d5b72 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,7 +26,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **效应解译器(2026-08-29,issue #16,ADR-18)**:`effect_interpreter.v1` 落地 L4 渠道边界——指数退避重试 / 断路器 / mock 解译器(纯离线 CI 面)收敛进解译层;五渠道工具 + realtime-pricing 已接,余下渠道增量迁移(D-23)。run-all §37。OTA 工具面照旧平铺,证据链逐源标注不变。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,三刀)**:11 个工具参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段——四高流量(flyai/session/hotel/weather)+ 七高频(session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list);刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量工具迁移与 canary 记 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,四刀)**:18 个工具参数面 blob → dsh typed ParameterSchemaSpec 平铺字段(`query:` blob 清零)——四高流量(flyai/session/hotel/weather)+ 七高频(session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list)+ wish_pool/trip_log/flight_verify/anything/web/video/github;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §12/§6/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量(feasibility/motivation/companion payload 形态)与 canary 记 D-30。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index a4f56fe4..24bcdcb6 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,7 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: -- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,三刀):11 个工具(flyai/session/hotel/weather 四高流量 + session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list 七高频)参数面 query blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §12/channel-registry-tests §8;余量工具与普通模型 canary 记 `architecture.md` §10 D-30。 +- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,四刀):18 个工具(`query:` blob 清零——四高流量+七高频+wish_pool/trip_log/flight_verify/anything/web/video/github)参数面 blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;trip_log evidence P0 红线进 schema),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §12/§6/channel-registry-tests §8;余量(feasibility/motivation/companion payload 形态)与普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 - S2 mock 切片 ✅ / S3 求解挂载 ✅ / S4 真 LLM ✅(MiniMax-M2,`bb880f3`)/ S5 编排+持久化 ✅(详见 `architecture.md` §9,那里是唯一的当前状态源);本文保留设计原文供追溯。**M1/M2 已退出;M3 工程面已就绪但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仍受各自 Entry gate 约束。 diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index f4562bf5..8161de7b 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -116,14 +116,14 @@ async function main() { if (!arView?.title?.includes('✅') || !arView.title.includes('v2ex.get_hot_topics')) throw new Error(`FAIL: agent_reach 结果卡,实际 ${arView?.title}`) console.log(`result cards on 5 tools; agent_reach card: ${arView.title}`) - // 6) #12/#13 参数形态兼容:字符串 query 与裸对象都能落到 url(不再「url 必填」) + // 6) D-30 typed 契约后(#12/#13 形态兼容的演进终态):字符串 blob 形态被宿主权结构化拒绝 + // (url required 进 schema);平铺 url 进 execute 走正常抓取/降级,不再有「裸形态落 url」 const ws = byName('gotry_web_search') - const r1 = await ws.execute({ query: 'not-a-url' } as never, null) as { summary?: string } + const r1 = await ws.execute({ query: 'not-a-url' } as never, null) as { ok?: boolean; summary?: string; evidence?: string } + if (!(r1.ok === false && !!r1.evidence)) throw new Error(`FAIL: 字符串 blob 形态应被宿主权结构化拒绝(ToolFailure),实际 ${JSON.stringify(r1).slice(0, 160)}`) const r2 = await ws.execute({ url: 'not-a-url' } as never, null) as { summary?: string } - for (const [name, r] of [['string', r1], ['bare-obj', r2]] as const) { - if (r.summary === 'url 必填') throw new Error(`FAIL: ${name} 形态未被 unwrapQuery 接住`) - } - console.log('unwrapQuery: string + bare-object shapes both accepted') + if (r2.summary === 'url 必填') throw new Error(`FAIL: 平铺 url 应进 execute 走降级,而非参数闸,实际 ${r2.summary}`) + console.log('D-30 typed 契约:web_search 字符串 blob → 宿主权拒绝;平铺 url → 正常降级') // 7) 骨架输出 schema 放宽回归:execute 正常返回三字段;guard 错误兜底(ok/summary) // 不再被 strict schema 拒(dsh 参数层拒错→guard 兜底→旧 strict schema 校验炸, diff --git a/ts/src/index.ts b/ts/src/index.ts index 161a277b..18cc811e 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -409,18 +409,22 @@ export function apply(ctx: Context, config: Config): void { + 'Muted wishes never surface. Surfacing records a recalled event in the memory-utility sidecar. ' + 'action="confirm-outcome" records the user-confirmed real-world outcome (attribution helpful/harmful/neutral) — ' + 'ONLY pass attribution the user explicitly stated; the agent must never self-attribute usefulness.', + // D-30 第四刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层 parameters: { - query: { - type: 'json', - required: true, - description: '{ action?: "recall"|"confirm-outcome", days?, budgetCny?, month?, wishId?, attribution?: "helpful"|"harmful"|"neutral", detail?, tripStart?: "行程起始(确认成行时挂时间线)" }', - }, + action: { type: 'string', description: 'recall(默认,召回下一次出发建议)或 confirm-outcome(确认某条建议成/不成)' }, + days: { type: 'integer', description: '回看天数窗(recall)' }, + budgetCny: { type: 'number', description: '预算上限(recall 过滤)' }, + month: { type: 'integer', description: '出行月份 1-12(recall)' }, + wishId: { type: 'string', description: 'confirm-outcome 目标愿望 id' }, + attribution: { type: 'string', enum: ['helpful', 'harmful', 'neutral'], description: 'confirm-outcome 归因' }, + detail: { type: 'string', description: '一句话细节(用户原话)' }, + tripStart: { type: 'string', description: '确认成行时挂时间线的行程起始日' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }], }, - async execute(args: { query: unknown }, _exec: unknown) { + async execute(args, _exec: unknown) { const q = unwrapQuery<{ action?: string; days?: number; budgetCny?: number; month?: number; wishId?: string; attribution?: 'helpful' | 'harmful' | 'neutral'; detail?: string; tripStart?: string }>(args, 'action') const ledger = ensureLedger(config.stateRoot) const now = new Date().toISOString() @@ -463,7 +467,7 @@ export function apply(ctx: Context, config: Config): void { summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`, } as never }, - presentCall: args => ({ card: 'generic', title: '「下一次出发」召回', kind: 'search', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: '「下一次出发」召回', kind: 'search', rawInput: args }), })) // ADR-24 v2 复访交付面:用户回问「行程规划好了吗」时,模型用这个只读工具 @@ -552,19 +556,21 @@ export function apply(ctx: Context, config: Config): void { + '(「去年国庆去了大理」) — dates resolve via the time anchor (词表外日期会拒绝,请用户给绝对日期), evidence MUST be the user\'s verbatim words. ' + 'Append-only and idempotent (same destination+start+source = same trip); overlapping same-destination trips are rejected for human adjudication. ' + 'Past trips power origin resolution and 「去过不再推」 ranking — never a hard filter.', + // D-30 第四刀(issue #112):平铺 typed;destination/evidence required → 宿主权入口拒缺参 + // (evidence 是 P0 溯源红线,进 schema 由宿主权闸,不再依赖 execute 散文报错) parameters: { - query: { - type: 'json', - required: true, - description: '{ destination: "大理", start: "2025-10-01 或绝对日期表达", end?: 同上, companions?: ["爸爸"], evidence: "<用户原话>" }', - }, + destination: { type: 'string', required: true, description: '目的地,如 大理' }, + start: { type: 'string', required: true, description: '行程起始:YYYY-MM-DD 或绝对日期表达(解析不了 execute 结构化报错不猜)' }, + end: { type: 'string', description: '行程结束,形态同 start(可选)' }, + companions: { type: 'array', items: { type: 'string' }, description: '同行人,如 ["爸爸"]' }, + evidence: { type: 'string', required: true, description: '用户原话(溯源 P0 红线)' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ destination?: string; start?: string; end?: string; companions?: string[]; evidence?: string }>(args, 'destination') + async execute(args, _exec: unknown) { + const q = args if (!q.destination) return { ok: false, summary: 'destination 必填' } as never if (!q.evidence) return { ok: false, summary: 'evidence 必填(用户原话,溯源 P0)' } as never const anchor = buildTimeAnchor(new Date()) @@ -580,7 +586,7 @@ export function apply(ctx: Context, config: Config): void { if (!res.appended && res.reason) return { ok: false, summary: res.reason } as never return { ok: true, appended: res.appended, trip_id: res.tripId, total: res.total, summary: res.appended ? `已入时间线:${q.destination} @ ${start}` : `已存在(幂等跳过):${res.tripId}` } as never }, - presentCall: args => ({ card: 'generic', title: '记录旅行', kind: 'edit', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: '记录旅行', kind: 'edit', rawInput: args }), })) registerGuarded(defineTool({ @@ -770,19 +776,18 @@ export function apply(ctx: Context, config: Config): void { + 'unavailable = API failure, gracefully degraded. ' + 'Use to ground "is this flight actually flying right now?" in real data, complementing ' + 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).', + // D-30 第四刀(issue #112):平铺 typed;callsign required → 宿主权即拒 parameters: { - query: { - type: 'json', - required: true, - description: '{ callsign: "EK329", airport?: "OMDB", timeoutMs?: 10000 }', - }, + callsign: { type: 'string', required: true, description: '航班呼号,如 EK329' }, + airport: { type: 'string', description: '预期机场 IATA,如 OMDB(观测命中时展示)' }, + timeoutMs: { type: 'integer', description: '超时毫秒,默认 10000' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 500)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ callsign: string; airport?: string; timeoutMs?: number }>(args, 'callsign') + async execute(args, _exec: unknown) { + const q = args const started = Date.now() if (!q.callsign) { return JSON.parse(JSON.stringify({ verdict: 'unavailable', evidence: '[校验不可用:无 callsign]', summary: 'callsign 必填' })) as Record @@ -804,7 +809,7 @@ export function apply(ctx: Context, config: Config): void { latency_ms: Date.now() - started, })) as Record }, - presentCall: args => ({ card: 'generic', title: `飞行校验:${String((args.query as { callsign?: string })?.callsign ?? '')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `飞行校验:${args.callsign ?? ''}`, kind: 'fetch', rawInput: args }), })) registerGuarded(defineTool({ @@ -1115,19 +1120,20 @@ export function apply(ctx: Context, config: Config): void { 'unavailable = hbcli failed (degraded, never blocks). ' + 'Use as the first stop when the user mentions a place/city/hotel name and you need to ground it in real catalog data ' + '(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).', + // D-30 第四刀(issue #112):平铺 typed;keyword required → 宿主权即拒 parameters: { - query: { - type: 'json', - required: true, - description: '{ keyword: "<搜索关键词>", contentType?: "city"|"hotel", parentDestinationId?: "?", timeoutMs?: 12000 }', - }, + keyword: { type: 'string', required: true, description: '搜索关键词' }, + contentType: { type: 'string', enum: ['city', 'hotel'], description: '内容类型(缺省由上游推断)' }, + parentDestinationId: { type: 'string', description: '父目的地 id(上游形态)' }, + timeoutMs: { type: 'integer', description: '超时毫秒,默认 12000' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ keyword: string; contentType?: 'city' | 'hotel'; parentDestinationId?: string | number; timeoutMs?: number }>(args, 'keyword') + async execute(args, _exec: unknown) { + const q = args + const rawQuery = String(q.keyword ?? '') const started = Date.now() if (!q.keyword) { return JSON.parse(JSON.stringify({ ok: false, verdict: 'error', summary: 'keyword 必填', evidence: '[hbcli-anything@error] empty' })) as Record @@ -1148,7 +1154,7 @@ export function apply(ctx: Context, config: Config): void { latency_ms: Date.now() - started, })) as Record }, - presentCall: args => ({ card: 'generic', title: `Anything search:${String((args.query as { keyword?: string })?.keyword ?? '')}`, kind: 'search', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `Anything search:${args.keyword ?? ''}`, kind: 'search', rawInput: args }), presentResult: (_args, value) => { const r = value as { hits?: unknown[]; total_candidates?: number; verdict?: string; keyword?: string; summary?: string } const n = Array.isArray(r.hits) ? r.hits.length : (r.total_candidates ?? 0) @@ -1168,19 +1174,17 @@ export function apply(ctx: Context, config: Config): void { 'NOT a general-purpose search engine — only fetches a URL you already know. ' + 'Three-valued: ok / error(非法 URL/超时)/not-reachable(r.jina.ai 不可用).' + 'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。', + // D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒(字符串 blob 形态退役) parameters: { - query: { - type: 'json', - required: true, - description: '{ url: "https://example.com", timeoutMs?: 20000 }', - }, + url: { type: 'string', required: true, description: '完整 URL,https://…' }, + timeoutMs: { type: 'integer', description: '超时毫秒,默认 20000' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { content?: string }).content?.slice(0, 800) ?? JSON.stringify(value).slice(0, 800)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ url?: string; timeoutMs?: number }>(args, 'url') + async execute(args, _exec: unknown) { + const q = args const started = Date.now() if (!q.url) { return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填', evidence: '[agent-reach:error] empty url' })) as Record @@ -1197,7 +1201,7 @@ export function apply(ctx: Context, config: Config): void { latency_ms: Date.now() - started, })) as Record }, - presentCall: args => ({ card: 'generic', title: `读网页:${String((args.query as { url?: string })?.url ?? '')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `读网页:${args.url ?? ''}`, kind: 'fetch', rawInput: args }), })) registerGuarded(defineTool({ @@ -1207,19 +1211,17 @@ export function apply(ctx: Context, config: Config): void { 'If yt-dlp is installed on this machine, returns the subtitle text (vtt, zh-Hans/zh/en preference). ' + 'If NOT installed, degrades gracefully with install instructions — never blocks. ' + 'Evidence chain: [agent-reach:yt-dlp@ts] / [@not-installed@ts].', + // D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒 parameters: { - query: { - type: 'json', - required: true, - description: '{ url: "https://www.youtube.com/watch?v=...", lang?: "zh-Hans,zh,en" }', - }, + url: { type: 'string', required: true, description: '视频页 URL(youtube 等)' }, + lang: { type: 'string', description: '字幕语言优先级,如 zh-Hans,zh,en' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ url?: string; lang?: string }>(args, 'url') + async execute(args, _exec: unknown) { + const q = args if (!q.url) { return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填' })) as Record } @@ -1235,7 +1237,7 @@ export function apply(ctx: Context, config: Config): void { latency_ms: r.latencyMs, })) as Record }, - presentCall: args => ({ card: 'generic', title: `视频字幕:${String((args.query as { url?: string })?.url ?? '')}`, kind: 'fetch', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `视频字幕:${args.url ?? ''}`, kind: 'fetch', rawInput: args }), })) registerGuarded(defineTool({ @@ -1245,19 +1247,17 @@ export function apply(ctx: Context, config: Config): void { 'If gh is installed and authenticated, returns repos with name/description/stars/url. ' + 'If NOT installed, degrades with install instructions — never blocks. ' + 'Evidence chain: [agent-reach:gh@ts] / [@not-installed@ts].', + // D-30 第四刀(issue #112):平铺 typed;query required → 宿主权即拒(顶层字段与旧内层同名,形态从对象变字符串) parameters: { - query: { - type: 'json', - required: true, - description: '{ query: "agent-reach", limit?: 5 }', - }, + query: { type: 'string', required: true, description: 'GitHub 搜索关键词,如 agent-reach' }, + limit: { type: 'integer', description: '结果数上限,默认 5' }, }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }], }, - async execute(args: { query: unknown }, _exec: unknown) { - const q = unwrapQuery<{ query?: string; limit?: number }>(args, 'query') + async execute(args, _exec: unknown) { + const q = args if (!q.query) { return JSON.parse(JSON.stringify({ ok: false, summary: 'query 必填' })) as Record } @@ -1273,7 +1273,7 @@ export function apply(ctx: Context, config: Config): void { latency_ms: r.latencyMs, })) as Record }, - presentCall: args => ({ card: 'generic', title: `GitHub 搜索:${String((args.query as { query?: string })?.query ?? '')}`, kind: 'search', rawInput: args.query }), + presentCall: args => ({ card: 'generic', title: `GitHub 搜索:${args.query ?? ''}`, kind: 'search', rawInput: args }), })) registerGuarded(defineTool({ From 63ae136b8a75a6b4c90cf590fb909dd86be821f4 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 18:48:32 +0400 Subject: [PATCH 05/12] =?UTF-8?q?feat(tools):=20D-30=20=E7=AC=AC=E4=BA=94?= =?UTF-8?q?=E5=88=80=E6=94=B6=E5=AE=98=E2=80=94=E2=80=9423/23=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=20typed=20=E5=8F=82=E6=95=B0=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=AE=8C=E6=88=90(issue=20#112)=E2=80=94?= =?UTF-8?q?=E2=80=94feasibility(payload=20=E7=BB=93=E6=9E=84=E5=8C=96?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1,=E5=B5=8C=E5=A5=97=E6=9D=83=E5=A8=81?= =?UTF-8?q?=E5=9C=A8=20unified=20=E5=BC=95=E6=93=8E=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E4=B8=8D=E8=B6=8A=E5=B1=82=E5=BC=BA=E7=BA=A6=E6=9D=9F)/motivat?= =?UTF-8?q?ion(profile=20=E9=97=AD=E5=90=88=E5=AF=B9=E8=B1=A1,evidence=20P?= =?UTF-8?q?0=20=E5=8F=8D=E4=BC=AA=E9=80=A0=E7=BA=A2=E7=BA=BF=E8=BF=9B?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=20schema=20=E7=94=B1=E5=AE=BF=E4=B8=BB?= =?UTF-8?q?=E6=9D=83=E9=97=B8,smoke=20=C2=A71=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=AE=BF=E4=B8=BB=E6=9D=83=E5=89=8D=E7=BD=AE?= =?UTF-8?q?=E6=8B=92=E7=BB=9D)/companion(=E9=97=AD=E5=90=88=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=20label/evidence=20required,additionalProperties:fals?= =?UTF-8?q?e=20=E4=B8=8E=E8=B4=9F=E9=9D=A2=E6=B8=85=E5=8D=95=E5=90=8C?= =?UTF-8?q?=E5=90=91);=E5=88=80=E6=B3=95=E6=80=BB=E7=BA=B2:required=20?= =?UTF-8?q?=E5=85=B7=E5=A4=87=E5=AE=BF=E4=B8=BB=E6=9D=83=E5=8D=B3=E6=8B=92?= =?UTF-8?q?,=E5=85=A8=E5=8F=AF=E9=80=89=E5=AE=B9=E5=BF=8D=E5=B1=82;evidenc?= =?UTF-8?q?e=20=E7=B1=BB=E7=BA=A2=E7=BA=BF=E5=85=A8=E6=95=B0=E8=BF=9B=20sc?= =?UTF-8?q?hema;tsc+smoke=20=E5=85=A8=E7=BB=BF,run-all=20ALL=20SUITES=20GR?= =?UTF-8?q?EEN;=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=90=8C=E6=AD=A5(arch=20=C2=A79/=C2=A710=E3=80=81roa?= =?UTF-8?q?dmap=20=E5=A2=9E=E9=87=8F=E3=80=81stage1=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=A4=B4);=E4=BD=99=E9=87=8F=E4=BB=85=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20canary(=E8=BF=87=E9=97=B8=E6=89=8D?= =?UTF-8?q?=E8=BF=9B=E5=8F=91=E5=B8=83=E9=9D=A2)=E8=AE=B0=20D-30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 4 ++-- docs/roadmap.md | 2 +- docs/stage1-top-down-design.md | 2 +- ts/scripts/smoke.ts | 7 ++++--- ts/src/index.ts | 33 +++++++++++++++++++++++++++------ 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a399144a..51aea5ba 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -400,7 +400,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - E2E:`ts/scripts/model-override-e2e.ts` mock 中转四场景(指定模型→请求体 model 一致/指定+预置用户层→env 压过/不设→内置默认 deepseek-v4-flash/不设+用户层→用户选择保留;②④ 同一 settings 文件仅差 env,对照证明覆盖因果),隔离 DSH_HOME、要求 current tarball 的 clean installed-package bin,全绿;不再靠临时移动 vendored 依赖伪装 package mode。smoke §17 单元回归(未设零监听/无事件总线不抛/覆盖语义)。附查发现一项环境债:vendored 仓内形态 Node 兼容窗口断裂,记 D-27。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,四刀)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`(destination required + checkIn/checkOut/adults)/`gotry_weather_check`(place/lat/lng/mode/month/days)同刀法迁移——其中 session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);hotel 因 destination required 宿主权即拒;skills-contract 契约对齐(destination/checkIn/checkOut 字面量进参数描述)。第三刀:七个模型高频工具——`gotry_session_login`/`gotry_doctor`/`gotry_artifacts_list`/`gotry_fact_gate`(全字段可选,容忍层;fact_gate 的 itinerary 保 object+additionalProperties:true,嵌套形状归一在闸内,收紧归 #118 D-26)/`gotry_agent_reach`(action 不设 enum——反射桥按 D-4a' 透传上游新增渠道,gotry 零改动)/`gotry_turn_handoff_list`(容忍层)/`gotry_artifacts_read`(path required,宿主权即拒)。第四刀:余下全部 `query:` blob 工具清零——`gotry_wish_pool_list`(容忍层)/`gotry_trip_log`(destination/start/evidence required,**evidence 是 P0 溯源红线,进 schema 由宿主权闸**)/`gotry_flight_verify`(callsign)/`gotry_anything_search`(keyword+contentType enum)/`gotry_web_search`(url;字符串 blob 形态退役,smoke §6 改锁宿主权拒绝)/`gotry_video_subtitle`(url)/`gotry_github_search`(顶层 query 字符串)。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:`gotry_feasibility_check`(payload)/`gotry_motivation_save`(profile)/`gotry_companion_save`(companion)三个引擎/记忆面 blob 与普通模型 canary 仍在 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`/`gotry_weather_check`——session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);skills-contract 契约对齐。第三刀:七个模型高频工具(session_login/doctor/artifacts_list/fact_gate/agent_reach/turn_handoff_list/artifacts_read;fact_gate 的 itinerary 保 object+additionalProperties:true,收紧归 #118 D-26;agent_reach 的 action 不设 enum——反射桥 D-4a' 开放面)。第四刀:`query:` blob 清零(wish_pool/trip_log/flight_verify/anything/web/video/github;trip_log 的 evidence P0 红线进 schema;web_search 字符串 blob 退役)。第五刀:最后三个 payload 形态——`gotry_feasibility_check`(payload 结构化对象,嵌套权威在 unified 引擎契约不越层强约束)/`gotry_motivation_save`(profile 闭合对象,**evidence P0 反伪造红线进嵌套 schema 由宿主权闸**,smoke §1 断言适配)/`gotry_companion_save`(companion 闭合对象 label/evidence required,additionalProperties:false 与负面清单同向)。**23/23 注册工具全部 typed 契约化**。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:普通模型 canary(跑前不合发布面)记 D-30。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -429,7 +429,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | | D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | -| D-30 检索工具 typed 参数契约迁移部分完成(ADR-25 配套;2026-09-04 四刀已迁 18 工具,`query:` blob 清零,余 feasibility/motivation/companion 三个 payload 形态) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):高流量工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已迁**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层):四高流量 + 七高频 + wish_pool/trip_log(evidence 红线进 schema)/flight_verify/anything/web/video/github——迁移锁 smoke §12/§6/channel-registry-tests §8。**余量**:feasibility_check(payload)/motivation_save(profile)/companion_save(companion)三个引擎与记忆面 blob;普通模型 canary(flyai 待跑,跑前不合发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | +| D-30 检索工具 typed 参数契约迁移完成待 canary(ADR-25 配套;2026-09-04 五刀 23/23 工具全 typed 化) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已全部迁移**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层;motivation/companion/trip_log 的 evidence P0 红线进 schema 由宿主权闸;agent_reach 反射桥 action 保持开放)——迁移锁 smoke §1/§6/§12/channel-registry-tests §8。**余量**:普通模型 canary 一轮为证据(过闸才进发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | **[D-NEW] dsh 进程保活缺失** diff --git a/docs/roadmap.md b/docs/roadmap.md index fc5d5b72..2ea03773 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,7 +26,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **效应解译器(2026-08-29,issue #16,ADR-18)**:`effect_interpreter.v1` 落地 L4 渠道边界——指数退避重试 / 断路器 / mock 解译器(纯离线 CI 面)收敛进解译层;五渠道工具 + realtime-pricing 已接,余下渠道增量迁移(D-23)。run-all §37。OTA 工具面照旧平铺,证据链逐源标注不变。 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 -- **typed 参数契约迁移(2026-09-04,issue #112,D-30,四刀)**:18 个工具参数面 blob → dsh typed ParameterSchemaSpec 平铺字段(`query:` blob 清零)——四高流量(flyai/session/hotel/weather)+ 七高频(session_login/doctor/artifacts_list/artifacts_read/fact_gate/agent_reach/turn_handoff_list)+ wish_pool/trip_log/flight_verify/anything/web/video/github;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §12/§6/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量(feasibility/motivation/companion payload 形态)与 canary 记 D-30。 +- **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema——四高流量(flyai/session/hotel/weather)+ 七高频 + wish_pool/trip_log/flight_verify/anything/web/video/github + feasibility/motivation/companion;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log/motivation/companion 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §1/§6/§12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量:普通模型 canary 记 D-30。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 24bcdcb6..e643ea00 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,7 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: -- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,四刀):18 个工具(`query:` blob 清零——四高流量+七高频+wish_pool/trip_log/flight_verify/anything/web/video/github)参数面 blob → dsh typed ParameterSchemaSpec 平铺字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;trip_log evidence P0 红线进 schema),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §12/§6/channel-registry-tests §8;余量(feasibility/motivation/companion payload 形态)与普通模型 canary 记 `architecture.md` §10 D-30。 +- 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,五刀收官):全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;evidence 类 P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §1/§6/§12/channel-registry-tests §8;普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 - S2 mock 切片 ✅ / S3 求解挂载 ✅ / S4 真 LLM ✅(MiniMax-M2,`bb880f3`)/ S5 编排+持久化 ✅(详见 `architecture.md` §9,那里是唯一的当前状态源);本文保留设计原文供追溯。**M1/M2 已退出;M3 工程面已就绪但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仍受各自 Entry gate 约束。 diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index 8161de7b..a26983da 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -54,14 +54,15 @@ async function main() { return t } - // 1) 动机画像:evidence 缺失必须被拒绝(P0 反幻觉红线) + // 1) 动机画像:evidence 缺失必须被拒绝(P0 反幻觉红线;D-30 后由宿主权 schema 闸—— + // evidence required 进嵌套 schema,拒绝更前置,ToolFailure 形状不变) const motivation = byName('gotry_motivation_save') const rejectedMotivation = await motivation.execute( { profile: { weights: { escape_rest: 0.7 } } }, null, ) as { ok?: boolean; summary?: string } - if (rejectedMotivation.ok !== false || !rejectedMotivation.summary?.includes('without evidence')) { - throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0, 120)}`) + if (rejectedMotivation.ok !== false || !/evidence/i.test(rejectedMotivation.summary ?? '')) { + throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0, 160)}`) } console.log(`motivation without evidence rejected: ${rejectedMotivation.summary.slice(0, 80)}...`) const saved = await motivation.execute({ diff --git a/ts/src/index.ts b/ts/src/index.ts index 18cc811e..fbe28147 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -280,11 +280,17 @@ export function apply(ctx: Context, config: Config): void { + 'structure { request: { motivation weights, hard constraints, window, budget, home hubs }, candidates: [ { id, label, services, transfers, stay, minDays } ] }. ' + 'Returns per-candidate verdicts, unsat cores with minimal-modification suggestions, ' + 'a wish-pool entry for infeasible aspirations, and a ready-to-show markdown answer.', + // D-30 第五刀(issue #112):payload json blob → 结构化对象(request/candidates 模型可见, + // 嵌套形状权威在 unified 引擎契约,工具面不越层强约束) parameters: { payload: { - type: 'json', + type: 'object', + additionalProperties: false, required: true, - description: 'The full engine payload: { request, candidates }.', + properties: { + request: { type: 'object', additionalProperties: true, required: true, description: '引擎请求:{ motivation weights, hard constraints, window, budget, home hubs }' }, + candidates: { type: 'array', items: { type: 'object', additionalProperties: true }, required: true, description: '候选列表:[{ id, label, services, transfers, stay, minDays }]' }, + }, }, }, output: { @@ -337,11 +343,18 @@ export function apply(ctx: Context, config: Config): void { + 'never the principal/sponsor distinction. MERGE semantics (T1): call again with just the NEW ' + 'facts learned this turn (weights delta optional but MUST bring fresh evidence; evidence = user quotes); ' + 'existing history is never deleted. Requires evidence on every call (P0 anti-fabrication rule).', + // D-30 第五刀(issue #112):profile blob → 结构化闭合对象;evidence(P0 反伪造红线)进嵌套 + // schema 由宿主权闸——无证据的保存请求入口即被结构化拒绝(ToolFailure 形状) parameters: { profile: { - type: 'json', + type: 'object', + additionalProperties: false, required: true, - description: '{ weights: {escape_rest: 0.7, ...}, evidence: [user quotes...], hard: {wake_not_before, min_arrival_energy_pct} }', + properties: { + weights: { type: 'object', additionalProperties: true, description: '动机权重增量,如 { escape_rest: 0.7 }(可选,权重变更须伴新证据)' }, + evidence: { type: 'array', items: { type: 'string' }, required: true, description: '本轮新事实的用户原话数组(P0:必带)' }, + hard: { type: 'object', additionalProperties: true, description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)' }, + }, }, }, output: { @@ -522,11 +535,19 @@ export function apply(ctx: Context, config: Config): void { + 'never a hard filter (「爸爸65轻度高血压」→ 不排高海拔/控制步行量;「晕车」→ 优先火车/备提示). ' + 'evidence MUST be the user\'s verbatim words (append-only, traceable). ' + 'NEGATIVE LIST: passport/ID/phone numbers are rejected on sight — such fields never enter storage.', + // D-30 第五刀(issue #112):companion blob → 结构化闭合对象;label/evidence required 由宿主权闸 + // (evidence append-only 溯源 P0;additionalProperties:false 与负面清单同向——护照/证件号类 + // 杂散字段入口即拒) parameters: { companion: { - type: 'json', + type: 'object', + additionalProperties: false, required: true, - description: '{ label: "爸爸", constraints: { mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }, evidence: "<用户原话>" }', + properties: { + label: { type: 'string', required: true, description: '称呼,如 爸爸' }, + constraints: { type: 'object', additionalProperties: true, description: '{ mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }' }, + evidence: { type: 'string', required: true, description: '用户原话(append-only 可溯源,P0)' }, + }, }, }, output: { From 33e5db274165604a89a3cfe8b4be392ea03c78f1 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 19:25:15 +0400 Subject: [PATCH 06/12] =?UTF-8?q?feat(tools):=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E9=A6=96=E8=A1=8C=E7=94=9F=E6=88=90=20+=20do?= =?UTF-8?q?ctor=20=E5=AE=BF=E4=B8=BB=E6=8F=92=E4=BB=B6=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E9=9D=A2(issue=20#113,L1=20=E6=AE=8B=E9=87=8F=E6=94=B6?= =?UTF-8?q?=E5=8F=A3)=E2=80=94=E2=80=94=E2=91=A0toolRoutingHeadline:?= =?UTF-8?q?=E4=B8=83=E4=B8=AA=E6=A3=80=E7=B4=A2=E5=B7=A5=E5=85=B7=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E5=89=8D=E7=BD=AE=E6=B3=A8=E5=86=8C=E8=A1=A8=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=9A=84=E3=80=8C=E6=9C=8D=E5=8A=A1=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=20=C3=97=20=E9=80=9A=E9=81=93=E9=A1=BA=E4=BD=8D(=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E7=BA=A7=E9=99=8D=E5=BA=8F)=E3=80=8D=E5=8D=A1,?= =?UTF-8?q?=E9=80=89=E5=B7=A5=E5=85=B7=E4=B8=8E=E5=A4=B1=E8=B4=A5=E6=94=B9?= =?UTF-8?q?=E9=81=93=E4=B8=A4=E5=86=B3=E7=AD=96=E7=82=B9=E5=90=8C=E8=A1=A8?= =?UTF-8?q?,=E6=B3=A8=E5=86=8C=E8=A1=A8=E5=8A=A0=E8=A1=8C=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E8=87=AA=E5=8A=A8=E4=B8=80=E8=87=B4,=E9=9D=9E?= =?UTF-8?q?=E6=A3=80=E7=B4=A2=E5=B7=A5=E5=85=B7=E9=9B=B6=E8=AF=AF=E4=BC=A4?= =?UTF-8?q?;=E2=91=A1doctor=20=E8=A1=A5=E9=BD=90=20patch=20=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB=E6=8F=92=E4=BB=B6=20dsh-map-tools/dsh-tool-ask-user?= =?UTF-8?q?=20=E4=B8=A4=E6=80=81(=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=95=B4=E5=9D=97=E9=9D=99=E9=BB=98=E5=89=94=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E6=8C=A1=E5=90=AF=E5=8A=A8,=E6=AD=A4=E5=89=8D=E6=97=A0?= =?UTF-8?q?=E4=BA=BA=E7=9F=A5),=E5=80=99=E9=80=89=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E4=B8=8E=20bin=20=E8=A7=A3=E6=9E=90=E5=90=8C=E5=8F=A3=E5=BE=84?= =?UTF-8?q?;=E5=AE=9E=E6=B5=8B=E6=9A=B4=E9=9C=B2=20source=20=E5=B8=83?= =?UTF-8?q?=E5=B1=80=20map-tools=20=E5=85=A8=E5=B8=83=E5=B1=80=E7=BC=BA?= =?UTF-8?q?=E5=B8=AD(=E5=9C=B0=E5=9B=BE=E5=B7=A5=E5=85=B7=E4=B8=80?= =?UTF-8?q?=E7=9B=B4=E8=A2=AB=E9=9D=99=E9=BB=98=E4=B8=A2=E5=BC=83),?= =?UTF-8?q?=E8=A1=A5=E4=BE=9D=E8=B5=96=E6=B8=A0=E9=81=93=E7=95=99=20founde?= =?UTF-8?q?r=20=E6=8B=8D=E6=9D=BF;=E8=B0=83=E8=AF=95=E5=89=AF=E4=BA=A7?= =?UTF-8?q?=E7=89=A9:apply()=20=E8=BF=9B=E7=A8=8B=E6=8A=A4=E6=A0=8F?= =?UTF-8?q?=E5=90=9E=E5=BC=82=E5=B8=B8=E8=BF=9B=20incident=20=E7=9A=84?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E8=A2=AB=E6=B5=8B=E8=AF=95=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E8=B8=A9=E5=AE=9E(=E6=96=AD=E8=A8=80=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=A1=BB=E8=AF=BB=E4=BA=8B=E6=95=85=E6=96=87=E4=BB=B6=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D);channel-registry-tests=20=C2=A77/=C2=A78+smoke+docto?= =?UTF-8?q?r-tests+skills-contract=20=E5=85=A8=E7=BB=BF,run-all=20ALL=20SU?= =?UTF-8?q?ITES=20GREEN;=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5(arch=20=C2=A79=E3=80=81roa?= =?UTF-8?q?dmap=20=E5=A2=9E=E9=87=8F=E3=80=81stage1=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=A4=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 1 + docs/roadmap.md | 1 + docs/stage1-top-down-design.md | 1 + ts/capabilities/channel-registry.ts | 20 +++++++++++++++ ts/capabilities/doctor.ts | 37 +++++++++++++++++++++++++-- ts/scripts/channel-registry-tests.ts | 28 ++++++++++++++++---- ts/scripts/smoke.ts | 2 +- ts/src/index.ts | 38 +++++++++++++++++----------- 8 files changed, 105 insertions(+), 23 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 51aea5ba..b227c1c8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -401,6 +401,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:三个工具调用 issue 统一根因=「失败瞬间模型手里没有结构化的通道状态与改道指引」(flyai 429 跨轮盲重试/配额不可见/未配置的 calendar 会话中段才撞错)。落地:①`channel-registry.ts` 通道×意图×配额类×证据级单一数据来源,persona 路由卡 `{{channel_routing_card}}` 与 `routing` 建议字段生成化(persona (19) 工具枚举收缩为查卡);②`channel-health.ts` 会话瞬态态(down/cooldown,hit 即清除)+ JSONL 持久事件面;③doctor v2=flyai 最近达限时间 + dsh-calendar 三态(D-9 默认不挂载;挂载与否由 **setup 状态面** `~/.gotry/calendar.json` 决定,`npx gotry setup calendar` on/off——禁止 env 控制产品行为,founder 2026-09-03 纠偏);④smoke 真跑命中真实 429 验证 needs-setup→routing 全链。设计全文 `tool-orchestration-design.md`(含「普通 LLM 长久保持工具调用性能」「开放生态可扩展性」两命题);run-all §50。typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`/`gotry_weather_check`——session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);skills-contract 契约对齐。第三刀:七个模型高频工具(session_login/doctor/artifacts_list/fact_gate/agent_reach/turn_handoff_list/artifacts_read;fact_gate 的 itinerary 保 object+additionalProperties:true,收紧归 #118 D-26;agent_reach 的 action 不设 enum——反射桥 D-4a' 开放面)。第四刀:`query:` blob 清零(wish_pool/trip_log/flight_verify/anything/web/video/github;trip_log 的 evidence P0 红线进 schema;web_search 字符串 blob 退役)。第五刀:最后三个 payload 形态——`gotry_feasibility_check`(payload 结构化对象,嵌套权威在 unified 引擎契约不越层强约束)/`gotry_motivation_save`(profile 闭合对象,**evidence P0 反伪造红线进嵌套 schema 由宿主权闸**,smoke §1 断言适配)/`gotry_companion_save`(companion 闭合对象 label/evidence required,additionalProperties:false 与负面清单同向)。**23/23 注册工具全部 typed 契约化**。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:普通模型 canary(跑前不合发布面)记 D-30。 +- **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:①`toolRoutingHeadline`(channel-registry)——检索工具描述统一前置注册表生成的「服务意图 × 当前通道顺位(证据级降序)」卡,七个检索工具(flyai/session/hotel/weather/flight_verify/anything/web)经 `routed()` 接线,模型选工具时(读描述)与失败后(读 routing)两个决策点拿到同一张表;注册表加行描述自动一致,非检索工具零误伤(channel-registry-tests §8 断言接线前缀)。②doctor 补齐 patch 分发面宿主插件两态——`dsh-map-tools`/`dsh-tool-ask-user`(此前只覆盖 calendar):这类插件运行时解析**失败即整块静默剔除,不挡启动**,模型只觉得「没有这个工具」没人说为什么;doctor 候选清单与 bin/gotry-inner.js 解析逻辑同口径(repo vendored-node_modules/包依赖/dsh 闭包),隔离环境 missing 断言进 channel-registry-tests §7。实测暴露:**当前 source 布局 dsh-map-tools 全布局缺席**(未 vendor、非 dsh/gotry 依赖)——地图工具一直被静默丢弃,doctor 现在照亮此面,补依赖渠道留 founder 拍板。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. diff --git a/docs/roadmap.md b/docs/roadmap.md index 2ea03773..91a5cbfb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -27,6 +27,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **通道注册表与健康面(2026-09-03,issue #106/#107/#108,ADR-25)**:检索通道单一数据来源(`channel-registry.ts`)——persona 路由卡 `{{channel_routing_card}}` 与 verdict≠hit 时结果内 `routing` 建议生成化(flyai 达限即改道 session,`hit` 即恢复);会话健康面 + doctor 配额可见(最近达限时间)+ dsh-calendar 默认不挂载(D-9;挂载走 setup 状态面 `npx gotry setup calendar`,不吃 env)。工具面照旧平铺,建议非派发。run-all §50;typed 参数契约迁移余量记 D-30。 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema——四高流量(flyai/session/hotel/weather)+ 七高频 + wish_pool/trip_log/flight_verify/anything/web/video/github + feasibility/motivation/companion;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log/motivation/companion 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §1/§6/§12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量:普通模型 canary 记 D-30。 +- **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:七个检索工具描述统一前置 channel-registry 生成的「服务意图 × 通道顺位」卡(选工具与失败改道两决策点同表,注册表加行自动一致);doctor 补齐 patch 宿主插件 `dsh-map-tools`/`dsh-tool-ask-user` 两态照亮(此前解析失败整块静默剔除无人知)——实测暴露 source 布局 dsh-map-tools 全布局缺席,地图工具一直被静默丢弃,补依赖渠道留 founder 拍板。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index e643ea00..076522b3 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,工具描述首行由通道注册表生成(issue #113):七个检索工具描述前置「服务意图 × 通道顺位」卡(与失败现场 routing 字段同表),doctor 补齐 patch 宿主插件 dsh-map-tools/dsh-tool-ask-user 两态(静默剔除面照亮;map-tools 全布局缺席待 founder 拍板补依赖)。 - 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,五刀收官):全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;evidence 类 P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §1/§6/§12/channel-registry-tests §8;普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 - 2026-09-03 起,检索通道编排收敛为通道注册表 + 健康面(ADR-25,issue #106/#107/#108):persona 路由卡 `{{channel_routing_card}}` 与工具结果内 `routing` 建议由 `channel-registry.ts` 生成,本文涉及 persona (19) 工具枚举的表述以该卡为准;dsh-calendar 默认不挂载(D-9)。 diff --git a/ts/capabilities/channel-registry.ts b/ts/capabilities/channel-registry.ts index 6cd088f0..6d1f68cc 100644 --- a/ts/capabilities/channel-registry.ts +++ b/ts/capabilities/channel-registry.ts @@ -254,6 +254,26 @@ export function routingAdvice(intent: ChannelIntent, opts: RoutingAdviceOptions return { intent, alternatives } } +/** + * 工具描述首行生成(§3.3④,issue #113):某检索工具在注册表内服务的意图 → + * 「服务意图 × 当前通道顺位」卡,前置进工具描述——模型在选工具时(读描述) + * 与失败后(读 routing 字段)两个决策点拿到同一张表。顺位=证据级降序(与 + * renderRoutingCard 同口径:通道状态变化不改变顺位表本身,只影响 routing + * 建议的可用性过滤)。注册表加行,描述首行自动一致,零手改。 + * 非检索工具(不在表内)返回 '',描述保持原样。纯函数。 + */ +export function toolRoutingHeadline(tool: string): string { + const intents = new Set() + for (const c of CHANNELS) { + if (c.routable === false || c.tool !== tool) continue + for (const i of c.intents) intents.add(i) + } + if (intents.size === 0) return '' + const lines = [...intents].map(intent => + `- ${INTENT_LABELS[intent]}: ${channelsForIntent(intent).map(c => `${c.label}(${c.tool}${c.hint ? ` ${c.hint}` : ''})`).join(' → ')}`) + return ['本工具服务的检索意图与通道顺位(证据级降序):', ...lines, 'verdict≠hit 时按结果内 routing 字段改道下一通道,勿盲试同通道。'].join('\n') +} + /** * persona 路由卡({{channel_routing_card}}):每意图一行顺位 + 额度口径脚注。 * 确定性渲染(零 IO,时钟只进脚注);只列有 ≥2 条可选通道的意图—— diff --git a/ts/capabilities/doctor.ts b/ts/capabilities/doctor.ts index b3419210..047d6326 100644 --- a/ts/capabilities/doctor.ts +++ b/ts/capabilities/doctor.ts @@ -25,7 +25,7 @@ export type DoctorStatus = 'ok' | 'missing' | 'degraded' export interface DoctorItem { /** 稳定 id(报告/测试锚点) */ - id: 'node' | 'extension' | 'agent-reach' | 'hbcli' | 'flyai' | 'sidebar' | 'llm-key' | 'calendar' + id: 'node' | 'extension' | 'agent-reach' | 'hbcli' | 'flyai' | 'sidebar' | 'llm-key' | 'calendar' | 'map-tools' | 'ask-user' /** 展示名 */ label: string status: DoctorStatus @@ -197,7 +197,40 @@ export async function runDoctorChecks(opts: DoctorOptions = {}): Promise existsSync(p)) + items.push(mapHit + ? { id: 'map-tools', label: 'dsh-map-tools(地图/路线/POI)', status: 'ok', detail: `已就位(${mapHit})——地图工具可用(零 key,走 OSM/OSRM)` } + : { + id: 'map-tools', label: 'dsh-map-tools(地图/路线/POI)', status: 'missing', + detail: '未随包解析——启动时该 patch 条目被静默剔除,地图/路线/POI 工具不会出现在模型工具箱(缺地图不挡旅行规划,只少能力)', + fix: 'npx gotry doctor --fix 不覆盖此项:source 布局把 dsh-map-tools 放进 ts/dsh-runtime/node_modules(或 ts/node_modules);npm 布局重装 @danceiny/gotry(随发行版依赖提供)', + }) + + const askCandidates = [ + join(repoRoot, 'ts/dsh-runtime/vendor/deepseek-ai-dsh-tool-ask-user/package.json'), + join(repoRoot, 'node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'), + join(home, '.dsh/profiles/web/node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'), + ] + const askHit = askCandidates.find(p => existsSync(p)) + items.push(askHit + ? { id: 'ask-user', label: 'dsh-tool-ask-user(结构化澄清卡)', status: 'ok', detail: `已就位(${askHit})——ask_user_question 澄清卡可用(web 原生卡片;headless+TTY 用 stdio 提供方)` } + : { + id: 'ask-user', label: 'dsh-tool-ask-user(结构化澄清卡)', status: 'missing', + detail: '未解析——启动时澄清卡注入被静默剔除,模型只能散文追问(人格契约 (5) 退化文本形态)', + fix: '重装 @danceiny/gotry——该依赖随 dsh 闭包自带,缺失多为安装不完整', + }) + + // 8. LLM key:显式让渡给 dsh 宿主(founder 2026-09-02:doctor 不管 key) items.push({ id: 'llm-key', label: 'LLM key', status: 'ok', detail: '由 dsh 宿主 UI 管理——不在 doctor 体检范围(gotry 不接触、不回显凭证)' }) const broken = items.filter(i => i.status !== 'ok' && i.id !== 'llm-key') diff --git a/ts/scripts/channel-registry-tests.ts b/ts/scripts/channel-registry-tests.ts index 20c2c529..d6ce02ee 100644 --- a/ts/scripts/channel-registry-tests.ts +++ b/ts/scripts/channel-registry-tests.ts @@ -7,8 +7,10 @@ * 4. noteChannelVerdict 映射:needs-setup→down/hit→清除/miss·error→不动/cooldown 过期 * 5. persona 路由卡:确定性渲染,含改道规则与意图顺位行 * 6. JSONL 持久面:record/readLatest 往返 + 坏行容忍 + limitDays 过滤 - * 7. doctor 集成:flyai 最近达限时间可见;calendar 三态(默认不挂载/opt-in 未配置/opt-in 已配置) - * 8. apply 接线:channel_routing_card 变量已注册;参数级拒绝不带 routing 字段 + * 7. doctor 集成:flyai 最近达限时间可见;calendar 三态(默认不挂载/opt-in 未配置/opt-in 已配置); + * patch 宿主插件两态照亮(map-tools/ask-user,issue #113 L1 残量) + * 8. apply 接线:channel_routing_card 变量已注册;参数级拒绝不带 routing 字段; + * 工具描述首行生成(检索工具前置意图顺位卡,非检索工具原样,#113) * * 运行: cd ts && npx tsx scripts/channel-registry-tests.ts */ @@ -18,7 +20,7 @@ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { - CHANNELS, channelsForIntent, routingAdvice, renderRoutingCard, + CHANNELS, channelsForIntent, routingAdvice, renderRoutingCard, toolRoutingHeadline, INTENT_LABELS, type ChannelIntent, type ChannelQuotaClass, type EvidenceTier, } from '../capabilities/channel-registry.ts' import { @@ -137,7 +139,14 @@ const d3 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: assert.equal(d3.items.find(i => i.id === 'calendar')!.status, 'ok', 'setup 开启且已配置=ok') const d4 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {} }) assert.ok(!/最近一次试用达限/.test(d4.items.find(i => i.id === 'flyai')!.detail), '无 stateRoot 不读持久面(向后兼容)') -console.log('7. doctor 集成(flyai 配额可见 + calendar setup 状态面三态)OK') +// patch 宿主插件两态照亮(issue #113):隔离环境两者皆 missing——静默剔除面进 doctor 可见 +const mapItem = d4.items.find(i => i.id === 'map-tools')! +const askItem = d4.items.find(i => i.id === 'ask-user')! +assert.equal(mapItem.status, 'missing', '隔离环境 map-tools=missing(静默剔除面可见)') +assert.match(mapItem.detail, /静默剔除/, 'map-tools detail 说明静默剔除语义') +assert.equal(askItem.status, 'missing', '隔离环境 ask-user=missing') +assert.match(askItem.detail, /散文追问/, 'ask-user detail 说明退化面') +console.log('7. doctor 集成(flyai 配额可见 + calendar setup 状态面三态 + 宿主插件两态)OK') // 8. apply 接线:变量注册 + 参数级拒绝不带 routing resetChannelHealth() @@ -159,7 +168,16 @@ assert.ok(!('routing' in pastDate), '参数级预校验拒绝不带 routing(未 const legacyBlob = await registered.find(t => t.name === 'gotry_flyai_search')!.execute( { query: { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' } } as never, null) as { ok?: boolean; routing?: unknown; evidence?: string } assert.ok(legacyBlob.ok === false && !('routing' in legacyBlob) && !!legacyBlob.evidence, 'legacy blob → 结构化拒绝(ToolFailure,无 routing)') -console.log('8. apply 接线 OK') +// 工具描述首行生成(issue #113):检索工具前置注册表生成的意图顺位卡;非检索工具原样 +const flyaiTool = registered.find(t => t.name === 'gotry_flyai_search') as unknown as { name: string; description?: string } +const doctorToolDesc = registered.find(t => t.name === 'gotry_doctor') as unknown as { name: string; description?: string } +assert.ok(flyaiTool, 'flyai 工具已注册') +assert.match(toolRoutingHeadline('gotry_flyai_search'), /机票: 飞猪官方 API\(gotry_flyai_search\) → 携程·用户登录态/, 'flyai 首行卡:机票顺位 flyai 在前(证据级)') +assert.match(toolRoutingHeadline('gotry_session_search'), /火车: .*12306 公开查询面\(gotry_session_search kind=train\)/, 'session 首行卡带 kind 消歧') +assert.equal(toolRoutingHeadline('gotry_doctor'), '', '非检索工具无首行卡(原样描述)') +assert.ok(flyaiTool.description?.startsWith('本工具服务的检索意图与通道顺位'), 'flyai 描述已前置首行卡(接线生效)') +assert.ok(!doctorToolDesc.description?.startsWith('本工具服务的检索意图'), 'doctor 描述无首行卡(接线不误伤)') +console.log('8. apply 接线 + 描述首行生成 OK') await rm(stateRoot, { recursive: true, force: true }) console.log('channel-registry-tests: 全部通过') diff --git a/ts/scripts/smoke.ts b/ts/scripts/smoke.ts index a26983da..c76d3cb3 100644 --- a/ts/scripts/smoke.ts +++ b/ts/scripts/smoke.ts @@ -64,7 +64,7 @@ async function main() { if (rejectedMotivation.ok !== false || !/evidence/i.test(rejectedMotivation.summary ?? '')) { throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0, 160)}`) } - console.log(`motivation without evidence rejected: ${rejectedMotivation.summary.slice(0, 80)}...`) + console.log(`motivation without evidence rejected: ${(rejectedMotivation.summary ?? '').slice(0, 80)}...`) const saved = await motivation.execute({ profile: { weights: { escape_rest: 0.7, curiosity: 0.3 }, diff --git a/ts/src/index.ts b/ts/src/index.ts index fbe28147..d4b3403c 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -46,7 +46,7 @@ import { factsFromFlyai, factsFromSession } from './bookable-facts.ts' import { gateArtifact, type AirlineAirportMap } from './artifact-gate.ts' import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.ts' import { noteChannelVerdict, recordChannelEvent } from '../capabilities/channel-health.ts' -import { routingAdvice, renderRoutingCard, type ChannelIntent } from '../capabilities/channel-registry.ts' +import { routingAdvice, renderRoutingCard, toolRoutingHeadline, type ChannelIntent } from '../capabilities/channel-registry.ts' import { registerBenchmarkEnvironmentBridge, type BenchmarkSubprocessService } from './benchmark-environment-bridge.ts' import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.ts' import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.ts' @@ -270,6 +270,14 @@ export function apply(ctx: Context, config: Config): void { ctx.tools.register(t as unknown as ReturnType) } + // 工具描述首行生成(issue #113,L1):检索工具描述统一前置注册表生成的 + // 「服务意图 × 通道顺位」卡——模型选工具时(读描述)与失败后(读 routing) + // 两个决策点拿到同一张表;注册表加行,描述自动一致。非检索工具原样返回。 + const routed = (tool: string, desc: string): string => { + const head = toolRoutingHeadline(tool) + return head ? `${head}\n${desc}` : desc + } + registerGuarded(defineTool({ name: 'gotry_feasibility_check', description: @@ -612,12 +620,12 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_hotel_search', - description: + description: routed('gotry_hotel_search', 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). ' + 'Input: destination city name + optional dates/adults. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — ' + 'the code layer resolves them against the time anchor; unresolved expressions degrade to an undated search with an explicit ' + 'date_notes entry instead of guessing. Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, ' - + 'or [static-pack:estimate]) per the L4 invariant.', + + 'or [static-pack:estimate]) per the L4 invariant.'), // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。 parameters: { destination: { type: 'string', required: true, description: '目的地城市,如 大理' }, @@ -719,11 +727,11 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_weather_check', - description: + description: routed('gotry_weather_check', 'Check weather for a destination: forecast (≤16 days) or historical climate (seasonality baseline). ' + 'Free Open-Meteo API, no key required. Input: place name (Chinese ok) or lat/lng. ' + 'Returns daily temp range, precipitation probability, weather code — with evidence chain tagging ' - + '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.', + + '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.'), parameters: { // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。 place: { type: 'string', description: '城市名(中文可),如 大理;与 lat/lng 二选一,都没给 execute 结构化报错' }, @@ -788,7 +796,7 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_flight_verify', - description: + description: routed('gotry_flight_verify', 'Verify whether a flight callsign is currently observable on the OpenSky ADS-B network. ' + 'Free anonymous API (~400 credits/day, 4 req/s burst). Three-valued semantics: ' + 'observed = strong positive (the aircraft is currently being broadcast); ' @@ -796,7 +804,7 @@ export function apply(ctx: Context, config: Config): void { + 'a missing signal does NOT disprove the flight); ' + 'unavailable = API failure, gracefully degraded. ' + 'Use to ground "is this flight actually flying right now?" in real data, complementing ' - + 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).', + + 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).'), // D-30 第四刀(issue #112):平铺 typed;callsign required → 宿主权即拒 parameters: { callsign: { type: 'string', required: true, description: '航班呼号,如 EK329' }, @@ -835,13 +843,13 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_flyai_search', - description: + description: routed('gotry_flyai_search', '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": from/to 中文城市名 + date YYYY-MM-DD — real schedules & prices, split 直达/中转 in results. ' + '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]. verdict=needs-setup (anonymous trial quota exhausted, upstream 429) is a CONFIG issue, not a search failure: surface the setup hint once, do NOT retry this tool this session — switch to gotry_session_search or web search. ' - + 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.', + + 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.'), // D-30 第一刀(issue #112):query json blob → 平铺 typed 契约。逐字段 schema 由 dsh // parameterSchemaSpecToJsonSchema 投影为模型可见 JSON Schema,validateArgs 在 execute 前 // 宿主权校验——畸形参数(缺 kind/枚举外值/类型错/legacy blob 包裹)在入口即被结构化拒绝 @@ -989,7 +997,7 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_session_search', - description: + description: routed('gotry_session_search', 'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). ' + '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). ' + 'Transport: GoTry Session Bridge browser extension (one-time install) — the agent side never talks to Chrome debugging, ZERO system dialogs; read-only by construction (the extension never issues requests; it only passively forwards the site\'s own search responses; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). ' @@ -998,7 +1006,7 @@ export function apply(ctx: Context, config: Config): void { + 'kind="train": from/to/date(YYYY-MM-DD) + fromStationTelecode?/toStationTelecode? (three-letter codes in the kyfw query URL, for cities outside the built-in table) — 12306 left-ticket query (public face): train codes, times, durations, seat availability; the list API carries NO prices (prices live on the 12306 page). Evidence [会话:train-12306@ts]. ' + 'verdict needs-login = call gotry_session_login (opens the Ctrip login entry in the user\'s own foreground tab — no terminal, no credentials through GoTry); ' + `needs-extension = one-time browser-extension install (Chrome Web Store one-click, installUrl is also surfaced as a clickable link in the verdict field for dsh UI to render) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. ` - + 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).', + + 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).'), // D-30 第二刀(issue #112):query blob → 平铺 typed 契约(逐字段 schema 模型可见)。 // 与 flyai 刀的差异:本工具三意图无公共 required 字段(kind 缺省=flight),根 schema 是 // 隐式开放对象,宿主权无法用 required 拒 legacy blob——故保留 interpretArgs 容忍层 @@ -1133,14 +1141,14 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_anything_search', - description: + description: routed('gotry_anything_search', 'Travel-domain search via hotel-byte CLI → hotel-be Anything (cities/hotels/destinations) — NOT general web search; for general internet facts use gotry_agent_reach. ' + 'Mixed destinations (cities / metropolitan areas / high-level regions) + hotels in one call. ' + 'Returns candidates with type, name, optional coordinates and hotel-id. ' + 'Three-valued semantics: hit = ≥1 candidate; miss = 0 candidates (try synonyms or contentType=city/hotel); ' + 'unavailable = hbcli failed (degraded, never blocks). ' + 'Use as the first stop when the user mentions a place/city/hotel name and you need to ground it in real catalog data ' + - '(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).', + '(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).'), // D-30 第四刀(issue #112):平铺 typed;keyword required → 宿主权即拒 parameters: { keyword: { type: 'string', required: true, description: '搜索关键词' }, @@ -1189,12 +1197,12 @@ export function apply(ctx: Context, config: Config): void { registerGuarded(defineTool({ name: 'gotry_web_search', - description: + description: routed('gotry_web_search', 'Read any public URL as markdown (Jina Reader, free, no key). ' + 'Use as the "last mile" web reader when hotel-be Anything or gotry tools lack the answer. ' + 'NOT a general-purpose search engine — only fetches a URL you already know. ' + 'Three-valued: ok / error(非法 URL/超时)/not-reachable(r.jina.ai 不可用).' + - 'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。', + 'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。'), // D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒(字符串 blob 形态退役) parameters: { url: { type: 'string', required: true, description: '完整 URL,https://…' }, From 4c2d8591dbb70c19138e30289c82279a60d07cc8 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 20:47:11 +0400 Subject: [PATCH 07/12] =?UTF-8?q?feat(bootstrap):=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=20doctor=20=E6=91=98=E8=A6=81(issue?= =?UTF-8?q?=20#114,L2)=E2=80=94=E2=80=94web/headless=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=97=B6=20inner=20=E5=9C=A8=20spawn=20dsh=20=E5=89=8D?= =?UTF-8?q?=E4=BB=A5=E5=88=86=E7=A6=BB=E5=AD=90=E8=BF=9B=E7=A8=8B=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E8=B7=91=E5=8F=AA=E8=AF=BB=E4=BD=93=E6=A3=80(gotry-bo?= =?UTF-8?q?otstrap.js=20doctor=20--summary):=E5=BE=85=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=A1=B9=E4=B8=80=E8=A1=8C=20stderr(=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E9=87=8C=20gotry=5Fdoctor=20=E7=9C=8B=E8=AF=A6=E6=83=85/?= =?UTF-8?q?=E7=BB=88=E7=AB=AF=20npx=20gotry=20doctor),=E5=85=A8=20ok=20?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E9=9B=B6=E8=BE=93=E5=87=BA;=E9=9B=B6=20heade?= =?UTF-8?q?r=20=E9=9B=B6=E5=86=99=E7=9B=98=E3=80=81detached+unref=20?= =?UTF-8?q?=E4=B8=8D=E9=98=BB=E5=A1=9E=E5=90=AF=E5=8A=A8=E3=80=81=E6=81=92?= =?UTF-8?q?=20exit=200=E3=80=81benchmark=20=E9=9D=A2=E8=B1=81=E5=85=8D?= =?UTF-8?q?=E4=BF=9D=E9=9B=B6=E6=9D=82=E9=9F=B3,inner=20=E7=94=9F=E5=91=BD?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E5=A4=A9=E7=84=B6=E4=B8=80=E6=AC=A1=E4=B8=8D?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=88=B7=E2=80=94=E2=80=94=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E5=8F=AF=E8=A7=81=E5=8F=96=E4=BB=A3=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E4=B8=AD=E6=AE=B5=E6=92=9E=E9=94=99(design=20=C2=A73.1?= =?UTF-8?q?=E2=91=A2);bootstrap-tests=20=C2=A710=20=E4=BB=A5=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=20HOME=20=E9=92=89=E8=A1=8C=E4=B8=BA(10/10),run-all?= =?UTF-8?q?=20ALL=20SUITES=20GREEN;=E5=85=AD=E7=8A=B6=E6=80=81=E9=9D=A2?= =?UTF-8?q?=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5(arch=20=C2=A79?= =?UTF-8?q?=E3=80=81roadmap=20=E5=A2=9E=E9=87=8F=E3=80=81stage1=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=A4=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/gotry-bootstrap.js | 19 ++++++++++++++++++- bin/gotry-inner.js | 11 +++++++++++ docs/architecture.md | 1 + docs/roadmap.md | 1 + docs/stage1-top-down-design.md | 1 + ts/scripts/bootstrap-tests.ts | 23 +++++++++++++++++++++-- 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/bin/gotry-bootstrap.js b/bin/gotry-bootstrap.js index c600227a..dc757dc4 100644 --- a/bin/gotry-bootstrap.js +++ b/bin/gotry-bootstrap.js @@ -288,6 +288,16 @@ const doctorIcon = { ok: '✅', missing: '❌', degraded: '⚠️' } /** 修复指引入表:命令类才加反引号( prose 类如「到控制台申请 key」原样) */ const fixCell = (fix) => (!fix ? '—' : /^(npx|hbcli|curl|pip|python|\$)/.test(fix) ? `\`${fix}\`` : fix) +/** 启动一次性摘要行(issue #114,design §3.1③):全 ok 返回 null(静默零输出); + * 有待处理项给一行人话 + 指路(对话里 gotry_doctor 看详情 / 终端 npx gotry doctor)。 + * 纯函数,bootstrap-tests 直接断言。 */ +function startupDoctorLine(items) { + const broken = (items ?? []).filter((i) => i.level && i.level !== 'ok') + if (broken.length === 0) return null + const human = (lv) => (lv === 'missing' ? '缺' : '半可用') + return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i.level)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx gotry doctor` +} + /** 体检报告 markdown(与 ts/capabilities/doctor.ts renderDoctorReportMd 同形) */ function renderDoctorReportMd(items) { const broken = items.filter((i) => i.level !== 'ok' && i.label !== 'LLM key') @@ -315,8 +325,15 @@ function renderDoctorReportMd(items) { /** doctor 主流程:体检 → 打印 →(可选)fix → 报告落盘。 * exit 0=就绪(或仅剩有自动回退的降级项);exit 1=仍有缺失类问题。 */ async function runDoctor() { - say('[gotry-doctor] GoTry 可选依赖体检(只读;LLM key 归 dsh 宿主管,不在范围)') let items = await doctorChecks() + // 启动一次性摘要(issue #114):inner 分离子进程带 --summary 调用——只读体检, + // 有待处理项打一行 stderr,零写盘零 header;全 ok 静默。与完整体检面(逐项/报告落盘/exit 语义)分离。 + if (process.argv.includes('--summary')) { + const line = startupDoctorLine(items) + if (line) console.error(line) + return 0 + } + say('[gotry-doctor] GoTry 可选依赖体检(只读;LLM key 归 dsh 宿主管,不在范围)') for (const i of items) { say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`) if (i.level !== 'ok' && i.fix) say(` ↳ 修复: ${i.fix}`) diff --git a/bin/gotry-inner.js b/bin/gotry-inner.js index 912f2042..a3e36aaf 100755 --- a/bin/gotry-inner.js +++ b/bin/gotry-inner.js @@ -488,6 +488,17 @@ const childStdio = mode === 'web' let benchmarkCapturedBytes = 0 let benchmarkDiagnosticBuffer = Buffer.alloc(0) let benchmarkOutputTruncated = false +// 启动一次性 doctor 摘要(issue #114,design §3.1③):分离子进程后台跑只读体检, +// 有待处理项打一行 stderr(stderr 继承,不污染 stdout;benchmark 面保持零杂音)。 +// detached+unref:不阻塞不拖慢启动;inner 生命周期天然一次,不重复刷;失败静默。 +if (!benchmarkEnvironmentConfig) { + try { + spawn(process.execPath, [join(here, 'gotry-bootstrap.js'), 'doctor', '--summary'], { + detached: true, + stdio: ['ignore', 'ignore', 'inherit'], + }).unref() + } catch { /* 体检不可用不挡启动 */ } +} child = spawn(process.execPath, [dshBin, ...binJs], { stdio: childStdio, env: childEnv, diff --git a/docs/architecture.md b/docs/architecture.md index b227c1c8..42fafbb8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -402,6 +402,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - **行为契约横评反哺(2026-09-04,issue #121/#122)**:`docs/persona-bench/` 同题横评(Kimi 13 轮/飞猪单轮,founder 拍板)暴露的两条访谈/呈现缺口进契约——①(1) 动机先行扩展「同行人到达链」:行程涉及同行人(见面/汇合/结伴)时必问从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算(横评 G4:两家都把「跟女朋友见面」做成了单人行程,飞猪预算表明写「单人」);②新增 (22) 到达账必达:红眼/凌晨起飞、或落地当天有硬安排的航段,必须显式给出当地到达时刻(含日期偏移)、时差、到达精力与前一晚落脚建议,时刻表正确≠行程可行(横评 G7:飞猪「约8小时」与自家「02:00-06:00+1」同页矛盾)。不改工具面、不新增工具;矩阵与评分卡见 `docs/persona-bench/README.md`。 - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`/`gotry_weather_check`——session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);skills-contract 契约对齐。第三刀:七个模型高频工具(session_login/doctor/artifacts_list/fact_gate/agent_reach/turn_handoff_list/artifacts_read;fact_gate 的 itinerary 保 object+additionalProperties:true,收紧归 #118 D-26;agent_reach 的 action 不设 enum——反射桥 D-4a' 开放面)。第四刀:`query:` blob 清零(wish_pool/trip_log/flight_verify/anything/web/video/github;trip_log 的 evidence P0 红线进 schema;web_search 字符串 blob 退役)。第五刀:最后三个 payload 形态——`gotry_feasibility_check`(payload 结构化对象,嵌套权威在 unified 引擎契约不越层强约束)/`gotry_motivation_save`(profile 闭合对象,**evidence P0 反伪造红线进嵌套 schema 由宿主权闸**,smoke §1 断言适配)/`gotry_companion_save`(companion 闭合对象 label/evidence required,additionalProperties:false 与负面清单同向)。**23/23 注册工具全部 typed 契约化**。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:普通模型 canary(跑前不合发布面)记 D-30。 - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:①`toolRoutingHeadline`(channel-registry)——检索工具描述统一前置注册表生成的「服务意图 × 当前通道顺位(证据级降序)」卡,七个检索工具(flyai/session/hotel/weather/flight_verify/anything/web)经 `routed()` 接线,模型选工具时(读描述)与失败后(读 routing)两个决策点拿到同一张表;注册表加行描述自动一致,非检索工具零误伤(channel-registry-tests §8 断言接线前缀)。②doctor 补齐 patch 分发面宿主插件两态——`dsh-map-tools`/`dsh-tool-ask-user`(此前只覆盖 calendar):这类插件运行时解析**失败即整块静默剔除,不挡启动**,模型只觉得「没有这个工具」没人说为什么;doctor 候选清单与 bin/gotry-inner.js 解析逻辑同口径(repo vendored-node_modules/包依赖/dsh 闭包),隔离环境 missing 断言进 channel-registry-tests §7。实测暴露:**当前 source 布局 dsh-map-tools 全布局缺席**(未 vendor、非 dsh/gotry 依赖)——地图工具一直被静默丢弃,doctor 现在照亮此面,补依赖渠道留 founder 拍板。 +- **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:`npx gotry web`/headless 启动时(bin/gotry-inner.js 在 spawn dsh 前)以**分离子进程**后台跑只读体检(`gotry-bootstrap.js doctor --summary`)——有待处理项打一行 stderr(「N 项待处理(…=缺/半可用)——对话里让助手调 gotry_doctor 看详情,或终端 npx gotry doctor」),全 ok 静默零输出;零写盘零 header、detached+unref 不阻塞启动、恒 exit 0、benchmark 面豁免保持零杂音。inner 生命周期天然一次不重复刷。「初始化时可见」取代「会话中段撞错」(design §3.1③);bootstrap-tests §10以隔离 HOME 钉行为(stderr 一行/零写盘/恒 exit 0)。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. diff --git a/docs/roadmap.md b/docs/roadmap.md index 91a5cbfb..eba6333f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -28,6 +28,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **行为契约横评反哺(2026-09-04,issue #121/#122)**:persona 契约 (1) 扩展同行人到达链访谈(见面/汇合类行程同行人必问:从哪出发/是否已订/有无自己的时间窗,问明落 `gotry_companion_save`,到达账与预算分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账:当地到达时刻含日期偏移/时差/到达精力/前一晚落脚建议)。行为契约 21→22 条,随下个 npm 版本分发;出处为 `docs/persona-bench/` 同题横评 G4/G7,founder 拍板。 - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema——四高流量(flyai/session/hotel/weather)+ 七高频 + wish_pool/trip_log/flight_verify/anything/web/video/github + feasibility/motivation/companion;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log/motivation/companion 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §1/§6/§12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量:普通模型 canary 记 D-30。 - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:七个检索工具描述统一前置 channel-registry 生成的「服务意图 × 通道顺位」卡(选工具与失败改道两决策点同表,注册表加行自动一致);doctor 补齐 patch 宿主插件 `dsh-map-tools`/`dsh-tool-ask-user` 两态照亮(此前解析失败整块静默剔除无人知)——实测暴露 source 布局 dsh-map-tools 全布局缺席,地图工具一直被静默丢弃,补依赖渠道留 founder 拍板。 +- **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:web/headless 启动时分离子进程后台跑只读体检——待处理项一行 stderr、全 ok 静默、零写盘、不阻塞不重复刷、benchmark 面豁免;「初始化时可见」取代「会话中段撞错」。bootstrap-tests §10 钉行为。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 076522b3..3abe2ded 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,启动一次性 doctor 摘要(issue #114):web/headless 启动时分离子进程后台只读体检,待处理项一行 stderr(全 ok 静默/零写盘/不阻塞/benchmark 豁免)——初始化可见取代会话中段撞错。 - 2026-09-04 起,工具描述首行由通道注册表生成(issue #113):七个检索工具描述前置「服务意图 × 通道顺位」卡(与失败现场 routing 字段同表),doctor 补齐 patch 宿主插件 dsh-map-tools/dsh-tool-ask-user 两态(静默剔除面照亮;map-tools 全布局缺席待 founder 拍板补依赖)。 - 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,五刀收官):全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;evidence 类 P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §1/§6/§12/channel-registry-tests §8;普通模型 canary 记 `architecture.md` §10 D-30。 - 2026-09-04 起,行为契约 22 条(横评反哺,issue #121/#122):(1) 动机先行扩展同行人到达链访谈(见面/汇合类必问,问明落 `gotry_companion_save`,分链核算)+ 新增 (22) 到达账必达(红眼/落地即消耗航段显式给到达账);出处与评分卡见 `docs/persona-bench/`。 diff --git a/ts/scripts/bootstrap-tests.ts b/ts/scripts/bootstrap-tests.ts index aec3f4f8..bd4a786b 100644 --- a/ts/scripts/bootstrap-tests.ts +++ b/ts/scripts/bootstrap-tests.ts @@ -16,7 +16,7 @@ */ import assert from 'node:assert/strict' -import { execFileSync } from 'node:child_process' +import { execFileSync, spawnSync } from 'node:child_process' import { mkdtempSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -138,4 +138,23 @@ const c9f = runBootstrap(['calendar', '--status'], calEnv) assert.ok(c9f.out.includes('默认未挂载'), '关闭后回到默认态') console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态,HOME 隔离)OK') -console.log('BOOTSTRAP TESTS: 9/9 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过)') +// 10. 启动一次性 doctor 摘要(issue #114,design §3.1③):inner 在 dsh 启动前以分离 +// 子进程跑 `doctor --summary`——有待处理项一行 stderr,全 ok 静默;零 header 零写盘, +// 恒 exit 0(不挡启动语义)。HOME 隔离到 tmp 保证缺失项确定性。 +{ + const sumHome = mkdtempSync(join(tmpdir(), 'gotry-sum-test-')) + const r = spawnSync('node', [bootstrap, 'doctor', '--summary'], { + encoding: 'utf-8', + timeout: 60_000, + env: { ...process.env, HOME: sumHome }, + }) + assert.equal(r.status, 0, `doctor --summary 应恒 exit 0(不挡启动),实际 ${r.status}\n${r.stderr}`) + assert.ok(!r.stdout.includes('报告已写'), 'summary 模式零写盘(不落 doctor-report.md)') + assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)') + assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr') + assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路') + assert.match(r.stderr, /扩展=缺/, '缺失项人话=缺(降级类=半可用)') +} +console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK') + +console.log('BOOTSTRAP TESTS: 10/10 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要)') From 01d5e0789ba3d7db2349d3c409944f64d2228781 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Fri, 4 Sep 2026 21:10:09 +0400 Subject: [PATCH 08/12] =?UTF-8?q?feat(effects):=20=E8=A7=A3=E8=AF=91?= =?UTF-8?q?=E5=99=A8=E8=BF=81=E7=A7=BB=E6=94=B6=E5=B0=BE(issue=20#115,D-23?= =?UTF-8?q?=20=E6=94=B6=E5=8F=A3)=E2=80=94=E2=80=94anything/web/github/vid?= =?UTF-8?q?eo/agent=5Freach/session=5Flogin=20=E5=85=AD=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E5=85=A5=E6=95=88=E5=BA=94=E6=B3=A8=E5=86=8C=E8=A1=A8,23=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=A4=96=E9=83=A8=E4=BE=9D=E8=B5=96=E9=9D=A2?= =?UTF-8?q?=E5=85=A8=E6=94=B6=E6=95=9B=20effect=5Finterpreter.v1(=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E7=AD=96=E7=95=A5=E8=A1=A8=E8=A1=8C=E5=B0=B1=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E6=95=88=E5=BA=94);=E7=AD=96=E7=95=A5=E8=A1=8C?= =?UTF-8?q?=E9=80=90=E6=9D=A1=E6=8B=8D=E6=9D=BF:ANYTHING=20=E5=90=8C=20HBC?= =?UTF-8?q?LI=20=E6=97=8F(timeout=20=E7=9E=AC=E6=97=B6=E9=87=8D=E8=AF=95?= =?UTF-8?q?=201=20=E6=AC=A1)/WEB=5FREAD=20=E5=85=8D=E8=B4=B9=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=BA=90=E6=97=8F(=E7=9E=AC=E6=97=B6=E6=8A=96?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E8=AF=95+=E7=86=94=E6=96=AD)/GITHUB=C2=B7VID?= =?UTF-8?q?EO(verdict=3Dtimeout=20=E9=87=8D=E8=AF=95,not-installed=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=80=81=E4=B8=8D=E9=87=8D=E8=AF=95)/AGENT?= =?UTF-8?q?=5FREACH=20=E5=8F=8D=E5=B0=84=E6=A1=A5=E9=80=8F=E4=BC=A0?= =?UTF-8?q?=E6=B0=B8=E4=B8=8D=E9=87=8D=E8=AF=95=E4=B8=8D=E7=86=94=E6=96=AD?= =?UTF-8?q?(wrapper=20=E4=B8=8D=E6=98=AF=20router,=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E5=8D=B3=E6=94=BE=E5=A4=A7=E4=B8=8A=E6=B8=B8=E9=85=8D=E9=A2=9D?= =?UTF-8?q?=E6=B6=88=E8=80=97)/SESSION=5FLOGIN=20=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E9=A3=8E=E6=8E=A7=E6=97=8F;=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=9D=A2=E7=85=A7=E6=97=A7=E5=B9=B3=E9=93=BA,=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E9=93=BE=E9=80=90=E6=BA=90=E6=A0=87=E6=B3=A8=E4=B8=8D?= =?UTF-8?q?=E5=8F=98,agent=5Freach=20status/reach=20=E4=B8=A4=E5=88=86?= =?UTF-8?q?=E6=94=AF=E6=94=B6=E6=95=9B=E5=8D=95=E4=B8=80=E6=95=88=E5=BA=94?= =?UTF-8?q?;effect-tests=20=C2=A712=20=E5=9B=9B=E7=BB=84=E6=96=AD=E8=A8=80?= =?UTF-8?q?(=E5=85=A5=E8=A1=A8=E9=80=8F=E4=BC=A0/=E6=B0=B8=E4=B8=8D?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E6=97=8F/timeout=20=E9=87=8D=E8=AF=95/mock?= =?UTF-8?q?=20=E5=90=8C=E8=AF=AD=E4=B9=89)12/12,tsc+smoke=20=E5=85=A8?= =?UTF-8?q?=E7=BB=BF,run-all=20ALL=20SUITES=20GREEN;=E5=85=AD=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5?= =?UTF-8?q?(arch=20=C2=A79=E3=80=81roadmap=20=E5=A2=9E=E9=87=8F=E3=80=81st?= =?UTF-8?q?age1=20=E7=8A=B6=E6=80=81=E5=A4=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture.md | 1 + docs/roadmap.md | 1 + docs/stage1-top-down-design.md | 1 + ts/capabilities/effect.ts | 90 ++++++++++++++++++++++++++++++++++ ts/scripts/effect-tests.ts | 70 +++++++++++++++++++++++++- ts/src/index.ts | 39 ++++++++++----- 6 files changed, 189 insertions(+), 13 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 42fafbb8..8e628589 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -403,6 +403,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:第一刀:最高流量工具 `gotry_flyai_search` 参数面从 `query:{type:'json'}` blob 迁 dsh typed ParameterSchemaSpec——平铺七字段(`kind` enum flight|train|hotel required + from/to/date/checkIn/checkOut/keyWords),`parameterSchemaSpecToJsonSchema` 投影模型可见逐字段 JSON Schema,`validateArgs` 在 execute 前宿主权校验;畸形参数(缺 kind/枚举外值/legacy blob 包裹)入口即结构化拒绝,ToolArgsError 经 guardToolExecute 保持 ADR-13 ToolFailure 形状(smoke §12 + channel-registry-tests §8 双迁移锁)。第二刀:`gotry_session_search`(十字段平铺)/`gotry_hotel_search`/`gotry_weather_check`——session 三意图无公共 required 字段,根 schema 开放无法宿主权拒 blob,按设计 §4③ 保留 `interpretArgs` 容忍层(blob 归一后照走业务闸,smoke 迁移锁钉死);skills-contract 契约对齐。第三刀:七个模型高频工具(session_login/doctor/artifacts_list/fact_gate/agent_reach/turn_handoff_list/artifacts_read;fact_gate 的 itinerary 保 object+additionalProperties:true,收紧归 #118 D-26;agent_reach 的 action 不设 enum——反射桥 D-4a' 开放面)。第四刀:`query:` blob 清零(wish_pool/trip_log/flight_verify/anything/web/video/github;trip_log 的 evidence P0 红线进 schema;web_search 字符串 blob 退役)。第五刀:最后三个 payload 形态——`gotry_feasibility_check`(payload 结构化对象,嵌套权威在 unified 引擎契约不越层强约束)/`gotry_motivation_save`(profile 闭合对象,**evidence P0 反伪造红线进嵌套 schema 由宿主权闸**,smoke §1 断言适配)/`gotry_companion_save`(companion 闭合对象 label/evidence required,additionalProperties:false 与负面清单同向)。**23/23 注册工具全部 typed 契约化**。条件必填仍由 execute 结构化报错给修正方向;**刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留容忍层**。余量:普通模型 canary(跑前不合发布面)记 D-30。 - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:①`toolRoutingHeadline`(channel-registry)——检索工具描述统一前置注册表生成的「服务意图 × 当前通道顺位(证据级降序)」卡,七个检索工具(flyai/session/hotel/weather/flight_verify/anything/web)经 `routed()` 接线,模型选工具时(读描述)与失败后(读 routing)两个决策点拿到同一张表;注册表加行描述自动一致,非检索工具零误伤(channel-registry-tests §8 断言接线前缀)。②doctor 补齐 patch 分发面宿主插件两态——`dsh-map-tools`/`dsh-tool-ask-user`(此前只覆盖 calendar):这类插件运行时解析**失败即整块静默剔除,不挡启动**,模型只觉得「没有这个工具」没人说为什么;doctor 候选清单与 bin/gotry-inner.js 解析逻辑同口径(repo vendored-node_modules/包依赖/dsh 闭包),隔离环境 missing 断言进 channel-registry-tests §7。实测暴露:**当前 source 布局 dsh-map-tools 全布局缺席**(未 vendor、非 dsh/gotry 依赖)——地图工具一直被静默丢弃,doctor 现在照亮此面,补依赖渠道留 founder 拍板。 - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:`npx gotry web`/headless 启动时(bin/gotry-inner.js 在 spawn dsh 前)以**分离子进程**后台跑只读体检(`gotry-bootstrap.js doctor --summary`)——有待处理项打一行 stderr(「N 项待处理(…=缺/半可用)——对话里让助手调 gotry_doctor 看详情,或终端 npx gotry doctor」),全 ok 静默零输出;零写盘零 header、detached+unref 不阻塞启动、恒 exit 0、benchmark 面豁免保持零杂音。inner 生命周期天然一次不重复刷。「初始化时可见」取代「会话中段撞错」(design §3.1③);bootstrap-tests §10以隔离 HOME 钉行为(stderr 一行/零写盘/恒 exit 0)。 +- **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:六渠道入效应注册表——`ANYTHING_SEARCH`/`WEB_READ`/`GITHUB_SEARCH`/`VIDEO_SUBTITLE`/`AGENT_REACH`/`SESSION_LOGIN`,**23 工具的全部外部依赖面收敛到 effect_interpreter.v1**(退避/熔断/declined 面统一,没有策略表行就没有效应)。策略行逐条拍板:ANYTHING 同 HBCLI 族(timeout 瞬时重试 1 次);WEB_READ 同免费公共源族(瞬时抖动重试+熔断);GITHUB/VIDEO(verdict=timeout 重试 1 次,not-installed=配置态不重试);AGENT_REACH 反射桥透传永不重试不熔断(wrapper 不是 router,重试即放大上游配额消耗);SESSION_LOGIN 同浏览器风控族。工具面照旧平铺,证据链逐源标注不变(effect-tests §12 四组断言:入表透传/永不重试族/timeout 重试/ mock 同语义)。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. diff --git a/docs/roadmap.md b/docs/roadmap.md index eba6333f..5bae9fcc 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -29,6 +29,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **typed 参数契约迁移(2026-09-04,issue #112,D-30,五刀收官)**:全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema——四高流量(flyai/session/hotel/weather)+ 七高频 + wish_pool/trip_log/flight_verify/anything/web/video/github + feasibility/motivation/companion;刀法总纲:有 required 字段的工具宿主权入口拒畸形参数(ToolFailure 形状不变;trip_log/motivation/companion 的 evidence P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层(blob 归一照走业务闸);agent_reach 的 action 不设 enum(反射桥开放面);迁移锁 smoke §1/§6/§12/channel-registry-tests §8。普通模型对参数形状不再靠猜。余量:普通模型 canary 记 D-30。 - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:七个检索工具描述统一前置 channel-registry 生成的「服务意图 × 通道顺位」卡(选工具与失败改道两决策点同表,注册表加行自动一致);doctor 补齐 patch 宿主插件 `dsh-map-tools`/`dsh-tool-ask-user` 两态照亮(此前解析失败整块静默剔除无人知)——实测暴露 source 布局 dsh-map-tools 全布局缺席,地图工具一直被静默丢弃,补依赖渠道留 founder 拍板。 - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:web/headless 启动时分离子进程后台跑只读体检——待处理项一行 stderr、全 ok 静默、零写盘、不阻塞不重复刷、benchmark 面豁免;「初始化时可见」取代「会话中段撞错」。bootstrap-tests §10 钉行为。 +- **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(策略行逐条拍板:timeout 类瞬时重试/配置态与风控类永不重试/反射桥透传零重试);工具面照旧平铺,证据链逐源标注不变;effect-tests §12 钉语义。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 3abe2ded..a18ebd8a 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,解译器迁移收尾(issue #115,D-23):anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(没有策略表行就没有效应);工具面照旧平铺,证据链逐源标注不变。 - 2026-09-04 起,启动一次性 doctor 摘要(issue #114):web/headless 启动时分离子进程后台只读体检,待处理项一行 stderr(全 ok 静默/零写盘/不阻塞/benchmark 豁免)——初始化可见取代会话中段撞错。 - 2026-09-04 起,工具描述首行由通道注册表生成(issue #113):七个检索工具描述前置「服务意图 × 通道顺位」卡(与失败现场 routing 字段同表),doctor 补齐 patch 宿主插件 dsh-map-tools/dsh-tool-ask-user 两态(静默剔除面照亮;map-tools 全布局缺席待 founder 拍板补依赖)。 - 2026-09-04 起,typed 参数契约迁移(issue #112,D-30,五刀收官):全部 23 个注册工具参数面 blob → dsh typed ParameterSchemaSpec 平铺/结构化字段,模型可见逐字段 JSON Schema;刀法总纲=有 required 字段的工具宿主权拒畸形参数(ToolFailure 形状不变;evidence 类 P0 红线进 schema 由宿主权闸),全可选工具保留 interpretArgs 容忍层;迁移锁 smoke §1/§6/§12/channel-registry-tests §8;普通模型 canary 记 `architecture.md` §10 D-30。 diff --git a/ts/capabilities/effect.ts b/ts/capabilities/effect.ts index 0263e661..2cc195c1 100644 --- a/ts/capabilities/effect.ts +++ b/ts/capabilities/effect.ts @@ -35,6 +35,10 @@ import { checkAvail, hotelRates, searchHotels } from './hbcli.ts' import { sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, type SessionFlightQuery, type SessionHotelQuery, type SessionTrainQuery } from './session-search.ts' import { geocodePlace, getClimate, getForecast, type WeatherPoint } from './weather.ts' import { verifyFlight, type FlightLiveQuery } from './opensky.ts' +import { anythingSearch, type AnythingQuery } from './anything.ts' +import { readUrl, reach, reachStatus } from './agent-reach.ts' +import { githubSearch, videoSubtitle } from './agent-reach-deep.ts' +import { sessionLogin } from './session-login.ts' // --------------------------------------------------------------------------- // 渠道 handler 注册表:effect 名(变体大写下划线,纯数据词表)→ 渠道实现 @@ -73,6 +77,20 @@ export interface HbCheckAvailEffectParams { timeoutMs?: number } +/** agent-reach 反射桥参数(status=上游 doctor;reach=渠道×方法透传) */ +export interface AgentReachEffectParams { + action?: string + channel?: string + method?: string + args?: string + timeoutMs?: number +} + +/** 账号会话登录引导参数(等待秒数;0 值凭证不过手) */ +export interface SessionLoginEffectParams { + waitSeconds?: number +} + /** 默认渠道实现(生产解译器的 dispatch 目标;全部满足「永不抛错」能力层契约) */ const DEFAULT_HANDLERS = { /** 飞猪官方只读通道(机/火/酒店;spawn CLI) */ @@ -109,6 +127,21 @@ const DEFAULT_HANDLERS = { WEATHER_CLIMATE: (p: WeatherPoint & { month: number; timeoutMs?: number }) => getClimate({ latitude: p.latitude, longitude: p.longitude }, p.month, { timeoutMs: p.timeoutMs }), /** OpenSky ADS-B 实时印证(免费匿名,~400 credits/天) */ OPENSKY_FLIGHT_VERIFY: (p: FlightLiveQuery) => verifyFlight(p), + /** hotel-be Anything 目的地/酒店候选(hbcli CLI;gotry_anything_search 同源,D-23 收编) */ + ANYTHING_SEARCH: (p: AnythingQuery) => anythingSearch(p), + /** 网页读取兜底(r.jina.ai 免费公共源;gotry_web_search 同源,D-23 收编) */ + WEB_READ: (p: { url: string; timeoutMs?: number }) => readUrl({ url: p.url, timeoutMs: p.timeoutMs }), + /** GitHub 仓库搜索(gh CLI;agent-reach-deep 执行面,gotry_github_search 同源,D-23 收编) */ + GITHUB_SEARCH: (p: { query: string; limit?: number; timeoutMs?: number }) => githubSearch(p), + /** 视频字幕拉取(yt-dlp;agent-reach-deep 执行面,gotry_video_subtitle 同源,D-23 收编) */ + VIDEO_SUBTITLE: (p: { url: string; lang?: string; timeoutMs?: number }) => videoSubtitle(p), + /** agent-reach 反射桥(status=上游 doctor;reach=渠道×方法透传,gotry_agent_reach 同源,D-23 收编) */ + AGENT_REACH: (p: AgentReachEffectParams) => + (p.action === 'status' || (!p.action && !p.channel)) + ? reachStatus(p.timeoutMs) + : reach({ channel: p.channel ?? '', method: p.method ?? '', args: p.args, timeoutMs: p.timeoutMs }), + /** 账号会话登录引导(浏览器通道;票据名只读 0 值过手,gotry_session_login 同源,D-23 收编) */ + SESSION_LOGIN: (p: SessionLoginEffectParams) => sessionLogin({ waitMs: typeof p.waitSeconds === 'number' ? p.waitSeconds * 1000 : undefined }), } as const export type EffectName = keyof typeof DEFAULT_HANDLERS @@ -164,6 +197,12 @@ const HBCLI_RETRY: RetryPolicy = { maxAttempts: 2, baseDelayMs: 300, maxDelayMs: * - SESSION:永不重试、不熔断——风控/挑战是「上游说不」,重试即红线; * 节律闸(≥30s)在渠道内(session-search §3.4),不在本层重复。 * - WEATHER/OPENSKY:免费源,瞬时网络抖动可重试,熔断防免费配额空转。 + * - ANYTHING:同 HBCLI 族(timeout 类瞬时重试 1 次;ENOENT/退码类永不); + * - WEB_READ:r.jina.ai 免费公共源,瞬时抖动可重试,熔断防公共配额空转; + * - GITHUB/VIDEO(gh/yt-dlp):verdict=timeout 瞬时重试 1 次;not-installed=配置态不重试; + * - AGENT_REACH:反射桥透传(wrapper 不是 router)——永不重试不熔断,上游超时自管; + * - SESSION_LOGIN:同 SESSION 浏览器族(风控红线)。 + * (D-23 收尾,issue #115:六渠道入表,没有策略表行就没有效应。) */ const SPECS: Record = { FLYAI_SEARCH: { @@ -227,6 +266,57 @@ const SPECS: Record = { isRetryable: (_r, e) => e != null || ((_r ?? {}) as { verdict?: string }).verdict === 'unavailable', isFailure: r => (r as { via?: string }).via === 'opensky-error', }, + // --- D-23 收尾(issue #115):六渠道入注册表——没有策略表行就没有效应 --- + // ANYTHING:同 HBCLI 族(hbcli CLI):仅 timeout 类瞬时(冷启动建后端 session)重试 1 次; + // ENOENT/退码类永不(「切换不是重试」契约);熔断防上游空转 + ANYTHING_SEARCH: { + channel: 'cli', + retry: HBCLI_RETRY, + isRetryable: (r) => hbcliTimeout(r), + breaker: { failureThreshold: 3, openMs: 60_000 }, + isFailure: r => (r as { via?: string }).via === 'hbcli-anything-error', + }, + // WEB_READ:r.jina.ai 免费公共源(同 WEATHER/OPENSKY 族):瞬时网络抖动可重试, + // 熔断防公共配额空转;非法 URL 是用户输入错(isFailure 不含,不重试不熔断) + WEB_READ: { + channel: 'api', + retry: API_RETRY, + breaker: API_BREAKER, + isFailure: r => (r as { via?: string }).via === 'r.jina.ai-error', + }, + // GITHUB/VIDEO(gh/yt-dlp CLI):verdict=timeout 瞬时重试 1 次(进程冷启动超时同 HBCLI + // 2026-09-02 实况);not-installed 是配置态(needs-setup 族,重试无意义),error 是上游说不; + // 两者都计入熔断(防坏环境空转) + GITHUB_SEARCH: { + channel: 'cli', + retry: HBCLI_RETRY, + isRetryable: (r) => (r as { verdict?: string }).verdict === 'timeout', + breaker: { failureThreshold: 3, openMs: 60_000 }, + isFailure: r => { const v = (r as { verdict?: string }).verdict; return v === 'error' || v === 'timeout' }, + }, + VIDEO_SUBTITLE: { + channel: 'cli', + retry: HBCLI_RETRY, + isRetryable: (r) => (r as { verdict?: string }).verdict === 'timeout', + breaker: { failureThreshold: 3, openMs: 60_000 }, + isFailure: r => { const v = (r as { verdict?: string }).verdict; return v === 'error' || v === 'timeout' }, + }, + // AGENT_REACH:反射桥透传(wrapper 不是 router,D-4a')——上游超时自管,重试会放大 + // 上游配额消耗,永不重试不熔断;needs-setup/not-installed 是「上游说不」同 SESSION 族 + AGENT_REACH: { + channel: 'cli', + retry: null, + breaker: null, + isFailure: r => (r as { verdict?: string }).verdict === 'error', + }, + // SESSION_LOGIN:同 SESSION_* 浏览器族——风控/挑战红线,永不重试不熔断; + // pending 是「等用户」不是失败 + SESSION_LOGIN: { + channel: 'browser', + retry: null, + breaker: null, + isFailure: defaultIsFailure, + }, } // --------------------------------------------------------------------------- diff --git a/ts/scripts/effect-tests.ts b/ts/scripts/effect-tests.ts index d748cc03..9ace560b 100644 --- a/ts/scripts/effect-tests.ts +++ b/ts/scripts/effect-tests.ts @@ -319,4 +319,72 @@ const itp11b = await ipEnoent({ effect: 'HBCLI_HOTEL_SEARCH', params: { destinat assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)') console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK') -console.log('EFFECT INTERPRETER TESTS: 11/11 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试,纯离线)') \ No newline at end of file +// --------------------------------------------------------------------------- +// 12. D-23 收尾(issue #115):六渠道入注册表——策略表行存在 + 横切语义正确 +// (anything/web/github/video/agent_reach/session_login;没有策略表行就没有效应) +// --------------------------------------------------------------------------- +__resetEffectBreakersForTest() +// 12a. 六效应注册在案:handler 注入 → 原样透传,trace.channel 与策略表一致 +const sixChannels: Array<[string, string, unknown]> = [ + ['ANYTHING_SEARCH', 'cli', { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: 'e', latencyMs: 1 }], + ['WEB_READ', 'api', { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 }], + ['GITHUB_SEARCH', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }], + ['VIDEO_SUBTITLE', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }], + ['AGENT_REACH', 'cli', { ok: true, verdict: 'found', channel: 'v2ex', method: 'read', evidence: 'e', latencyMs: 1 }], + ['SESSION_LOGIN', 'browser', { ok: true, via: 'session-login', verdict: 'pending', evidence: 'e', latencyMs: 1 }], +] +for (const [fx, channel, obs] of sixChannels) { + const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => obs } }) + const out = await ip({ effect: fx, params: {} } satisfies GotryEffect) + assert.equal(out.result, obs, `${fx} 透传原样`) + assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`) + assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`) +} +console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK') + +// 12b. 永不重试族:AGENT_REACH / SESSION_LOGIN handler 恒抛错 → attempts=1(透传面不放大上游消耗) +for (const fx of ['AGENT_REACH', 'SESSION_LOGIN']) { + const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => { throw new Error('upstream says no') } } }) + const out = await ip({ effect: fx, params: {} } satisfies GotryEffect) + assert.equal(out.result, null, `${fx} 抛错 → result=null`) + assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`) +} +console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK') + +// 12c. WEB_READ 瞬时抖动重试 1 次(throw → 成功,attempts=2);GITHUB timeout 重试 / not-installed 不重试 +__resetEffectBreakersForTest() +let webCalls = 0 +const ipWeb = makeProductionInterpreter({ + sleep: sleep0, breakers: new Map(), + handlers: { WEB_READ: async () => { webCalls += 1; if (webCalls === 1) throw new Error('socket hang up'); return { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 } } }, +}) +const outWeb = await ipWeb({ effect: 'WEB_READ', params: { url: 'https://example.com' } } satisfies GotryEffect) +assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`) +let ghTimeoutCalls = 0 +const ipGhTimeout = makeProductionInterpreter({ + sleep: sleep0, breakers: new Map(), + handlers: { GITHUB_SEARCH: async () => { ghTimeoutCalls += 1; if (ghTimeoutCalls === 1) return { ok: false, verdict: 'timeout', evidence: 'e', latencyMs: 1, stdout: '', stderr: '' }; return { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 } } }, +}) +const outGhT = await ipGhTimeout({ effect: 'GITHUB_SEARCH', params: { query: 'x' } } satisfies GotryEffect) +assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)') +let ghNiCalls = 0 +const ipGhNi = makeProductionInterpreter({ + sleep: sleep0, breakers: new Map(), + handlers: { GITHUB_SEARCH: async () => { ghNiCalls += 1; return { ok: false, verdict: 'not-installed', evidence: 'e', latencyMs: 0 } } }, +}) +const outGhNi = await ipGhNi({ effect: 'GITHUB_SEARCH', params: { query: 'x' } } satisfies GotryEffect) +assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试') +console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK') + +// 12d. mock 面:新效应夹具回放 + 未登记夹具结构化拒绝(与既有效应同语义) +__resetEffectBreakersForTest() +const mockIp = makeMockInterpreter({ ANYTHING_SEARCH: { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: '[实时API:hbcli-anything@ts]', latencyMs: 1 } }) +const outMock = await mockIp({ effect: 'ANYTHING_SEARCH', params: { keyword: '大理' } } satisfies GotryEffect) +assert.equal((outMock.result as { verdict?: string }).verdict, 'hit', 'mock 夹具回放原样') +assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注') +const outMockBad = await mockIp({ effect: 'SESSION_LOGIN', params: {} } satisfies GotryEffect) +assert.equal(outMockBad.result, null, '未登记夹具 → result=null') +assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝') +console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK') + +console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)') \ No newline at end of file diff --git a/ts/src/index.ts b/ts/src/index.ts index d4b3403c..13958c39 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -31,11 +31,10 @@ import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, r import { buildTimeAnchor } from './time-anchor.ts' import { resolveSlotDate } from './slot-spec.ts' import { wmoLabel } from '../capabilities/weather.ts' -import { anythingSearch } from '../capabilities/anything.ts' -import { readUrl, reach, reachStatus } from '../capabilities/agent-reach.ts' +// D-23 收编(issue #115):anything/web/github/video/agent_reach/session_login 六渠道改经 +// 效应注册表(退避/熔断/declined 面统一);能力层直接 import 仅留类型位(reach/reachStatus) +import { reach, reachStatus } from '../capabilities/agent-reach.ts' import { runDoctorChecks, renderDoctorReportMd } from '../capabilities/doctor.ts' -import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.ts' -import { sessionLogin } from '../capabilities/session-login.ts' import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.ts' import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.ts' import { installModelOverride } from '../capabilities/model-override.ts' @@ -964,7 +963,10 @@ export function apply(ctx: Context, config: Config): void { output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { evidence?: string }).evidence ?? JSON.stringify(v).slice(0, 600)) }] }, async execute(args, _exec: unknown) { const q = unwrapQuery<{ waitSeconds?: number }>(args, 'waitSeconds') - const r = await sessionLogin({ waitMs: typeof q?.waitSeconds === 'number' ? q.waitSeconds * 1000 : undefined }) + // D-23 收编(issue #115):登录引导入效应注册表(SESSION_LOGIN,浏览器族永不重试不熔断) + const itpL = await interpretEffect({ effect: 'SESSION_LOGIN', params: { waitSeconds: q?.waitSeconds } }) + if (!itpL.result) return declinedObservation('SESSION_LOGIN', itpL.trace) + const r = itpL.result const summary = r.verdict === 'logged-in' ? `${r.site} 登录完成确认(票据 cookie 名已检出,只读名字)。说明:登录是在携程官网、用你自己的浏览器完成的——gotry 全程未接触任何密码/验证码/cookie 值。现在可以继续会话检索了。${r.evidence}` : r.verdict === 'pending' @@ -1167,7 +1169,9 @@ export function apply(ctx: Context, config: Config): void { if (!q.keyword) { return JSON.parse(JSON.stringify({ ok: false, verdict: 'error', summary: 'keyword 必填', evidence: '[hbcli-anything@error] empty' })) as Record } - const r = await anythingSearch(q) + const itpA = await interpretEffect({ effect: 'ANYTHING_SEARCH', params: q }) + if (!itpA.result) return declinedObservation('ANYTHING_SEARCH', itpA.trace) + const r = itpA.result const dir = await ensureStateDir(config.stateRoot) await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(() => {}) const top5 = (r.hits ?? []).slice(0, 5) @@ -1218,7 +1222,9 @@ export function apply(ctx: Context, config: Config): void { if (!q.url) { return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填', evidence: '[agent-reach:error] empty url' })) as Record } - const r = await readUrl({ url: q.url, timeoutMs: q.timeoutMs }) + const itpW = await interpretEffect({ effect: 'WEB_READ', params: { url: q.url, timeoutMs: q.timeoutMs } }) + if (!itpW.result) return declinedObservation('WEB_READ', itpW.trace) + const r = itpW.result const dir = await ensureStateDir(config.stateRoot) await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(() => {}) const summary = r.ok @@ -1254,7 +1260,9 @@ export function apply(ctx: Context, config: Config): void { if (!q.url) { return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填' })) as Record } - const r = await videoSubtitle({ url: q.url, lang: q.lang }) + const itpV = await interpretEffect({ effect: 'VIDEO_SUBTITLE', params: { url: q.url, lang: q.lang } }) + if (!itpV.result) return declinedObservation('VIDEO_SUBTITLE', itpV.trace) + const r = itpV.result const summary = r.verdict === 'found' ? `${q.url} 字幕提取成功 (${r.latencyMs}ms)\n${r.evidence}\n---\n${(r.subtitles ?? '').slice(0, 800)}` : r.verdict === 'not-installed' @@ -1290,7 +1298,9 @@ export function apply(ctx: Context, config: Config): void { if (!q.query) { return JSON.parse(JSON.stringify({ ok: false, summary: 'query 必填' })) as Record } - const r = await githubSearch({ query: q.query, limit: q.limit }) + const itpG = await interpretEffect({ effect: 'GITHUB_SEARCH', params: { query: q.query, limit: q.limit } }) + if (!itpG.result) return declinedObservation('GITHUB_SEARCH', itpG.trace) + const r = itpG.result const summary = r.verdict === 'found' ? `${q.query} → ${r.repos?.length ?? 0} repos\n${(r.repos ?? []).map((x, i) => ` ${i + 1}. ${x.name} ★${x.stars ?? '?'} — ${(x.description ?? '').slice(0, 60)}`).join('\n')}\n${r.evidence}` : r.verdict === 'not-installed' @@ -1330,12 +1340,17 @@ export function apply(ctx: Context, config: Config): void { render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }], }, async execute(args, _exec: unknown) { - const q = unwrapQuery<{ action?: string; channel?: string; method?: string; args?: string; timeoutMs?: number }>(args, 'channel') + const q = unwrapQuery<{ action?: string; channel?: string; method?: string; args?: string; timeoutMs?: number }>(args) const started = Date.now() const dir = await ensureStateDir(config.stateRoot) + // D-23 收编(issue #115):status/reach 两分支收敛为单一 AGENT_REACH 效应—— + // 解译器横切(trace/declined 面)统一,渲染逻辑留在工具层不变 + const itpAR = await interpretEffect({ effect: 'AGENT_REACH', params: { action: q.action, channel: q.channel, method: q.method, args: q.args, timeoutMs: q.timeoutMs } }) + if (!itpAR.result) return declinedObservation('AGENT_REACH', itpAR.trace) + if (q.action === 'status' || (!q.action && !q.channel)) { - const st = await reachStatus(q.timeoutMs) + const st = itpAR.result as Awaited> await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(() => {}) const summary = st.via === 'agent-reach-cli' ? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}` @@ -1349,7 +1364,7 @@ export function apply(ctx: Context, config: Config): void { if (!q.channel || !q.method) { return JSON.parse(JSON.stringify({ ok: false, summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表' })) as Record } - const r = await reach({ channel: q.channel, method: q.method, args: q.args, timeoutMs: q.timeoutMs }) + const r = itpAR.result as Awaited> // 长结果干净截断:数组按条目边界保留(dsh 工具上限会拦腰断 JSON,模型只能看到半条) const dataStr = (v: unknown): unknown => { if (Array.isArray(v)) { From c51237796eaa76fcc7150ac31290a539438b886e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=8C=AF?= Date: Fri, 4 Sep 2026 21:43:55 +0400 Subject: [PATCH 09/12] =?UTF-8?q?feat(session):=20=E5=95=86=E5=BA=97?= =?UTF-8?q?=E7=89=88=E6=89=A9=E5=B1=95=E6=A3=80=E6=B5=8B=E2=80=94=E2=80=94?= =?UTF-8?q?needs-extension=20=E6=96=87=E6=A1=88=E8=87=AA=E9=80=82=E5=BA=94?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=20dev-mode(#117,D-24=20=E6=B8=85=E5=81=BF)?= =?UTF-8?q?=E2=80=94=E2=80=94needsExtensionSummary/localExtensionInstalled?= =?UTF-8?q?(extension-bridge):=E6=9C=AC=E5=9C=B0=20unpacked=20=E8=90=BD?= =?UTF-8?q?=E4=BD=8D(~/.gotry/extension/manifest.json=20=E5=9C=A8)?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=AE=8C=E6=95=B4=E5=8F=8C=E9=80=9A=E9=81=93?= =?UTF-8?q?=E6=8C=87=E5=BC=95;=E6=9C=AA=E8=90=BD=E4=BD=8D(=E5=95=86?= =?UTF-8?q?=E5=BA=97=E7=89=88=E7=94=A8=E6=88=B7/=E6=9C=AA=E8=A3=85)?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BF=87=E5=BC=80=E5=8F=91=E8=80=85?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F/=E6=9C=AC=E5=9C=B0=E9=80=9A=E9=81=93?= =?UTF-8?q?=E6=96=87=E6=A1=88,=E5=8F=AA=E6=8E=A8=E5=95=86=E5=BA=97?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E8=A3=85+=E3=80=8C=E5=B7=B2=E8=A3=85?= =?UTF-8?q?=E5=95=86=E5=BA=97=E7=89=88=3F=E6=89=93=E5=BC=80=20Chrome=20?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF=E3=80=8D;=E6=A1=A5=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=91=98=E8=A6=81(extension-not-connected)=E5=90=8C=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=90=8C=E8=AF=AD=E4=B9=89;doctor=20=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E9=A1=B9=20detail=20=E6=98=8E=E7=A4=BA=E5=95=86?= =?UTF-8?q?=E5=BA=97=E7=89=88=E7=94=A8=E6=88=B7=E5=8F=AF=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E6=9C=AC=E9=A1=B9(=E4=BB=A5=E8=BF=90=E8=A1=8C=E6=97=B6=20verdi?= =?UTF-8?q?ct=20=E4=B8=BA=E5=87=86);extension-channel=20=E9=9D=99=E6=80=81?= =?UTF-8?q?=20NEEDS=5FEXTENSION=5FHINT=20=E9=80=80=E5=BD=B9=E4=B8=BA?= =?UTF-8?q?=E8=87=AA=E9=80=82=E5=BA=94=E5=87=BD=E6=95=B0(=E9=9D=99?= =?UTF-8?q?=E6=80=81=E6=96=87=E6=A1=88=E6=97=A0=E6=B3=95=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E7=8A=B6=E6=80=81=E6=AD=A3=E6=98=AF=20D-24?= =?UTF-8?q?=20=E6=AE=8B=E9=87=8F=E7=97=85=E7=81=B6);extension-tests=20?= =?UTF-8?q?=C2=A738=20=E4=B8=89=E6=9D=A1=E6=96=B0=E6=96=AD=E8=A8=80=2031?= =?UTF-8?q?=20=E6=AE=B5=E5=85=A8=E7=BB=BF,doctor-tests=20=E7=BB=BF,tsc=20?= =?UTF-8?q?=E7=BB=BF,run-all=20ALL=20SUITES=20GREEN;=E5=85=AD=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C=E6=AD=A5?= =?UTF-8?q?(arch=20=C2=A79=E3=80=81roadmap=20=E5=A2=9E=E9=87=8F=E3=80=81st?= =?UTF-8?q?age1=20=E7=8A=B6=E6=80=81=E5=A4=B4)=20(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danceiny --- docs/architecture.md | 1 + docs/roadmap.md | 1 + docs/stage1-top-down-design.md | 1 + ts/capabilities/doctor.ts | 6 ++-- ts/capabilities/session/extension-bridge.ts | 32 +++++++++++++++++++- ts/capabilities/session/extension-channel.ts | 17 ++++++----- ts/scripts/extension-tests.ts | 26 +++++++++++++++- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8e628589..4ed6dc03 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -404,6 +404,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:①`toolRoutingHeadline`(channel-registry)——检索工具描述统一前置注册表生成的「服务意图 × 当前通道顺位(证据级降序)」卡,七个检索工具(flyai/session/hotel/weather/flight_verify/anything/web)经 `routed()` 接线,模型选工具时(读描述)与失败后(读 routing)两个决策点拿到同一张表;注册表加行描述自动一致,非检索工具零误伤(channel-registry-tests §8 断言接线前缀)。②doctor 补齐 patch 分发面宿主插件两态——`dsh-map-tools`/`dsh-tool-ask-user`(此前只覆盖 calendar):这类插件运行时解析**失败即整块静默剔除,不挡启动**,模型只觉得「没有这个工具」没人说为什么;doctor 候选清单与 bin/gotry-inner.js 解析逻辑同口径(repo vendored-node_modules/包依赖/dsh 闭包),隔离环境 missing 断言进 channel-registry-tests §7。实测暴露:**当前 source 布局 dsh-map-tools 全布局缺席**(未 vendor、非 dsh/gotry 依赖)——地图工具一直被静默丢弃,doctor 现在照亮此面,补依赖渠道留 founder 拍板。 - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:`npx gotry web`/headless 启动时(bin/gotry-inner.js 在 spawn dsh 前)以**分离子进程**后台跑只读体检(`gotry-bootstrap.js doctor --summary`)——有待处理项打一行 stderr(「N 项待处理(…=缺/半可用)——对话里让助手调 gotry_doctor 看详情,或终端 npx gotry doctor」),全 ok 静默零输出;零写盘零 header、detached+unref 不阻塞启动、恒 exit 0、benchmark 面豁免保持零杂音。inner 生命周期天然一次不重复刷。「初始化时可见」取代「会话中段撞错」(design §3.1③);bootstrap-tests §10以隔离 HOME 钉行为(stderr 一行/零写盘/恒 exit 0)。 - **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:六渠道入效应注册表——`ANYTHING_SEARCH`/`WEB_READ`/`GITHUB_SEARCH`/`VIDEO_SUBTITLE`/`AGENT_REACH`/`SESSION_LOGIN`,**23 工具的全部外部依赖面收敛到 effect_interpreter.v1**(退避/熔断/declined 面统一,没有策略表行就没有效应)。策略行逐条拍板:ANYTHING 同 HBCLI 族(timeout 瞬时重试 1 次);WEB_READ 同免费公共源族(瞬时抖动重试+熔断);GITHUB/VIDEO(verdict=timeout 重试 1 次,not-installed=配置态不重试);AGENT_REACH 反射桥透传永不重试不熔断(wrapper 不是 router,重试即放大上游配额消耗);SESSION_LOGIN 同浏览器风控族。工具面照旧平铺,证据链逐源标注不变(effect-tests §12 四组断言:入表透传/永不重试族/timeout 重试/ mock 同语义)。 +- **商店版扩展检测·自适应文案(2026-09-04,issue #117,D-24 清偿)**:needs-extension 文案按本地通道落位状态自适应——`needsExtensionSummary`/`localExtensionInstalled`(extension-bridge):本地 unpacked 已落位(`~/.gotry/extension/manifest.json` 在)→ 保留完整双通道指引(商店+GitHub+开发者模式);未落位(商店版用户/未装)→ **自动跳过开发者模式/本地通道文案**,只推商店一键装并给「已装商店版?打开 Chrome 即可」提示。桥失败摘要(extension-not-connected)同函数同语义;doctor 扩展项 detail 明示「商店版用户可忽略本项,以运行时 verdict 为准」;extension-channel 静态常量 `NEEDS_EXTENSION_HINT` 退役为自适应函数(静态文案无法感知安装状态正是 D-24 残量病灶)。extension-tests §38 三条新断言(31 段全绿)。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. diff --git a/docs/roadmap.md b/docs/roadmap.md index 5bae9fcc..8233b18f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -30,6 +30,7 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **工具描述首行生成 + doctor 宿主插件覆盖面(2026-09-04,issue #113,L1 残量收口)**:七个检索工具描述统一前置 channel-registry 生成的「服务意图 × 通道顺位」卡(选工具与失败改道两决策点同表,注册表加行自动一致);doctor 补齐 patch 宿主插件 `dsh-map-tools`/`dsh-tool-ask-user` 两态照亮(此前解析失败整块静默剔除无人知)——实测暴露 source 布局 dsh-map-tools 全布局缺席,地图工具一直被静默丢弃,补依赖渠道留 founder 拍板。 - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:web/headless 启动时分离子进程后台跑只读体检——待处理项一行 stderr、全 ok 静默、零写盘、不阻塞不重复刷、benchmark 面豁免;「初始化时可见」取代「会话中段撞错」。bootstrap-tests §10 钉行为。 - **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(策略行逐条拍板:timeout 类瞬时重试/配置态与风控类永不重试/反射桥透传零重试);工具面照旧平铺,证据链逐源标注不变;effect-tests §12 钉语义。 +- **商店版扩展检测·自适应文案(2026-09-04,issue #117,D-24 清偿)**:needs-extension 文案按本地通道落位自适应——商店版用户不再看到开发者模式/本地通道指引,只推商店一键装与「已装即可」提示;桥失败摘要与 doctor 扩展项同步自适应;extension-tests §38 钉行为。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index a18ebd8a..a50d7b81 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,needs-extension 文案自适应(issue #117,D-24 清偿):按本地通道落位自动跳过开发者模式/本地通道指引——商店版用户只推商店一键装与「已装即可」;桥失败摘要与 doctor 扩展项同步自适应。 - 2026-09-04 起,解译器迁移收尾(issue #115,D-23):anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(没有策略表行就没有效应);工具面照旧平铺,证据链逐源标注不变。 - 2026-09-04 起,启动一次性 doctor 摘要(issue #114):web/headless 启动时分离子进程后台只读体检,待处理项一行 stderr(全 ok 静默/零写盘/不阻塞/benchmark 豁免)——初始化可见取代会话中段撞错。 - 2026-09-04 起,工具描述首行由通道注册表生成(issue #113):七个检索工具描述前置「服务意图 × 通道顺位」卡(与失败现场 routing 字段同表),doctor 补齐 patch 宿主插件 dsh-map-tools/dsh-tool-ask-user 两态(静默剔除面照亮;map-tools 全布局缺席待 founder 拍板补依赖)。 diff --git a/ts/capabilities/doctor.ts b/ts/capabilities/doctor.ts index 047d6326..71ddbcfb 100644 --- a/ts/capabilities/doctor.ts +++ b/ts/capabilities/doctor.ts @@ -88,12 +88,14 @@ export async function runDoctorChecks(opts: DoctorOptions = {}): Promise { assert.equal(e.retry_allowed, false) }) + // D-24 自适应文案(issue #117):按本地通道是否落位自动跳过开发者模式/本地通道指引 + await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', () => { + const s = needsExtensionSummary({ localInstalled: true }) + assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引') + assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在') + }) + await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', () => { + const s = needsExtensionSummary({ localInstalled: false }) + assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)') + assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样') + assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)') + assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引') + }) + await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', () => { + const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-')) + assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位') + mkdirSync(join(fakeHome, '.gotry', 'extension'), { recursive: true }) + writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8') + assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true') + rmSync(fakeHome, { recursive: true, force: true }) + }) + await b.close() console.log(`\n§38 extension bridge: ${passed} 段全绿`) From f7eed982dea0424c8a778ce02cda5517154d48f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=8C=AF?= Date: Fri, 4 Sep 2026 22:49:44 +0400 Subject: [PATCH 10/12] =?UTF-8?q?runtime:=20=E7=A7=BB=E9=99=A4=20legacy=20?= =?UTF-8?q?vendored=20dsh=20=E5=9B=9E=E9=80=80=E8=B7=AF=E5=BE=84(#120,D-27?= =?UTF-8?q?=20=E6=B8=85=E5=81=BF,=E5=86=B3=E7=AD=96=3D=E7=A7=BB=E9=99=A4)?= =?UTF-8?q?=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(facts): 事实闸覆盖面——酒店 claim 入闸 + 渲染原语单向生成(issue #118,D-26 收口)——HotelFact 第三形态:exact-date 酒店检索 hit/miss 落账(flyai-hotel/session:ctrip-hotel 接线),摸底不落账/传输失败不落负事实/打码价不落数字价;gotry_fact_gate 酒店 claim 入闸(目的地+档期回溯,无事实 fail-closed unverifiable_hotel_claim,miss 在册写有房=not_in_source);渲染原语单向生成:renderFlightFact/renderHotelFact 行内嵌 fact: 锚点,闸侧锚点优先确定性回溯(启发式让位),手改/伪造锚点=fact_anchor_unknown;policy 窄化断言显式化(FlightFact);fact-gate-tests §10 八断言 56 pass,tsc+smoke 绿,run-all ALL SUITES GREEN;六状态面同提交同步(arch §9、roadmap 增量、stage1 状态头);政策生产端(实时签证 API)仍记 D-26 外部依赖 * fix(runtime): 移除 legacy vendored dsh 回退路径(issue #120,D-27 清偿,决策=移除运行时路径)——selectDshRuntime 只认 root manifest/依赖解析(216 精确依赖闭包),找不到即 fail-closed 报错指重装;决策依据:旧 vendored 形态 Node 兼容窗口已断(D-27/Round 5「不承诺可运行」),解析成功只会把 dsh 缺失变成下游玄学失败;inner vendoredDshEarly/legacy-vendored source 分支/报错文案同步移除,DshRuntime.source 收敛 'root';§48 e2e 断言改为非 benchmark 也不再回退 vendored;vendor 闭包目录不属本清偿范围(锁一致性面另行处置)。tsc 绿,§48 e2e 绿,run-all ALL SUITES GREEN;六状态面同提交同步(arch §9/§10 D-27 清偿、roadmap 增量、stage1 状态头) --------- Co-authored-by: Danceiny --- bin/gotry-inner.js | 16 ++- bin/gotry-runtime-resolution.d.ts | 2 +- bin/gotry-runtime-resolution.js | 15 ++- docs/architecture.md | 8 +- docs/roadmap.md | 2 + docs/stage1-top-down-design.md | 2 + scripts/build-dist.mjs | 2 +- .../benchmark-environment-bridge-e2e.ts | 2 +- ts/scripts/fact-gate-tests.ts | 43 +++++++ ts/src/artifact-gate.ts | 84 +++++++++++++- ts/src/bookable-facts.ts | 109 +++++++++++++++++- ts/src/index.ts | 14 ++- 12 files changed, 269 insertions(+), 30 deletions(-) diff --git a/bin/gotry-inner.js b/bin/gotry-inner.js index a3e36aaf..fd220bbd 100755 --- a/bin/gotry-inner.js +++ b/bin/gotry-inner.js @@ -9,8 +9,8 @@ * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL) * 2. 定位 dsh runtime: * a. repo checkout / npm 安装 → root package 解析锁定的 @deepseek-ai/dsh - * b. 非 benchmark 且 root 缺失时 → legacy vendored node_modules fallback(不走 npx: - * dsh cordis-loader 在子 cwd 求值 plugin name,必须绝对路径 patch) + * b. legacy vendored fallback 已按 D-27 移除(issue #120)——root 缺失即 + * fail-closed 报错指重装,不再有「解析成功却跑不起来」的玄学形态 * 3. 运行时生成 patch(os.tmpdir):把 gotry-tools 插件路径重写为按本包 * 位置解析的绝对路径 —— 仓内 cordis.gotry-patch.yml 里的 name 行只是 * 占位(本机绝对路径),随 tarball 分发后对其他机器必错。 @@ -39,8 +39,7 @@ const rootRequire = createRequire(join(repoRoot, 'package.json')) // --- 环境 .env 加载(provider-neutral) --- // npm 安装模式优先读用户当前目录的 .env(包目录内不该有凭证);repo 检出读仓根。 const sourceDshEarly = resolveDshPackage(rootRequire) -const vendoredDshEarly = !sourceDshEarly && existsSync(join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')) -const envCandidates = sourceCheckoutMode || vendoredDshEarly +const envCandidates = sourceCheckoutMode || sourceDshEarly ? [join(repoRoot, '.env')] : [join(process.cwd(), '.env'), join(repoRoot, '.env')] const envLines = [] @@ -112,8 +111,7 @@ if (!supportsNodeVersion(process.versions.node)) { process.exit(1) } -// --- dsh runtime 定位:root manifest/require.resolve 优先;旧 vendored 仅作 legacy fallback --- -const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js') +// --- dsh runtime 定位(D-27 清偿,issue #120):仅 root manifest/依赖解析;legacy vendored 回退已移除 --- const installedPackageMode = !sourceCheckoutMode const selectedDsh = selectDshRuntime({ repoRoot, @@ -136,7 +134,7 @@ if (!dshBin) { console.error('[gotry] benchmark dsh runtime unavailable') process.exit(1) } - console.error(`[gotry] 找不到 dsh runtime(既无 vendored ${vendoredDsh},依赖里也没有 @deepseek-ai/dsh)。`) + console.error('[gotry] 找不到 dsh runtime(依赖里没有 @deepseek-ai/dsh;legacy vendored 回退已按 D-27 移除)。') console.error('repo checkout: npm ci && npm --prefix ts ci && node scripts/build-dist.mjs;npm 安装: npm install(检查 node_modules)。') process.exit(1) } @@ -147,12 +145,12 @@ if (benchmarkEnvironmentConfig && !benchmarkRuntimeSupported(selectedDsh)) { // --- 运行时 patch:插件路径按本包位置重写为绝对路径 --- // 安装包始终指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts)。 -// repo 检出仅在 vendored runtime 或显式 tsx loader 可用时走 .ts;否则 +// repo 检出仅在显式 tsx loader 可用时走 .ts;否则 // 使用当前 worktree 已构建的 dist,避免 Node strip-only 拒绝参数属性语法。 const distEntry = join(repoRoot, 'dist/src/index.js') const tsEntry = join(repoRoot, 'ts/src/index.ts') const tsxLoaderActive = /(?:^|\s)--(?:import|loader)(?:=|\s)(?:"[^"]*tsx[^"]*"|'[^']*tsx[^']*'|\S*tsx\S*)/.test(process.env.NODE_OPTIONS ?? '') -const sourceTypeScriptMode = !installedPackageMode && (dshSource === 'legacy-vendored' || tsxLoaderActive) +const sourceTypeScriptMode = !installedPackageMode && tsxLoaderActive const pluginEntry = !sourceTypeScriptMode && existsSync(distEntry) ? distEntry : tsEntry const distModuleMode = pluginEntry === distEntry const staticPatch = join(repoRoot, 'cordis.gotry-patch.yml') diff --git a/bin/gotry-runtime-resolution.d.ts b/bin/gotry-runtime-resolution.d.ts index a7b7eb63..cbce843c 100644 --- a/bin/gotry-runtime-resolution.d.ts +++ b/bin/gotry-runtime-resolution.d.ts @@ -5,7 +5,7 @@ export type DshResolver = { export type DshRuntime = { bin: string version: string - source: 'root' | 'legacy-vendored' + source: 'root' } export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.2-alpha.3' diff --git a/bin/gotry-runtime-resolution.js b/bin/gotry-runtime-resolution.js index 9b395e8d..cfe25f08 100644 --- a/bin/gotry-runtime-resolution.js +++ b/bin/gotry-runtime-resolution.js @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' export const REQUIRED_BENCHMARK_DSH_VERSION = '0.1.2-alpha.3' @@ -29,15 +29,14 @@ export function supportsNodeVersion(version) { return major > 22 || (major === 22 && minor >= 15) } -export function selectDshRuntime({ repoRoot, rootResolver, benchmark }) { +export function selectDshRuntime({ rootResolver }) { const root = resolveDshPackage(rootResolver) if (root) return { ...root, source: 'root' } - if (benchmark) return null - - const vendoredBin = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js') - if (!existsSync(vendoredBin)) return null - const vendored = readDshPackage(vendoredBin) - return vendored ? { ...vendored, source: 'legacy-vendored' } : null + // D-27 清偿(issue #120):legacy vendored 回退路径已移除——该形态的 Node 兼容窗口 + // 断裂(Round 5 起「不承诺可运行」),解析成功只会把「dsh 缺失」变成下游玄学失败。 + // 唯一受支持形态 = root manifest / 依赖解析(216 精确依赖闭包);找不到即 fail-closed, + // 由调用方给明确重装指引。benchmark 原本就禁用回退,语义不变(入参保留以稳调用面)。 + return null } export function benchmarkRuntimeSupported(runtime) { diff --git a/docs/architecture.md b/docs/architecture.md index 4ed6dc03..dc74fdc9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -405,6 +405,7 @@ Booking Copilot 是既有工作台内的 BFF-only embedded read-action 面:v1 - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:`npx gotry web`/headless 启动时(bin/gotry-inner.js 在 spawn dsh 前)以**分离子进程**后台跑只读体检(`gotry-bootstrap.js doctor --summary`)——有待处理项打一行 stderr(「N 项待处理(…=缺/半可用)——对话里让助手调 gotry_doctor 看详情,或终端 npx gotry doctor」),全 ok 静默零输出;零写盘零 header、detached+unref 不阻塞启动、恒 exit 0、benchmark 面豁免保持零杂音。inner 生命周期天然一次不重复刷。「初始化时可见」取代「会话中段撞错」(design §3.1③);bootstrap-tests §10以隔离 HOME 钉行为(stderr 一行/零写盘/恒 exit 0)。 - **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:六渠道入效应注册表——`ANYTHING_SEARCH`/`WEB_READ`/`GITHUB_SEARCH`/`VIDEO_SUBTITLE`/`AGENT_REACH`/`SESSION_LOGIN`,**23 工具的全部外部依赖面收敛到 effect_interpreter.v1**(退避/熔断/declined 面统一,没有策略表行就没有效应)。策略行逐条拍板:ANYTHING 同 HBCLI 族(timeout 瞬时重试 1 次);WEB_READ 同免费公共源族(瞬时抖动重试+熔断);GITHUB/VIDEO(verdict=timeout 重试 1 次,not-installed=配置态不重试);AGENT_REACH 反射桥透传永不重试不熔断(wrapper 不是 router,重试即放大上游配额消耗);SESSION_LOGIN 同浏览器风控族。工具面照旧平铺,证据链逐源标注不变(effect-tests §12 四组断言:入表透传/永不重试族/timeout 重试/ mock 同语义)。 - **商店版扩展检测·自适应文案(2026-09-04,issue #117,D-24 清偿)**:needs-extension 文案按本地通道落位状态自适应——`needsExtensionSummary`/`localExtensionInstalled`(extension-bridge):本地 unpacked 已落位(`~/.gotry/extension/manifest.json` 在)→ 保留完整双通道指引(商店+GitHub+开发者模式);未落位(商店版用户/未装)→ **自动跳过开发者模式/本地通道文案**,只推商店一键装并给「已装商店版?打开 Chrome 即可」提示。桥失败摘要(extension-not-connected)同函数同语义;doctor 扩展项 detail 明示「商店版用户可忽略本项,以运行时 verdict 为准」;extension-channel 静态常量 `NEEDS_EXTENSION_HINT` 退役为自适应函数(静态文案无法感知安装状态正是 D-24 残量病灶)。extension-tests §38 三条新断言(31 段全绿)。 +- **事实闸覆盖面·酒店入闸 + 渲染原语(2026-09-04,issue #118,D-26 收口)**:①`HotelFact`(gotry_bookable_fact.v1 第三形态)——exact-date 酒店检索 hit/miss 落账(flyai-hotel/session:ctrip-hotel 两通道接线);纪律与机/火同源:摸底(未定档期)不落账、needs-setup/error 传输失败永不落负事实、**不落数字价**(上游打码价保真,只记在架家数)。②`gotry_fact_gate` 酒店 claim 入闸——断言可住性(有房/可订/已核验/✓)的行按目的地+档期回溯酒店事实,无事实 fail-closed(`unverifiable_hotel_claim`),exact-date miss 在册却写有房 = `not_in_source`。③**渲染原语单向生成**:`renderFlightFact`/`renderHotelFact` 每条事实行内嵌 `` 锚点——产物经原语渲染即自带溯源锚,闸侧**锚点优先确定性回溯**(启发式让位),手改/伪造锚点 = `fact_anchor_unknown` 直接违例。fact-gate-tests §10 八断言(56 pass)。余量:政策事实生产端(实时签证 API)仍记 D-26 外部依赖。 Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched diagnostic fixtures/test-only aggregate admission plus a deterministic PR/nightly/weekly/milestone cadence policy/planner. It returns admission, `pass^k`, budgets, calibration, failure-registry, and cross-benchmark synthesis obligations only; it does not schedule or launch adapters, spend, generate a benchmark score, create an Agent optimization round, or support an uplift claim. No external runner, Python runtime dependency, baseline, or matched production evidence is included. @@ -431,7 +432,6 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched | D-22 pending_writes 空 receipt 无物理 CHECK(booking_saga_fsm.v1 已知边界) | 词汇层审计链已兜住(`sagaTraceViolations` 对空 receipt 报违例,run-all §36);**赎回时机 = M5 Entry 拍板**:pending_writes 随 schema 升版加 `receipt 非空 CHECK` + 具名 seam 词汇冻结(`booking-saga-fsm.md` §4),未到 M5 Entry 不动写路径 | | D-23 效应解译器迁移未完成(ADR-18) | 见下方「D-23 效应解译器迁移未完成」 | | D-26 事实闸覆盖面缺口(ADR-19) | v1 闸覆盖航班+政策 claim;酒店 claim 未入抽取面(flyai hotel 打码价语义独立,不落 bookable facts);政策事实只有渲染侧+闸侧,生产端无实时签证/入境源——政策 claim 只能降级「未确认」或省掉;反向抽取为正则启发式,不保证 100% claim 召回,根治方向=产物只由渲染原语单向生成;M5 WriteGate 接线预订类写工具时复审 | `ts/src/artifact-gate.ts`;run-all §39 | -| D-27 vendored 仓内形态 Node 兼容窗口断裂 | 见下方「D-27 vendored 仓内形态 Node 兼容窗口断裂」 | | D-28 外部 benchmark 驱动的 Agent 泛化证据缺口 | 见下方「D-28 外部 benchmark 驱动的 Agent 泛化证据缺口」 | | D-30 检索工具 typed 参数契约迁移完成待 canary(ADR-25 配套;2026-09-04 五刀 23/23 工具全 typed 化) | 普通模型性能的最大单项杠杆(tool-orchestration-design.md §4③):工具参数面曾是无类型 `query: json` blob,模型看不到逐字段 schema——dsh `defineTool` 原生支持 typed ParameterSchemaSpec(object/properties/enum/required + execute 前 validateArgs),blob 是本仓的选择不是上游限制。**已全部迁移**(刀法总纲:有 required 字段的工具宿主权拒 blob,全可选工具保留 interpretArgs 容忍层;motivation/companion/trip_log 的 evidence P0 红线进 schema 由宿主权闸;agent_reach 反射桥 action 保持开放)——迁移锁 smoke §1/§6/§12/channel-registry-tests §8。**余量**:普通模型 canary 一轮为证据(过闸才进发布面);fact_gate 的 itinerary 嵌套收紧归 #118 D-26 | `ts/src/index.ts` 参数面;dsh-tools schema;issue #102 同模式 | @@ -469,11 +469,11 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched **部分清偿**:词汇层+生产/mock 解译器+韧性横切已落地,五工具(flyai/hotel/session/weather/flight_verify)与 realtime-pricing 默认查询口已走 `interpretEffect`;`anything/web_search/video_subtitle/github_search/agent_reach/session_login` 等其余渠道工具仍直连能力层(同款永不抛错契约,无退避/熔断/mock 面)。按渠道逐个搬,搬一个删一横切;全部走通即抄销 | `ts/capabilities/effect.ts`;`docs/effect-interpreter.md` §4;run-all §37 -**D-27 vendored 仓内形态 Node 兼容窗口断裂** +**D-27 vendored 仓内形态 Node 兼容窗口断裂——✅ 已清偿(2026-09-04,issue #120)** -**发现于 2026-08-30(issue #77 E2E 前置),Round 5 部分清偿**: +**发现于 2026-08-30(issue #77 E2E 前置),Round 5 部分清偿,2026-09-04 全面清偿**: - `session-persistence-jsonl` 需 `node:zlib` 的 `createZstdDecompress`(Node ≥22.15,22.14 实测缺导出);Round 5 已把 package engines、CLI 启动前闸与源码安装文档统一到 22.15+,并让 source checkout 优先解析 root alpha.3 closure;source 普通运行仍保持 `ts/dsh-runtime/` cwd 与状态连续性。 -- legacy `ts/dsh-runtime` alpha.1 只保留为非 benchmark 解析兼容;其 `.ts` 直载仍受 Node strip-only 限制,parameter properties(`ts/capabilities/resilience.ts:109` 与 `session/action-cache.ts:61`)在 Node 26 会触发 `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`。benchmark 对该 legacy runtime 在 spawn 前稳定拒绝,npm/dist 形态不受影响,且 legacy fallback 不承诺可运行。**剩余赎回**:若继续保留 legacy vendored 工作流,独立 PR 升级整套 vendor 或改两处 parameter properties 并真跑 smoke;不得把 legacy fallback 当 alpha.3 parity 证据 | `ts/capabilities/resilience.ts`;`ts/capabilities/session/action-cache.ts` +- legacy `ts/dsh-runtime` alpha.1 曾保留为非 benchmark 解析兼容;其 `.ts` 直载受 Node strip-only 限制(parameter properties 触发 `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`)。**清偿动作(issue #120,决策=移除运行时路径)**:`selectDshRuntime` 删除 legacy vendored 回退——dsh 解析只认 root manifest/依赖闭包,找不到即 fail-closed 并给明确重装指引;inner 的 vendoredDshEarly/`legacy-vendored` source 分支/报错文案同步移除,`DshRuntime.source` 收敛为 `'root'`;§48 e2e 断言改为「非 benchmark 也不再回退 vendored」。Round 5 的「不承诺可运行」口径就此落地为「不再解析」;残余的 `ts/dsh-runtime/vendor/` 闭包目录不属本清偿范围(锁一致性面,另行处置)。 **D-28 外部 benchmark 驱动的 Agent 泛化证据缺口** diff --git a/docs/roadmap.md b/docs/roadmap.md index 8233b18f..ef442300 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,6 +31,8 @@ Evaluation Phase 0 foundation boundary: contracts/registry/validators/unmatched - **启动一次性 doctor 摘要(2026-09-04,issue #114,L2)**:web/headless 启动时分离子进程后台跑只读体检——待处理项一行 stderr、全 ok 静默、零写盘、不阻塞不重复刷、benchmark 面豁免;「初始化时可见」取代「会话中段撞错」。bootstrap-tests §10 钉行为。 - **解译器迁移收尾(2026-09-04,issue #115,D-23 收口)**:anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(策略行逐条拍板:timeout 类瞬时重试/配置态与风控类永不重试/反射桥透传零重试);工具面照旧平铺,证据链逐源标注不变;effect-tests §12 钉语义。 - **商店版扩展检测·自适应文案(2026-09-04,issue #117,D-24 清偿)**:needs-extension 文案按本地通道落位自适应——商店版用户不再看到开发者模式/本地通道指引,只推商店一键装与「已装即可」提示;桥失败摘要与 doctor 扩展项同步自适应;extension-tests §38 钉行为。 +- **事实闸覆盖面·酒店入闸 + 渲染原语(2026-09-04,issue #118,D-26 收口)**:HotelFact 第三形态(exact-date 酒店检索 hit/miss 落账,flyai/session 两通道接线;摸底不落账、传输失败不落负事实、打码价不落数字价);gotry_fact_gate 酒店 claim 入闸(目的地+档期回溯,无事实 fail-closed);渲染原语单向生成——renderFlightFact/renderHotelFact 行内嵌 fact 锚点,闸侧锚点优先确定性回溯,手改锚点即违例。fact-gate-tests §10(56 pass)。政策生产端(实时签证 API)仍记 D-26 外部依赖。 +- **legacy vendored dsh 回退移除(2026-09-04,issue #120,D-27 清偿)**:决策=移除运行时路径——dsh 解析只认 root manifest/依赖闭包,找不到即 fail-closed 报错指重装(旧回退 Node 窗口已断,解析成功只会变玄学失败);inner vendoredDshEarly/legacy-vendored 分支同步移除,DshRuntime.source 收敛 'root';§48 e2e 断言改为「非 benchmark 也不再回退」。vendor 闭包目录不属本清偿范围(锁一致性面另行处置)。 - **可下单事实闸(2026-08-30,issue #46,ADR-19)**:`gotry_bookable_fact.v1` 单一数据源 + 产物事实闸——flyai/session exact-date 检索结果 hit/miss 逐条落账(query_id 可重放),**exact-date miss 禁止用历史班期/相邻日期/航线页回填**;`gotry_fact_gate` 交付前必过(claim 反向抽取回溯 + 夜数/O&D/预算不变式),blocked 不得宣称「已验证方案」;persona (20) 红线化。run-all §39 locked golden 2027 E2E + smoke §16。覆盖面缺口记 D-26。 - **npm 形态自定义端点修复(2026-08-30,issue #48)**:bin env 映射补 `LLM_BASE_URL → DEEPSEEK_BASE_URL`——rc.15 回拉实测暴露只映射 key 不映射 base,OpenAI 兼容端点 key 被发往 DeepSeek 官方端点必然 401。修复后三件套 `.env`(`LLM_API_KEY`/`LLM_BASE_URL`/`LLM_MODEL`)即全链通;显式 `DEEPSEEK_BASE_URL` 仍优先,默认官方路径零改变。dsh 侧(llm-deepseek)与仓内 `dsh-llm.ts` 拼接语义核验一致(`${base}/chat/completions`)。README 双语 + `.env.example` + bin help 同步。 - **价表 v2 + 价格漂移长机制(2026-08-30,issue #49,ADR-20)**: diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index a50d7b81..14ce71fb 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,8 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,legacy vendored dsh 回退移除(issue #120,D-27 清偿):dsh 解析只认 root manifest/依赖闭包,找不到即 fail-closed 报错指重装;DshRuntime.source 收敛 'root'。 +- 2026-09-04 起,事实闸覆盖面(issue #118,D-26 收口):HotelFact 第三形态(exact-date 酒店检索落账,摸底/传输失败/打码价纪律同机火)+ gotry_fact_gate 酒店 claim 入闸 + 渲染原语单向生成(renderFlightFact/renderHotelFact 内嵌 fact 锚点,闸侧锚点优先确定性回溯);政策生产端(实时签证 API)仍记 D-26 外部依赖。 - 2026-09-04 起,needs-extension 文案自适应(issue #117,D-24 清偿):按本地通道落位自动跳过开发者模式/本地通道指引——商店版用户只推商店一键装与「已装即可」;桥失败摘要与 doctor 扩展项同步自适应。 - 2026-09-04 起,解译器迁移收尾(issue #115,D-23):anything/web/github/video/agent_reach/session_login 六渠道入效应注册表,23 工具外部依赖面全收敛 effect_interpreter.v1(没有策略表行就没有效应);工具面照旧平铺,证据链逐源标注不变。 - 2026-09-04 起,启动一次性 doctor 摘要(issue #114):web/headless 启动时分离子进程后台只读体检,待处理项一行 stderr(全 ok 静默/零写盘/不阻塞/benchmark 豁免)——初始化可见取代会话中段撞错。 diff --git a/scripts/build-dist.mjs b/scripts/build-dist.mjs index 8b378089..bf5406ac 100644 --- a/scripts/build-dist.mjs +++ b/scripts/build-dist.mjs @@ -6,7 +6,7 @@ * (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING)——npm 安装后 dsh 加载 * gotry-tools 插件必炸。发布前把 ts/{src,capabilities,scripts} 编成 * dist/ 纯 JS,bin/gotry-inner.js 在 npm 模式和无 tsx loader 的源码模式下 - * 指向 dist/src/index.js;legacy vendored/显式 tsx 才直载 .ts。 + * 指向 dist/src/index.js;显式 tsx loader 才直载 .ts(D-27 后无 legacy vendored 形态)。 * * 运行: node scripts/build-dist.mjs(发布脚本自动调) */ diff --git a/ts/scripts/benchmark-environment-bridge-e2e.ts b/ts/scripts/benchmark-environment-bridge-e2e.ts index 65e70230..242b3904 100644 --- a/ts/scripts/benchmark-environment-bridge-e2e.ts +++ b/ts/scripts/benchmark-environment-bridge-e2e.ts @@ -61,7 +61,7 @@ function assertRuntimeSelectionAndVersionGuards(): void { assert.deepEqual(sourcePriority, { source: 'root', version: '0.1.2-alpha.3' }, 'source checkout uses the root dsh package even when legacy vendor is alpha.1') const legacyFallback = runRuntimeProbe({ vendorVersion: '0.1.2-alpha.1' }) - assert.deepEqual(legacyFallback, { source: 'legacy-vendored', version: '0.1.2-alpha.1' }, 'non-benchmark source checkout may use the legacy vendored dsh fallback') + assert.equal(legacyFallback, null, 'D-27 removal (#120): legacy vendored fallback no longer resolves even outside benchmark — fail-closed') const wrongBenchmarkVersion = runRuntimeProbe({ rootVersion: '0.1.2-alpha.1', vendorVersion: '0.1.2-alpha.1', benchmark: true }) assert.deepEqual(wrongBenchmarkVersion, { source: 'root', version: '0.1.2-alpha.1' }) diff --git a/ts/scripts/fact-gate-tests.ts b/ts/scripts/fact-gate-tests.ts index 8d58a227..ad698bcf 100644 --- a/ts/scripts/fact-gate-tests.ts +++ b/ts/scripts/fact-gate-tests.ts @@ -27,12 +27,14 @@ import { dedupeFacts, detectBacktrack, factsFromFlyai, + factsFromHotel, flightClaimVerdict, itineraryInvariants, latestFactsForRouteDate, negativeFact, renderConnection, renderFlightFact, + renderHotelFact, renderNightsLine, renderPolicyFact, type FlightFact, @@ -319,5 +321,46 @@ assert(goodReport.verdict === 'pass' && goodReport.presentation === 'verified_it assert(goodFlights.every(f => f.bookability === 'bookable_exact_date' && f.query_id.includes('flyai:flight:')), 'good artifact 的每个可下单事实均可回溯到 tool result/query id(验收⑧)') +// --------------------------------------------------------------------------- +// §10 酒店事实闸(D-26,issue #118):酒店 claim 入闸 + 渲染原语单向生成 +// --------------------------------------------------------------------------- +{ + const fetchedAt = '2026-09-04T00:00:00.000Z' + // 10a. 摸底(未定档期)不落账;needs-setup/error 传输失败不落负事实 + assert(factsFromHotel({ source: 'flyai-hotel', destination: '大理', verdict: 'hit', options: 9, evidence: 'e', fetchedAt }).length === 0, + '摸底(无档期)酒店检索不落账(无 exact-date 语义)') + assert(factsFromHotel({ source: 'flyai-hotel', destination: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03', verdict: 'needs-setup', options: 0, evidence: 'e', fetchedAt }).length === 0 + && factsFromHotel({ source: 'flyai-hotel', destination: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03', verdict: 'error', options: 0, evidence: 'e', fetchedAt }).length === 0, + 'needs-setup/error 传输失败永不落负事实(ADR-19 不变量)') + // 10b. hit/miss 落账 + 打码纪律(不落数字价) + const hitFacts = factsFromHotel({ source: 'flyai-hotel', destination: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03', verdict: 'hit', options: 9, evidence: 'e', fetchedAt }) + const missFacts = factsFromHotel({ source: 'session:ctrip-hotel', destination: '大理', checkIn: '2026-10-01', checkOut: '2026-10-03', verdict: 'miss', options: 0, evidence: 'e', fetchedAt }) + assert(hitFacts.length === 1 && hitFacts[0]!.bookability === 'bookable_exact_date' && hitFacts[0]!.options_masked === 9, + 'exact-date hit → 正事实(在架家数,无数字价字段)') + assert(!('price' in (hitFacts[0] as unknown as Record)), '酒店事实无 price 字段(打码价保真,不落数字价)') + assert(missFacts.length === 1 && missFacts[0]!.bookability === 'unavailable_exact_date', 'exact-date miss → 负事实') + // 10c. 渲染原语单向生成:renderHotelFact 行带锚点,闸锚点回溯 pass + const hotelLine = renderHotelFact(hitFacts[0]!) + assert(hotelLine.includes(`fact:${hitFacts[0]!.fact_id}`), 'renderHotelFact 行内嵌 fact 锚点') + const anchoredArtifact = ['## 住宿', hotelLine].join('\n') + const anchoredReport = gateArtifact(anchoredArtifact, [...hitFacts], map, { trip_year: 2026 }) + assert(anchoredReport.verdict === 'pass' && anchoredReport.traceable === 1, + `锚点酒店行 → 锚点确定性回溯 pass(实际 ${anchoredReport.verdict}/traceable=${anchoredReport.traceable})`) + // 10d. 手改锚点 → fact_anchor_unknown(伪造即抓) + const forged = anchoredArtifact.replace(hitFacts[0]!.fact_id, '0123456789abcdef') + const forgedReport = gateArtifact(forged, [...hitFacts], map, { trip_year: 2026 }) + assert(forgedReport.violations.some(v => v.kind === 'fact_anchor_unknown'), '手改/伪造锚点 → fact_anchor_unknown') + // 10e. 无锚点启发式:hit 在册 → traceable;exact-date miss 在册却写有房 → not_in_source + 无条件✓ + const claimHit = gateArtifact('## 住宿\n- 大理 2026-10-01→2026-10-03 洱海民宿有房可订', [...hitFacts], map, { trip_year: 2026 }) + assert(claimHit.verdict === 'pass' && claimHit.traceable === 1, '启发式酒店 claim 命中在册 hit → traceable') + const claimMiss = gateArtifact('## 住宿\n- 大理 2026-10-01→2026-10-03 洱海民宿有房可订 ✓', [...missFacts], map, { trip_year: 2026 }) + assert(claimMiss.violations.some(v => v.kind === 'not_in_source'), 'exact-date miss 在册却写有房 → not_in_source') + assert(claimMiss.violations.some(v => v.kind === 'unconditional_check'), '对未核验酒店用 ✓ → unconditional_check') + // 10f. 无目的地上下文的可住断言 → fail-closed + const noCtx = gateArtifact('## 住宿\n- 住宿:湖景房有房可订 ✓', [], map, { trip_year: 2026 }) + assert(noCtx.violations.some(v => v.kind === 'unverifiable_hotel_claim'), '缺目的地上下文 → unverifiable_hotel_claim(fail closed)') + console.log(` ok - §10 酒店事实闸(D-26)八断言完成`) +} + console.log(`\nFACT GATE TESTS: ${pass} pass, ${fail} fail`) if (fail > 0) process.exit(1) diff --git a/ts/src/artifact-gate.ts b/ts/src/artifact-gate.ts index d9afb6f3..3e725c17 100644 --- a/ts/src/artifact-gate.ts +++ b/ts/src/artifact-gate.ts @@ -15,6 +15,7 @@ import { flightClaimVerdict, + hotelClaimVerdict, itineraryInvariants, latestFactsForRouteDate, type BookableFact, @@ -62,12 +63,24 @@ export interface ExtractedAirportClaim { city: string } +/** 酒店 claim(D-26):断言「有房/可订/已核验」的行——目的地与档期缺失时 fail-closed */ +export interface ExtractedHotelClaim { + line: number + text: string + destination?: string + check_in?: string + check_out?: string +} + export interface ExtractedClaims { flights: ExtractedFlightClaim[] policies: ExtractedPolicyClaim[] airports: ExtractedAirportClaim[] /** 含「直飞」断言且可解析航线的行(通用直飞规则:该日无任何在架直飞即违例) */ direct_lines: Array<{ line: number; text: string; origin?: string; destination?: string; date?: string }> + hotels: ExtractedHotelClaim[] + /** 渲染原语锚点(fact:):命中行只走确定性回溯,启发式让位 */ + anchors: Map } /** 航班号:2 位承运码 + 3-4 位数字(UO784/EK328/MF1538/CZ8582/9C8781/FD597) */ @@ -94,6 +107,14 @@ function hasDirectAssertion(line: string): boolean { interface SectionCtx { origin?: string; destination?: string; date?: string } +/** 行内第一个命中词表的城市中文名(酒店 claim 的目的地键;无命中=undefined) */ +function occ1(line: string, map: AirlineAirportMap): string | undefined { + for (const city of Object.keys(map.city_alias)) { + if (line.includes(city)) return city + } + return undefined +} + /** 从文本(小节标题或行)解析航线上下文:箭头紧邻城市对(方向最可靠)优先,否则按出现位置取前两个;日期 M.D 或 MM-DD */ function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number): SectionCtx { const occ: Array<{ city: string; iata: string; pos: number }> = [] @@ -140,12 +161,19 @@ function routeCtxOf(text: string, map: AirlineAirportMap, defaultYear?: number): * 行内自带上下文优先。默认年份由 trip window 提供(远期行程的年份不含糊)。 */ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: { trip_year?: number }): ExtractedClaims { - const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [] } + const claims: ExtractedClaims = { flights: [], policies: [], airports: [], direct_lines: [], hotels: [], anchors: new Map() } const lines = markdown.split('\n') let section: SectionCtx = {} + // 渲染原语锚点(单向生成):带 fact: 的行只走锚点确定性回溯,启发式抽取让位 + const ANCHOR = /fact:([0-9a-f]{16})/ for (let i = 0; i < lines.length; i++) { const line = lines[i]! const lineNo = i + 1 + const anchorMatch = line.match(ANCHOR) + if (anchorMatch) { + claims.anchors.set(lineNo, anchorMatch[1]!.toLowerCase()) + continue + } if (/^#{1,4}\s/.test(line)) { const ctx = routeCtxOf(line, map, opts?.trip_year) section = { origin: ctx.origin, destination: ctx.destination, date: ctx.date ?? section.date } @@ -179,6 +207,19 @@ export function extractClaims(markdown: string, map: AirlineAirportMap, opts?: { if (POLICY_WORD.test(line)) { claims.policies.push({ line: lineNo, text: line.trim().slice(0, 160), has_as_of: AS_OF_WORD.test(line) }) } + // 酒店 claim(D-26):断言可住性(有房/可订/已核验/✓)的行——目的地与档期缺失时 fail-closed + const HOTEL_ASSERT = /酒店|住宿|客栈|民宿/ + const HOTEL_BOOKABLE = /有房|可订|在架|已核验|已验证|可住|✓|✅/ + if (HOTEL_ASSERT.test(line) && HOTEL_BOOKABLE.test(line)) { + const stays = [...line.matchAll(/(? m[1]!) + claims.hotels.push({ + line: lineNo, + text: line.trim().slice(0, 160), + destination: occ1(line, map), + check_in: stays[0], + check_out: stays[1], + }) + } // 航司→机场映射 claim:同一行出现承运码 + 机场码,且该城是双机场映射面 for (const [city, carriers] of Object.entries(map.carrier_airport)) { const airports = [...new Set(Object.values(carriers))] @@ -207,6 +248,8 @@ export type GateViolationKind = | 'legs_inconsistent' // flight O&D 段与 legs 口径不一致 | 'budget_floor_inconsistent' // 预算下限话术低于分项最低合计 | 'date_order' // 住宿/段日期越窗或倒挂 + | 'unverifiable_hotel_claim' // 酒店可住断言无 exact-date 事实回溯(D-26) + | 'fact_anchor_unknown' // 渲染锚点 fact: 在注册表不存在(锚点被手改/伪造) export interface GateViolation { kind: GateViolationKind @@ -242,6 +285,43 @@ export function gateArtifact( const violations: GateViolation[] = [] let traceable = 0 + // 渲染锚点优先(issue #118 单向生成):带 fact: 的行确定性回溯注册表—— + // 锚点在=按事实 bookability 判;锚点不存在=手改/伪造,直接违例。启发式对锚点行让位。 + for (const [lineNo, factId] of claims.anchors) { + const f = facts.find(x => x.fact_id === factId) + if (!f) { + violations.push({ kind: 'fact_anchor_unknown', line: lineNo, detail: `渲染锚点 fact:${factId} 不在事实注册表——锚点被手改或伪造,产物不可信` }) + continue + } + if (f.kind !== 'policy' && f.bookability === 'unavailable_exact_date') { + violations.push({ kind: 'not_in_source', line: lineNo, detail: `锚点事实为 exact-date 负事实(${(f as { fetched_at?: string }).fetched_at ?? ''})——负事实对应的可住/可订断言不得出现` }) + continue + } + traceable++ + } + + // 酒店 claim(D-26):目的地+档期回溯酒店事实;无事实 fail-closed + for (const c of claims.hotels) { + if (claims.anchors.has(c.line)) continue + if (!c.destination) { + violations.push({ kind: 'unverifiable_hotel_claim', line: c.line, detail: `酒店可住断言缺目的地上下文,无法回溯 exact-date 事实——fail closed 按未核验处理` }) + continue + } + const r = hotelClaimVerdict(facts, { destination: c.destination, check_in: c.check_in, check_out: c.check_out }) + if (r.verdict === 'traceable') { + traceable++ + continue + } + if (r.verdict === 'unverified') { + violations.push({ kind: 'unverifiable_hotel_claim', line: c.line, detail: r.reason }) + } else { + violations.push({ kind: 'not_in_source', line: c.line, detail: r.reason }) + } + if (CHECK_MARK.test(c.text)) { + violations.push({ kind: 'unconditional_check', line: c.line, detail: '对未核验酒店使用无条件 ✓/✅——只有 bookable_exact_date 才允许确定性标记' }) + } + } + for (const c of claims.flights) { if (c.carrier_only) { // 承运级直飞断言:注册表该 route+date 须存在该承运的 bookable 直飞 @@ -334,7 +414,7 @@ export function gateArtifact( if (!line.includes('联程')) return const nos = [...line.matchAll(FLIGHT_NO)].map(m => m[1]!.toUpperCase()) const matched = facts.filter((f): f is Extract => - f.kind !== 'policy' && nos.includes(f.flight_no.toUpperCase())) + (f.kind === 'flight' || f.kind === 'train') && nos.includes(f.flight_no.toUpperCase())) const allProtected = nos.length > 0 && matched.length === nos.length && matched.every(f => f.protected_connection === true) diff --git a/ts/src/bookable-facts.ts b/ts/src/bookable-facts.ts index f2f483c1..06d6d4a6 100644 --- a/ts/src/bookable-facts.ts +++ b/ts/src/bookable-facts.ts @@ -89,7 +89,34 @@ export interface PolicyFact { review_by?: string } -export type BookableFact = FlightFact | PolicyFact +export type BookableFact = FlightFact | PolicyFact | HotelFact + +/** + * 酒店事实(D-26,issue #118):exact-date 酒店检索(hit/miss)的唯一事实形态。 + * 纪律与机/火同源:①未定档期的摸底检索不落账(无 exact-date 语义); + * ②needs-setup/error 是传输失败,永不落负事实;③价格上游打码(¥7xx)—— + * 本事实**不落数字价**,只记在架家数,真实价以落地页为准(打码价保真纪律)。 + */ +export interface HotelFact { + schema: typeof BOOKABLE_FACT_SCHEMA + /** 语义派生 id:sha(destination|checkIn|checkOut|query_id)——同查询重放幂等 */ + fact_id: string + kind: 'hotel' + /** 目的地中文(flyai/session 的 to;闸侧 claim 匹配键) */ + destination: string + check_in?: string // YYYY-MM-DD + check_out?: string // YYYY-MM-DD + verdict: 'hit' | 'miss' + bookability: Bookability + /** 在架条数(hit 时;价格打码不落数字价) */ + options_masked: number + source: string // flyai-hotel / session:ctrip-hotel + /** 可重放查询 id(如 flyai:hotel:大理:2026-10-01_2026-10-03) */ + query_id: string + fetched_at: string + as_of: string + tier: EvidenceTier +} // --------------------------------------------------------------------------- // id 与查询词(可重放:query_id 即重放凭证) @@ -279,7 +306,7 @@ export function dedupeFacts(facts: T[]): T[] { /** route+date 的最新查询结论(同日多次查询:最新一次为准,避免「昨天 miss 今天 hit」陈旧否定) */ export function latestFactsForRouteDate(facts: BookableFact[], origin: string, destination: string, date: string): FlightFact[] { const hits = facts.filter((f): f is FlightFact => - f.kind !== 'policy' + (f.kind === 'flight' || f.kind === 'train') && f.route.origin === origin && f.route.destination === destination && f.date === date) const latestQueryTs = new Map() for (const f of hits) { @@ -311,7 +338,7 @@ export interface FlightClaim { export function flightClaimVerdict(facts: BookableFact[], claim: FlightClaim): { verdict: FlightClaimVerdict; fact?: FlightFact; reason: string } { const no = claim.flight_no.toUpperCase().replace(/\s+/g, '') const sameNo = facts.filter((f): f is FlightFact => - f.kind !== 'policy' && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date') + (f.kind === 'flight' || f.kind === 'train') && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date') const routeMatched = sameNo.filter(f => (!claim.origin || f.route.origin === claim.origin || f.route.origin_airport === claim.origin) && (!claim.destination || f.route.destination === claim.destination || f.route.dest_airport === claim.destination) @@ -503,6 +530,12 @@ export function defaultReviewBy(tripDate: string): string { * 永不输出无条件 ✓;证据链(source/query_id/fetched_at)随行必达。 */ export function renderFlightFact(f: FlightFact): string { + return renderFlightFactInner(f) + ` ` +} + +function renderFlightFactInner(f: FlightFact): string { + // 单向生成锚点(D-26):每条事实行内嵌 fact_id,闸侧锚点匹配 = 确定性回溯 + const anchor = ` ` const route = `${f.route.origin}→${f.route.destination}` const evidence = `[${f.source}@${f.fetched_at} #${f.query_id}]` if (f.bookability === 'unavailable_exact_date') { @@ -556,3 +589,73 @@ export function renderNightsLine(it: ItineraryFacts): string { const flightLegs = it.od_segments.filter(s => s.mode === 'flight').reduce((acc, s) => acc + s.legs, 0) return `共 ${daysBetween(it.trip_start, it.trip_end) + 1} 天 ${totalNights} 晚 = ${hotelNights} 个酒店夜 + ${it.onboard_nights} 个机上夜;航班按 O&D ${flightOd} 段(共 ${flightLegs} 个 flight legs)` } + +// --------------------------------------------------------------------------- +// 酒店事实(D-26,issue #118):exact-date 酒店检索 hit/miss 落账 +// 纪律:摸底(未定档期)不落账(无 exact-date 语义);needs-setup/error 传输失败永不落负事实 +// --------------------------------------------------------------------------- + +export function factsFromHotel(input: { + source: string + destination: string + checkIn?: string + checkOut?: string + verdict: 'hit' | 'miss' | 'needs-setup' | 'error' + options: number + evidence: string + fetchedAt: string +}): HotelFact[] { + if (!input.checkIn || !input.checkOut) return [] + if (input.verdict !== 'hit' && input.verdict !== 'miss') return [] + const queryId = `hotel:${input.source}:${input.destination}:${input.checkIn}_${input.checkOut}` + return [{ + schema: BOOKABLE_FACT_SCHEMA, + kind: 'hotel', + fact_id: makeFactId([input.destination, input.checkIn, input.checkOut, queryId]), + destination: input.destination, + check_in: input.checkIn, + check_out: input.checkOut, + verdict: input.verdict, + bookability: input.verdict === 'hit' ? 'bookable_exact_date' : 'unavailable_exact_date', + options_masked: input.options, + source: input.source, + query_id: queryId, + fetched_at: input.fetchedAt, + as_of: input.fetchedAt.slice(0, 10), + tier: 'live_inventory', + }] +} + +/** 酒店 claim 回溯:同目的地+同档期的 exact-date 事实裁决(无事实=unverified,fail-closed) */ +export function hotelClaimVerdict( + facts: BookableFact[], + claim: { destination: string; check_in?: string; check_out?: string }, +): { verdict: 'traceable' | 'not_in_source' | 'unverified'; fact?: HotelFact; reason: string } { + const scoped = facts.filter((f): f is HotelFact => + f.kind === 'hotel' && f.destination === claim.destination + && (!claim.check_in || f.check_in === claim.check_in) + && (!claim.check_out || f.check_out === claim.check_out)) + if (scoped.length === 0) { + return { + verdict: 'unverified', + reason: `酒店 claim(${claim.destination}${claim.check_in ? ` ${claim.check_in}→${claim.check_out ?? '?'}` : ''})未经 exact-date 源核验——先检索再写结论`, + } + } + const hit = scoped.find(f => f.bookability === 'bookable_exact_date') + if (hit) return { verdict: 'traceable', fact: hit, reason: 'exact-date 命中' } + const latest = scoped[scoped.length - 1]! + return { verdict: 'not_in_source', fact: latest, reason: `该档期 exact-date 源正常返回 0 条(${latest.fetched_at})——不得写有房/可订` } +} + +// --------------------------------------------------------------------------- +// 渲染原语(单向生成,issue #118):事实 → 行,锚点内嵌 fact_id; +// 产物经原语渲染即自带溯源锚,闸侧锚点匹配 = 确定性回溯,不靠行文猜测 +// --------------------------------------------------------------------------- + + +export function renderHotelFact(f: HotelFact): string { + if (f.verdict !== 'hit') { + return `- ${f.destination} ${f.check_in}→${f.check_out}:exact-date 源 0 条在架(不可订) ` + } + return `- ${f.destination} ${f.check_in}→${f.check_out}:住宿已核验(在架 ${f.options_masked} 家;价格上游打码,真实价以落地页为准) ` +} diff --git a/ts/src/index.ts b/ts/src/index.ts index 13958c39..ec582032 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -41,7 +41,7 @@ import { installModelOverride } from '../capabilities/model-override.ts' import { listArtifacts, readArtifact } from '../capabilities/artifacts.ts' import { interpretEffect, declinedObservation } from '../capabilities/effect.ts' import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.ts' -import { factsFromFlyai, factsFromSession } from './bookable-facts.ts' +import { factsFromFlyai, factsFromHotel, factsFromSession } from './bookable-facts.ts' import { gateArtifact, type AirlineAirportMap } from './artifact-gate.ts' import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.ts' import { noteChannelVerdict, recordChannelEvent } from '../capabilities/channel-health.ts' @@ -890,6 +890,11 @@ export function apply(ctx: Context, config: Config): void { if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace) const r = itp.result await noteChannel('flyai', r.verdict) + // D-26(issue #118):exact-date 酒店检索 hit/miss 落账(摸底无档期不落,传输失败不落负事实) + await appendFacts(config.stateRoot ?? '.', factsFromHotel({ + source: 'flyai-hotel', destination: dest, checkIn: q.checkIn, checkOut: q.checkOut, + verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(), + })) const top = (r.hotels ?? []).slice(0, 8).map(o => `${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`) const summary = r.verdict === 'hit' ? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}` @@ -1079,6 +1084,13 @@ export function apply(ctx: Context, config: Config): void { if (!itp.result) return declinedObservation('SESSION_HOTEL_SEARCH', itp.trace) const r = itp.result await noteChannel('session:ctrip-hotel', r.verdict) + // D-26(issue #118):exact-date 会话酒店 hit/miss 落账(摸底无档期不落;cooldown 是节律闸非结论,不落) + if (r.verdict === 'hit' || r.verdict === 'miss') { + await appendFacts(config.stateRoot ?? '.', factsFromHotel({ + source: 'session:ctrip-hotel', destination: q.to ?? '', checkIn: q.checkIn, checkOut: q.checkOut, + verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(), + })) + } const top = (r.hotels ?? []).slice(0, 8).map(h => `${h.name}${h.star ? `(${h.star}星)` : ''} ${h.priceRaw ?? (h.price > 0 ? `¥${h.price}` : '价待询')}${h.score ? ` 评分${h.score}` : ''}`) const summary = r.verdict === 'hit' ? `${q.to} 酒店(携程,用户本人登录态,真实价)前 ${top.length} 家(预订以 jumpUrl 落地页为准):\n${top.join('\n')}\n${r.evidence}` From 7b11fa980537e47aa4eb05af6b4f3914e3963b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=8C=AF?= Date: Fri, 4 Sep 2026 23:04:21 +0400 Subject: [PATCH 11/12] =?UTF-8?q?feat(facts):=20=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?=E9=97=B8=E8=A6=86=E7=9B=96=E9=9D=A2=E2=80=94=E2=80=94=E9=85=92?= =?UTF-8?q?=E5=BA=97=20claim=20=E5=85=A5=E9=97=B8=20+=20=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=8E=9F=E8=AF=AD=E5=8D=95=E5=90=91=E7=94=9F=E6=88=90(issue=20?= =?UTF-8?q?#118,D-26=20=E6=94=B6=E5=8F=A3)=E2=80=94=E2=80=94HotelFact=20?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E5=BD=A2=E6=80=81:exact-date=20=E9=85=92?= =?UTF-8?q?=E5=BA=97=E6=A3=80=E7=B4=A2=20hit/miss=20=E8=90=BD=E8=B4=A6(fly?= =?UTF-8?q?ai-hotel/session:ctrip-hotel=20=E6=8E=A5=E7=BA=BF),=E6=91=B8?= =?UTF-8?q?=E5=BA=95=E4=B8=8D=E8=90=BD=E8=B4=A6/=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E4=B8=8D=E8=90=BD=E8=B4=9F=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?/=E6=89=93=E7=A0=81=E4=BB=B7=E4=B8=8D=E8=90=BD=E6=95=B0?= =?UTF-8?q?=E5=AD=97=E4=BB=B7;gotry=5Ffact=5Fgate=20=E9=85=92=E5=BA=97=20c?= =?UTF-8?q?laim=20=E5=85=A5=E9=97=B8(=E7=9B=AE=E7=9A=84=E5=9C=B0+=E6=A1=A3?= =?UTF-8?q?=E6=9C=9F=E5=9B=9E=E6=BA=AF,=E6=97=A0=E4=BA=8B=E5=AE=9E=20fail-?= =?UTF-8?q?closed=20unverifiable=5Fhotel=5Fclaim,miss=20=E5=9C=A8=E5=86=8C?= =?UTF-8?q?=E5=86=99=E6=9C=89=E6=88=BF=3Dnot=5Fin=5Fsource);=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=8E=9F=E8=AF=AD=E5=8D=95=E5=90=91=E7=94=9F=E6=88=90?= =?UTF-8?q?:renderFlightFact/renderHotelFact=20=E8=A1=8C=E5=86=85=E5=B5=8C?= =?UTF-8?q?=20fact:=20=E9=94=9A=E7=82=B9,=E9=97=B8=E4=BE=A7?= =?UTF-8?q?=E9=94=9A=E7=82=B9=E4=BC=98=E5=85=88=E7=A1=AE=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E5=9B=9E=E6=BA=AF(=E5=90=AF=E5=8F=91=E5=BC=8F=E8=AE=A9?= =?UTF-8?q?=E4=BD=8D),=E6=89=8B=E6=94=B9/=E4=BC=AA=E9=80=A0=E9=94=9A?= =?UTF-8?q?=E7=82=B9=3Dfact=5Fanchor=5Funknown;policy=20=E7=AA=84=E5=8C=96?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E6=98=BE=E5=BC=8F=E5=8C=96(FlightFact);fact-?= =?UTF-8?q?gate-tests=20=C2=A710=20=E5=85=AB=E6=96=AD=E8=A8=80=2056=20pass?= =?UTF-8?q?,tsc+smoke=20=E7=BB=BF,run-all=20ALL=20SUITES=20GREEN;=E5=85=AD?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=9D=A2=E5=90=8C=E6=8F=90=E4=BA=A4=E5=90=8C?= =?UTF-8?q?=E6=AD=A5(arch=20=C2=A79=E3=80=81roadmap=20=E5=A2=9E=E9=87=8F?= =?UTF-8?q?=E3=80=81stage1=20=E7=8A=B6=E6=80=81=E5=A4=B4);=E6=94=BF?= =?UTF-8?q?=E7=AD=96=E7=94=9F=E4=BA=A7=E7=AB=AF(=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E7=AD=BE=E8=AF=81=20API)=E4=BB=8D=E8=AE=B0=20D-26=20=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E4=BE=9D=E8=B5=96=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danceiny --- docs/architecture.md | 1 + docs/stage1-top-down-design.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index dc74fdc9..3429956a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -570,6 +570,7 @@ Round 7 将 benchmark opt-in 收敛为 minimal kernel,并把 system-prompt/roo |---|---| | `roadmap.md` | **时间线唯一来源**:M0-M6 里程碑三线视图与旧模型归并 | | `data-sources.md` | **数据源唯一权威面**:领域矩阵/四层架构/Google Place 链路/证据链契约/TREK 参考 | +| `adapter-authoring-guide.md` | **Session 适配器作者手册**(D-13):四步法(探测→第一方金标准→双源 shape gate→漂移锁)/接入清单/纪律红线/携程真会话校准清单 | | `tokens.md` | **token 唯一权威面**:npm 三路径(web会话/granular bypass/OIDC)+ agent-reach 8 渠道获取表 + 统一 .env 存放 | | `tech-strategy.md` | 技术选型与半年迭代路线(M2–M4):选型矩阵/评测体系/分工/持续优化回路/决策登记 | | 本文 | 技术:系统/模块/模型/循环/数据概要/ADR/演进/债务 | diff --git a/docs/stage1-top-down-design.md b/docs/stage1-top-down-design.md index 14ce71fb..bedabf39 100644 --- a/docs/stage1-top-down-design.md +++ b/docs/stage1-top-down-design.md @@ -1,6 +1,7 @@ # Stage 1 顶层设计:自顶向下(契约 → 循环 → 智能接真) > **状态速览**: +- 2026-09-04 起,事实闸覆盖面(issue #118,D-26 收口):HotelFact 第三形态(exact-date 酒店检索落账,摸底/传输失败/打码价纪律同机火)+ gotry_fact_gate 酒店 claim 入闸 + 渲染原语单向生成(renderFlightFact/renderHotelFact 内嵌 fact 锚点,闸侧锚点优先确定性回溯);政策生产端(实时签证 API)仍记 D-26 外部依赖。 - 2026-09-04 起,legacy vendored dsh 回退移除(issue #120,D-27 清偿):dsh 解析只认 root manifest/依赖闭包,找不到即 fail-closed 报错指重装;DshRuntime.source 收敛 'root'。 - 2026-09-04 起,事实闸覆盖面(issue #118,D-26 收口):HotelFact 第三形态(exact-date 酒店检索落账,摸底/传输失败/打码价纪律同机火)+ gotry_fact_gate 酒店 claim 入闸 + 渲染原语单向生成(renderFlightFact/renderHotelFact 内嵌 fact 锚点,闸侧锚点优先确定性回溯);政策生产端(实时签证 API)仍记 D-26 外部依赖。 - 2026-09-04 起,needs-extension 文案自适应(issue #117,D-24 清偿):按本地通道落位自动跳过开发者模式/本地通道指引——商店版用户只推商店一键装与「已装即可」;桥失败摘要与 doctor 扩展项同步自适应。 From fcc1957449d7a41cddec48a1cf01eab82125c380 Mon Sep 17 00:00:00 2001 From: Danceiny Date: Sat, 5 Sep 2026 01:16:04 +0400 Subject: [PATCH 12/12] =?UTF-8?q?chore(release):=20rc.19=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20bump=E2=80=94=E2=80=94extension/manifest.json=20?= =?UTF-8?q?=E8=B7=9F=E9=9A=8F=E4=B8=BB=E7=89=88=E6=9C=AC(0.0.1-rc.18?= =?UTF-8?q?=E2=86=920.0.1-rc.19,=E6=89=A9=E5=B1=95=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E8=B7=9F=E9=9A=8F=E4=B8=BB=E7=89=88=E6=9C=AC=E7=BA=A6=E5=AE=9A?= =?UTF-8?q?);root=20package.json=20=E5=B7=B2=E5=9C=A8=E5=90=8C=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=89=8D=E6=8F=90=E4=BA=A4=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/manifest.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/manifest.json b/extension/manifest.json index b731f314..a15e0a33 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "GoTry Session Bridge", - "version": "0.0.1.18", + "version": "0.0.1.19", "description": "GoTry 会话检索桥(issue #21):在你自己的登录态里只读嗅探检索回包(机票/酒店/火车)与登录票据 cookie 名;零凭证经手,登录在官网由你完成,检索只读不写。", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnuzBX8rT8+RZKEUip5VRA0cY056pExy9PNdN9YCiuY3dgglRbcIBhjuFJoR6EX3/MaUv82vJVyItRVrowXLTwDk4WZof0vwFgKL0zBK304PC46pUaAKTSu1PgXeL1+j5KTAGz/9a9+RnPdbj7jND5rM/MOmBHNPVPpp1NlwxRGIgF9OxHSOfXORcH4Iyte1aJSFUJbFtNS/DaE5D+KjyupPLyIqVN/dCiKnNsTCEHQB9ngeli665PFp76hkccHQQZ4sVuMLS4zBPcazUsp1kcnW+xsQoUATkODTCY+Pzk3zUVF1QOESTjRCVnbNi8tkO43RMRs2q63gX/3inFivcEwIDAQAB", "permissions": [ @@ -46,5 +46,5 @@ "run_at": "document_start" } ], - "version_name": "0.0.1-rc.18" -} \ No newline at end of file + "version_name": "0.0.1-rc.19" +} diff --git a/package.json b/package.json index 6ee552ee..c9ce3679 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@danceiny/gotry", - "version": "0.0.1-rc.18", + "version": "0.0.1-rc.19", "description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm/源码入口共用锁定的 dsh runtime + README 安装路径。", "type": "module", "main": "ts/src/index.ts",