@danceiny/gotry 0.0.1-rc.19 → 0.0.1-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +14 -3
  2. package/README.zh-CN.md +15 -4
  3. package/bin/gotry-bootstrap.js +136 -45
  4. package/bin/gotry-inner.js +11 -6
  5. package/cordis.gotry-patch.yml +19 -8
  6. package/dist/capabilities/anything.js +62 -29
  7. package/dist/capabilities/doctor.js +113 -19
  8. package/dist/capabilities/hbcli.js +28 -2
  9. package/dist/capabilities/session/health-watch.js +10 -3
  10. package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -1
  11. package/dist/scripts/anything-tests.js +70 -21
  12. package/dist/scripts/benchmark-environment-bridge-e2e.js +178 -33
  13. package/dist/scripts/benchmark-environment-bridge-tests.js +1480 -271
  14. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +211 -2
  15. package/dist/scripts/booking-copilot-runtime-proof-tests.js +132 -2
  16. package/dist/scripts/bootstrap-tests.js +30 -3
  17. package/dist/scripts/doctor-tests.js +55 -1
  18. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  19. package/dist/scripts/hbcli-tests.js +38 -1
  20. package/dist/scripts/health-watch-cli.js +9 -1
  21. package/dist/scripts/map-tools-vendor-package-proof.js +276 -0
  22. package/dist/scripts/persona-surface-guard-tests.js +8 -3
  23. package/dist/src/benchmark-agent-conformance.js +187 -17
  24. package/dist/src/benchmark-environment-bridge.js +287 -102
  25. package/dist/src/booking-surface/dsh-planner.js +24 -8
  26. package/dist/src/index.js +5 -2
  27. package/extension/manifest.json +2 -2
  28. package/package.json +3 -1
  29. package/ts/capabilities/anything.ts +89 -38
  30. package/ts/capabilities/hbcli.ts +53 -4
  31. package/ts/capabilities/session/health-watch.ts +9 -2
  32. package/ts/dsh-runtime/vendor/README.md +60 -0
  33. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  34. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  35. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  36. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  37. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  38. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  39. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  40. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  41. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  42. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  43. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  44. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  45. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  46. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  47. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  48. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  49. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  50. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  51. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  52. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  53. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  54. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  55. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  56. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  57. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  58. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  59. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  60. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  61. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  62. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  63. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  64. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  65. package/ts/package.json +17 -1
  66. package/ts/scripts/map-tools-vendor-package-proof.ts +268 -0
  67. package/ts/src/benchmark-agent-conformance.ts +175 -13
  68. package/ts/src/benchmark-environment-bridge.ts +220 -66
  69. package/ts/src/booking-surface/dsh-planner.ts +25 -13
  70. package/ts/src/index.ts +4 -2
  71. package/ts/src/turn-deadline.ts +4 -0
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { homedir } from 'node:os';
4
5
  import { join, resolve } from 'node:path';
5
6
  import { readLatestChannelEvents } from './channel-health.js';
@@ -42,10 +43,68 @@ function probe(bin, args, timeoutMs = 8_000) {
42
43
  });
43
44
  });
44
45
  }
