@danceiny/gotry 0.0.1-rc.14 → 0.0.1-rc.16
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 +26 -20
- package/README.zh-CN.md +27 -21
- package/bin/gotry-bootstrap.js +331 -3
- package/cordis.gotry-patch.yml +10 -0
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +217 -0
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +1 -1
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +345 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +377 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +77 -1
- package/dist/capabilities/session-search.js +69 -2
- package/dist/capabilities/weather.js +1 -1
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/booking-saga-tests.js +187 -0
- package/dist/scripts/bootstrap-tests.js +49 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/extension-tests.js +468 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/nightly-evidence-tests.js +186 -0
- package/dist/scripts/nightly-evidence.js +289 -0
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +218 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +1 -1
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +1 -1
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke.js +159 -1
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +1 -1
- package/dist/scripts/wizard-bootstrap.js +32 -0
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +153 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +31 -7
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +322 -46
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +1 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +26 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +8 -2
- package/ts/capabilities/artifacts.ts +235 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +354 -0
- package/ts/capabilities/session/extension-channel.ts +92 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/wizard.ts +282 -0
- package/ts/capabilities/session-login.ts +53 -2
- package/ts/capabilities/session-search.ts +68 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +230 -28
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { request } from 'node:http';
|
|
2
|
+
import { BRIDGE_PORTS, EXTENSION_CONNECTED_WINDOW_MS } from './extension-bridge.js';
|
|
3
|
+
export async function combinedProbe(opts, businessProbe) {
|
|
4
|
+
if (!await defaultProbe(opts)) return false;
|
|
5
|
+
if (!businessProbe) return true;
|
|
6
|
+
try {
|
|
7
|
+
const channel = await import('./extension-channel.js');
|
|
8
|
+
const bridge = await import('./extension-bridge.js');
|
|
9
|
+
bridge.getOrCreateSessionBridge({
|
|
10
|
+
keepBridge: opts.keepBridge === true
|
|
11
|
+
});
|
|
12
|
+
const cookie = await channel.extensionCookieNames({
|
|
13
|
+
site: 'ctrip-flight',
|
|
14
|
+
domain: 'ctrip.com',
|
|
15
|
+
ticketNames: [
|
|
16
|
+
'cticket',
|
|
17
|
+
'uid'
|
|
18
|
+
],
|
|
19
|
+
timeoutMs: 8_000
|
|
20
|
+
});
|
|
21
|
+
return cookie.ok;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function defaultProbe(opts) {
|
|
27
|
+
const ports = opts.ports ?? [
|
|
28
|
+
...BRIDGE_PORTS
|
|
29
|
+
];
|
|
30
|
+
for (const port of ports){
|
|
31
|
+
const ok = await new Promise((resolve)=>{
|
|
32
|
+
const req = request({
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
port,
|
|
35
|
+
path: '/status',
|
|
36
|
+
method: 'GET',
|
|
37
|
+
timeout: 2_000
|
|
38
|
+
}, (res)=>{
|
|
39
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
40
|
+
resolve(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let buf = '';
|
|
44
|
+
res.setEncoding('utf8');
|
|
45
|
+
res.on('data', (c)=>{
|
|
46
|
+
buf += c;
|
|
47
|
+
});
|
|
48
|
+
res.on('end', ()=>{
|
|
49
|
+
try {
|
|
50
|
+
const payload = JSON.parse(buf);
|
|
51
|
+
resolve(payload.extensionConnected === true);
|
|
52
|
+
} catch {
|
|
53
|
+
resolve(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
res.on('error', ()=>resolve(false));
|
|
57
|
+
});
|
|
58
|
+
req.on('error', ()=>resolve(false));
|
|
59
|
+
req.on('timeout', ()=>{
|
|
60
|
+
req.destroy();
|
|
61
|
+
resolve(false);
|
|
62
|
+
});
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
if (ok) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export function startExtensionHealthWatch(opts = {}) {
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
71
|
+
const intervalMs = opts.intervalMs ?? 5_000;
|
|
72
|
+
const maxRetries = opts.maxRetries ?? 1;
|
|
73
|
+
const businessProbe = opts.businessProbe ?? true;
|
|
74
|
+
const probe = opts.probe ?? (async ()=>{
|
|
75
|
+
const bridge = await import('./extension-bridge.js');
|
|
76
|
+
await bridge.getOrCreateSessionBridge({
|
|
77
|
+
keepBridge: opts.keepBridge === true
|
|
78
|
+
});
|
|
79
|
+
return combinedProbe(opts, businessProbe);
|
|
80
|
+
});
|
|
81
|
+
const now = opts.now ?? Date.now;
|
|
82
|
+
const state = {
|
|
83
|
+
cancelled: false,
|
|
84
|
+
readyCount: 0,
|
|
85
|
+
resolveOutcome: null
|
|
86
|
+
};
|
|
87
|
+
const finish = (outcome)=>{
|
|
88
|
+
if (state.resolveOutcome) {
|
|
89
|
+
const resolve = state.resolveOutcome;
|
|
90
|
+
state.resolveOutcome = null;
|
|
91
|
+
resolve(outcome);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onAbort = ()=>{
|
|
95
|
+
state.cancelled = true;
|
|
96
|
+
finish({
|
|
97
|
+
ready: false,
|
|
98
|
+
reason: 'cancelled',
|
|
99
|
+
attempts: state.readyCount,
|
|
100
|
+
waitedMs: 0
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
opts.signal?.addEventListener('abort', onAbort, {
|
|
104
|
+
once: true
|
|
105
|
+
});
|
|
106
|
+
const waitReady = ()=>new Promise((resolve)=>{
|
|
107
|
+
if (state.cancelled) {
|
|
108
|
+
resolve({
|
|
109
|
+
ready: false,
|
|
110
|
+
reason: 'cancelled',
|
|
111
|
+
attempts: 0,
|
|
112
|
+
waitedMs: 0
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (timeoutMs === 0) {
|
|
117
|
+
resolve({
|
|
118
|
+
ready: false,
|
|
119
|
+
reason: 'timeout',
|
|
120
|
+
attempts: 0,
|
|
121
|
+
waitedMs: 0
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
state.resolveOutcome = resolve;
|
|
126
|
+
const deadline = now() + timeoutMs;
|
|
127
|
+
let attempts = 0;
|
|
128
|
+
let waitedMs = 0;
|
|
129
|
+
const tick = async ()=>{
|
|
130
|
+
if (state.cancelled || state.resolveOutcome === null) return;
|
|
131
|
+
attempts += 1;
|
|
132
|
+
try {
|
|
133
|
+
const ready = await probe();
|
|
134
|
+
if (ready && !state.cancelled) {
|
|
135
|
+
state.readyCount += 1;
|
|
136
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
137
|
+
if (state.readyCount <= maxRetries) {
|
|
138
|
+
finish({
|
|
139
|
+
ready: true,
|
|
140
|
+
attempts,
|
|
141
|
+
waitedMs: waitedMs > 0 ? waitedMs : 0
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
148
|
+
if (now() >= deadline) {
|
|
149
|
+
finish({
|
|
150
|
+
ready: false,
|
|
151
|
+
reason: 'timeout',
|
|
152
|
+
attempts,
|
|
153
|
+
waitedMs
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const remaining = deadline - now();
|
|
158
|
+
const delay = Math.min(intervalMs, Math.max(0, remaining));
|
|
159
|
+
if (delay <= 0) {
|
|
160
|
+
finish({
|
|
161
|
+
ready: false,
|
|
162
|
+
reason: 'timeout',
|
|
163
|
+
attempts,
|
|
164
|
+
waitedMs
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setTimeout(tick, delay);
|
|
169
|
+
};
|
|
170
|
+
void tick();
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
waitReady,
|
|
174
|
+
cancel: ()=>onAbort(),
|
|
175
|
+
get readyCount () {
|
|
176
|
+
return state.readyCount;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function withAutoRetryOnExtension(args) {
|
|
181
|
+
const first = await args.fetch();
|
|
182
|
+
if (!args.isExtensionNeeded(first)) {
|
|
183
|
+
return {
|
|
184
|
+
result: first,
|
|
185
|
+
retried: false,
|
|
186
|
+
attempts: 1,
|
|
187
|
+
outcome: {
|
|
188
|
+
ready: true,
|
|
189
|
+
attempts: 0,
|
|
190
|
+
waitedMs: 0
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const watch = startExtensionHealthWatch(args.watch ?? {});
|
|
195
|
+
const outcome = await watch.waitReady();
|
|
196
|
+
if (!outcome.ready) {
|
|
197
|
+
return {
|
|
198
|
+
result: first,
|
|
199
|
+
retried: false,
|
|
200
|
+
attempts: 1,
|
|
201
|
+
outcome
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const retriedResult = await (args.retry ?? args.fetch)();
|
|
205
|
+
return {
|
|
206
|
+
result: retriedResult,
|
|
207
|
+
retried: true,
|
|
208
|
+
attempts: 2,
|
|
209
|
+
outcome
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export const HEALTH_WATCH_INTERVAL_FLOOR_MS = 1_000;
|
|
213
|
+
export const HEALTH_WATCH_INTERVAL_CEIL_MS = EXTENSION_CONNECTED_WINDOW_MS - 1_000;
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/session/health-watch.ts
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const DEFAULT_SOURCE_DIR_CANDIDATES = [
|
|
6
|
+
join(process.cwd(), 'extension'),
|
|
7
|
+
join(process.cwd(), 'dist', 'extension'),
|
|
8
|
+
join(process.cwd(), 'ts', '..', 'extension')
|
|
9
|
+
];
|
|
10
|
+
function resolveExtensionSourceDir() {
|
|
11
|
+
for (const candidate of DEFAULT_SOURCE_DIR_CANDIDATES){
|
|
12
|
+
if (existsSync(join(candidate, 'manifest.json'))) return candidate;
|
|
13
|
+
}
|
|
14
|
+
return DEFAULT_SOURCE_DIR_CANDIDATES[0];
|
|
15
|
+
}
|
|
16
|
+
export function defaultExtensionDir() {
|
|
17
|
+
return join(homedir(), '.gotry', 'extension');
|
|
18
|
+
}
|
|
19
|
+
function detectPlatform(force) {
|
|
20
|
+
if (force) return force;
|
|
21
|
+
if (process.env.GOTRY_ONBOARDING_HEADLESS === '1') return 'headless';
|
|
22
|
+
const p = platform();
|
|
23
|
+
if (p === 'darwin' || p === 'linux' || p === 'win32') {
|
|
24
|
+
if (p !== 'darwin' && !process.env.DISPLAY) return 'headless';
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
return 'headless';
|
|
28
|
+
}
|
|
29
|
+
function ensureExtensionFiles(source, target, dryRun) {
|
|
30
|
+
if (dryRun) return {
|
|
31
|
+
step: 'ensure-extension-files',
|
|
32
|
+
status: 'skip',
|
|
33
|
+
summary: '[dry-run] skip 扩展落位'
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(source) || !existsSync(join(source, 'manifest.json'))) {
|
|
37
|
+
return {
|
|
38
|
+
step: 'ensure-extension-files',
|
|
39
|
+
status: 'fail',
|
|
40
|
+
summary: `源扩展目录缺失 manifest.json: ${source}(先跑 npx gotry setup 落位)`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
mkdirSync(target, {
|
|
44
|
+
recursive: true
|
|
45
|
+
});
|
|
46
|
+
const required = [
|
|
47
|
+
'manifest.json',
|
|
48
|
+
'background.js',
|
|
49
|
+
'content-main.js',
|
|
50
|
+
'content-bridge.js'
|
|
51
|
+
];
|
|
52
|
+
for (const f of required){
|
|
53
|
+
const src = join(source, f);
|
|
54
|
+
if (!existsSync(src)) return {
|
|
55
|
+
step: 'ensure-extension-files',
|
|
56
|
+
status: 'fail',
|
|
57
|
+
summary: `源扩展缺 ${f}(重新 npm install 或 gotry setup)`
|
|
58
|
+
};
|
|
59
|
+
copyFileSync(src, join(target, f));
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
step: 'ensure-extension-files',
|
|
63
|
+
status: 'ok',
|
|
64
|
+
summary: `扩展已落位 ${target}(manifest+3 文件)`
|
|
65
|
+
};
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return {
|
|
68
|
+
step: 'ensure-extension-files',
|
|
69
|
+
status: 'fail',
|
|
70
|
+
summary: `扩展落位失败: ${e instanceof Error ? e.message.slice(0, 120) : String(e)}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function openChromeExtensions(sp, plat, dryRun) {
|
|
75
|
+
if (dryRun) return {
|
|
76
|
+
step: 'open-chrome-extensions',
|
|
77
|
+
status: 'skip',
|
|
78
|
+
summary: '[dry-run] skip 打开 chrome://extensions'
|
|
79
|
+
};
|
|
80
|
+
if (plat === 'headless') return {
|
|
81
|
+
step: 'open-chrome-extensions',
|
|
82
|
+
status: 'skip',
|
|
83
|
+
summary: 'headless 环境跳过 GUI;请在你桌面 Chrome 手动打开 chrome://extensions'
|
|
84
|
+
};
|
|
85
|
+
const cmd = (()=>{
|
|
86
|
+
if (plat === 'darwin') return {
|
|
87
|
+
bin: 'open',
|
|
88
|
+
args: [
|
|
89
|
+
'-a',
|
|
90
|
+
'Google Chrome',
|
|
91
|
+
'chrome://extensions'
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
if (plat === 'linux') return {
|
|
95
|
+
bin: 'xdg-open',
|
|
96
|
+
args: [
|
|
97
|
+
'chrome://extensions'
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
bin: 'cmd',
|
|
102
|
+
args: [
|
|
103
|
+
'/c',
|
|
104
|
+
'start',
|
|
105
|
+
'chrome',
|
|
106
|
+
'chrome://extensions'
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
})();
|
|
110
|
+
try {
|
|
111
|
+
const child = sp(cmd.bin, cmd.args, {
|
|
112
|
+
detached: true,
|
|
113
|
+
stdio: 'ignore'
|
|
114
|
+
});
|
|
115
|
+
child.unref?.();
|
|
116
|
+
return {
|
|
117
|
+
step: 'open-chrome-extensions',
|
|
118
|
+
status: 'ok',
|
|
119
|
+
summary: `已打开 chrome://extensions(${plat})`
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return {
|
|
123
|
+
step: 'open-chrome-extensions',
|
|
124
|
+
status: 'fail',
|
|
125
|
+
summary: `自动打开失败: ${e instanceof Error ? e.message.slice(0, 80) : String(e)};请手开 chrome://extensions`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function clipboardExtensionPath(sp, plat, path, dryRun) {
|
|
130
|
+
if (dryRun) return {
|
|
131
|
+
step: 'clipboard-extension-path',
|
|
132
|
+
status: 'skip',
|
|
133
|
+
summary: '[dry-run] skip 剪贴板复制'
|
|
134
|
+
};
|
|
135
|
+
if (plat === 'headless') return {
|
|
136
|
+
step: 'clipboard-extension-path',
|
|
137
|
+
status: 'skip',
|
|
138
|
+
summary: `headless 无剪贴板;扩展目录路径:${path}`
|
|
139
|
+
};
|
|
140
|
+
const cmd = (()=>{
|
|
141
|
+
if (plat === 'darwin') return {
|
|
142
|
+
bin: 'pbcopy',
|
|
143
|
+
args: []
|
|
144
|
+
};
|
|
145
|
+
if (plat === 'linux') return {
|
|
146
|
+
bin: 'xclip',
|
|
147
|
+
args: [
|
|
148
|
+
'-selection',
|
|
149
|
+
'clipboard'
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
bin: 'clip',
|
|
154
|
+
args: []
|
|
155
|
+
};
|
|
156
|
+
})();
|
|
157
|
+
return new Promise((resolveStep)=>{
|
|
158
|
+
let child;
|
|
159
|
+
try {
|
|
160
|
+
child = sp(cmd.bin, cmd.args, {
|
|
161
|
+
stdio: [
|
|
162
|
+
'pipe',
|
|
163
|
+
'ignore',
|
|
164
|
+
'ignore'
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
resolveStep({
|
|
169
|
+
step: 'clipboard-extension-path',
|
|
170
|
+
status: 'skip',
|
|
171
|
+
summary: `剪贴板工具不可用(${cmd.bin});扩展目录路径手动复制: ${path}`
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let settled = false;
|
|
176
|
+
const settle = (s)=>{
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
resolveStep(s);
|
|
180
|
+
};
|
|
181
|
+
child.on('error', ()=>settle({
|
|
182
|
+
step: 'clipboard-extension-path',
|
|
183
|
+
status: 'skip',
|
|
184
|
+
summary: `剪贴板工具启动失败(${cmd.bin});扩展目录路径手动复制: ${path}`
|
|
185
|
+
}));
|
|
186
|
+
child.on('exit', (code)=>{
|
|
187
|
+
if (code === 0) settle({
|
|
188
|
+
step: 'clipboard-extension-path',
|
|
189
|
+
status: 'ok',
|
|
190
|
+
summary: `已复制扩展目录到剪贴板: ${path}`
|
|
191
|
+
});
|
|
192
|
+
else settle({
|
|
193
|
+
step: 'clipboard-extension-path',
|
|
194
|
+
status: 'fail',
|
|
195
|
+
summary: `剪贴板工具退出码 ${code}(${cmd.bin});扩展目录路径手动复制: ${path}`
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
child.stdin?.write(path);
|
|
200
|
+
child.stdin?.end();
|
|
201
|
+
} catch {
|
|
202
|
+
settle({
|
|
203
|
+
step: 'clipboard-extension-path',
|
|
204
|
+
status: 'skip',
|
|
205
|
+
summary: `剪贴板工具 stdin 不可写(${cmd.bin});扩展目录路径手动复制: ${path}`
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function panelGuideTerminal(plat, extensionDir, watchMs) {
|
|
211
|
+
const lines = [
|
|
212
|
+
'┌─ GoTry 扩展安装 ─────────────────────────────────┐',
|
|
213
|
+
'│ 共 3 步,约 30 秒,装完零弹窗 │',
|
|
214
|
+
'│ │',
|
|
215
|
+
'│ ① 在 Chrome 右上角开启「开发者模式」 │',
|
|
216
|
+
`│ ② 点「加载已解压的扩展程序」选这个目录(已复制): │`,
|
|
217
|
+
`│ ${extensionDir}`,
|
|
218
|
+
'│ ③ 弹窗「添加扩展」点「添加」 │',
|
|
219
|
+
`│ │`,
|
|
220
|
+
`│ 后台探活中(最长 ${Math.round(watchMs / 1000)}s)… │`,
|
|
221
|
+
'└───────────────────────────────────────────────────┘'
|
|
222
|
+
];
|
|
223
|
+
if (plat === 'headless') {
|
|
224
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
225
|
+
return {
|
|
226
|
+
step: 'panel-guide',
|
|
227
|
+
status: 'skip',
|
|
228
|
+
summary: 'headless 终端面板已打印'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
232
|
+
return {
|
|
233
|
+
step: 'panel-guide',
|
|
234
|
+
status: 'skip',
|
|
235
|
+
summary: '终端面板已打印(GUI 面板降级)'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function panelGuideGui(sp, plat, extensionDir, dryRun) {
|
|
239
|
+
if (dryRun) return {
|
|
240
|
+
step: 'panel-guide',
|
|
241
|
+
status: 'skip',
|
|
242
|
+
summary: '[dry-run] skip GUI 面板'
|
|
243
|
+
};
|
|
244
|
+
if (plat === 'headless') return panelGuideTerminal(plat, extensionDir, 120_000);
|
|
245
|
+
const message = `在 Chrome 右上角开启「开发者模式」 → 点「加载已解压的扩展程序」 → 选择(已复制):${extensionDir} → 弹窗点「添加扩展」`;
|
|
246
|
+
try {
|
|
247
|
+
if (plat === 'darwin') {
|
|
248
|
+
const script = `display dialog "${message.replace(/"/g, '\\"')}" with title "GoTry 扩展安装" buttons {"我装好了"} default button 1`;
|
|
249
|
+
sp('osascript', [
|
|
250
|
+
'-e',
|
|
251
|
+
script
|
|
252
|
+
], {
|
|
253
|
+
stdio: 'ignore',
|
|
254
|
+
detached: true
|
|
255
|
+
}).unref?.();
|
|
256
|
+
return {
|
|
257
|
+
step: 'panel-guide',
|
|
258
|
+
status: 'ok',
|
|
259
|
+
summary: '已弹 macOS Cocoa 引导面板'
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (plat === 'linux') {
|
|
263
|
+
const child = sp('zenity', [
|
|
264
|
+
'--info',
|
|
265
|
+
'--title=GoTry 扩展安装',
|
|
266
|
+
`--text=${message}`
|
|
267
|
+
], {
|
|
268
|
+
stdio: 'ignore',
|
|
269
|
+
detached: true
|
|
270
|
+
});
|
|
271
|
+
child.unref?.();
|
|
272
|
+
child.on('error', ()=>{
|
|
273
|
+
process.stdout.write(`(zenity 不可用,走终端面板)扩展目录: ${extensionDir}\n`);
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
step: 'panel-guide',
|
|
277
|
+
status: 'ok',
|
|
278
|
+
summary: '已弹 zenity 引导面板'
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return panelGuideTerminal(plat, extensionDir, 120_000);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return panelGuideTerminal(plat, extensionDir, 120_000);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function watchExtensionReady(dryRun, sourceDir, extensionDir) {
|
|
287
|
+
if (dryRun) return {
|
|
288
|
+
step: 'watch-extension-ready',
|
|
289
|
+
status: 'skip',
|
|
290
|
+
summary: '[dry-run] skip health-watch'
|
|
291
|
+
};
|
|
292
|
+
if (!existsSync(extensionDir) || !existsSync(join(extensionDir, 'manifest.json'))) {
|
|
293
|
+
return {
|
|
294
|
+
step: 'watch-extension-ready',
|
|
295
|
+
status: 'fail',
|
|
296
|
+
summary: `落位缺失 manifest.json:${extensionDir}`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const srcStat = statSync(sourceDir);
|
|
300
|
+
const dstStat = statSync(extensionDir);
|
|
301
|
+
if (srcStat.mtimeMs > dstStat.mtimeMs) {
|
|
302
|
+
return {
|
|
303
|
+
step: 'watch-extension-ready',
|
|
304
|
+
status: 'fail',
|
|
305
|
+
summary: `扩展文件可能过期(源比落位新);重跑 npx gotry setup`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const manifest = JSON.parse(await (await import('node:fs/promises')).readFile(join(extensionDir, 'manifest.json'), 'utf8'));
|
|
309
|
+
if (typeof manifest.key !== 'string' || manifest.key.length < 100) {
|
|
310
|
+
return {
|
|
311
|
+
step: 'watch-extension-ready',
|
|
312
|
+
status: 'fail',
|
|
313
|
+
summary: 'manifest.key 缺失或过短,扩展 ID 不能稳定'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
step: 'watch-extension-ready',
|
|
318
|
+
status: 'ok',
|
|
319
|
+
summary: '扩展文件就绪(manifest.key 校验通过,后台轮询就绪态由 health-watch 接管)'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export async function runOnboardingWizard(opts = {}) {
|
|
323
|
+
const sp = opts.spawn ?? spawn;
|
|
324
|
+
const dryRun = opts.dryRun ?? false;
|
|
325
|
+
const plat = detectPlatform(opts.forcePlatform);
|
|
326
|
+
const extensionDir = opts.extensionDir ?? defaultExtensionDir();
|
|
327
|
+
const sourceDir = opts.sourceDir ?? resolveExtensionSourceDir();
|
|
328
|
+
const steps = [];
|
|
329
|
+
steps.push(ensureExtensionFiles(sourceDir, extensionDir, dryRun));
|
|
330
|
+
steps.push(openChromeExtensions(sp, plat, dryRun));
|
|
331
|
+
if (plat !== 'headless') {
|
|
332
|
+
steps.push(await clipboardExtensionPath(sp, plat, extensionDir, dryRun));
|
|
333
|
+
steps.push(panelGuideGui(sp, plat, extensionDir, dryRun));
|
|
334
|
+
} else {
|
|
335
|
+
steps.push({
|
|
336
|
+
step: 'clipboard-extension-path',
|
|
337
|
+
status: 'skip',
|
|
338
|
+
summary: 'headless 跳过剪贴板'
|
|
339
|
+
});
|
|
340
|
+
steps.push(panelGuideTerminal(plat, extensionDir, 120_000));
|
|
341
|
+
}
|
|
342
|
+
steps.push(await watchExtensionReady(dryRun, sourceDir, extensionDir));
|
|
343
|
+
const ok = steps.every((s)=>s.status !== 'fail');
|
|
344
|
+
return {
|
|
345
|
+
ok,
|
|
346
|
+
steps,
|
|
347
|
+
platform: plat,
|
|
348
|
+
extensionDir
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export function printWizardResult(result, destination = process.stdout) {
|
|
352
|
+
const tag = (s)=>{
|
|
353
|
+
if (s.status === 'ok') return '✅';
|
|
354
|
+
if (s.status === 'skip') return '⏭ ';
|
|
355
|
+
return '❌';
|
|
356
|
+
};
|
|
357
|
+
for (const s of result.steps){
|
|
358
|
+
destination.write(`${tag(s)} ${s.step}: ${s.summary}\n`);
|
|
359
|
+
}
|
|
360
|
+
destination.write(`platform=${result.platform}; extensionDir=${result.extensionDir}; overall=${result.ok ? 'ok' : 'has-failures'}\n`);
|
|
361
|
+
}
|
|
362
|
+
export function describeExtensionDir(extensionDir) {
|
|
363
|
+
if (!existsSync(extensionDir)) return {
|
|
364
|
+
exists: false,
|
|
365
|
+
fileCount: 0,
|
|
366
|
+
hasManifest: false
|
|
367
|
+
};
|
|
368
|
+
const files = readdirSync(extensionDir);
|
|
369
|
+
return {
|
|
370
|
+
exists: true,
|
|
371
|
+
fileCount: files.length,
|
|
372
|
+
hasManifest: files.includes('manifest.json')
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/session/wizard.ts
|