@danceiny/gotry 0.2.0-rc.31 → 0.2.0-rc.32

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 (54) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +63 -53
  4. package/bin/gotry-hbcli-release.d.ts +3 -0
  5. package/bin/gotry-hbcli-release.js +4 -0
  6. package/client/client.js +66 -21
  7. package/dist/capabilities/doctor.js +7 -8
  8. package/dist/capabilities/effect.js +45 -12
  9. package/dist/capabilities/flyai-config.js +12 -10
  10. package/dist/capabilities/flyai.js +87 -56
  11. package/dist/capabilities/hbcli-config.js +156 -0
  12. package/dist/capabilities/hbcli.js +29 -2
  13. package/dist/capabilities/resilience.js +6 -6
  14. package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
  16. package/dist/capabilities/session/ctrip-city.js +186 -0
  17. package/dist/capabilities/session/extension-bridge.js +8 -1
  18. package/dist/capabilities/session/gui-flight.js +737 -0
  19. package/dist/capabilities/session/gui-hotel.js +413 -0
  20. package/dist/capabilities/session-search.js +98 -11
  21. package/dist/scripts/bootstrap-tests.js +23 -6
  22. package/dist/scripts/ctrip-city-tests.js +80 -0
  23. package/dist/scripts/effect-tests.js +602 -504
  24. package/dist/scripts/extension-tests.js +14 -8
  25. package/dist/scripts/flight-malformed-tests.js +1 -1
  26. package/dist/scripts/flyai-setup-tests.js +19 -4
  27. package/dist/scripts/flyai-tests.js +768 -653
  28. package/dist/scripts/gotry-web-api-tests.js +5 -0
  29. package/dist/scripts/gotry-web-client-tests.js +20 -1
  30. package/dist/scripts/gui-flight-live.js +27 -0
  31. package/dist/scripts/hbcli-tests.js +1 -1
  32. package/dist/scripts/hbcli-upgrade-tests.js +107 -0
  33. package/dist/scripts/hbcli-web-setup-tests.js +114 -0
  34. package/dist/scripts/hotel-date-gate-tests.js +1 -1
  35. package/dist/scripts/hotel-result-tests.js +169 -0
  36. package/dist/scripts/issue-271-liveness-tests.js +1 -1
  37. package/dist/scripts/session-tests.js +177 -6
  38. package/dist/src/gotry-web-api.js +64 -2
  39. package/dist/src/index.js +40 -18
  40. package/extension/manifest.json +2 -2
  41. package/package.json +2 -1
  42. package/ts/capabilities/flyai-config.ts +12 -10
  43. package/ts/capabilities/flyai.ts +91 -50
  44. package/ts/capabilities/hbcli-config.ts +128 -0
  45. package/ts/capabilities/hbcli.ts +30 -3
  46. package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
  47. package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
  48. package/ts/capabilities/session/ctrip-city.ts +158 -0
  49. package/ts/capabilities/session/extension-bridge.ts +9 -2
  50. package/ts/capabilities/session/gui-flight.ts +792 -0
  51. package/ts/capabilities/session/gui-hotel.ts +451 -0
  52. package/ts/capabilities/session-search.ts +115 -15
  53. package/ts/src/gotry-web-api.ts +51 -2
  54. package/ts/src/index.ts +41 -19
