@danceiny/gotry 0.2.0-rc.30 → 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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +119 -136
- package/bin/gotry-extension-status.d.ts +12 -0
- package/bin/gotry-extension-status.js +113 -0
- package/bin/gotry-flyai-verification.d.ts +9 -0
- package/bin/gotry-flyai-verification.js +63 -0
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/bin/gotry-inner.js +2 -1
- package/client/client.js +228 -0
- package/dist/capabilities/doctor.js +14 -23
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +8 -1
- package/dist/capabilities/session/gui-flight.js +737 -0
- package/dist/capabilities/session/gui-hotel.js +413 -0
- package/dist/capabilities/session-search.js +98 -11
- package/dist/scripts/artifact-client-contract-tests.js +4 -3
- package/dist/scripts/artifact-delivery-tests.js +31 -0
- package/dist/scripts/bootstrap-tests.js +37 -10
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/doctor-tests.js +36 -7
- package/dist/scripts/dsh-artifact-web-e2e.js +204 -43
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +93 -5
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +119 -0
- package/dist/scripts/gotry-web-client-tests.js +47 -0
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +3 -1
- package/dist/scripts/session-tests.js +177 -6
- package/dist/src/artifact-delivery.js +26 -0
- package/dist/src/gotry-web-api.js +219 -0
- package/dist/src/index.js +45 -17
- package/extension/manifest.json +2 -2
- package/package.json +8 -2
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +9 -2
- package/ts/capabilities/session/gui-flight.ts +792 -0
- package/ts/capabilities/session/gui-hotel.ts +451 -0
- package/ts/capabilities/session-search.ts +115 -15
- package/ts/src/artifact-delivery.ts +25 -0
- package/ts/src/gotry-web-api.ts +189 -0
- package/ts/src/index.ts +46 -18
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface FlyaiCandidateVerification {
|
|
2
|
+
verdict: 'hit' | 'auth-error' | 'forbidden' | 'needs-setup' | 'rate-limited' | 'timeout' | 'cancelled' | 'error'
|
|
3
|
+
count?: number
|
|
4
|
+
error?: string
|
|
5
|
+
}
|
|
6
|
+
export declare function verifyFlyaiCandidate(candidate: string, options?: {
|
|
7
|
+
env?: NodeJS.ProcessEnv
|
|
8
|
+
signal?: AbortSignal
|
|
9
|
+
}): Promise<FlyaiCandidateVerification>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Shared, bounded, read-only candidate verification for terminal and native Web setup. */
|
|
2
|
+
import { spawnOwnedChild, terminateOwnedChild } from './gotry-process-liveness.js'
|
|
3
|
+
|
|
4
|
+
export async function verifyFlyaiCandidate(candidate, options = {}) {
|
|
5
|
+
const configured = options.env ?? process.env
|
|
6
|
+
if (options.signal?.aborted) return { verdict: 'cancelled', error: '验证已取消。' }
|
|
7
|
+
const env = { ...configured, FLYAI_API_KEY: candidate }
|
|
8
|
+
delete env.DEBUG_FLYAI_API_KEY
|
|
9
|
+
const command = configured.GOTRY_FLYAI_CLI_BIN || 'npx'
|
|
10
|
+
const prefix = configured.GOTRY_FLYAI_CLI_BIN ? [] : ['-y', '@fly-ai/flyai-cli@1.0.16']
|
|
11
|
+
const date = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 10)
|
|
12
|
+
const args = [...prefix, 'search-flight', '--origin', '上海', '--destination', '丽江', '--dep-date', date]
|
|
13
|
+
const limit = Number(configured.GOTRY_FLYAI_VERIFY_TIMEOUT_MS ?? 20_000)
|
|
14
|
+
const timeoutMs = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 60_000) : 20_000
|
|
15
|
+
const owned = spawnOwnedChild(command, args, { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
16
|
+
let stopReason, timer, stdout = '', stderr = '', bytes = 0
|
|
17
|
+
const stop = (reason) => {
|
|
18
|
+
stopReason ??= reason
|
|
19
|
+
void terminateOwnedChild({ ...owned, signal: 'SIGKILL', termGraceMs: 100, killWaitMs: 1000 })
|
|
20
|
+
}
|
|
21
|
+
const abort = () => stop('cancelled')
|
|
22
|
+
try {
|
|
23
|
+
const result = await new Promise(resolve => {
|
|
24
|
+
let settled = false
|
|
25
|
+
const finish = value => { if (!settled) { settled = true; resolve(value) } }
|
|
26
|
+
timer = setTimeout(() => stop('timeout'), timeoutMs)
|
|
27
|
+
options.signal?.addEventListener('abort', abort, { once: true })
|
|
28
|
+
if (options.signal?.aborted) abort()
|
|
29
|
+
owned.child.stdout?.on('data', chunk => {
|
|
30
|
+
bytes += chunk.length
|
|
31
|
+
if (bytes > 256 * 1024) return stop('output-limit')
|
|
32
|
+
stdout += chunk.toString()
|
|
33
|
+
})
|
|
34
|
+
owned.child.stderr?.on('data', chunk => {
|
|
35
|
+
bytes += chunk.length
|
|
36
|
+
if (bytes > 256 * 1024) return stop('output-limit')
|
|
37
|
+
stderr += chunk.toString()
|
|
38
|
+
})
|
|
39
|
+
owned.child.once('error', () => finish({ verdict: 'error', error: '无法启动验证进程。' }))
|
|
40
|
+
owned.child.once('close', code => {
|
|
41
|
+
if (stopReason) return finish({ verdict: stopReason === 'output-limit' ? 'error' : stopReason,
|
|
42
|
+
error: stopReason === 'cancelled' ? '验证已取消。' : stopReason === 'timeout' ? `验证调用超时 ${timeoutMs}ms。` : '验证响应过大。' })
|
|
43
|
+
const combined = `${stderr}\n${stdout}`
|
|
44
|
+
if (/Invalid API key|HTTP\s*401|\b401\b/.test(combined)) return finish({ verdict: 'auth-error', error: '401 Invalid API key' })
|
|
45
|
+
if (/HTTP\s*403|\b403\b/.test(combined)) return finish({ verdict: 'forbidden', error: 'HTTP 403:无访问权限,请核对控制台权限。' })
|
|
46
|
+
if (/Trial limit reached/.test(combined)) return finish({ verdict: 'needs-setup', error: '试用额度已用尽,请核对控制台 Key。' })
|
|
47
|
+
if (/HTTP\s*429|\b429\b/.test(combined)) return finish({ verdict: 'rate-limited', error: '普通限流,稍后重试。' })
|
|
48
|
+
if (code !== 0) return finish({ verdict: 'error', error: `验证进程失败(exit ${code});请检查网络与服务状态。` })
|
|
49
|
+
try {
|
|
50
|
+
const envelope = JSON.parse(stdout)
|
|
51
|
+
finish(Array.isArray(envelope?.data?.itemList)
|
|
52
|
+
? { verdict: 'hit', count: envelope.data.itemList.length }
|
|
53
|
+
: { verdict: 'error', error: '响应缺 data.itemList。' })
|
|
54
|
+
} catch { finish({ verdict: 'error', error: '响应不是合法 JSON。' }) }
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
return result
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
options.signal?.removeEventListener('abort', abort)
|
|
61
|
+
await terminateOwnedChild({ ...owned, signal: 'SIGKILL', termGraceMs: 100, killWaitMs: 1000 })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Official stable hbcli baseline. npm's staicli@latest still points to 0.0.3. */
|
|
2
|
+
export const HBCLI_RELEASE_VERSION = '0.0.4'
|
|
3
|
+
export const HBCLI_INSTALL_URL = `https://github.com/hotelbyte-com/docs/releases/download/staicli-v${HBCLI_RELEASE_VERSION}/install.sh`
|
|
4
|
+
export const HBCLI_INSTALL_CMD = `curl -fsSL --connect-timeout 15 --max-time 60 ${HBCLI_INSTALL_URL} | bash -s -- --version ${HBCLI_RELEASE_VERSION}`
|
package/bin/gotry-inner.js
CHANGED
|
@@ -88,8 +88,9 @@ Usage:
|
|
|
88
88
|
# 跳过该问: --no-onboarding 或 GOTRY_ONBOARDING_SKIP=1
|
|
89
89
|
gotry setup # 扩展就位检查/指引(商店一键装)
|
|
90
90
|
gotry setup calendar # 可选日历(CalDAV 工作窗口)挂载开关:默认关;--off 关闭;--status 查看
|
|
91
|
+
gotry setup flyai # FlyAI 配置向导:控制台取 key → 隐藏粘贴 → 验证后保存;--status 查看;--clear 清除
|
|
91
92
|
gotry doctor # 可选依赖体检:扩展/agent-reach/hbcli/flyai/sidebar 状态 + 补装指引
|
|
92
|
-
gotry doctor --fix #
|
|
93
|
+
gotry doctor --fix # 补装可选依赖;交互终端可选择接着配置 FlyAI
|
|
93
94
|
gotry "一段完整任务..." # headless 一问一答
|
|
94
95
|
gotry help # this help
|
|
95
96
|
gotry --version # 版本号(bug 报告必填项)
|
package/client/client.js
CHANGED
|
@@ -12,6 +12,219 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
var module = { exports: {} }
|
|
13
13
|
var exports = module.exports
|
|
14
14
|
var React = require('react')
|
|
15
|
+
var UI = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
16
|
+
|
|
17
|
+
var webLocale = {
|
|
18
|
+
en: {
|
|
19
|
+
title: 'GoTry', description: 'Travel data sources and saved itineraries', flyai: 'FlyAI',
|
|
20
|
+
loading: 'Loading…', unavailable: 'Configuration is unavailable. Retry to read its current state.',
|
|
21
|
+
readOnly: 'The current key comes from the launch environment, or this platform cannot safely store it here.',
|
|
22
|
+
key: 'API Key', keyHint: 'Paste the key from the FlyAI console. It never enters the conversation.',
|
|
23
|
+
configured: 'A key is configured', anonymous: 'Anonymous trial · shared quota', verified: 'Current configuration verified',
|
|
24
|
+
unverified: 'Configured · verification required', debug: 'Debug endpoint', console: 'Open FlyAI console',
|
|
25
|
+
save: 'Verify and save', saving: 'Verifying…', saveFailed: 'The new key was not saved; the previous configuration is retained.',
|
|
26
|
+
saved: 'Verified and saved.', clear: 'Clear local key', cleared: 'Local key cleared. Anonymous trial is now active.',
|
|
27
|
+
retry: 'Refresh status', source: 'Source', config: 'Local configuration', env: 'Environment variable',
|
|
28
|
+
'env-debug': 'Debug environment variable', none: 'Shared trial',
|
|
29
|
+
hbcli: 'HotelByte · sandbox', appKey: 'App Key', appSecret: 'App Secret',
|
|
30
|
+
hbHint: 'Use the credentials supplied with your sandbox account. They never enter the conversation.',
|
|
31
|
+
hbRegistration: 'Apply for a sandbox account', hbUnconfigured: 'Sandbox API credentials are not configured', hbConfigured: 'Sandbox API credentials configured',
|
|
32
|
+
hbReadOnly: 'Credentials come from the launch environment, or the local configuration cannot be safely written.',
|
|
33
|
+
hbSaveFailed: 'Credentials were not saved; the previous configuration is retained.',
|
|
34
|
+
hbSaved: 'Credentials saved and the read-only hotel query completed.', hbClear: 'Clear local sandbox credentials',
|
|
35
|
+
hbCleared: 'Local sandbox API credentials cleared.', hbNoSuppliers: 'Saved · no available suppliers configured',
|
|
36
|
+
hbNoSuppliersHint: 'HotelByte must enable suppliers for your account. This result does not mean the destination has no rooms.',
|
|
37
|
+
hbMiss: 'Query verified · no hotels in this check', hbProbeHint: 'Verification makes one read-only Shanghai hotel query for tomorrow, one night and two adults.',
|
|
38
|
+
artifacts: 'GoTry artifacts', artifactsHint: 'Browse saved itineraries and planning deliveries',
|
|
39
|
+
search: 'Search artifacts', searchPlaceholder: 'Title or filename', refresh: 'Refresh',
|
|
40
|
+
empty: 'No saved artifacts in this workspace.', noMatches: 'No matching artifacts.', noSession: 'Open a conversation to browse its workspace.',
|
|
41
|
+
previous: 'Previous', next: 'Next', html: 'Open HTML preview', open: 'Open preview',
|
|
42
|
+
htmlHint: 'HTML previews may run the document’s scripts.', failed: 'Could not read the current state. Please retry.',
|
|
43
|
+
},
|
|
44
|
+
zh: {
|
|
45
|
+
title: 'GoTry', description: '旅行数据源与已保存行程', flyai: 'FlyAI(飞猪)',
|
|
46
|
+
loading: '正在读取…', unavailable: '配置暂时不可用,请重新读取当前状态。',
|
|
47
|
+
readOnly: '当前 Key 来自启动环境变量,或此平台无法在这里安全保存。',
|
|
48
|
+
key: 'API Key', keyHint: '粘贴飞猪控制台中的 Key,凭据不会进入对话。',
|
|
49
|
+
configured: '已配置 Key', anonymous: '匿名试用 · 共享额度', verified: '当前配置已验证',
|
|
50
|
+
unverified: '已配置 · 尚未验证通过', debug: '调试服务地址', console: '打开飞猪控制台',
|
|
51
|
+
save: '验证并保存', saving: '正在验证…', saveFailed: '新 Key 未保存,旧配置已保留。',
|
|
52
|
+
saved: '验证通过,已保存。', clear: '清除本机 Key', cleared: '本机 Key 已清除,已恢复匿名试用。',
|
|
53
|
+
retry: '重新读取状态', source: '来源', config: '本机配置', env: '环境变量',
|
|
54
|
+
'env-debug': '调试环境变量', none: '共享试用',
|
|
55
|
+
hbcli: 'HotelByte(沙箱)', appKey: 'App Key', appSecret: 'App Secret',
|
|
56
|
+
hbHint: '填写开通沙箱账号后取得的凭证,内容不会进入对话。',
|
|
57
|
+
hbRegistration: '申请沙箱账号', hbUnconfigured: '尚未配置沙箱 API 凭证', hbConfigured: '已配置沙箱 API 凭证',
|
|
58
|
+
hbReadOnly: '当前凭证来自启动环境变量,或本机配置无法安全写入。',
|
|
59
|
+
hbSaveFailed: '新凭证未保存,旧配置已保留。',
|
|
60
|
+
hbSaved: '凭证已保存,只读酒店查询已完成。', hbClear: '清除本机沙箱凭证',
|
|
61
|
+
hbCleared: '本机沙箱 API 凭证已清除。', hbNoSuppliers: '已保存 · 未配置可用供应商',
|
|
62
|
+
hbNoSuppliersHint: '请联系 HotelByte 开通当前账号的供应商。这次结果不能判断目的地无房。',
|
|
63
|
+
hbMiss: '查询通道已验证 · 本次未返回酒店', hbProbeHint: '验证会只读查询上海明天入住的一晚酒店,两位成人。',
|
|
64
|
+
artifacts: 'GoTry 产物', artifactsHint: '查看已保存行程和规划交付物',
|
|
65
|
+
search: '搜索产物', searchPlaceholder: '标题或文件名', refresh: '刷新',
|
|
66
|
+
empty: '这个工作区尚无已保存产物。', noMatches: '没有匹配的产物。', noSession: '打开一个对话后即可查看其工作区产物。',
|
|
67
|
+
previous: '上一页', next: '下一页', html: '打开 HTML 预览', open: '打开预览',
|
|
68
|
+
htmlHint: 'HTML 预览可能运行文档中的脚本。', failed: '无法读取当前状态,请重试。',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function webRequest(path, options) {
|
|
73
|
+
var response = await fetch('api/gotry/' + path, Object.assign({ credentials: 'same-origin' }, options))
|
|
74
|
+
var value = await response.json()
|
|
75
|
+
if (!value || typeof value !== 'object') throw new Error('Invalid Web response')
|
|
76
|
+
if (!response.ok || value.ok === false) throw new Error(value.error || 'Request failed')
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SourceSettings(props) {
|
|
81
|
+
var t = props.t
|
|
82
|
+
var hotelbyte = props.provider === 'hbcli', provider = hotelbyte ? 'hbcli' : 'flyai'
|
|
83
|
+
var state = React.useState(null), status = state[0], setStatus = state[1]
|
|
84
|
+
var draft = React.useState(''), key = draft[0], setKey = draft[1]
|
|
85
|
+
var secretDraft = React.useState(''), secret = secretDraft[0], setSecret = secretDraft[1]
|
|
86
|
+
var noticeState = React.useState(''), notice = noticeState[0], setNotice = noticeState[1]
|
|
87
|
+
var errorState = React.useState(''), error = errorState[0], setError = errorState[1]
|
|
88
|
+
var busyState = React.useState(false), busy = busyState[0], setBusy = busyState[1]
|
|
89
|
+
var reloadState = React.useState(0), reload = reloadState[0], setReload = reloadState[1]
|
|
90
|
+
var lifetime = React.useRef(null), activeRequest = React.useRef(false)
|
|
91
|
+
React.useEffect(function () {
|
|
92
|
+
var controller = new AbortController()
|
|
93
|
+
lifetime.current = controller
|
|
94
|
+
setError('')
|
|
95
|
+
webRequest(provider, { signal: controller.signal }).then(function (value) {
|
|
96
|
+
if (!controller.signal.aborted) setStatus(value)
|
|
97
|
+
}).catch(function () { if (!controller.signal.aborted) setError(t('failed')) })
|
|
98
|
+
return function () { controller.abort(); lifetime.current = null }
|
|
99
|
+
}, [reload])
|
|
100
|
+
async function write(action) {
|
|
101
|
+
if (activeRequest.current || !lifetime.current || !status?.writable) return
|
|
102
|
+
var controller = lifetime.current
|
|
103
|
+
activeRequest.current = true
|
|
104
|
+
setBusy(true); setError(''); setNotice('')
|
|
105
|
+
var candidate = key, candidateSecret = secret
|
|
106
|
+
setKey(''); setSecret('')
|
|
107
|
+
try {
|
|
108
|
+
var payload = action === 'clear' ? { action: action } : hotelbyte
|
|
109
|
+
? { action: action, appKey: candidate, appSecret: candidateSecret } : { action: action, key: candidate }
|
|
110
|
+
var value = await webRequest(provider + '/write', { method: 'POST', signal: controller.signal,
|
|
111
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
112
|
+
if (!controller.signal.aborted) {
|
|
113
|
+
setStatus(value)
|
|
114
|
+
setNotice(hotelbyte && action === 'save' && value.verdict === 'no-suppliers' ? ''
|
|
115
|
+
: t(hotelbyte ? action === 'clear' ? 'hbCleared' : 'hbSaved' : action === 'save' ? 'saved' : 'cleared'))
|
|
116
|
+
}
|
|
117
|
+
} catch (failure) {
|
|
118
|
+
if (!controller.signal.aborted) setError(failure.message || t(hotelbyte ? 'hbSaveFailed' : 'saveFailed'))
|
|
119
|
+
} finally {
|
|
120
|
+
activeRequest.current = false
|
|
121
|
+
if (!controller.signal.aborted) setBusy(false)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (props.view === 'summary') return t('description')
|
|
125
|
+
var disabled = busy || !status?.writable
|
|
126
|
+
var stateLabel = !status ? t('loading') : !status.configured ? t(hotelbyte ? 'hbUnconfigured' : 'anonymous')
|
|
127
|
+
: hotelbyte && status.verdict === 'no-suppliers' ? t('hbNoSuppliers') : hotelbyte && status.verdict === 'miss' ? t('hbMiss')
|
|
128
|
+
: status.verified ? t('verified') : t('unverified')
|
|
129
|
+
var sectionProps = { style: { display: 'grid', gap: '16px', maxWidth: '640px' } }
|
|
130
|
+
sectionProps['data-gotry-' + provider + '-settings'] = ''
|
|
131
|
+
var statusProps = { key: 'status', role: 'status', style: { display: 'grid', gap: '6px' } }
|
|
132
|
+
statusProps['data-gotry-' + provider + '-status'] = ''
|
|
133
|
+
return element('section', sectionProps, [
|
|
134
|
+
element('h3', { key: 'heading', style: { margin: 0 } }, t(provider)),
|
|
135
|
+
element('div', statusProps, [
|
|
136
|
+
element(UI.Tag, { key: 'badge', tone: status?.verified ? 'success' : 'neutral' }, stateLabel),
|
|
137
|
+
status ? element('span', { key: 'source', style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, t('source') + ':' + t(hotelbyte && status.source === 'none' ? 'hbUnconfigured' : status.source)) : null,
|
|
138
|
+
status?.endpointDebug ? element('span', { key: 'debug' }, t('debug') + ':' + status.endpoint) : null,
|
|
139
|
+
]),
|
|
140
|
+
element('a', { key: 'console', href: hotelbyte ? 'https://hotelbyte.com/zh/guides/sandbox-verification' : 'https://flyai.open.fliggy.com/console', target: '_blank', rel: 'noopener noreferrer' }, t(hotelbyte ? 'hbRegistration' : 'console')),
|
|
141
|
+
hotelbyte ? element('p', { key: 'probe-hint', style: { margin: 0, fontSize: '12px' } }, t('hbProbeHint')) : null,
|
|
142
|
+
hotelbyte && status?.verdict === 'no-suppliers' ? element('p', { key: 'supplier-hint', role: 'status', style: { margin: 0 } }, t('hbNoSuppliersHint')) : null,
|
|
143
|
+
element(UI.SettingsForm, { key: 'form', labels: { unavailable: t('loading'), readOnly: t(hotelbyte ? 'hbReadOnly' : 'readOnly'), saveFailed: t(hotelbyte ? 'hbSaveFailed' : 'saveFailed'), save: t('save'), saving: t('saving') },
|
|
144
|
+
state: { available: Boolean(status), writable: Boolean(status?.writable), dirty: key.trim().length > 0 && (!hotelbyte || secret.trim().length > 0), invalid: false, saving: busy, failed: false },
|
|
145
|
+
onSave: function () { void write('save') }, onDiscard: function () { setKey(''); setSecret('') } }, [
|
|
146
|
+
element(UI.SettingsSecretField, { key: 'key', id: 'gotry-' + provider + '-key', label: t(hotelbyte ? 'appKey' : 'key'), hint: t(hotelbyte ? 'hbHint' : 'keyHint'), text: key,
|
|
147
|
+
configured: Boolean(status?.configured), stateLabel: t(status?.configured ? hotelbyte ? 'hbConfigured' : 'configured' : hotelbyte ? 'hbUnconfigured' : 'anonymous'), disabled: disabled,
|
|
148
|
+
onEdit: function (value) { setKey(value); setError(''); setNotice('') } }),
|
|
149
|
+
hotelbyte ? element(UI.SettingsSecretField, { key: 'secret', id: 'gotry-hbcli-secret', label: t('appSecret'), text: secret,
|
|
150
|
+
configured: Boolean(status?.configured), stateLabel: t(status?.configured ? 'hbConfigured' : 'hbUnconfigured'), disabled: disabled,
|
|
151
|
+
onEdit: function (value) { setSecret(value); setError(''); setNotice('') } }) : null,
|
|
152
|
+
]),
|
|
153
|
+
error ? element('p', { key: 'error', role: 'alert', style: { margin: 0, color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
|
|
154
|
+
notice ? element('p', { key: 'notice', role: 'status', style: { margin: 0 } }, notice) : null,
|
|
155
|
+
element('div', { key: 'actions', style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } }, [
|
|
156
|
+
element(UI.Button, { key: 'retry', type: 'button', variant: 'outline', disabled: busy,
|
|
157
|
+
onClick: function () { setReload(function (value) { return value + 1 }) } }, t('retry')),
|
|
158
|
+
status?.configured && status.writable ? element(UI.Button, { key: 'clear', type: 'button', disabled: busy,
|
|
159
|
+
onClick: function () { void write('clear') } }, t(hotelbyte ? 'hbClear' : 'clear')) : null,
|
|
160
|
+
]),
|
|
161
|
+
])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function GoTrySettings(props) {
|
|
165
|
+
if (props.view === 'summary') return props.t('description')
|
|
166
|
+
return element('div', { style: { display: 'grid', gap: '32px' } }, [
|
|
167
|
+
element(SourceSettings, Object.assign({}, props, { key: 'flyai', provider: 'flyai' })),
|
|
168
|
+
element(SourceSettings, Object.assign({}, props, { key: 'hbcli', provider: 'hbcli' })),
|
|
169
|
+
])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ArtifactBrowser(props) {
|
|
173
|
+
var t = props.t, info = props.useTabInfo(), sessionId = props.sessionId
|
|
174
|
+
var cwd = props.useSessions(function (sessions) { return sessions.byId[sessionId]?.cwd })
|
|
175
|
+
var state = React.useState(null), result = state[0], setResult = state[1]
|
|
176
|
+
var queryState = React.useState(''), search = queryState[0], setSearch = queryState[1]
|
|
177
|
+
var pageState = React.useState(0), offset = pageState[0], setOffset = pageState[1]
|
|
178
|
+
var refreshState = React.useState(0), refresh = refreshState[0], setRefresh = refreshState[1]
|
|
179
|
+
var busyState = React.useState(false), busy = busyState[0], setBusy = busyState[1]
|
|
180
|
+
var errorState = React.useState(''), error = errorState[0], setError = errorState[1]
|
|
181
|
+
React.useEffect(function () {
|
|
182
|
+
if (!sessionId || !info.tab.visible) return
|
|
183
|
+
var controller = new AbortController()
|
|
184
|
+
setBusy(true); setError('')
|
|
185
|
+
var query = new URLSearchParams({ sessionId: sessionId, search: search, offset: String(offset), limit: '20' })
|
|
186
|
+
webRequest('artifacts?' + query, { signal: controller.signal }).then(function (value) {
|
|
187
|
+
if (!controller.signal.aborted) setResult(value)
|
|
188
|
+
}).catch(function () { if (!controller.signal.aborted) setError(t('failed')) })
|
|
189
|
+
.finally(function () { if (!controller.signal.aborted) setBusy(false) })
|
|
190
|
+
return function () { controller.abort() }
|
|
191
|
+
}, [sessionId, info.tab.visible, search, offset, refresh])
|
|
192
|
+
function open(path) {
|
|
193
|
+
var normalized = String(path).replace(/\\/g, '/')
|
|
194
|
+
var root = String(cwd || '').replace(/\\/g, '/').replace(/\/$/, '')
|
|
195
|
+
var relative = root && normalized.startsWith(root + '/') ? normalized.slice(root.length + 1) : normalized
|
|
196
|
+
info.tab.actions.openResource('dsh-resource://file/session/' + encodeURIComponent(sessionId) + '/' + relative.split('/').map(encodeURIComponent).join('/'))
|
|
197
|
+
}
|
|
198
|
+
var entries = result?.artifacts || []
|
|
199
|
+
return element('section', { 'data-gotry-artifacts-browser': '', style: { padding: '16px', display: 'grid', alignContent: 'start', gap: '12px', height: '100%', overflow: 'auto', boxSizing: 'border-box', minWidth: 0 } }, [
|
|
200
|
+
element('div', { key: 'controls', style: { display: 'flex', gap: '8px', alignItems: 'center', minWidth: 0 } }, [
|
|
201
|
+
element(UI.Input, { key: 'search', 'aria-label': t('search'), placeholder: t('searchPlaceholder'), value: search,
|
|
202
|
+
style: { minWidth: 0, width: '100%' }, onChange: function (event) { setSearch(event.target.value); setOffset(0) } }),
|
|
203
|
+
element(UI.Button, { key: 'refresh', size: 'sm', disabled: busy || !sessionId,
|
|
204
|
+
onClick: function () { setRefresh(function (value) { return value + 1 }) } }, busy ? t('loading') : t('refresh')),
|
|
205
|
+
]),
|
|
206
|
+
error ? element('p', { key: 'error', role: 'alert' }, error) : null,
|
|
207
|
+
!sessionId ? element('p', { key: 'no-session' }, t('noSession')) : !busy && entries.length === 0 ? element('p', { key: 'empty' }, t(search ? 'noMatches' : 'empty')) : null,
|
|
208
|
+
element('div', { key: 'list', 'aria-busy': busy, style: { display: 'grid', gap: '8px' } }, entries.map(function (entry) {
|
|
209
|
+
var html = /\.html?$/i.test(entry.path)
|
|
210
|
+
return element(UI.Button, { key: entry.path, variant: 'outline', 'data-gotry-artifact-entry': entry.path,
|
|
211
|
+
title: html ? t('htmlHint') : entry.path, onClick: function () { open(entry.path) },
|
|
212
|
+
style: { width: '100%', height: 'auto', padding: '12px', textAlign: 'left', justifyContent: 'flex-start', minWidth: 0 } }, [
|
|
213
|
+
element(UI.FileTypeIcon, { key: 'icon', path: entry.path }),
|
|
214
|
+
element('span', { key: 'body', style: { display: 'grid', gap: '4px', minWidth: 0, flex: 1 } }, [
|
|
215
|
+
element('span', { key: 'title', style: { overflowWrap: 'anywhere' } }, entry.title),
|
|
216
|
+
element('span', { key: 'meta', style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)', overflowWrap: 'anywhere' } },
|
|
217
|
+
[entry.status, entry.updated ? new Date(entry.updated).toLocaleString() : '', html ? t('html') : t('open')].filter(Boolean).join(' · ')),
|
|
218
|
+
]),
|
|
219
|
+
])
|
|
220
|
+
})),
|
|
221
|
+
result?.total > 0 ? element('div', { key: 'pagination', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', flexWrap: 'wrap' } }, [
|
|
222
|
+
element(UI.Button, { key: 'previous', size: 'sm', disabled: busy || offset === 0, onClick: function () { setOffset(Math.max(0, offset - 20)) } }, t('previous')),
|
|
223
|
+
element('span', { key: 'count', role: 'status', style: { fontSize: '12px' } }, (entries.length ? offset + 1 : 0) + '–' + (offset + entries.length) + ' / ' + result.total),
|
|
224
|
+
element(UI.Button, { key: 'next', size: 'sm', disabled: busy || !result.truncated, onClick: function () { setOffset(result.nextOffset) } }, t('next')),
|
|
225
|
+
]) : null,
|
|
226
|
+
])
|
|
227
|
+
}
|
|
15
228
|
|
|
16
229
|
function element(type, props, children) {
|
|
17
230
|
return React.createElement(type, props, ...(Array.isArray(children) ? children : [children]))
|
|
@@ -194,6 +407,21 @@ window.__ModuleLoader__.load({
|
|
|
194
407
|
return scope.slots.register({ name: 'tool.call.toolview', key: 'gotry_artifacts_read' }, ArtifactToolView)
|
|
195
408
|
})
|
|
196
409
|
})
|
|
410
|
+
ctx.inject(['slots', 'locale'], function (scope) {
|
|
411
|
+
var ns = 'gotry.web'
|
|
412
|
+
var t = scope.locale.bind(ns)
|
|
413
|
+
scope.effect(function () { return scope.locale.register(ns, webLocale) })
|
|
414
|
+
scope.slots.inject('plugins.item', function () {
|
|
415
|
+
return scope.slots.register({ name: 'plugins.item', id: 'gotry', order: 50, label: function () { return t('title') }, locale: ns }, GoTrySettings)
|
|
416
|
+
})
|
|
417
|
+
scope.inject(['sidebarRightTabs'], function (sidebar) {
|
|
418
|
+
sidebar.effect(function () { return sidebar.sidebarRightTabs.register({ id: '@danceiny/gotry:artifacts', kind: 'gotry-artifacts',
|
|
419
|
+
title: function () { return t('artifacts') }, guide: [{ id: 'gotry-artifacts', order: 15, title: function () { return t('artifacts') }, description: function () { return t('artifactsHint') } }] }) })
|
|
420
|
+
sidebar.slots.inject('sidebar.right.pane.tab', function () {
|
|
421
|
+
return sidebar.slots.register({ name: 'sidebar.right.pane.tab', key: '@danceiny/gotry:artifacts', locale: ns }, ArtifactBrowser)
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
})
|
|
197
425
|
}
|
|
198
426
|
|
|
199
427
|
exports.apply = apply
|
|
@@ -19,6 +19,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { join, resolve } from 'node:path';
|
|
22
|
+
import { checkExtensionStatus } from '../../bin/gotry-extension-status.js';
|
|
23
|
+
import { HBCLI_RELEASE_VERSION, HBCLI_INSTALL_CMD } from '../../bin/gotry-hbcli-release.js';
|
|
22
24
|
import { readLatestChannelEvents } from './channel-health.js';
|
|
23
25
|
import { displayEndpoint, endpointFingerprint, readFlyaiVerification, resolveFlyaiEndpoint, resolveFlyaiKey, sha256Hex, } from './flyai-config.js';
|
|
24
26
|
/** hbcli 已知安装位(与 capabilities/hbcli.ts hbcliBinCandidates 同清单) */
|
|
@@ -109,10 +111,8 @@ export function nodeOk(version) {
|
|
|
109
111
|
const [maj = '0', min = '0'] = version.split('.');
|
|
110
112
|
return Number(maj) > 22 || (Number(maj) === 22 && Number(min) >= 15);
|
|
111
113
|
}
|
|
112
|
-
/** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖)
|
|
113
|
-
|
|
114
|
-
* 当 bearer 发到 hotel-be,让 trade.* / search/checkAvail 全部 401。 */
|
|
115
|
-
const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? '0.0.3';
|
|
114
|
+
/** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖),与 CLI doctor 共用发布基线。 */
|
|
115
|
+
const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? HBCLI_RELEASE_VERSION;
|
|
116
116
|
/** 单项检查:全部只读、永不抛错;env/homeDir/repoRoot 可注入(测试确定性) */
|
|
117
117
|
export async function runDoctorChecks(opts = {}) {
|
|
118
118
|
const repoRoot = opts.repoRoot ? resolve(opts.repoRoot) : resolve(import.meta.dirname, '..', '..');
|
|
@@ -124,17 +124,8 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
124
124
|
items.push(nodeOk(nodeVersion)
|
|
125
125
|
? { id: 'node', label: 'Node 运行时', status: 'ok', detail: `Node ${nodeVersion}(≥22.15)` }
|
|
126
126
|
: { id: 'node', label: 'Node 运行时', status: 'missing', detail: `Node ${nodeVersion} 过旧(gotry 需 ≥22.15)`, fix: '升级 Node.js 至 22.15+(https://nodejs.org)' });
|
|
127
|
-
// 1.
|
|
128
|
-
|
|
129
|
-
// missing detail 明示「商店版用户可忽略本项」,不再把商店版用户误导成本地通道缺失
|
|
130
|
-
const extManifest = join(home, '.gotry', 'extension', 'manifest.json');
|
|
131
|
-
items.push(existsSync(extManifest)
|
|
132
|
-
? { id: 'extension', label: 'Stai Travel Bridge 扩展', status: 'ok', detail: `已就位(本地通道:${extManifest})` }
|
|
133
|
-
: {
|
|
134
|
-
id: 'extension', label: 'Stai Travel Bridge 扩展', status: 'missing',
|
|
135
|
-
detail: '本地通道未落位(~/.gotry/extension/manifest.json 不存在)。影响面:gotry_session_search / gotry_session_login(账号会话通道)不可用;携程机票/酒店实时检索、12306 余票、Dida 实时报价都依赖此通道。其它工具(机票 FlyAI、酒店 hbcli、地图、天气等)不受影响。若你已从 Chrome 商店安装(自动更新,商店版 ID oeajpicc…),本项可忽略——会话检索可用性以运行时为准(gotry_session_search 的 verdict)。若未安装:应用商店一键装即可',
|
|
136
|
-
fix: '在 Chrome 应用商店一键安装(自动更新): https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd',
|
|
137
|
-
});
|
|
127
|
+
// 1. CLI 与工具层共用只读检测:安装、落位、连接是不同证据。
|
|
128
|
+
items.push({ id: 'extension', label: 'Stai Travel Bridge 扩展', ...await checkExtensionStatus({ homeDir: home, env, ports: opts.extensionPorts }) });
|
|
138
129
|
// 2. agent-reach(网页/社媒读取;.venv 装在包内,与 agent-reach.ts venvPython 同位)
|
|
139
130
|
const venvPython = join(repoRoot, '.venv/bin/python');
|
|
140
131
|
const reachBin = join(repoRoot, '.venv/bin/agent-reach');
|
|
@@ -157,7 +148,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
157
148
|
}
|
|
158
149
|
// 3. hbcli(酒店实时源;静态包自动降级,缺了不致命)
|
|
159
150
|
// 裸名 'hbcli' 靠 PATH 解析(existsSync 对裸名是 cwd 相对,无意义)——先探测再落位
|
|
160
|
-
// 版本 < MIN_HBCLI_VERSION 视为 missing
|
|
151
|
+
// 版本 < MIN_HBCLI_VERSION 视为 missing,修复走版本钉扎的官方安装器
|
|
161
152
|
let hbPresent = '';
|
|
162
153
|
for (const p of hbcliCandidates(home)) {
|
|
163
154
|
if (p !== 'hbcli' && existsSync(p)) {
|
|
@@ -175,8 +166,8 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
175
166
|
if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
|
|
176
167
|
items.push({
|
|
177
168
|
id: 'hbcli', label: 'hbcli(酒店实时源)', status: 'missing',
|
|
178
|
-
detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})
|
|
179
|
-
fix:
|
|
169
|
+
detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——请升级到支持当前认证与酒店配置的版本`,
|
|
170
|
+
fix: HBCLI_INSTALL_CMD,
|
|
180
171
|
});
|
|
181
172
|
}
|
|
182
173
|
else {
|
|
@@ -202,7 +193,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
202
193
|
: {
|
|
203
194
|
id: 'hbcli', label: 'hbcli(酒店实时源)', status: 'degraded',
|
|
204
195
|
detail: '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
|
|
205
|
-
fix: 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(
|
|
196
|
+
fix: 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(客户邮箱验证码登录,新邮箱即注册);B 端租户注册走 hbcli auth register(先 auth send-code);或在 插件 → GoTry → HotelByte 配置沙箱 App Key/App Secret',
|
|
206
197
|
});
|
|
207
198
|
}
|
|
208
199
|
}
|
|
@@ -229,7 +220,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
229
220
|
items.push({
|
|
230
221
|
id: 'flyai', label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪)', status: 'degraded',
|
|
231
222
|
detail: `未配置 key——匿名试用中(共享额度易达限;达限报 Trial limit reached)${flyaiQuotaNote}。影响面:共享池额度小,频繁会话易触顶;达限本会话内 gotry_flyai_search(机票/酒店/票务/AI/万豪 8 类)失败,改走 gotry_session_search 账号会话通道。`,
|
|
232
|
-
fix: '
|
|
223
|
+
fix: 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`:① 打开 https://flyai.open.fliggy.com/console 复制 API Key;② 在终端隐藏粘贴;③ 验证后保存。也可运行 `npx @danceiny/gotry doctor --fix` 按提示配置',
|
|
233
224
|
});
|
|
234
225
|
}
|
|
235
226
|
else {
|
|
@@ -254,7 +245,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
254
245
|
items.push({
|
|
255
246
|
id: 'flyai', label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪)', status: 'degraded',
|
|
256
247
|
detail: `${reason}(${sourceText};endpoint ${displayEndpoint(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})——非空不等于鉴权通过`,
|
|
257
|
-
fix: '
|
|
248
|
+
fix: 'Web 打开「插件 → GoTry → FlyAI」重新验证;或运行 `npx @danceiny/gotry setup flyai`;`npx @danceiny/gotry setup flyai --clear` 回退匿名',
|
|
258
249
|
});
|
|
259
250
|
}
|
|
260
251
|
}
|
|
@@ -264,7 +255,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
264
255
|
? { id: 'sidebar', label: 'dsh-better-sidebar(侧栏工作台)', status: 'ok', detail: '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' }
|
|
265
256
|
: {
|
|
266
257
|
id: 'sidebar', label: 'dsh-better-sidebar(侧栏工作台)', status: 'missing',
|
|
267
|
-
detail: '
|
|
258
|
+
detail: '未安装此增强组件——Web 自带 GoTry 产物页与原生预览;可选补装以增强文件浏览',
|
|
268
259
|
fix: 'npx @danceiny/gotry doctor --fix',
|
|
269
260
|
});
|
|
270
261
|
// 6. dsh-calendar(patch 分发面宿主插件;D-9 拍板:默认不挂载)
|
|
@@ -371,7 +362,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
371
362
|
const broken = items.filter(i => i.status !== 'ok' && i.id !== 'llm-key');
|
|
372
363
|
const summary = broken.length === 0
|
|
373
364
|
? `体检通过:${items.length} 项全部就绪(可选依赖齐,LLM key 归 dsh 宿主管)。`
|
|
374
|
-
: `体检发现 ${broken.length} 项待处理:${broken.map(i => `${i.label}(${i.status === 'missing' ? '未装' : '半可用'})`).join('、')}。补装:终端跑 npx @danceiny/gotry doctor --fix,或按各项 fix 指引逐项处理。`;
|
|
365
|
+
: `体检发现 ${broken.length} 项待处理:${broken.map(i => `${i.label}(${i.status === 'missing' ? '未装' : i.id === 'extension' ? '待确认' : '半可用'})`).join('、')}。补装:终端跑 npx @danceiny/gotry doctor --fix,或按各项 fix 指引逐项处理。`;
|
|
375
366
|
return { ok: broken.length === 0, items, summary };
|
|
376
367
|
}
|
|
377
368
|
/** 修复指引入表:命令类才加反引号(prose 类如「到控制台申请 key」原样) */
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
* 本层只让「每条通道」更可替换、可熔断、可 mock。
|
|
24
24
|
*/
|
|
25
25
|
import { CircuitBreaker, withRetry, } from './resilience.js';
|
|
26
|
-
import { flyaiSearch } from './flyai.js';
|
|
26
|
+
import { flyaiBreakerScope, flyaiSearch } from './flyai.js';
|
|
27
27
|
import { checkAvail, hotelRates, searchHotels } from './hbcli.js';
|
|
28
|
-
import { sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, sessionDidaSearch } from './session-search.js';
|
|
28
|
+
import { sessionFlightSearch, sessionFlightGuiSearch, sessionHotelGuiSearch, sessionHotelSearch, sessionTrainSearch, sessionDidaSearch } from './session-search.js';
|
|
29
29
|
import { geocodePlace, getClimate, getForecast } from './weather.js';
|
|
30
30
|
import { verifyFlight } from './opensky.js';
|
|
31
31
|
import { anythingSearch } from './anything.js';
|
|
@@ -86,8 +86,12 @@ const DEFAULT_HANDLERS = {
|
|
|
86
86
|
HBCLI_CHECK_AVAIL: (p) => checkAvail({ ratePkgId: p.ratePkgId }, { hbcliBin: p.hbcliBin, timeoutMs: p.timeoutMs }),
|
|
87
87
|
/** 用户本人登录态会话检索(浏览器解译通道:扩展桥默认 + cdp 显式后备;ReadGuard+节律闸) */
|
|
88
88
|
SESSION_FLIGHT_SEARCH: (p) => sessionFlightSearch(p),
|
|
89
|
+
/** 携程机票 GUI 车道(2026-10-14 实装;gotry 隔离匿名窗口驱动 PC 表单,往返/任意城市原生;匿名公开价) */
|
|
90
|
+
SESSION_FLIGHT_GUI_SEARCH: (p) => sessionFlightGuiSearch(p),
|
|
89
91
|
/** 用户本人登录态会话酒店检索(2026-09-03 实装;同浏览器解译通道,风控红线同款) */
|
|
90
92
|
SESSION_HOTEL_SEARCH: (p) => sessionHotelSearch(p),
|
|
93
|
+
/** 携程酒店 GUI 车道(2026-10-14 实装,#664;匿名窗口驱动表单,cityId 码表退出主链路) */
|
|
94
|
+
SESSION_HOTEL_GUI_SEARCH: (p) => sessionHotelGuiSearch(p),
|
|
91
95
|
/** 12306 余票会话检索(2026-09-03 实装;公开查询面,无登录闸;同浏览器解译通道) */
|
|
92
96
|
SESSION_TRAIN_SEARCH: (p) => sessionTrainSearch(p),
|
|
93
97
|
/** Dida 供应商门户实时价会话检索(2026-09-09 实装;hotel-be portal integration 迁移线) */
|
|
@@ -133,8 +137,8 @@ const hbcliTimeout = (r) => {
|
|
|
133
137
|
const HBCLI_RETRY = { maxAttempts: 2, baseDelayMs: 300, maxDelayMs: 1_000 };
|
|
134
138
|
/**
|
|
135
139
|
* 渠道韧性策略表(权威面;docs/design/effect-interpreter.md §3 同表逐行有依据):
|
|
136
|
-
* - FLYAI
|
|
137
|
-
*
|
|
140
|
+
* - FLYAI:明确瞬时故障重试 1 次;同 kind/endpoint/key 连续 3 次明确
|
|
141
|
+
* 上游健康故障(含 Sentinel)熔断 60s;业务、配置与未知失败不计熔断;
|
|
138
142
|
* - HBCLI:仅 timeout 类失败重试 1 次(冷启动建后端 session 可超时,重试即恢复);
|
|
139
143
|
* ENOENT/退码类永不重试(上游契约「候选路径是切换不是重试」),熔断同上;
|
|
140
144
|
* RATES/CHECK_AVAIL 同族,且价格面无静态降级(fail-closed,不估算房价——
|
|
@@ -159,7 +163,12 @@ const SPECS = {
|
|
|
159
163
|
// Only explicit transient classification permits retry; terminal failures stay final.
|
|
160
164
|
return result?.retryable === true;
|
|
161
165
|
},
|
|
162
|
-
isFailure:
|
|
166
|
+
isFailure: r => r?.upstreamHealth === 'unhealthy',
|
|
167
|
+
isNeutral: r => {
|
|
168
|
+
const health = r?.upstreamHealth;
|
|
169
|
+
return health !== 'healthy' && health !== 'unhealthy';
|
|
170
|
+
},
|
|
171
|
+
breakerScope: p => flyaiBreakerScope(p?.kind),
|
|
163
172
|
},
|
|
164
173
|
HBCLI_HOTEL_SEARCH: {
|
|
165
174
|
channel: 'cli',
|
|
@@ -190,12 +199,24 @@ const SPECS = {
|
|
|
190
199
|
breaker: null,
|
|
191
200
|
isFailure: defaultIsFailure,
|
|
192
201
|
},
|
|
202
|
+
SESSION_FLIGHT_GUI_SEARCH: {
|
|
203
|
+
channel: 'browser',
|
|
204
|
+
retry: null,
|
|
205
|
+
breaker: null,
|
|
206
|
+
isFailure: defaultIsFailure,
|
|
207
|
+
},
|
|
193
208
|
SESSION_HOTEL_SEARCH: {
|
|
194
209
|
channel: 'browser',
|
|
195
210
|
retry: null,
|
|
196
211
|
breaker: null,
|
|
197
212
|
isFailure: defaultIsFailure,
|
|
198
213
|
},
|
|
214
|
+
SESSION_HOTEL_GUI_SEARCH: {
|
|
215
|
+
channel: 'browser',
|
|
216
|
+
retry: null,
|
|
217
|
+
breaker: null,
|
|
218
|
+
isFailure: defaultIsFailure,
|
|
219
|
+
},
|
|
199
220
|
SESSION_TRAIN_SEARCH: {
|
|
200
221
|
channel: 'browser',
|
|
201
222
|
retry: null,
|
|
@@ -286,9 +307,12 @@ export function declinedObservation(effect, trace) {
|
|
|
286
307
|
summary: trace.declined === 'aborted'
|
|
287
308
|
? `${effect} 检索已取消,未产生新的查询结果。`
|
|
288
309
|
: trace.declined === 'circuit-open'
|
|
289
|
-
?
|
|
310
|
+
? trace.breaker === 'half-open'
|
|
311
|
+
? `${effect} 通道的恢复探测正在进行;等待当前探测结果后再查询,或使用其他工具。`
|
|
312
|
+
: `${effect} 通道被断路器开启保护(连续上游故障达到阈值,冷却中);${typeof trace.retryAfterMs === 'number' ? `约 ${Math.ceil(trace.retryAfterMs / 1_000)} 秒后可发起恢复探测` : '稍后可发起恢复探测'},当前可使用其他工具。`
|
|
290
313
|
: `${effect} 未登记效应(effect_interpreter.v1 注册表外,生产解译器拒绝)`,
|
|
291
314
|
evidence: trace.evidence.join(';'),
|
|
315
|
+
...(trace.retryAfterMs !== undefined ? { retryAfterMs: trace.retryAfterMs } : {}),
|
|
292
316
|
};
|
|
293
317
|
}
|
|
294
318
|
const defaultBreakers = new Map();
|
|
@@ -314,16 +338,18 @@ export function makeProductionInterpreter(opts = {}) {
|
|
|
314
338
|
const br = (() => {
|
|
315
339
|
if (!spec.breaker)
|
|
316
340
|
return null;
|
|
317
|
-
const
|
|
341
|
+
const key = spec.breakerScope ? `${fx.effect}:${spec.breakerScope(fx.params)}` : fx.effect;
|
|
342
|
+
const existing = breakers.get(key);
|
|
318
343
|
if (existing)
|
|
319
344
|
return existing;
|
|
320
345
|
const created = new CircuitBreaker({ ...spec.breaker, ...(opts.now ? { now: opts.now } : {}) });
|
|
321
|
-
breakers.set(
|
|
346
|
+
breakers.set(key, created);
|
|
322
347
|
return created;
|
|
323
348
|
})();
|
|
324
349
|
const gate = br?.canAttempt() ?? { allowed: true, state: 'closed', token: undefined };
|
|
325
350
|
if (!gate.allowed) {
|
|
326
|
-
const
|
|
351
|
+
const retryAfterMs = 'retryAfterMs' in gate ? gate.retryAfterMs : undefined;
|
|
352
|
+
const trace = { effect: fx.effect, channel: spec.channel, attempts: 0, backoffMs: 0, breaker: gate.state, declined: 'circuit-open', ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), evidence: [`[效应:${fx.effect}@${ts}] breaker=${gate.state} 拒绝${retryAfterMs !== undefined ? ` retryAfterMs=${retryAfterMs}` : '(恢复探测在途)'}`] };
|
|
327
353
|
return { result: null, trace };
|
|
328
354
|
}
|
|
329
355
|
const policy = spec.retry
|
|
@@ -346,9 +372,16 @@ export function makeProductionInterpreter(opts = {}) {
|
|
|
346
372
|
};
|
|
347
373
|
return { result: null, trace: abortedTrace };
|
|
348
374
|
}
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
375
|
+
const neutral = spec.isNeutral?.(outcome.result, outcome.error) === true;
|
|
376
|
+
const failed = !neutral && (outcome.error != null || spec.isFailure(outcome.result));
|
|
377
|
+
if (br) {
|
|
378
|
+
if (neutral)
|
|
379
|
+
br.releaseProbe(gate.token);
|
|
380
|
+
else if (failed)
|
|
381
|
+
br.onFailure(gate.token);
|
|
382
|
+
else
|
|
383
|
+
br.onSuccess(gate.token);
|
|
384
|
+
}
|
|
352
385
|
const breakerState = br?.state() ?? 'off';
|
|
353
386
|
const evidence = [`[效应:${fx.effect}@${ts}] attempts=${outcome.attempts} backoff=${outcome.backoffMs}ms breaker=${breakerState}`];
|
|
354
387
|
return {
|