46
+ function probeStdout(bin, args, timeoutMs = 8_000) {
47
+ return new Promise((resolveProbe)=>{
48
+ const child = spawn(bin, args, {
49
+ stdio: [
50
+ 'ignore',
51
+ 'pipe',
52
+ 'ignore'
53
+ ],
54
+ env: process.env
55
+ });
56
+ let out = '';
57
+ let done = false;
58
+ const timer = setTimeout(()=>{
59
+ if (!done) {
60
+ done = true;
61
+ try {
62
+ child.kill('SIGKILL');
63
+ } catch {}
64
+ resolveProbe(null);
65
+ }
66
+ }, timeoutMs);
67
+ child.on('error', ()=>{
68
+ if (!done) {
69
+ done = true;
70
+ clearTimeout(timer);
71
+ resolveProbe(null);
72
+ }
73
+ });
74
+ child.stdout?.on('data', (d)=>{
75
+ out += d.toString();
76
+ });
77
+ child.on('exit', (code)=>{
78
+ if (!done) {
79
+ done = true;
80
+ clearTimeout(timer);
81
+ resolveProbe(code === 0 ? out.trim() : null);
82
+ }
83
+ });
84
+ });
85
+ }
86
+ function parseVersion(v) {
87
+ const m = String(v || '').match(/(\d+)\.(\d+)\.(\d+)/);
88
+ return m ? [
89
+ Number(m[1]),
90
+ Number(m[2]),
91
+ Number(m[3])
92
+ ] : null;
93
+ }
94
+ function versionAtLeast(v, min) {
95
+ const a = v;
96
+ const b = parseVersion(min);
97
+ if (!a || !b) return false;
98
+ for(let i = 0; i < 3; i++){
99
+ if (a[i] !== b[i]) return a[i] > b[i];
100
+ }
101
+ return true;
102
+ }
45
103
  export function nodeOk(version) {
46
104
  const [maj = '0', min = '0'] = version.split('.');
47
105
  return Number(maj) > 22 || Number(maj) === 22 && Number(min) >= 15;
48
106
  }
107
+ const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? '0.0.3';
49
108
  export async function runDoctorChecks(opts = {}) {
50
109
  const repoRoot = opts.repoRoot ? resolve(opts.repoRoot) : resolve(import.meta.dirname, '..', '..');
51
110
  const home = opts.homeDir ?? homedir();
@@ -117,22 +176,36 @@ export async function runDoctorChecks(opts = {}) {
117
176
  }
118
177
  }