@@ -103,12 +103,22 @@ assert.ok(!c7b.out.includes('下载通道'), '非法值不应进入 github 通
103
103
  console.log('7. 扩展分发 github 通道(不可达基址即时降级 + 非法值回落 bundled)OK');
104
104
  // 8. doctor 子命令(2026-09-02 迪拜 session 复盘:可选依赖不撒手——doctor 统一
105
105
  // 显示状态 + 精确补装指引;LLM key 显式让渡;报告落 gotry-state/doctor-report.md)
106
- const c8 = runBootstrap(['doctor'], {});
106
+ const doctorHome = mkdtempSync(join(tmpdir(), 'gotry-doctor-home-'));
107
+ let c8;
108
+ try {
109
+ c8 = runBootstrap(['doctor'], { HOME: doctorHome, FLYAI_API_KEY: '', DEBUG_FLYAI_API_KEY: '', STAICLI_HOME: join(doctorHome, '.staicli'), HOTELBYTE_TOKEN: '' });
110
+ }
111
+ finally {
112
+ rmSync(doctorHome, { recursive: true, force: true });
113
+ }
107
114
  assert.ok(c8.out.includes('[gotry-doctor]'), '应输出 [gotry-doctor] 标签');
108
115
  assert.ok(c8.out.includes('Agent Reach'), '体检应含 agent-reach 项');
109
116
  assert.ok(c8.out.includes('hbcli'), '体检应含 hbcli 项');
110
117
  assert.ok(c8.out.includes('FlyAI(飞猪官方检索'), '体检应含 flyai 项');
111
- assert.ok(c8.out.includes('匿名试用') || c8.out.includes('已配置,未验证'), '体检应明确 FlyAI 当前未验证/匿名降级面');
118
+ // FlyAI 面三合法态(doctor.ts #521:configured≠verified):匿名试用 / 已配置,未验证 / 已验证。
119
+ // 机器态依赖断言:本机配置并验证过 key 的环境(如 founder 机)doctor 输出「已验证 …」,
120
+ // 两态断言会把健康态误判为红(2026-10-14 发版门禁实测)。
121
+ assert.ok(c8.out.includes('匿名试用') || c8.out.includes('已配置,未验证') || c8.out.includes('已验证'), '体检应明确 FlyAI 当前状态(匿名试用/已配置,未验证/已验证)');
112
122
  assert.ok(c8.out.includes('LLM key'), '体检应含 LLM key 让渡说明(doctor 不管 key)');
113
123
  assert.ok(c8.out.includes('dsh-map-tools'), '体检应含 dsh-map-tools 项(#139;与 ts/capabilities/doctor.ts 两面成对)');
114
124
  assert.ok(c8.out.includes('dsh-tool-ask-user'), '体检应含 dsh-tool-ask-user 项(与 ts/capabilities/doctor.ts 两面成对)');
@@ -396,6 +406,7 @@ console.log('18. onboarding CLI 跳过(非 TTY / CI / GOTRY_SETUP_SKIP / GOTRY_O
396
406
  copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(fixture, 'bin', 'gotry-flyai-verification.js'));
397
407
  copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(fixture, 'bin', 'gotry-process-liveness.js'));
398
408
  copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(fixture, 'bin', 'gotry-extension-status.js'));
409
+ copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(fixture, 'bin', 'gotry-hbcli-release.js'));
399
410
  copyFileSync(join(repoRoot, 'package.json'), join(fixture, 'package.json'));
400
411
  const vendorDir = join(fixture, 'ts', 'dsh-runtime', 'vendor');
401
412
  mkdirSync(vendorDir, { recursive: true });
@@ -647,6 +658,7 @@ function buildPkgFixture(tmpRoot) {
647
658
  copyFileSync(join(repoRoot, 'bin', 'gotry-bootstrap.js'), join(pkgBin, 'gotry-bootstrap.js'));
648
659
  copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(pkgBin, 'gotry-flyai-verification.js'));
649
660
  copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(pkgBin, 'gotry-extension-status.js'));
661
+ copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(pkgBin, 'gotry-hbcli-release.js'));
650
662
  copyFileSync(join(repoRoot, 'bin', 'gotry-runtime-resolution.js'), join(pkgBin, 'gotry-runtime-resolution.js'));
651
663
  copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(pkgBin, 'gotry-process-liveness.js'));
652
664
  copyFileSync(join(repoRoot, 'cordis.gotry-patch.yml'), join(pkgRoot, 'cordis.gotry-patch.yml'));
@@ -1508,8 +1520,9 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
1508
1520
  assert.deepEqual(unit('not json'), { ok: true, customer: false }, '非 JSON(超旧版/异常)保守视为有效,与工具层同语义');
1509
1521
  assert.deepEqual(unit(''), { ok: true, customer: false }, '空输出同属不可解析,保守视为有效');
1510
1522
  assert.deepEqual(unit(null), { ok: false, customer: false }, '取不到输出(非零退出/超时/spawn 失败)→ 未确认');
