@danceiny/gotry 0.2.0-rc.30 → 0.2.0-rc.31
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 +56 -83
- 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-inner.js +2 -1
- package/client/client.js +183 -0
- package/dist/capabilities/doctor.js +7 -15
- 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 +14 -4
- package/dist/scripts/doctor-tests.js +36 -7
- package/dist/scripts/dsh-artifact-web-e2e.js +204 -43
- package/dist/scripts/flyai-setup-tests.js +75 -2
- package/dist/scripts/gotry-web-api-tests.js +114 -0
- package/dist/scripts/gotry-web-client-tests.js +28 -0
- package/dist/scripts/issue-271-liveness-tests.js +3 -1
- package/dist/src/artifact-delivery.js +26 -0
- package/dist/src/gotry-web-api.js +157 -0
- package/dist/src/index.js +6 -0
- package/extension/manifest.json +2 -2
- package/package.json +7 -2
- package/ts/src/artifact-delivery.ts +25 -0
- package/ts/src/gotry-web-api.ts +140 -0
- package/ts/src/index.ts +6 -0
package/client/client.js
CHANGED
|
@@ -12,6 +12,174 @@ 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
|
+
artifacts: 'GoTry artifacts', artifactsHint: 'Browse saved itineraries and planning deliveries',
|
|
30
|
+
search: 'Search artifacts', searchPlaceholder: 'Title or filename', refresh: 'Refresh',
|
|
31
|
+
empty: 'No saved artifacts in this workspace.', noMatches: 'No matching artifacts.', noSession: 'Open a conversation to browse its workspace.',
|
|
32
|
+
previous: 'Previous', next: 'Next', html: 'Open HTML preview', open: 'Open preview',
|
|
33
|
+
htmlHint: 'HTML previews may run the document’s scripts.', failed: 'Could not read the current state. Please retry.',
|
|
34
|
+
},
|
|
35
|
+
zh: {
|
|
36
|
+
title: 'GoTry', description: '旅行数据源与已保存行程', flyai: 'FlyAI(飞猪)',
|
|
37
|
+
loading: '正在读取…', unavailable: '配置暂时不可用,请重新读取当前状态。',
|
|
38
|
+
readOnly: '当前 Key 来自启动环境变量,或此平台无法在这里安全保存。',
|
|
39
|
+
key: 'API Key', keyHint: '粘贴飞猪控制台中的 Key,凭据不会进入对话。',
|
|
40
|
+
configured: '已配置 Key', anonymous: '匿名试用 · 共享额度', verified: '当前配置已验证',
|
|
41
|
+
unverified: '已配置 · 尚未验证通过', debug: '调试服务地址', console: '打开飞猪控制台',
|
|
42
|
+
save: '验证并保存', saving: '正在验证…', saveFailed: '新 Key 未保存,旧配置已保留。',
|
|
43
|
+
saved: '验证通过,已保存。', clear: '清除本机 Key', cleared: '本机 Key 已清除,已恢复匿名试用。',
|
|
44
|
+
retry: '重新读取状态', source: '来源', config: '本机配置', env: '环境变量',
|
|
45
|
+
'env-debug': '调试环境变量', none: '共享试用',
|
|
46
|
+
artifacts: 'GoTry 产物', artifactsHint: '查看已保存行程和规划交付物',
|
|
47
|
+
search: '搜索产物', searchPlaceholder: '标题或文件名', refresh: '刷新',
|
|
48
|
+
empty: '这个工作区尚无已保存产物。', noMatches: '没有匹配的产物。', noSession: '打开一个对话后即可查看其工作区产物。',
|
|
49
|
+
previous: '上一页', next: '下一页', html: '打开 HTML 预览', open: '打开预览',
|
|
50
|
+
htmlHint: 'HTML 预览可能运行文档中的脚本。', failed: '无法读取当前状态,请重试。',
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function webRequest(path, options) {
|
|
55
|
+
var response = await fetch('api/gotry/' + path, Object.assign({ credentials: 'same-origin' }, options))
|
|
56
|
+
var value = await response.json()
|
|
57
|
+
if (!value || typeof value !== 'object') throw new Error('Invalid Web response')
|
|
58
|
+
if (!response.ok || value.ok === false) throw new Error(value.error || 'Request failed')
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function FlyaiSettings(props) {
|
|
63
|
+
var t = props.t
|
|
64
|
+
var state = React.useState(null), status = state[0], setStatus = state[1]
|
|
65
|
+
var draft = React.useState(''), key = draft[0], setKey = draft[1]
|
|
66
|
+
var noticeState = React.useState(''), notice = noticeState[0], setNotice = noticeState[1]
|
|
67
|
+
var errorState = React.useState(''), error = errorState[0], setError = errorState[1]
|
|
68
|
+
var busyState = React.useState(false), busy = busyState[0], setBusy = busyState[1]
|
|
69
|
+
var reloadState = React.useState(0), reload = reloadState[0], setReload = reloadState[1]
|
|
70
|
+
var lifetime = React.useRef(null), activeRequest = React.useRef(false)
|
|
71
|
+
React.useEffect(function () {
|
|
72
|
+
var controller = new AbortController()
|
|
73
|
+
lifetime.current = controller
|
|
74
|
+
setError('')
|
|
75
|
+
webRequest('flyai', { signal: controller.signal }).then(function (value) {
|
|
76
|
+
if (!controller.signal.aborted) setStatus(value)
|
|
77
|
+
}).catch(function () { if (!controller.signal.aborted) setError(t('failed')) })
|
|
78
|
+
return function () { controller.abort(); lifetime.current = null }
|
|
79
|
+
}, [reload])
|
|
80
|
+
async function write(action) {
|
|
81
|
+
if (activeRequest.current || !lifetime.current || !status?.writable) return
|
|
82
|
+
var controller = lifetime.current
|
|
83
|
+
activeRequest.current = true
|
|
84
|
+
setBusy(true); setError(''); setNotice('')
|
|
85
|
+
var candidate = key
|
|
86
|
+
setKey('')
|
|
87
|
+
try {
|
|
88
|
+
var value = await webRequest('flyai/write', { method: 'POST', signal: controller.signal,
|
|
89
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(action === 'save' ? { action: action, key: candidate } : { action: action }) })
|
|
90
|
+
if (!controller.signal.aborted) { setStatus(value); setNotice(t(action === 'save' ? 'saved' : 'cleared')) }
|
|
91
|
+
} catch (failure) {
|
|
92
|
+
if (!controller.signal.aborted) setError(failure.message || t('saveFailed'))
|
|
93
|
+
} finally {
|
|
94
|
+
activeRequest.current = false
|
|
95
|
+
if (!controller.signal.aborted) setBusy(false)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (props.view === 'summary') return t('description')
|
|
99
|
+
var disabled = busy || !status?.writable
|
|
100
|
+
var stateLabel = !status ? t('loading') : !status.configured ? t('anonymous') : status.verified ? t('verified') : t('unverified')
|
|
101
|
+
return element('section', { 'data-gotry-flyai-settings': '', style: { display: 'grid', gap: '16px', maxWidth: '640px' } }, [
|
|
102
|
+
element('h3', { key: 'heading', style: { margin: 0 } }, t('flyai')),
|
|
103
|
+
element('div', { key: 'status', role: 'status', 'data-gotry-flyai-status': '', style: { display: 'grid', gap: '6px' } }, [
|
|
104
|
+
element(UI.Tag, { key: 'badge', tone: status?.verified ? 'success' : 'neutral' }, stateLabel),
|
|
105
|
+
status ? element('span', { key: 'source', style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, t('source') + ':' + t(status.source)) : null,
|
|
106
|
+
status?.endpointDebug ? element('span', { key: 'debug' }, t('debug') + ':' + status.endpoint) : null,
|
|
107
|
+
]),
|
|
108
|
+
element('a', { key: 'console', href: 'https://flyai.open.fliggy.com/console', target: '_blank', rel: 'noopener noreferrer' }, t('console')),
|
|
109
|
+
element(UI.SettingsForm, { key: 'form', labels: { unavailable: t('loading'), readOnly: t('readOnly'), saveFailed: t('saveFailed'), save: t('save'), saving: t('saving') },
|
|
110
|
+
state: { available: Boolean(status), writable: Boolean(status?.writable), dirty: key.trim().length > 0, invalid: false, saving: busy, failed: false },
|
|
111
|
+
onSave: function () { void write('save') }, onDiscard: function () { setKey('') } }, [
|
|
112
|
+
element(UI.SettingsSecretField, { key: 'key', id: 'gotry-flyai-key', label: t('key'), hint: t('keyHint'), text: key,
|
|
113
|
+
configured: Boolean(status?.configured), stateLabel: t(status?.configured ? 'configured' : 'anonymous'), disabled: disabled,
|
|
114
|
+
onEdit: function (value) { setKey(value); setError(''); setNotice('') } }),
|
|
115
|
+
]),
|
|
116
|
+
error ? element('p', { key: 'error', role: 'alert', style: { margin: 0, color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
|
|
117
|
+
notice ? element('p', { key: 'notice', role: 'status', style: { margin: 0 } }, notice) : null,
|
|
118
|
+
element('div', { key: 'actions', style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } }, [
|
|
119
|
+
element(UI.Button, { key: 'retry', type: 'button', variant: 'outline', disabled: busy,
|
|
120
|
+
onClick: function () { setReload(function (value) { return value + 1 }) } }, t('retry')),
|
|
121
|
+
status?.configured && status.writable ? element(UI.Button, { key: 'clear', type: 'button', disabled: busy,
|
|
122
|
+
onClick: function () { void write('clear') } }, t('clear')) : null,
|
|
123
|
+
]),
|
|
124
|
+
])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ArtifactBrowser(props) {
|
|
128
|
+
var t = props.t, info = props.useTabInfo(), sessionId = props.sessionId
|
|
129
|
+
var cwd = props.useSessions(function (sessions) { return sessions.byId[sessionId]?.cwd })
|
|
130
|
+
var state = React.useState(null), result = state[0], setResult = state[1]
|
|
131
|
+
var queryState = React.useState(''), search = queryState[0], setSearch = queryState[1]
|
|
132
|
+
var pageState = React.useState(0), offset = pageState[0], setOffset = pageState[1]
|
|
133
|
+
var refreshState = React.useState(0), refresh = refreshState[0], setRefresh = refreshState[1]
|
|
134
|
+
var busyState = React.useState(false), busy = busyState[0], setBusy = busyState[1]
|
|
135
|
+
var errorState = React.useState(''), error = errorState[0], setError = errorState[1]
|
|
136
|
+
React.useEffect(function () {
|
|
137
|
+
if (!sessionId || !info.tab.visible) return
|
|
138
|
+
var controller = new AbortController()
|
|
139
|
+
setBusy(true); setError('')
|
|
140
|
+
var query = new URLSearchParams({ sessionId: sessionId, search: search, offset: String(offset), limit: '20' })
|
|
141
|
+
webRequest('artifacts?' + query, { signal: controller.signal }).then(function (value) {
|
|
142
|
+
if (!controller.signal.aborted) setResult(value)
|
|
143
|
+
}).catch(function () { if (!controller.signal.aborted) setError(t('failed')) })
|
|
144
|
+
.finally(function () { if (!controller.signal.aborted) setBusy(false) })
|
|
145
|
+
return function () { controller.abort() }
|
|
146
|
+
}, [sessionId, info.tab.visible, search, offset, refresh])
|
|
147
|
+
function open(path) {
|
|
148
|
+
var normalized = String(path).replace(/\\/g, '/')
|
|
149
|
+
var root = String(cwd || '').replace(/\\/g, '/').replace(/\/$/, '')
|
|
150
|
+
var relative = root && normalized.startsWith(root + '/') ? normalized.slice(root.length + 1) : normalized
|
|
151
|
+
info.tab.actions.openResource('dsh-resource://file/session/' + encodeURIComponent(sessionId) + '/' + relative.split('/').map(encodeURIComponent).join('/'))
|
|
152
|
+
}
|
|
153
|
+
var entries = result?.artifacts || []
|
|
154
|
+
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 } }, [
|
|
155
|
+
element('div', { key: 'controls', style: { display: 'flex', gap: '8px', alignItems: 'center', minWidth: 0 } }, [
|
|
156
|
+
element(UI.Input, { key: 'search', 'aria-label': t('search'), placeholder: t('searchPlaceholder'), value: search,
|
|
157
|
+
style: { minWidth: 0, width: '100%' }, onChange: function (event) { setSearch(event.target.value); setOffset(0) } }),
|
|
158
|
+
element(UI.Button, { key: 'refresh', size: 'sm', disabled: busy || !sessionId,
|
|
159
|
+
onClick: function () { setRefresh(function (value) { return value + 1 }) } }, busy ? t('loading') : t('refresh')),
|
|
160
|
+
]),
|
|
161
|
+
error ? element('p', { key: 'error', role: 'alert' }, error) : null,
|
|
162
|
+
!sessionId ? element('p', { key: 'no-session' }, t('noSession')) : !busy && entries.length === 0 ? element('p', { key: 'empty' }, t(search ? 'noMatches' : 'empty')) : null,
|
|
163
|
+
element('div', { key: 'list', 'aria-busy': busy, style: { display: 'grid', gap: '8px' } }, entries.map(function (entry) {
|
|
164
|
+
var html = /\.html?$/i.test(entry.path)
|
|
165
|
+
return element(UI.Button, { key: entry.path, variant: 'outline', 'data-gotry-artifact-entry': entry.path,
|
|
166
|
+
title: html ? t('htmlHint') : entry.path, onClick: function () { open(entry.path) },
|
|
167
|
+
style: { width: '100%', height: 'auto', padding: '12px', textAlign: 'left', justifyContent: 'flex-start', minWidth: 0 } }, [
|
|
168
|
+
element(UI.FileTypeIcon, { key: 'icon', path: entry.path }),
|
|
169
|
+
element('span', { key: 'body', style: { display: 'grid', gap: '4px', minWidth: 0, flex: 1 } }, [
|
|
170
|
+
element('span', { key: 'title', style: { overflowWrap: 'anywhere' } }, entry.title),
|
|
171
|
+
element('span', { key: 'meta', style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)', overflowWrap: 'anywhere' } },
|
|
172
|
+
[entry.status, entry.updated ? new Date(entry.updated).toLocaleString() : '', html ? t('html') : t('open')].filter(Boolean).join(' · ')),
|
|
173
|
+
]),
|
|
174
|
+
])
|
|
175
|
+
})),
|
|
176
|
+
result?.total > 0 ? element('div', { key: 'pagination', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', flexWrap: 'wrap' } }, [
|
|
177
|
+
element(UI.Button, { key: 'previous', size: 'sm', disabled: busy || offset === 0, onClick: function () { setOffset(Math.max(0, offset - 20)) } }, t('previous')),
|
|
178
|
+
element('span', { key: 'count', role: 'status', style: { fontSize: '12px' } }, (entries.length ? offset + 1 : 0) + '–' + (offset + entries.length) + ' / ' + result.total),
|
|
179
|
+
element(UI.Button, { key: 'next', size: 'sm', disabled: busy || !result.truncated, onClick: function () { setOffset(result.nextOffset) } }, t('next')),
|
|
180
|
+
]) : null,
|
|
181
|
+
])
|
|
182
|
+
}
|
|
15
183
|
|
|
16
184
|
function element(type, props, children) {
|
|
17
185
|
return React.createElement(type, props, ...(Array.isArray(children) ? children : [children]))
|
|
@@ -194,6 +362,21 @@ window.__ModuleLoader__.load({
|
|
|
194
362
|
return scope.slots.register({ name: 'tool.call.toolview', key: 'gotry_artifacts_read' }, ArtifactToolView)
|
|
195
363
|
})
|
|
196
364
|
})
|
|
365
|
+
ctx.inject(['slots', 'locale'], function (scope) {
|
|
366
|
+
var ns = 'gotry.web'
|
|
367
|
+
var t = scope.locale.bind(ns)
|
|
368
|
+
scope.effect(function () { return scope.locale.register(ns, webLocale) })
|
|
369
|
+
scope.slots.inject('plugins.item', function () {
|
|
370
|
+
return scope.slots.register({ name: 'plugins.item', id: 'gotry', order: 50, label: function () { return t('title') }, locale: ns }, FlyaiSettings)
|
|
371
|
+
})
|
|
372
|
+
scope.inject(['sidebarRightTabs'], function (sidebar) {
|
|
373
|
+
sidebar.effect(function () { return sidebar.sidebarRightTabs.register({ id: '@danceiny/gotry:artifacts', kind: 'gotry-artifacts',
|
|
374
|
+
title: function () { return t('artifacts') }, guide: [{ id: 'gotry-artifacts', order: 15, title: function () { return t('artifacts') }, description: function () { return t('artifactsHint') } }] }) })
|
|
375
|
+
sidebar.slots.inject('sidebar.right.pane.tab', function () {
|
|
376
|
+
return sidebar.slots.register({ name: 'sidebar.right.pane.tab', key: '@danceiny/gotry:artifacts', locale: ns }, ArtifactBrowser)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
})
|
|
197
380
|
}
|
|
198
381
|
|
|
199
382
|
exports.apply = apply
|
|
@@ -19,6 +19,7 @@ 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';
|
|
22
23
|
import { readLatestChannelEvents } from './channel-health.js';
|
|
23
24
|
import { displayEndpoint, endpointFingerprint, readFlyaiVerification, resolveFlyaiEndpoint, resolveFlyaiKey, sha256Hex, } from './flyai-config.js';
|
|
24
25
|
/** hbcli 已知安装位(与 capabilities/hbcli.ts hbcliBinCandidates 同清单) */
|
|
@@ -124,17 +125,8 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
124
125
|
items.push(nodeOk(nodeVersion)
|
|
125
126
|
? { id: 'node', label: 'Node 运行时', status: 'ok', detail: `Node ${nodeVersion}(≥22.15)` }
|
|
126
127
|
: { 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
|
-
});
|
|
128
|
+
// 1. CLI 与工具层共用只读检测:安装、落位、连接是不同证据。
|
|
129
|
+
items.push({ id: 'extension', label: 'Stai Travel Bridge 扩展', ...await checkExtensionStatus({ homeDir: home, env, ports: opts.extensionPorts }) });
|
|
138
130
|
// 2. agent-reach(网页/社媒读取;.venv 装在包内,与 agent-reach.ts venvPython 同位)
|
|
139
131
|
const venvPython = join(repoRoot, '.venv/bin/python');
|
|
140
132
|
const reachBin = join(repoRoot, '.venv/bin/agent-reach');
|
|
@@ -229,7 +221,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
229
221
|
items.push({
|
|
230
222
|
id: 'flyai', label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪)', status: 'degraded',
|
|
231
223
|
detail: `未配置 key——匿名试用中(共享额度易达限;达限报 Trial limit reached)${flyaiQuotaNote}。影响面:共享池额度小,频繁会话易触顶;达限本会话内 gotry_flyai_search(机票/酒店/票务/AI/万豪 8 类)失败,改走 gotry_session_search 账号会话通道。`,
|
|
232
|
-
fix: '
|
|
224
|
+
fix: 'Web 打开「插件 → GoTry → FlyAI」验证并保存;或运行 `npx @danceiny/gotry setup flyai`:① 打开 https://flyai.open.fliggy.com/console 复制 API Key;② 在终端隐藏粘贴;③ 验证后保存。也可运行 `npx @danceiny/gotry doctor --fix` 按提示配置',
|
|
233
225
|
});
|
|
234
226
|
}
|
|
235
227
|
else {
|
|
@@ -254,7 +246,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
254
246
|
items.push({
|
|
255
247
|
id: 'flyai', label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪)', status: 'degraded',
|
|
256
248
|
detail: `${reason}(${sourceText};endpoint ${displayEndpoint(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})——非空不等于鉴权通过`,
|
|
257
|
-
fix: '
|
|
249
|
+
fix: 'Web 打开「插件 → GoTry → FlyAI」重新验证;或运行 `npx @danceiny/gotry setup flyai`;`npx @danceiny/gotry setup flyai --clear` 回退匿名',
|
|
258
250
|
});
|
|
259
251
|
}
|
|
260
252
|
}
|
|
@@ -264,7 +256,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
264
256
|
? { id: 'sidebar', label: 'dsh-better-sidebar(侧栏工作台)', status: 'ok', detail: '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' }
|
|
265
257
|
: {
|
|
266
258
|
id: 'sidebar', label: 'dsh-better-sidebar(侧栏工作台)', status: 'missing',
|
|
267
|
-
detail: '
|
|
259
|
+
detail: '未安装此增强组件——Web 自带 GoTry 产物页与原生预览;可选补装以增强文件浏览',
|
|
268
260
|
fix: 'npx @danceiny/gotry doctor --fix',
|
|
269
261
|
});
|
|
270
262
|
// 6. dsh-calendar(patch 分发面宿主插件;D-9 拍板:默认不挂载)
|
|
@@ -371,7 +363,7 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
371
363
|
const broken = items.filter(i => i.status !== 'ok' && i.id !== 'llm-key');
|
|
372
364
|
const summary = broken.length === 0
|
|
373
365
|
? `体检通过:${items.length} 项全部就绪(可选依赖齐,LLM key 归 dsh 宿主管)。`
|
|
374
|
-
: `体检发现 ${broken.length} 项待处理:${broken.map(i => `${i.label}(${i.status === 'missing' ? '未装' : '半可用'})`).join('、')}。补装:终端跑 npx @danceiny/gotry doctor --fix,或按各项 fix 指引逐项处理。`;
|
|
366
|
+
: `体检发现 ${broken.length} 项待处理:${broken.map(i => `${i.label}(${i.status === 'missing' ? '未装' : i.id === 'extension' ? '待确认' : '半可用'})`).join('、')}。补装:终端跑 npx @danceiny/gotry doctor --fix,或按各项 fix 指引逐项处理。`;
|
|
375
367
|
return { ok: broken.length === 0, items, summary };
|
|
376
368
|
}
|
|
377
369
|
/** 修复指引入表:命令类才加反引号(prose 类如「到控制台申请 key」原样) */
|
|
@@ -36,13 +36,14 @@ const fakeReact = {
|
|
|
36
36
|
useState(initial) { return [initial, () => { }]; },
|
|
37
37
|
};
|
|
38
38
|
const clientExports = registrations[0].factory(name => {
|
|
39
|
-
assert.
|
|
40
|
-
return fakeReact;
|
|
39
|
+
assert.ok(['react', '@deepseek-ai/dsh-client-ui-primitives'].includes(name));
|
|
40
|
+
return name === 'react' ? fakeReact : {};
|
|
41
41
|
});
|
|
42
42
|
assert.deepEqual(Object.keys(clientExports), ['apply', 'inject']);
|
|
43
43
|
const slotRegistrations = [];
|
|
44
44
|
const registrationContext = {
|
|
45
|
-
inject(
|
|
45
|
+
inject(deps, callback) { if (deps.every(name => name in registrationContext))
|
|
46
|
+
callback(registrationContext); },
|
|
46
47
|
slots: {
|
|
47
48
|
inject(_slot, callback) { callback(); },
|
|
48
49
|
register(definition, view) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Successful bounded renders produce native delivery events; failures and escaped paths never do. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
const moduleUrl = new URL('../src/artifact-delivery.js', import.meta.url).href;
|
|
7
|
+
const delivery = await import(moduleUrl).catch(() => undefined);
|
|
8
|
+
assert.equal(typeof delivery?.registerArtifactDeliveries, 'function', 'bounded render tools must publish native file deliveries');
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-artifact-delivery-'));
|
|
10
|
+
try {
|
|
11
|
+
const path = join(root, 'trip.html');
|
|
12
|
+
writeFileSync(path, '<h1>Trip</h1>');
|
|
13
|
+
let observe = () => { throw new Error('observer absent'); };
|
|
14
|
+
const events = [];
|
|
15
|
+
const session = { header: { cwd: root }, view: () => ({ getSnapshot: () => ({ openTurnStartSeq: 1, lastTurn: 7 }) }),
|
|
16
|
+
append: (name, value) => events.push({ name, value }) };
|
|
17
|
+
delivery.registerArtifactDeliveries({ get: () => ({ stateOf: () => ({ openTurnStartSeq: 1, lastTurn: 7 }) }),
|
|
18
|
+
on: (name, fn) => { assert.equal(name, 'tools/result'); observe = fn; } });
|
|
19
|
+
const exec = { name: 'gotry_itinerary_render', callId: 'render-1', agent: { session }, signal: new AbortController().signal };
|
|
20
|
+
observe(exec, { isError: false, value: { ok: true, path } });
|
|
21
|
+
assert.deepEqual(events, [{ name: 'deliverables/presented', value: { turn: 7, callId: 'render-1', files: [{ path, description: 'GoTry itinerary' }] } }]);
|
|
22
|
+
for (const value of [{ ok: false, path }, { ok: true, path: join(root, 'missing.html') }, { ok: true, path: join(root, '..', 'private.html') }])
|
|
23
|
+
observe(exec, { isError: false, value });
|
|
24
|
+
observe({ ...exec, name: 'gotry_artifacts_read' }, { isError: false, value: { ok: true, path } });
|
|
25
|
+
observe(exec, { isError: true, value: { ok: true, path } });
|
|
26
|
+
assert.equal(events.length, 1);
|
|
27
|
+
console.log('GoTry native artifact deliveries: real bounded render only PASS');
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
rmSync(root, { recursive: true, force: true });
|
|
31
|
+
}
|
|
@@ -21,9 +21,14 @@ import { EventEmitter } from 'node:events';
|
|
|
21
21
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { join, dirname, delimiter } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { runDoctorChecks as diagnose } from '../capabilities/doctor.js';
|
|
25
26
|
const repoRoot = join(import.meta.dirname, '..', '..');
|
|
26
27
|
const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
|
|
28
|
+
const offlinePreload = pathToFileURL(join(repoRoot, 'scripts', 'doctor-test-isolation.mjs')).href;
|
|
29
|
+
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --import=${offlinePreload}`.trim();
|
|
30
|
+
process.env.GOTRY_TEST_DOCTOR_PORT = '0';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
27
32
|
function runBootstrap(extraArgs, extraEnv, bootstrapPath = bootstrap) {
|
|
28
33
|
try {
|
|
29
34
|
const out = execFileSync('node', [bootstrapPath, ...extraArgs], {
|
|
@@ -156,7 +161,7 @@ console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态
|
|
|
156
161
|
assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)');
|
|
157
162
|
assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr');
|
|
158
163
|
assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路');
|
|
159
|
-
assert.match(r.stderr,
|
|
164
|
+
assert.match(r.stderr, /扩展=待确认/, '没有安装或连接证据时,摘要显示待确认');
|
|
160
165
|
}
|
|
161
166
|
console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK');
|
|
162
167
|
// 11. setupSidebar 落盘状态复核(2026-09-08 安装链修复):pnpm ≥10.5 在 profile 目录
|
|
@@ -388,6 +393,9 @@ console.log('18. onboarding CLI 跳过(非 TTY / CI / GOTRY_SETUP_SKIP / GOTRY_O
|
|
|
388
393
|
const fixture = mkdtempSync(join(tmpdir(), 'gotry-onboard-pkg-'));
|
|
389
394
|
mkdirSync(join(fixture, 'bin'), { recursive: true });
|
|
390
395
|
copyFileSync(bootstrap, join(fixture, 'bin', 'gotry-bootstrap.js'));
|
|
396
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(fixture, 'bin', 'gotry-flyai-verification.js'));
|
|
397
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(fixture, 'bin', 'gotry-process-liveness.js'));
|
|
398
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(fixture, 'bin', 'gotry-extension-status.js'));
|
|
391
399
|
copyFileSync(join(repoRoot, 'package.json'), join(fixture, 'package.json'));
|
|
392
400
|
const vendorDir = join(fixture, 'ts', 'dsh-runtime', 'vendor');
|
|
393
401
|
mkdirSync(vendorDir, { recursive: true });
|
|
@@ -637,6 +645,8 @@ function buildPkgFixture(tmpRoot) {
|
|
|
637
645
|
writeFileSync(join(pkgRoot, 'package.json'), `${JSON.stringify({ name: 'gotry', version: '0.0.1-test', type: 'module' })}\n`);
|
|
638
646
|
copyFileSync(join(repoRoot, 'bin', 'gotry-inner.js'), join(pkgBin, 'gotry-inner.js'));
|
|
639
647
|
copyFileSync(join(repoRoot, 'bin', 'gotry-bootstrap.js'), join(pkgBin, 'gotry-bootstrap.js'));
|
|
648
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(pkgBin, 'gotry-flyai-verification.js'));
|
|
649
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(pkgBin, 'gotry-extension-status.js'));
|
|
640
650
|
copyFileSync(join(repoRoot, 'bin', 'gotry-runtime-resolution.js'), join(pkgBin, 'gotry-runtime-resolution.js'));
|
|
641
651
|
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(pkgBin, 'gotry-process-liveness.js'));
|
|
642
652
|
copyFileSync(join(repoRoot, 'cordis.gotry-patch.yml'), join(pkgRoot, 'cordis.gotry-patch.yml'));
|
|
@@ -699,8 +709,8 @@ echo "bash-finished $$" >> "$GOTRY_TEST_INSTALLER_MARKER"
|
|
|
699
709
|
}
|
|
700
710
|
const closedFixtures = new WeakSet();
|
|
701
711
|
function spawnInner(fixture, opts) {
|
|
702
|
-
//
|
|
703
|
-
const nodeOptions = opts.tty ?
|
|
712
|
+
// 保留离线诊断 preload,但移除父进程 tsx loader;临时包没有 tsx。
|
|
713
|
+
const nodeOptions = `--import=${offlinePreload}${opts.tty ? ` --require ${fixture.preloadPath}` : ''}`;
|
|
704
714
|
const child = spawn(process.execPath, [join(fixture.pkgBin, 'gotry-inner.js'), 'web', '--no-open'], {
|
|
705
715
|
env: {
|
|
706
716
|
...process.env,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* doctor 能力层测试(离线,注入 repoRoot/homeDir/env,零安装零网络写路径):
|
|
3
|
-
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=
|
|
3
|
+
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=degraded(连接未确认) / flyai=degraded(无 key)/ LLM key 恒 ok 且不进 broken
|
|
4
4
|
* 2. 补齐假 .venv 双文件 → agent-reach=ok;只补 python → degraded(半可用态被显式区分)
|
|
5
5
|
* 3. FLYAI_API_KEY 仅注入 → flyai=degraded(未验证);匹配验证回执 → flyai=ok
|
|
6
6
|
* 4. nodeOk 边界(22.14/22.15/23.0)
|
|
@@ -25,9 +25,13 @@ import { existsSync } from 'node:fs';
|
|
|
25
25
|
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
26
26
|
import { tmpdir } from 'node:os';
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
|
-
import { runDoctorChecks, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
28
|
+
import { runDoctorChecks as diagnose, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
29
29
|
import { endpointFingerprint, maskFlyaiKey, resolveFlyaiEndpoint, sha256Hex, writeFlyaiVerification } from '../capabilities/flyai-config.js';
|
|
30
30
|
import { apply } from '../src/index.js';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
32
|
+
// The registered model tool calls diagnose internally rather than this wrapper.
|
|
33
|
+
// Isolate its HTTP diagnostic seam too, without changing the product config.
|
|
34
|
+
await import(new URL('../../scripts/doctor-test-isolation.mjs', import.meta.url).href);
|
|
31
35
|
const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
|
|
32
36
|
const emptyRepo = join(tmp, 'repo-empty');
|
|
33
37
|
const emptyHome = join(tmp, 'home-empty');
|
|
@@ -39,11 +43,10 @@ const byId = (id) => r1.items.find(i => i.id === id);
|
|
|
39
43
|
assert.ok(byId('agent-reach'), 'agent-reach 项存在');
|
|
40
44
|
assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
|
|
41
45
|
assert.match(byId('agent-reach').fix ?? '', /npx @danceiny\/gotry doctor --fix/, 'missing 项带精确补装指引');
|
|
42
|
-
assert.equal(byId('extension').status, '
|
|
43
|
-
|
|
44
|
-
assert.match(byId('extension').
|
|
45
|
-
assert.match(byId('extension').detail,
|
|
46
|
-
assert.match(byId('extension').detail, /其它工具/, '扩展 missing detail 必显「其它不受影响」');
|
|
46
|
+
assert.equal(byId('extension').status, 'degraded', '空 home 无法断言扩展未安装');
|
|
47
|
+
assert.match(byId('extension').detail, /影响面/, '扩展 detail 必说明影响面');
|
|
48
|
+
assert.match(byId('extension').fix ?? '', /chrome:\/\/extensions/, '扩展未确认时可检查浏览器安装/启用状态');
|
|
49
|
+
assert.match(byId('extension').detail, /其它工具/, '扩展 detail 必说明其它工具不受影响');
|
|
47
50
|
assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
|
|
48
51
|
// #559 B 步:无 key flyai 必须显式说明「匿名共享额度」影响面
|
|
49
52
|
assert.match(byId('flyai').detail, /影响面/, 'flyai 无 key detail 必带影响面行(issue #559 B 步)');
|
|
@@ -155,6 +158,32 @@ console.log('5b. npm 提升布局解析链(map-tools/ask-user 误报修复)OK');
|
|
|
155
158
|
await rm(vend, { recursive: true, force: true });
|
|
156
159
|
}
|
|
157
160
|
console.log('5c. 随包 vendor 布局(map-tools tarball 分发面)OK');
|
|
161
|
+
// #651: the model-facing diagnostic must recognize the same Chrome Store
|
|
162
|
+
// installation as the CLI without requiring the local unpacked directory.
|
|
163
|
+
{
|
|
164
|
+
const home = join(tmp, 'chrome-store-home');
|
|
165
|
+
const browserRoot = process.platform === 'darwin'
|
|
166
|
+
? join(home, 'Library', 'Application Support', 'Google', 'Chrome')
|
|
167
|
+
: process.platform === 'win32'
|
|
168
|
+
? join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data')
|
|
169
|
+
: join(home, '.config', 'google-chrome');
|
|
170
|
+
const profile = join(browserRoot, 'Profile 3');
|
|
171
|
+
const id = 'oeajpiccmonococjcegddlooeeohlbgd';
|
|
172
|
+
const manifest = join(profile, 'Extensions', id, '0.2.0.27_0', 'manifest.json');
|
|
173
|
+
await mkdir(dirname(manifest), { recursive: true });
|
|
174
|
+
await writeFile(manifest, JSON.stringify({ manifest_version: 3, version: '0.2.0.27', name: 'Stai Travel Bridge' }));
|
|
175
|
+
const prefs = join(profile, 'Secure Preferences');
|
|
176
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { disable_reasons: [] } } } }));
|
|
177
|
+
const report = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} });
|
|
178
|
+
const extension = report.items.find(item => item.id === 'extension');
|
|
179
|
+
assert.equal(extension.status, 'degraded');
|
|
180
|
+
assert.match(extension.detail, /已安装/);
|
|
181
|
+
assert.match(report.summary, /Stai Travel Bridge 扩展\(待确认\)/);
|
|
182
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { state: 0 } } } }));
|
|
183
|
+
const disabled = (await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} })).items.find(item => item.id === 'extension');
|
|
184
|
+
assert.match(disabled.detail, /已安装.*停用/);
|
|
185
|
+
}
|
|
186
|
+
console.log('5d. Chrome 商店安装与停用状态,工具层不误报 missing/ready OK');
|
|
158
187
|
// 6. MCP 工具面:gotry_doctor 注册 + isolated stateRoot 报告落盘
|
|
159
188
|
const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
|
|
160
189
|
const registered = [];
|