119
178
  if (hbPresent) {
120
- const whoami = await probe(hbPresent === 'hbcli(PATH)' ? 'hbcli' : hbPresent, [
121
- 'auth',
122
- 'whoami'
123
- ]);
124
- items.push(whoami ? {
125
- id: 'hbcli',
126
- label: 'hbcli(酒店实时源)',
127
- status: 'ok',
128
- detail: `已安装且凭证有效(${hbPresent})`
129
- } : {
130
- id: 'hbcli',
131
- label: 'hbcli(酒店实时源)',
132
- status: 'degraded',
133
- detail: '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
134
- fix: 'hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)'
135
- });
179
+ const hbCmd = hbPresent === 'hbcli(PATH)' ? 'hbcli' : hbPresent;
180
+ const v = parseVersion(await probeStdout(hbCmd, [
181
+ '--version'
182
+ ]));
183
+ if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
184
+ items.push({
185
+ id: 'hbcli',
186
+ label: 'hbcli(酒店实时源)',
187
+ status: 'missing',
188
+ detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——trade.* / search/checkAvail 会 401(issue #142)`,
189
+ fix: 'npm install -g staicli --registry=https://registry.npmjs.org/'
190
+ });
191
+ } else {
192
+ const whoami = await probe(hbCmd, [
193
+ 'auth',
194
+ 'whoami'
195
+ ]);
196
+ items.push(whoami ? {
197
+ id: 'hbcli',
198
+ label: 'hbcli(酒店实时源)',
199
+ status: 'ok',
200
+ detail: `已安装且凭证有效(${hbPresent}, v${v ? v.join('.') : '?'})`
201
+ } : {
202
+ id: 'hbcli',
203
+ label: 'hbcli(酒店实时源)',
204
+ status: 'degraded',
205
+ detail: '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
206
+ fix: 'hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)'
207
+ });
208
+ }
136
209
  } else {
137
210
  items.push({
138
211
  id: 'hbcli',
@@ -206,13 +279,22 @@ export async function runDoctorChecks(opts = {}) {
206
279
  detail: '已挂载但 calendar 未配置 username——日历工具会话中会报「未配置」',
207
280
  fix: `npx gotry setup calendar --off(恢复默认不挂载),或在 ${calProfilePatch} 覆盖 calendar 行 config 填 username(指引: npx gotry setup calendar --status)`
208
281
  });
282
+ const rootRequire = createRequire(join(repoRoot, 'package.json'));
209
283
  const mapCandidates = [
284
+ join(repoRoot, 'ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js'),
210
285
  join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js'),
211
286
  join(repoRoot, 'node_modules/dsh-map-tools/package.json'),
212
287
  join(repoRoot, 'ts/node_modules/dsh-map-tools/package.json'),
213
288
  join(home, '.dsh/profiles/web/node_modules/dsh-map-tools/package.json')
214
289
  ];
215
- const mapHit = mapCandidates.find((p)=>existsSync(p));
290
+ let mapHit = mapCandidates.find((p)=>existsSync(p));
291
+ if (!mapHit) {
292
+ try {
293
+ mapHit = rootRequire.resolve('dsh-map-tools');
294
+ } catch {
295
+ mapHit = undefined;
296
+ }
297
+ }
216
298
  items.push(mapHit ? {
217
299
  id: 'map-tools',
218
300
  label: 'dsh-map-tools(地图/路线/POI)',
@@ -223,14 +305,26 @@ export async function runDoctorChecks(opts = {}) {
223
305
  label: 'dsh-map-tools(地图/路线/POI)',
224
306
  status: 'missing',
225
307
  detail: '未随包解析——启动时该 patch 条目被静默剔除,地图/路线/POI 工具不会出现在模型工具箱(缺地图不挡旅行规划,只少能力)',
226
- fix: 'npx gotry doctor --fix 不覆盖此项:source 布局把 dsh-map-tools 放进 ts/dsh-runtime/node_modules(或 ts/node_modules);npm 布局重装 @danceiny/gotry(随发行版依赖提供)'
308
+ fix: '重装 @danceiny/gotry(地图插件随包 vendor 分发,缺失多为安装不完整)'
227
309
  });
228
310
  const askCandidates = [
229
311
  join(repoRoot, 'ts/dsh-runtime/vendor/deepseek-ai-dsh-tool-ask-user/package.json'),
230
312
  join(repoRoot, 'node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'),
231
313
  join(home, '.dsh/profiles/web/node_modules/@deepseek-ai/dsh-tool-ask-user/package.json')
232
314
  ];
233
- const askHit = askCandidates.find((p)=>existsSync(p));
315
+ let askHit = askCandidates.find((p)=>existsSync(p));
316
+ if (!askHit) {
317
+ try {
318
+ const reqFromDsh = createRequire(rootRequire.resolve('@deepseek-ai/dsh/lib/bin.js'));
319
+ askHit = reqFromDsh.resolve('@deepseek-ai/dsh-tool-ask-user');
320
+ } catch {
321
+ try {
322
+ askHit = rootRequire.resolve('@deepseek-ai/dsh-tool-ask-user');
323
+ } catch {
324
+ askHit = undefined;
325
+ }
326
+ }
327
+ }
234
328
  items.push(askHit ? {
235
329
  id: 'ask-user',
236
330
  label: 'dsh-tool-ask-user(结构化澄清卡)',
@@ -112,6 +112,25 @@ export async function callHbcliJson(args, opts = {}) {
112
112
  }
113
113
  return last;
114
114
  }
115
+ function i18nName(v) {
116
+ if (!v) return '';
117
+ if (typeof v === 'string') return v;
118
+ return v.zh || v.en || v.ar || '';
119
+ }
120
+ function hotelLines(result, max = 8) {
121
+ const list = result?.list;
122
+ if (!Array.isArray(list) || list.length === 0) return '';
123
+ const lines = [];
124
+ for (const item of list.slice(0, max)){
125
+ const h = item;
126
+ const name = i18nName(h.name) || `hotel ${h.id ?? '?'}`;
127
+ const star = typeof h.star === 'number' && h.star > 0 ? ` ${h.star}★` : '';
128
+ const price = h.minPrice && typeof h.minPrice.amount === 'number' ? ` 低至 ${h.minPrice.amount}${h.minPrice.currency ? ` ${h.minPrice.currency}` : ''}` : '';
129
+ lines.push(`- id=${h.id ?? '?'} ${name}${star}${price}`);
130
+ }
131
+ if (list.length > max) lines.push(`…(共 ${list.length} 家,仅列前 ${max})`);
132
+ return lines.join('\n');
133
+ }
115
134
  export async function searchHotels(query, opts = {}) {
116
135
  const hbArgs = [
117
136
  'search',
@@ -121,6 +140,8 @@ export async function searchHotels(query, opts = {}) {
121
140
  '10'
122
141
  ];
123
142
  if (query.destination) hbArgs.push('--destination-name', query.destination);
143
+ if (query.checkIn) hbArgs.push('--check-in', query.checkIn);
144
+ if (query.checkOut) hbArgs.push('--check-out', query.checkOut);
124
145
  if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([
125
146
  {
126
147
  adultCount: query.adults,
@@ -129,10 +150,15 @@ export async function searchHotels(query, opts = {}) {
129
150
  ]));
130
151
  const live = await callHbcliJson(hbArgs, opts);
131
152
  if (live.via === 'hbcli-realtime') {
153
+ const list = live.result?.list;
154
+ const n = Array.isArray(list) ? list.length : 0;
155
+ const dateTag = query.checkIn ? `入住 ${query.checkIn}${query.checkOut ? ` → 退房 ${query.checkOut}` : ''}` : '未传日期(上游按当前窗口价)';
156
+ const lines = hotelLines(live.result);
157
+ const summary = `${query.destination}:hbcli 实时 ${n} 家(${dateTag})${lines ? `\n${lines}` : ''}\n${live.evidence}`;
132
158
  return {
133
159
  ...live,
134
160
  hotels: live.result,
135
- summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}`
161
+ summary
136
162
  };
137
163
  }
138
164
  const rawReason = live.error ?? live.via;
@@ -151,7 +177,7 @@ export async function searchHotels(query, opts = {}) {
151
177
  hotels: {
152
178
  stays: matched
153
179
  },
154
- summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块`
180
+ summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块:\n${JSON.stringify(matched).slice(0, 1600)}`
155
181
  };
156
182
  }
157
183
  return {
@@ -73,10 +73,17 @@ export function startExtensionHealthWatch(opts = {}) {
73
73
  const businessProbe = opts.businessProbe ?? true;
74
74
  const probe = opts.probe ?? (async ()=>{
75
75
  const bridge = await import('./extension-bridge.js');
76
- await bridge.getOrCreateSessionBridge({
77
- keepBridge: opts.keepBridge === true
76
+ const created = await bridge.getOrCreateSessionBridge({
77
+ keepBridge: opts.keepBridge === true,
78
+ ports: opts.ports
78
79
  });
79
- return combinedProbe(opts, businessProbe);
80
+ const probeOpts = created.ok && opts.ports ? {
81
+ ...opts,
82
+ ports: [
83
+ created.bridge.port
84
+ ]
85
+ } : opts;
86
+ return combinedProbe(probeOpts, businessProbe);
80
87
  });
81
88
  const now = opts.now ?? Date.now;
82
89
  const state = {
@@ -117,7 +117,8 @@ async function startRelay() {
117
117
  if (toolNames(body).includes(TOOL)) {
118
118
  plannerBodies.push(body);
119
119
  const plannerIndex = plannerBodies.length;
120
- const alreadyHandedOff = JSON.stringify(body).includes(HANDOFF);
120
+ const realToolMessages = (body.messages ?? []).filter((message)=>message.role === 'tool');
121
+ const alreadyHandedOff = realToolMessages.some((message)=>JSON.stringify(message).includes(HANDOFF));
121
122
  const stall = !alreadyHandedOff && plannerIndex === 1 ? RELAY_STALL_MS : 0;
122
123
  setTimeout(()=>{
123
124
  if (!alreadyHandedOff && plannerIndex === 1) {
@@ -11,11 +11,17 @@ try {
11
11
  await writeFile(p, `#!/bin/sh
12
12
  cat <<'JSON'
13
13
  {"candidates":[
14
- {"type":"hotel","name":"Park Hyatt","hotel":{"id":"h1","name":"Park Hyatt","latitude":22.31,"longitude":114.16}},
15
- {"type":"city","name":"大理市","region":{"id":"d1","name":"Dali","latitude":25.58,"longitude":100.21}},
16
- {"type":"place","name":"洱海","region":{"id":"r1","name":"Erhai Lake","latitude":25.74,"longitude":100.25}}
14
+ {"type":"hotel","matchScore":180,"hotel":{"id":"h1","name":{"en":"Park Hyatt","zh":"柏悦酒店"},"star":5,"destinationId":"d1","latlngCoordinator":{"google":{"lat":22.31,"lng":114.16}}}},
15
+ {"type":"city","matchScore":250,"region":{"id":"d1","name":{"en":"Dali","zh":"大理市"},"countryCode":"CN","coordinates":{"centerLat":25.58,"centerLng":100.21}}},
16
+ {"type":"place","matchScore":90,"region":{"id":"r1","name":{"zh":"洱海"}},"place":{"latlngCoordinator":{"google":{"lat":25.74,"lng":100.25}}}}
17
17
  ]}
18
18
  JSON
19
+ `, {
20
+ mode: 0o755
21
+ });
22
+ } else if (behaviour === 'echo-args') {
23
+ await writeFile(p, `#!/bin/sh
24
+ printf '{"candidates":[{"type":"city","region":{"id":"%s"}}]}' "$*"
19
25
  `, {
20
26
  mode: 0o755
21
27
  });
@@ -31,6 +37,13 @@ JSON
31
37
  await writeFile(p, `#!/bin/sh
32
38
  echo 'hotelbe down' >&2
33
39
  exit 1
40
+ `, {
41
+ mode: 0o755
42
+ });
43
+ } else if (behaviour === 'unknown-command') {
44
+ await writeFile(p, `#!/bin/sh
45
+ echo "error: unknown command 'anything'" >&2
46
+ exit 1
34
47
  `, {
35
48
  mode: 0o755
36
49
  });
@@ -46,44 +59,80 @@ while :; do sleep 5; done
46
59
  }
47
60
  const ok = await fakeBin('hbcli-ok', 'ok');
48
61
  const r1 = await anythingSearch({
49
- keyword: 'Park Hyatt',
62
+ keyword: '大理',
50
63
  hbcliBin: ok
51
64
  });
52
65
  assert.equal(r1.verdict, 'hit');
53
66
  assert.equal(r1.hits.length, 3);
54
67
  assert.ok(r1.evidence.includes('hbcli-anything@'));
55
- console.log(`1. Park Hyatt hit (${r1.hits.length} candidates) OK`);
56
- const empty = await fakeBin('hbcli-empty', 'empty');
68
+ const [hotelHit, cityHit, placeHit] = r1.hits;
69
+ assert.equal(hotelHit.type, 'hotel');
70
+ assert.equal(hotelHit.name, '柏悦酒店', 'hotel i18n name 应 zh 优先');
71
+ assert.equal(hotelHit.hotelId, 'h1');
72
+ assert.equal(hotelHit.destinationId, 'd1', 'hotel 候选 destinationId 取所属目的地');
73
+ assert.equal(hotelHit.star, 5);
74
+ assert.equal(hotelHit.latitude, 22.31);
75
+ assert.equal(hotelHit.longitude, 114.16);
76
+ assert.equal(hotelHit.score, 180);
77
+ assert.equal(cityHit.type, 'city');
78
+ assert.equal(cityHit.name, '大理市', 'region i18n name 应 zh 优先');
79
+ assert.equal(cityHit.destinationId, 'd1');
80
+ assert.equal(cityHit.latitude, 25.58, 'city 坐标取 region.coordinates.centerLat');
81
+ assert.equal(cityHit.longitude, 100.21);
82
+ assert.equal(placeHit.type, 'place');
83
+ assert.equal(placeHit.name, '洱海');
84
+ assert.equal(placeHit.latitude, 25.74, 'place 坐标回退 place.latlngCoordinator');
85
+ console.log('1. 真实候选 wire 形状(hotel/city/place 逐字段)OK');
86
+ const echo = await fakeBin('hbcli-echo-args', 'echo-args');
57
87
  const r2 = await anythingSearch({
88
+ keyword: '大理',
89
+ contentType: 'city',
90
+ parentDestinationId: 'd1',
91
+ hbcliBin: echo
92
+ });
93
+ assert.equal(r2.verdict, 'hit');
94
+ assert.equal(r2.hits[0].destinationId, '--json search anything 大理 --content-type city --destination-id d1', 'spawn 旗标形态(--json 前置/--destination-id 映射)应稳定,实际 ' + r2.hits[0].destinationId);
95
+ console.log('2. 旗标回归(--json 前置 + --destination-id 映射)OK');
96
+ const empty = await fakeBin('hbcli-empty', 'empty');
97
+ const r3 = await anythingSearch({
58
98
  keyword: 'nothing',
59
99
  hbcliBin: empty
60
100
  });
61
- assert.equal(r2.verdict, 'miss');
62
- assert.equal(r2.hits.length, 0);
63
- console.log('2. nothing → miss OK');
101
+ assert.equal(r3.verdict, 'miss');
102
+ assert.equal(r3.hits.length, 0);
103
+ console.log('3. nothing → miss OK');
64
104
  const fail = await fakeBin('hbcli-fail', 'fail');
65
- const r3 = await anythingSearch({
105
+ const r4 = await anythingSearch({
66
106
  keyword: 'x',
67
107
  hbcliBin: fail
68
108
  });
69
- assert.equal(r3.verdict, 'error');
70
- assert.equal(r3.ok, false);
71
- assert.match(r3.evidence, /error/);
72
- console.log('3. fail → error (降级) OK');
109
+ assert.equal(r4.verdict, 'error');
110
+ assert.equal(r4.ok, false);
111
+ assert.match(r4.evidence, /error/);
112
+ console.log('4. fail → error (降级) OK');
113
+ const old = await fakeBin('hbcli-old', 'unknown-command');
114
+ const r5 = await anythingSearch({
115
+ keyword: 'x',
116
+ hbcliBin: old
117
+ });
118
+ assert.equal(r5.verdict, 'error');
119
+ assert.match(r5.error ?? '', /hbcli update|gotry setup/, 'error 应带升级指引,实际 ' + r5.error);
120
+ assert.match(r5.evidence, /unknown command/, 'evidence 保留上游原话(溯源)');
121
+ console.log(`5. 旧 CLI unknown command → 升级指引 OK(${r5.error})`);
73
122
  const hang = await fakeBin('hbcli-hang', 'hang');
74
- const r4 = await anythingSearch({
123
+ const r6 = await anythingSearch({
75
124
  keyword: 'x',
76
125
  hbcliBin: hang,
77
126
  timeoutMs: 800
78
127
  });
79
- assert.equal(r4.verdict, 'error', 'timeout 应判 error');
80
- console.log(`4. hang/timeout → error OK (latency=${r4.latencyMs}ms)`);
81
- const r5 = await anythingSearch({
128
+ assert.equal(r6.verdict, 'error', 'timeout 应判 error');
129
+ console.log(`6. hang/timeout → error OK (latency=${r6.latencyMs}ms)`);
130
+ const r7 = await anythingSearch({
82
131
  keyword: ' '
83
132
  });
84
- assert.equal(r5.verdict, 'error');
85
- console.log('5. empty keyword → error OK');
86
- console.log('\nANYTHING TESTS: 5/5 OK(hbcli fake + graceful degrade)');
133
+ assert.equal(r7.verdict, 'error');
134
+ console.log('7. empty keyword → error OK');
135
+ console.log('\nANYTHING TESTS: 7/7 OK(hbcli fake + wire 对齐 + 降级诚实)');
87
136
  } finally{
88
137
  await rm(tmp, {
89
138
  recursive: true,