1511
- const fakeHbcli = (whoamiArm) => `#!/bin/sh\ncase "$*" in\n *--version*) echo '0.0.4';;\n *version*) echo '0.0.4';;\n *whoami*) ${whoamiArm};;\nesac\n`;
1523
+ const fakeHbcli = (whoamiArm, version = '0.0.4') => `#!/bin/sh\ncase "$*" in\n *--version*) echo '${version}';;\n *version*) echo '${version}';;\n *whoami*) ${whoamiArm};;\nesac\n`;
1512
1524
  const cases = [
1525
+ { name: 'old-version', version: '0.0.3', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'missing' },
1513
1526
  { name: 'allfalse', arm: `echo '${allFalse}'`, expect: 'degraded' },
1514
1527
  { name: 'customer', arm: `echo '${allFalse.replace('"customer":{"configured":false}', '"customer":{"configured":true}')}'`, expect: 'ok', customer: true },
1515
1528
  { name: 'apikey', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'ok' },
@@ -1527,12 +1540,12 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
1527
1540
  mkdirSync(binDir);
1528
1541
  mkdirSync(home);
1529
1542
  symlinkSync(process.execPath, join(binDir, 'node'));
1530
- writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm), { mode: 0o755 });
1543
+ writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm, c.version), { mode: 0o755 });
1531
1544
  // CLI 面
1532
1545
  const cli = runBootstrap(['doctor'], { HOME: home, PATH: binDir, FLYAI_API_KEY: '' });
1533
1546
  const line = cli.out.split('\n').find((l) => l.includes('hbcli(酒店实时源)'));
1534
1547
  assert.ok(line, `${c.name}: CLI doctor 应输出 hbcli 行\n${cli.out}`);
1535
- const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : 'other';
1548
+ const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : line.includes('版本过旧') ? 'missing' : 'other';
1536
1549
  assert.equal(cliLevel, c.expect, `${c.name}: CLI doctor 的 hbcli 结论应为 ${c.expect}\n${line}`);
1537
1550
  if (c.customer)
1538
1551
  assert.ok(line.includes('customer 档在用'), `${c.name}: CLI 应标注 customer 档在用\n${line}`);
@@ -1542,6 +1555,10 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
1542
1555
  process.env.PATH = prevPath;
1543
1556
  assert.equal(tool.status, c.expect, `${c.name}: 工具层 doctor 的 hbcli 结论应为 ${c.expect},实际 ${tool.status}`);
1544
1557
  assert.equal(cliLevel, tool.status, `${c.name}: CLI 与工具层必须同结论(两份实现不得漂移)`);
1558
+ if (c.expect === 'missing') {
1559
+ assert.match(cli.out, /staicli-v0\.0\.4\/install\.sh/, 'CLI 应提供当前官方版本的安装入口');
1560
+ assert.match(tool.fix ?? '', /staicli-v0\.0\.4\/install\.sh/, '工具层应提供相同升级入口');
1561
+ }
1545
1562
  }
1546
1563
  finally {
1547
1564
  process.env.PATH = prevPath;
@@ -1560,5 +1577,5 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
1560
1577
  catch { /* ignore */ }
1561
1578
  }
1562
1579
  }
1563
- console.log('24. hbcli 凭证判定(CLI doctor 按 whoami 三档 JSON 判,不看退出码;与工具层 doctor 对拍 5 夹具同结论:全 false=degraded / customer·api_key=ok / 非 JSON=保守 ok / 非零退出=degraded)OK');
1580
+ console.log('24. hbcli 版本与凭证判定(CLI↔工具层 6 夹具同结论:旧版=missing / 全 false=degraded / customer·api_key=ok / 非 JSON=保守 ok / 非零退出=degraded)OK');
1564
1581
  console.log('BOOTSTRAP TESTS: 26/26 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要 / sidebar 落盘状态复核 / onboarding 分类+计划+env opt-out+跳过原因+yes+partial-failure+幂等+prompt+CLI 跳过+--scan / 编排缝 orchestrateWebLaunch × 6 / prompt waiter failure deterministic process-group reap / waiter 原始错误 + cleanup timeout/failure handled / 临时安装包 spawned inner 21a TTY-eligible 真实 prompt→n→web + 摘要抑制 + 无残留 / 21b non-TTY 零 prompt / 21c POSIX 信号清理 SIGTERM→清 result+patch 目录 + 无残留子进程 / 21f yes-path installer 子树信号清理 / 21g yes-path timeout installer 子树清理 / 21d timeout 诊断 + 无残留 / 21e dsh-lifecycle 信号转发 / win32 平台边界 / result 通道排他写入 / hbcli 凭证判定 CLI↔工具层对拍)');
@@ -0,0 +1,80 @@
1
+ /** Supplier metadata fixtures; no vendor requests, credentials or browser session. */
2
+ import assert from 'node:assert/strict';
3
+ import { parseCtripHotelCities, resolveCtripHotelCity, parseCtripCityCatalog, parseCtripCitySuggestions, matchCtripCity, resolveCtripFlightCity } from '../capabilities/session/ctrip-city.js';
4
+ import { resolveFlightEntryUrl } from '../capabilities/session/adapters/ctrip-flight.js';
5
+ const catalog = { ResponseStatus: { Ack: 'Success' }, responseHead: { errorCode: '0' }, recommendGroupList: [{ indexedCity: { cityList: [
6
+ { id: 2, code: 'SHA', name: '上海', eName: 'Shanghai', airportlst: [{ code: 'PVG', name: '上海浦东国际机场' }] },
7
+ { id: 58, code: 'HKG', name: '中国香港', nameEn: 'Hong Kong' },
8
+ ] }, geographicalRegionList: [{ hotCityList: [{ cityList: [{ id: 220, code: 'DXB', name: '迪拜', eName: 'Dubai', specName: '迪拜(阿联酋)', sameSpecCity: 1 }] }] }] }] };
9
+ const cities = parseCtripCityCatalog(catalog);
10
+ for (const [name, code] of [['迪拜(阿联酋)', 'DXB'], ['DXB', 'DXB'], ['香港', 'HKG'], ['Hong Kong', 'HKG'], ['Shanghai', 'SHA'], ['上海浦东国际机场', 'PVG']]) {
11
+ const r = matchCtripCity(name, cities);
12
+ assert.ok(r.ok, name);
13
+ assert.equal(r.city.code, code);
14
+ }
15
+ assert.equal(matchCtripCity('Dubai', cities).ok, false, 'supplier-marked same-name cities require qualification');
16
+ assert.equal(matchCtripCity('迪', cities).ok, false, 'no prefix or nearby-city substitution');
17
+ assert.equal(matchCtripCity('同名城', [{ name: '同名城', code: 'AAA', aliases: ['同名城'], context: '甲国' }, { name: '同名城', code: 'BBB', aliases: ['同名城'], context: '乙国' }]).ok, false);
18
+ assert.throws(() => parseCtripCityCatalog({ ...catalog, responseHead: { errorCode: '1' } }));
19
+ const suggestions = { Data: [{ Type: 4, Name: '未缓存城市', Code: 'NEW', EName: 'New City', Country: '示例国', Nearby: [{ Type: 4, Name: '邻近城市', Code: 'NEA' }] }] };
20
+ assert.equal(parseCtripCitySuggestions(suggestions).length, 1);
21
+ let requests = [];
22
+ const fakeFetch = async (url) => {
23
+ requests.push(String(url));
24
+ return Response.json(requests.length === 1 ? catalog : suggestions);
25
+ };
26
+ const resolved = await resolveCtripFlightCity('未缓存城市', fakeFetch);
27
+ assert.ok(resolved.ok);
28
+ assert.equal(resolved.city.code, 'NEW');
29
+ assert.equal(requests.length, 2, 'catalog miss queries supplier autocomplete');
30
+ const blocked = await resolveCtripFlightCity('未知城市', async () => new Response('whaleguard block'));
31
+ assert.ok(!blocked.ok && blocked.reason === 'unavailable', 'challenge is unavailable, not unsupported/no inventory');
32
+ const oversized = await resolveCtripFlightCity('未知城市', async () => new Response('x'.repeat(512001)));
33
+ assert.ok(!oversized.ok && oversized.reason === 'unavailable');
34
+ const entry = await resolveFlightEntryUrl('迪拜(阿联酋)', '香港', '2026-12-01', {}, async (q) => matchCtripCity(q, cities));
35
+ assert.ok(entry.ok && entry.url?.includes('oneway-dxb-hkg'));
36
+ let lookups = 0;
37
+ assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', 'bad-date', {}, async () => { lookups++; throw Error('should not run'); })).ok);
38
+ assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', '2026-12-01', { fromCode: '../dxb' }, async () => { lookups++; throw Error('should not run'); })).ok);
39
+ assert.equal(lookups, 0);
40
+ console.log('Ctrip city metadata: international names/airports, ambiguity, autocomplete fallback, challenge/bounds and exact URL PASS');
41
+ const hotelRecord = (id, name, english, country) => ({ keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: String(id), displayTexts: [{ key: 'MAIN_TITLE', value: english }] } }, controlInfo: { regionInfo: { basicCityModel: { cityId: id }, displayCityModel: { cityName: name, countryName: country, destinationNameEN: english } } } });
42
+ const hotels = { ResponseStatus: { Ack: 'Success' }, data: { mainKeywordList: { keywords: [hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), hotelRecord(596578, '迪拜', 'Dubai', '印度')] } } };
43
+ const hotelFetch = async () => Response.json(hotels);
44
+ const ambiguous = await resolveCtripHotelCity('迪拜', undefined, hotelFetch);
45
+ assert.ok(!ambiguous.ok && ambiguous.reason === 'ambiguous');
46
+ assert.equal(ambiguous.candidates.length, 2);
47
+ const qualified = await resolveCtripHotelCity('迪拜', '阿联酋', hotelFetch);
48
+ assert.ok(qualified.ok && qualified.city.id === '220');
49
+ const rejected = await resolveCtripHotelCity('迪拜', undefined, async () => Response.json({ ...hotels, data: { ...hotels.data, htlSpiderActionErrorCode: 'captcha' } }));
50
+ assert.ok(!rejected.ok && rejected.reason === 'unavailable');
51
+ assert.throws(() => parseCtripHotelCities({ ...hotels, data: { mainKeywordList: { keywords: [{ ...hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: '58' } } }] } } }));
52
+ console.log('Hotel destination namespace: same-name city ambiguity, country confirmation, challenge and ID conflict PASS');
53
+ // Exercise the real registered schema and tool dispatch, using an isolated state root.
54
+ const { apply } = await import('../src/index.js');
55
+ const { mkdtempSync, rmSync } = await import('node:fs');
56
+ const { tmpdir } = await import('node:os');
57
+ const { join } = await import('node:path');
58
+ const root = mkdtempSync(join(tmpdir(), 'gotry-city-tool-'));
59
+ const tools = [];
60
+ const effects = [];
61
+ try {
62
+ apply({ tools: { register: (t) => tools.push(t) }, systemPrompt: { variable: () => { } }, get: () => undefined, on: () => () => { } }, { stateRoot: root, timeoutMs: 1000, hbcliBin: 'unused', sessionAccess: 'allow' }, { effect: (async (fx) => { effects.push(fx); return { result: { ok: false, verdict: 'error', error: 'fixture only', evidence: '[fixture:city-tool]' }, trace: { effect: fx.effect, channel: 'browser', attempts: 1, backoffMs: 0, breaker: 'off', evidence: [] } }; }) });
63
+ const session = tools.find(t => t.name === 'gotry_session_search');
64
+ const hotel = tools.find(t => t.name === 'gotry_hotel_search');
65
+ const parameters = session.parameters?.properties ?? session.parameters;
66
+ assert.ok(parameters.country && parameters.fromCityCode && parameters.toCityCode, 'model must see disambiguation parameters on session search');
67
+ const hotelParameters = hotel.parameters?.properties ?? hotel.parameters;
68
+ assert.ok(!hotelParameters.country, 'HBCLI hotel tool must not advertise an ignored session parameter');
69
+ await session.execute({ kind: 'hotel', to: '迪拜', country: '阿联酋' }, {});
70
+ assert.equal(effects.at(-1).effect, 'SESSION_HOTEL_SEARCH');
71
+ assert.equal(effects.at(-1).params.country, '阿联酋');
72
+ await session.execute({ kind: 'flight', from: '迪拜', to: '香港', date: '2099-12-01', fromCityCode: 'DXB', toCityCode: 'HKG' }, {});
73
+ assert.equal(effects.at(-1).effect, 'SESSION_FLIGHT_SEARCH');
74
+ assert.equal(effects.at(-1).params.fromCityCode, 'DXB');
75
+ assert.equal(effects.at(-1).params.toCityCode, 'HKG');
76
+ console.log('Registered session tool: country/code schema and effect dispatch PASS');
77
+ }
78
+ finally {
79
+ rmSync(root, { recursive: true, force: true });
80
+ }