@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Isolated Web setup regressions: real child verifier, official files, and Host-owned roots. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
const moduleUrl = new URL('../src/gotry-web-api.js', import.meta.url).href;
|
|
7
|
+
const api = await import(moduleUrl).catch(() => undefined);
|
|
8
|
+
assert.equal(typeof api?.createGotryWebHandlers, 'function', 'GoTry must expose authenticated Web configuration and artifact handlers');
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-web-api-'));
|
|
10
|
+
try {
|
|
11
|
+
const home = join(root, 'home'), workspace = join(root, 'workspace'), outside = join(root, 'outside');
|
|
12
|
+
for (const dir of [home, workspace, outside])
|
|
13
|
+
mkdirSync(dir);
|
|
14
|
+
const verifier = join(root, 'verifier.cjs');
|
|
15
|
+
writeFileSync(verifier, `#!/usr/bin/env node
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
fs.writeFileSync(process.env.VERIFIER_CAPTURE, JSON.stringify({ candidate: process.env.FLYAI_API_KEY, debug: process.env.DEBUG_FLYAI_API_KEY }));
|
|
18
|
+
if (process.env.FLYAI_API_KEY === 'sk-bad-candidate') { process.stderr.write('HTTP 401 ' + process.env.FLYAI_API_KEY); process.exit(1); }
|
|
19
|
+
if (process.env.FLYAI_API_KEY === 'sk-pending-candidate') { setTimeout(() => process.stdout.write(JSON.stringify({data:{itemList:[]}})), 1000); }
|
|
20
|
+
else process.stdout.write(JSON.stringify({data:{itemList:[]}}));
|
|
21
|
+
`);
|
|
22
|
+
chmodSync(verifier, 0o700);
|
|
23
|
+
const env = { PATH: process.env.PATH, HOME: home, GOTRY_FLYAI_CLI_BIN: verifier,
|
|
24
|
+
VERIFIER_CAPTURE: join(root, 'capture.json'), GOTRY_FLYAI_VERIFY_TIMEOUT_MS: '5000' };
|
|
25
|
+
const handlers = api.createGotryWebHandlers({ homeDir: home, env, stateRoot: workspace,
|
|
26
|
+
workspaceForSession: async (id) => id === 'known-session' ? workspace : undefined });
|
|
27
|
+
const request = (action, key, signal) => new Request('http://localhost/api/gotry/flyai', {
|
|
28
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, ...(key === undefined ? {} : { key }) }), signal,
|
|
29
|
+
});
|
|
30
|
+
const status = () => handlers.flyai(new Request('http://localhost/api/gotry/flyai')).then((r) => r.json());
|
|
31
|
+
assert.deepEqual(await status(), { ok: true, configured: false, verified: false, writable: true, source: 'none', endpoint: 'https://flyai.open.fliggy.com/mcp', endpointDebug: false });
|
|
32
|
+
let response = await handlers.flyai(request('save', 'sk-good-candidate-1234'));
|
|
33
|
+
assert.equal(response.status, 200);
|
|
34
|
+
let body = await response.json();
|
|
35
|
+
assert.equal(body.ok, true);
|
|
36
|
+
assert.equal(body.verified, true);
|
|
37
|
+
assert.equal(body.source, 'config');
|
|
38
|
+
assert.ok(!JSON.stringify(body).includes('sk-good-candidate'));
|
|
39
|
+
const config = join(home, '.flyai/config.json'), receipt = join(home, '.gotry/flyai-verification.json');
|
|
40
|
+
assert.equal(JSON.parse(readFileSync(config, 'utf8')).FLYAI_API_KEY, 'sk-good-candidate-1234');
|
|
41
|
+
assert.equal(statSync(config).mode & 0o777, 0o600);
|
|
42
|
+
const previous = readFileSync(config, 'utf8'), previousReceipt = readFileSync(receipt, 'utf8');
|
|
43
|
+
response = await handlers.flyai(request('save', 'sk-bad-candidate'));
|
|
44
|
+
body = await response.json();
|
|
45
|
+
assert.equal(body.ok, false);
|
|
46
|
+
assert.equal(body.verdict, 'auth-error');
|
|
47
|
+
assert.ok(!JSON.stringify(body).includes('sk-bad-candidate'));
|
|
48
|
+
assert.equal(readFileSync(config, 'utf8'), previous);
|
|
49
|
+
assert.equal(readFileSync(receipt, 'utf8'), previousReceipt);
|
|
50
|
+
assert.equal((await status()).verified, true);
|
|
51
|
+
const cancelled = new AbortController();
|
|
52
|
+
const pending = handlers.flyai(request('save', 'sk-pending-candidate', cancelled.signal));
|
|
53
|
+
setTimeout(() => cancelled.abort(), 100);
|
|
54
|
+
assert.equal((await (await pending).json()).ok, false);
|
|
55
|
+
assert.equal(readFileSync(config, 'utf8'), previous);
|
|
56
|
+
env.FLYAI_API_KEY = 'sk-environment-key';
|
|
57
|
+
assert.equal((await status()).writable, false);
|
|
58
|
+
response = await handlers.flyai(request('save', 'sk-good-candidate-5678'));
|
|
59
|
+
assert.equal(response.status, 409);
|
|
60
|
+
assert.equal(readFileSync(config, 'utf8'), previous);
|
|
61
|
+
delete env.FLYAI_API_KEY;
|
|
62
|
+
response = await handlers.flyai(request('save', 'x'.repeat(9000)));
|
|
63
|
+
assert.equal(response.status, 413);
|
|
64
|
+
assert.equal(readFileSync(config, 'utf8'), previous);
|
|
65
|
+
const concurrent = handlers.flyai(request('save', 'sk-pending-candidate'));
|
|
66
|
+
setTimeout(() => writeFileSync(config, JSON.stringify({ FLYAI_API_KEY: 'sk-external-change' })), 150);
|
|
67
|
+
assert.equal((await concurrent).status, 409);
|
|
68
|
+
assert.equal(JSON.parse(readFileSync(config, 'utf8')).FLYAI_API_KEY, 'sk-external-change');
|
|
69
|
+
response = await handlers.flyai(request('clear'));
|
|
70
|
+
assert.equal((await response.json()).configured, false);
|
|
71
|
+
assert.ok(!existsSync(receipt));
|
|
72
|
+
writeFileSync(config, '{corrupt configuration', { mode: 0o600 });
|
|
73
|
+
assert.equal((await handlers.flyai(request('save', 'sk-good-candidate-1234'))).status, 409);
|
|
74
|
+
assert.equal(readFileSync(config, 'utf8'), '{corrupt configuration');
|
|
75
|
+
writeFileSync(config, previous);
|
|
76
|
+
// A receipt cannot be published when its parent is a file. A successful
|
|
77
|
+
// candidate must not replace the previously usable key in this case.
|
|
78
|
+
rmSync(join(home, '.gotry'), { recursive: true, force: true });
|
|
79
|
+
writeFileSync(join(home, '.gotry'), 'receipt directory unavailable');
|
|
80
|
+
assert.equal((await handlers.flyai(request('save', 'sk-good-candidate-5678'))).status, 409);
|
|
81
|
+
assert.equal(JSON.parse(readFileSync(config, 'utf8')).FLYAI_API_KEY, 'sk-good-candidate-1234');
|
|
82
|
+
rmSync(join(home, '.gotry'));
|
|
83
|
+
const queuedSave = handlers.flyai(request('save', 'sk-pending-candidate'));
|
|
84
|
+
const queuedClear = handlers.flyai(request('clear'));
|
|
85
|
+
const queuedSaved = await queuedSave;
|
|
86
|
+
assert.equal(queuedSaved.status, 200, await queuedSaved.text());
|
|
87
|
+
assert.equal((await (await queuedClear).json()).configured, false);
|
|
88
|
+
writeFileSync(join(workspace, 'trip-one.md'), '# A trip\n');
|
|
89
|
+
writeFileSync(join(workspace, 'trip-two.html'), '<h1>Another trip</h1>');
|
|
90
|
+
writeFileSync(join(outside, 'private-trip.md'), '# Outside\n');
|
|
91
|
+
response = await handlers.artifacts(new Request('http://localhost/api/gotry/artifacts?sessionId=known-session&search=trip&limit=1'));
|
|
92
|
+
body = await response.json();
|
|
93
|
+
assert.equal(body.total, 2);
|
|
94
|
+
assert.equal(body.artifacts.length, 1);
|
|
95
|
+
assert.equal(body.nextOffset, 1);
|
|
96
|
+
response = await handlers.artifacts(new Request('http://localhost/api/gotry/artifacts?sessionId=known-session&search=two'));
|
|
97
|
+
assert.ok((await response.json()).artifacts[0].path.endsWith('trip-two.html'));
|
|
98
|
+
response = await handlers.artifacts(new Request('http://localhost/api/gotry/artifacts?sessionId=unknown'));
|
|
99
|
+
assert.equal(response.status, 404);
|
|
100
|
+
response = await handlers.artifacts(new Request('http://localhost/api/gotry/artifacts?sessionId=known-session&cwd=' + encodeURIComponent(outside)));
|
|
101
|
+
assert.equal(response.status, 400);
|
|
102
|
+
const handoff = join(outside, 'gotry-state', 'turn-handoffs');
|
|
103
|
+
mkdirSync(handoff, { recursive: true });
|
|
104
|
+
writeFileSync(join(handoff, 'host-ticket.json'), JSON.stringify({ schema: 'gotry_turn_handoff.v1', id: 'host-ticket',
|
|
105
|
+
status: 'settled', objective: 'Host-owned handoff', deliverableFile: 'host-ticket.deliverable.md' }));
|
|
106
|
+
writeFileSync(join(handoff, 'host-ticket.deliverable.md'), '# Handoff delivery\n');
|
|
107
|
+
env.GOTRY_TURN_HANDOFF_ROOT = outside;
|
|
108
|
+
response = await handlers.artifacts(new Request('http://localhost/api/gotry/artifacts?sessionId=known-session&search=Host-owned'));
|
|
109
|
+
assert.equal((await response.json()).artifacts[0]?.id, 'host-ticket', 'Web must use the same configured Host handoff root as conversational listing');
|
|
110
|
+
console.log('GoTry Web API: verified save, rollback, cancellation, override, bounds, conflict, clear and Session-owned artifact search PASS');
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Native Web entry registration, without a model invocation or additional UI shell. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import vm from 'node:vm';
|
|
6
|
+
const registrations = [], slots = [], tabs = [];
|
|
7
|
+
const source = readFileSync(join(import.meta.dirname, '../../client/client.js'), 'utf8');
|
|
8
|
+
vm.runInNewContext(source, { window: { __ModuleLoader__: { load: (r) => registrations.push(r) } }, console, fetch, AbortController });
|
|
9
|
+
const react = { createElement: (type, props, ...children) => ({ type, props, children }) };
|
|
10
|
+
const primitives = { SettingsForm: () => { }, SettingsSecretField: () => { }, Button: () => { }, Input: () => { }, Tag: () => { }, FileTypeIcon: () => { } };
|
|
11
|
+
const plugin = registrations[0].factory((name) => name === 'react' ? react : primitives);
|
|
12
|
+
const context = {
|
|
13
|
+
locale: { bind: () => (key) => key, register: () => () => { } },
|
|
14
|
+
slots: { inject: (_name, f) => f(), register: (definition, component) => { slots.push({ definition, component }); return () => { }; } },
|
|
15
|
+
sidebarRightTabs: { register: (definition) => { tabs.push(definition); return () => { }; } },
|
|
16
|
+
effect: (f) => f(),
|
|
17
|
+
inject: (names, f) => { if (names.every(name => name in context))
|
|
18
|
+
f(context); },
|
|
19
|
+
};
|
|
20
|
+
plugin.apply(context);
|
|
21
|
+
assert.ok(slots.some(s => s.definition.name === 'plugins.item' && s.definition.id === 'gotry'), 'Plugins must contain a directly visible GoTry configuration entry');
|
|
22
|
+
assert.ok(tabs.some(t => t.kind === 'gotry-artifacts' && t.guide?.length), 'right Sidebar must contain an always discoverable GoTry artifacts page');
|
|
23
|
+
assert.ok(slots.some(s => s.definition.name === 'sidebar.right.pane.tab' && s.definition.key === '@danceiny/gotry:artifacts'));
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
25
|
+
for (const dependency of ['@deepseek-ai/dsh-client-ui-primitives', '@deepseek-ai/dsh-client-ui-plugin-manager', '@deepseek-ai/dsh-client-ui-sidebar-right']) {
|
|
26
|
+
assert.ok(pkg.dsh.client.inject.includes(dependency), `the shipped client graph must include ${dependency}`);
|
|
27
|
+
}
|
|
28
|
+
console.log('GoTry native Plugins and artifact Sidebar entries PASS');
|
|
@@ -32,6 +32,8 @@ function privateEnv(root) {
|
|
|
32
32
|
TMPDIR: root,
|
|
33
33
|
TEMP: root,
|
|
34
34
|
TMP: root,
|
|
35
|
+
NODE_OPTIONS: `--import=${pathToFileURL(join(repoRoot, 'scripts', 'doctor-test-isolation.mjs')).href}`,
|
|
36
|
+
GOTRY_TEST_DOCTOR_PORT: '0',
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
function waitForClose(child, timeoutMs) {
|
|
@@ -300,7 +302,7 @@ function writeProductFixture(root, options = { writeTerminalOutput: false, mode:
|
|
|
300
302
|
mkdirSync(presentDir, { recursive: true });
|
|
301
303
|
writeFileSync(join(presentDir, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-tool-present', main: 'index.js' }));
|
|
302
304
|
writeFileSync(join(presentDir, 'index.js'), 'module.exports = {}\n');
|
|
303
|
-
for (const file of ['gotry-inner.js', 'gotry-bootstrap.js', 'gotry-runtime-resolution.js', 'gotry-process-liveness.js']) {
|
|
305
|
+
for (const file of ['gotry-inner.js', 'gotry-bootstrap.js', 'gotry-flyai-verification.js', 'gotry-extension-status.js', 'gotry-runtime-resolution.js', 'gotry-process-liveness.js']) {
|
|
304
306
|
copyFileSync(join(repoRoot, 'bin', file), join(binDir, file));
|
|
305
307
|
}
|
|
306
308
|
copyFileSync(join(repoRoot, 'ts', 'capabilities', 'incident-log.ts'), join(tsCapabilitiesDir, 'incident-log.ts'));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
3
|
+
export function registerArtifactDeliveries(ctx) {
|
|
4
|
+
// Tool-registration-only hosts have no conversation event surface.
|
|
5
|
+
if (typeof ctx.on !== 'function')
|
|
6
|
+
return;
|
|
7
|
+
ctx.on('tools/result', (exec, result) => {
|
|
8
|
+
if (result.isError || exec.signal.aborted || !['gotry_itinerary_render', 'gotry_itinerary_deck_render'].includes(exec.name))
|
|
9
|
+
return;
|
|
10
|
+
const value = result.value;
|
|
11
|
+
const session = exec.agent?.session, cwd = session?.header.cwd;
|
|
12
|
+
if (!session || !cwd || !isAbsolute(cwd) || value?.ok !== true || typeof value.path !== 'string' || !isAbsolute(value.path))
|
|
13
|
+
return;
|
|
14
|
+
const boundary = ctx.get('sessionProjections')?.stateOf(session, 'turnBoundary');
|
|
15
|
+
if (!boundary || boundary.openTurnStartSeq === null)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
const path = realpathSync(value.path), root = realpathSync(cwd), inside = relative(root, path);
|
|
19
|
+
if (inside === '..' || inside.startsWith('..' + sep) || isAbsolute(inside) || !statSync(path).isFile())
|
|
20
|
+
return;
|
|
21
|
+
session.append('deliverables/presented', { turn: boundary.lastTurn, callId: exec.callId,
|
|
22
|
+
files: [{ path: value.path, description: 'GoTry itinerary' }] });
|
|
23
|
+
}
|
|
24
|
+
catch { /* A deleted or escaped file cannot become a delivery. */ }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { verifyFlyaiCandidate } from '../../bin/gotry-flyai-verification.js';
|
|
5
|
+
import { listArtifacts } from '../capabilities/artifacts.js';
|
|
6
|
+
import { atomicWriteJson, clearFlyaiKey, displayEndpoint, endpointFingerprint, flyaiConfigPath, isPlaceholderFlyaiKey, maskFlyaiKey, readFlyaiConfig, readFlyaiVerification, removeFlyaiVerification, resolveFlyaiEndpoint, resolveFlyaiKey, saveFlyaiKey, sha256Hex, writeFlyaiVerification, } from '../capabilities/flyai-config.js';
|
|
7
|
+
const json = (value, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
|
|
8
|
+
const rejection = (error, status = 400) => json({ ok: false, error }, status);
|
|
9
|
+
const BODY_LIMIT = 8192;
|
|
10
|
+
async function readInput(request) {
|
|
11
|
+
if (request.headers.get('content-type')?.split(';')[0].trim() !== 'application/json')
|
|
12
|
+
return rejection('需要 JSON 请求。', 415);
|
|
13
|
+
const reader = request.body?.getReader();
|
|
14
|
+
if (!reader)
|
|
15
|
+
return rejection('请求为空。');
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const chunk = await reader.read();
|
|
21
|
+
if (chunk.done)
|
|
22
|
+
break;
|
|
23
|
+
bytes += chunk.value.byteLength;
|
|
24
|
+
if (bytes > BODY_LIMIT) {
|
|
25
|
+
await reader.cancel();
|
|
26
|
+
return rejection('请求过大。', 413);
|
|
27
|
+
}
|
|
28
|
+
chunks.push(chunk.value);
|
|
29
|
+
}
|
|
30
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
31
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : rejection('请求无效。');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return rejection('请求无效。');
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function createGotryWebHandlers(options) {
|
|
41
|
+
const homeDir = options.homeDir ?? homedir(), env = options.env ?? process.env;
|
|
42
|
+
const paths = { homeDir, env }, configPath = flyaiConfigPath(homeDir);
|
|
43
|
+
let mutations = Promise.resolve();
|
|
44
|
+
const status = () => {
|
|
45
|
+
const current = resolveFlyaiKey(paths), endpoint = resolveFlyaiEndpoint(paths), receipt = readFlyaiVerification(homeDir);
|
|
46
|
+
const matches = current.key && receipt?.keySha256 === sha256Hex(current.key) && receipt.source === current.source
|
|
47
|
+
&& receipt.endpointFingerprint === endpointFingerprint(endpoint.url) && receipt.endpointDebug === endpoint.debug;
|
|
48
|
+
return { ok: true, configured: Boolean(current.key), verified: Boolean(matches && receipt?.verdict === 'verified'),
|
|
49
|
+
writable: current.source !== 'env' && current.source !== 'env-debug' && process.platform !== 'win32', source: current.source,
|
|
50
|
+
endpoint: displayEndpoint(endpoint.url), endpointDebug: endpoint.debug,
|
|
51
|
+
...(matches && receipt ? { checkedAt: receipt.at, verdict: receipt.verdict } : {}) };
|
|
52
|
+
};
|
|
53
|
+
const bytesAt = () => existsSync(configPath) ? readFileSync(configPath, 'utf8') : undefined;
|
|
54
|
+
async function mutate(input, signal) {
|
|
55
|
+
if (signal.aborted)
|
|
56
|
+
return rejection('操作已取消。', 409);
|
|
57
|
+
if (!status().writable)
|
|
58
|
+
return rejection('当前 Key 由环境变量提供或此平台不支持安全写入。', 409);
|
|
59
|
+
if (input.action === 'clear') {
|
|
60
|
+
const result = clearFlyaiKey(paths);
|
|
61
|
+
if (!result.ok)
|
|
62
|
+
return rejection('无法清除配置,请检查本机配置文件及权限。', 409);
|
|
63
|
+
removeFlyaiVerification(homeDir);
|
|
64
|
+
return json(status());
|
|
65
|
+
}
|
|
66
|
+
const key = typeof input.key === 'string' ? input.key.trim() : '';
|
|
67
|
+
if (!key || key.length > 2048 || /[\r\n\0]/.test(key) || isPlaceholderFlyaiKey(key))
|
|
68
|
+
return rejection('请粘贴控制台中的真实 API Key。');
|
|
69
|
+
const before = readFlyaiConfig(configPath);
|
|
70
|
+
if (!before.ok)
|
|
71
|
+
return rejection('原配置损坏,已保留原文件,请先修复。', 409);
|
|
72
|
+
const previousBytes = bytesAt(), endpoint = resolveFlyaiEndpoint(paths), fingerprint = endpointFingerprint(endpoint.url);
|
|
73
|
+
if (!fingerprint)
|
|
74
|
+
return rejection('当前服务地址无效。', 409);
|
|
75
|
+
const verify = await verifyFlyaiCandidate(key, { env: { ...env, HOME: homeDir }, signal });
|
|
76
|
+
if (signal.aborted || verify.verdict !== 'hit')
|
|
77
|
+
return json({ ok: false, verdict: signal.aborted ? 'cancelled' : verify.verdict,
|
|
78
|
+
error: signal.aborted ? '验证已取消,旧配置保留。' : verify.error ?? '验证失败,旧配置保留。' }, 422);
|
|
79
|
+
if (!status().writable || bytesAt() !== previousBytes || fingerprint !== endpointFingerprint(resolveFlyaiEndpoint(paths).url)) {
|
|
80
|
+
return rejection('配置在验证期间发生变化,请重新读取后再试。', 409);
|
|
81
|
+
}
|
|
82
|
+
const saved = saveFlyaiKey(key, paths);
|
|
83
|
+
if (!saved.ok)
|
|
84
|
+
return rejection('安全保存失败,旧配置保留,请检查本机权限。', 409);
|
|
85
|
+
const receiptSaved = writeFlyaiVerification({ schema: 'gotry.flyai-verification.v1', source: 'config',
|
|
86
|
+
keySha256: sha256Hex(key), maskedKey: maskFlyaiKey(key), verdict: 'verified', endpointFingerprint: fingerprint,
|
|
87
|
+
endpointDebug: endpoint.debug, at: new Date().toISOString() }, homeDir);
|
|
88
|
+
if (!receiptSaved) {
|
|
89
|
+
const restored = previousBytes === undefined
|
|
90
|
+
? (() => { try {
|
|
91
|
+
rmSync(configPath, { force: true });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
} })()
|
|
97
|
+
: atomicWriteJson(configPath, before.data).ok;
|
|
98
|
+
return rejection(restored ? '验证回执保存失败,旧配置已恢复。' : '验证回执保存失败,恢复旧配置失败,请检查本机文件。', 409);
|
|
99
|
+
}
|
|
100
|
+
return json(status());
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
async flyai(request) {
|
|
104
|
+
if (request.method === 'GET')
|
|
105
|
+
return json(status());
|
|
106
|
+
if (request.method !== 'POST')
|
|
107
|
+
return rejection('不支持此操作。', 405);
|
|
108
|
+
const input = await readInput(request);
|
|
109
|
+
if (input instanceof Response)
|
|
110
|
+
return input;
|
|
111
|
+
if (!['save', 'clear'].includes(String(input.action)) || Object.keys(input).some(key => key !== 'action' && key !== 'key')
|
|
112
|
+
|| (input.action === 'clear' && 'key' in input))
|
|
113
|
+
return rejection('请求无效。');
|
|
114
|
+
const operation = mutations.then(() => mutate(input, request.signal)).catch(() => rejection('操作失败,未能确认保存,请重新读取状态。', 500));
|
|
115
|
+
mutations = operation.then(() => undefined);
|
|
116
|
+
return operation;
|
|
117
|
+
},
|
|
118
|
+
async artifacts(request) {
|
|
119
|
+
const query = new URL(request.url).searchParams;
|
|
120
|
+
if ([...query.keys()].some(key => !['sessionId', 'search', 'offset', 'limit'].includes(key)))
|
|
121
|
+
return rejection('查询字段无效。');
|
|
122
|
+
const sessionId = query.get('sessionId') ?? '';
|
|
123
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(sessionId))
|
|
124
|
+
return rejection('需要有效会话。');
|
|
125
|
+
const cwd = await options.workspaceForSession(sessionId);
|
|
126
|
+
if (!cwd || !isAbsolute(cwd))
|
|
127
|
+
return rejection('会话工作目录不可用。', 404);
|
|
128
|
+
try {
|
|
129
|
+
return json({ ok: true, ...await listArtifacts({ stateRoot: options.stateRoot, cwd,
|
|
130
|
+
handoffRoot: env.GOTRY_TURN_HANDOFF_ROOT ?? (options.stateRoot !== '.' ? options.stateRoot : cwd),
|
|
131
|
+
search: query.get('search') ?? undefined, offset: query.has('offset') ? Number(query.get('offset')) : undefined,
|
|
132
|
+
limit: query.has('limit') ? Number(query.get('limit')) : 20 }) });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return rejection('查询参数无效或产物暂时不可读取。');
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function registerGotryWebApi(ctx, stateRoot) {
|
|
141
|
+
if (typeof ctx.inject !== 'function')
|
|
142
|
+
return;
|
|
143
|
+
ctx.inject(['connection', 'sessions'], scope => {
|
|
144
|
+
const connection = scope.get('connection');
|
|
145
|
+
const handlers = createGotryWebHandlers({ stateRoot, workspaceForSession: async (sessionId) => {
|
|
146
|
+
const id = sessionId;
|
|
147
|
+
const live = scope.sessions.get(id)?.header;
|
|
148
|
+
const stored = live ? undefined : await scope.get('sessionPersistence')?.stat(sessionId);
|
|
149
|
+
return (live ?? stored?.header)?.cwd;
|
|
150
|
+
} });
|
|
151
|
+
// DSH's streaming adapter always attaches a request body; GET cannot use
|
|
152
|
+
// it. Exact routes have one body mode, so reads and bounded writes differ.
|
|
153
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/flyai', methods: ['GET'], requestBody: 'buffered', fetch: handlers.flyai }));
|
|
154
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/flyai/write', methods: ['POST'], requestBody: 'streaming', fetch: handlers.flyai }));
|
|
155
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/artifacts', methods: ['GET'], requestBody: 'buffered', fetch: handlers.artifacts }));
|
|
156
|
+
});
|
|
157
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -42,6 +42,8 @@ import { generateItineraryDeckExport } from '../capabilities/itinerary-deck-expo
|
|
|
42
42
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.js';
|
|
43
43
|
import { customerSendCode, customerLogin } from '../capabilities/hbcli.js';
|
|
44
44
|
import { FLYAI_POI_CATEGORIES } from '../capabilities/flyai.js';
|
|
45
|
+
import { registerGotryWebApi } from './gotry-web-api.js';
|
|
46
|
+
import { registerArtifactDeliveries } from './artifact-delivery.js';
|
|
45
47
|
import { createFlyaiSetupTool } from './flyai-setup-tool.js';
|
|
46
48
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.js';
|
|
47
49
|
import { factsFromFlyai, factsFromHotel, factsFromSession, factsFromSessionTrain } from './bookable-facts.js';
|
|
@@ -429,6 +431,10 @@ export function apply(ctx, config, seams = {}) {
|
|
|
429
431
|
// 固定 policy 保证评测可复现,不走路由。GOTRY_HANDOFF_CHILD=1 是收集器
|
|
430
432
|
// 派生的后台规划会话:唯一出口 converge + 长 leash——前台转后台,后台
|
|
431
433
|
// 必须产出最终交付物,不得再次 handoff(否则无限递归)。
|
|
434
|
+
if (!rawBenchmarkEnvironmentConfigPath) {
|
|
435
|
+
registerGotryWebApi(ctx, config.stateRoot ?? '.');
|
|
436
|
+
registerArtifactDeliveries(ctx);
|
|
437
|
+
}
|
|
432
438
|
const handoffChild = process.env.GOTRY_HANDOFF_CHILD === '1';
|
|
433
439
|
installTurnDeadline(ctx, handoffChild
|
|
434
440
|
? { fixedPolicy: { softMs: 300_000, hardMs: 900_000, exit: 'converge' } }
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Stai Travel Bridge",
|
|
4
|
-
"version": "0.2.0.
|
|
4
|
+
"version": "0.2.0.31",
|
|
5
5
|
"description": "Stai 连接获准的旅行检索与供应商门户:检索结果经本机或授权后端桥接;门户可触发一次性登录代填,扩展不持久保存凭据。",
|
|
6
6
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnuzBX8rT8+RZKEUip5VRA0cY056pExy9PNdN9YCiuY3dgglRbcIBhjuFJoR6EX3/MaUv82vJVyItRVrowXLTwDk4WZof0vwFgKL0zBK304PC46pUaAKTSu1PgXeL1+j5KTAGz/9a9+RnPdbj7jND5rM/MOmBHNPVPpp1NlwxRGIgF9OxHSOfXORcH4Iyte1aJSFUJbFtNS/DaE5D+KjyupPLyIqVN/dCiKnNsTCEHQB9ngeli665PFp76hkccHQQZ4sVuMLS4zBPcazUsp1kcnW+xsQoUATkODTCY+Pzk3zUVF1QOESTjRCVnbNi8tkO43RMRs2q63gX/3inFivcEwIDAQAB",
|
|
7
7
|
"permissions": [
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"run_at": "document_start"
|
|
57
57
|
}
|
|
58
58
|
],
|
|
59
|
-
"version_name": "0.2.0-rc.
|
|
59
|
+
"version_name": "0.2.0-rc.31"
|
|
60
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danceiny/gotry",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.31",
|
|
4
4
|
"description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm/源码入口共用锁定的 dsh runtime + README 安装路径。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ts/src/index.ts",
|
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
"client": {
|
|
45
45
|
"platform": "web",
|
|
46
46
|
"inject": [
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-tool"
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-tool",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
48
51
|
]
|
|
49
52
|
}
|
|
50
53
|
},
|
|
@@ -57,6 +60,8 @@
|
|
|
57
60
|
"bin/",
|
|
58
61
|
"ts/src/index.ts",
|
|
59
62
|
"ts/src/flyai-setup-tool.ts",
|
|
63
|
+
"ts/src/gotry-web-api.ts",
|
|
64
|
+
"ts/src/artifact-delivery.ts",
|
|
60
65
|
"ts/src/subagent-job-id-guard.ts",
|
|
61
66
|
"ts/src/benchmark-environment-bridge.ts",
|
|
62
67
|
"ts/src/benchmark-tool-isolation.ts",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Bridge successful GoTry render results to DSH's native present/delivery protocol. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import type {} from '@deepseek-ai/dsh-tool-present/types'
|
|
4
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
5
|
+
import { realpathSync, statSync } from 'node:fs'
|
|
6
|
+
import { isAbsolute, relative, sep } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export function registerArtifactDeliveries(ctx: Context): void {
|
|
9
|
+
// Tool-registration-only hosts have no conversation event surface.
|
|
10
|
+
if (typeof ctx.on !== 'function') return
|
|
11
|
+
ctx.on('tools/result', (exec, result) => {
|
|
12
|
+
if (result.isError || exec.signal.aborted || !['gotry_itinerary_render', 'gotry_itinerary_deck_render'].includes(exec.name)) return
|
|
13
|
+
const value = result.value as { ok?: boolean; path?: unknown } | null
|
|
14
|
+
const session = exec.agent?.session, cwd = session?.header.cwd
|
|
15
|
+
if (!session || !cwd || !isAbsolute(cwd) || value?.ok !== true || typeof value.path !== 'string' || !isAbsolute(value.path)) return
|
|
16
|
+
const boundary = ctx.get('sessionProjections')?.stateOf(session, 'turnBoundary')
|
|
17
|
+
if (!boundary || boundary.openTurnStartSeq === null) return
|
|
18
|
+
try {
|
|
19
|
+
const path = realpathSync(value.path), root = realpathSync(cwd), inside = relative(root, path)
|
|
20
|
+
if (inside === '..' || inside.startsWith('..' + sep) || isAbsolute(inside) || !statSync(path).isFile()) return
|
|
21
|
+
session.append('deliverables/presented', { turn: boundary.lastTurn, callId: exec.callId,
|
|
22
|
+
files: [{ path: value.path, description: 'GoTry itinerary' }] })
|
|
23
|
+
} catch { /* A deleted or escaped file cannot become a delivery. */ }
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Native Connection routes. Secrets never enter model tools or conversation events. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection'
|
|
4
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { isAbsolute } from 'node:path'
|
|
7
|
+
import { verifyFlyaiCandidate } from '../../bin/gotry-flyai-verification.js'
|
|
8
|
+
import { listArtifacts } from '../capabilities/artifacts.ts'
|
|
9
|
+
import {
|
|
10
|
+
atomicWriteJson, clearFlyaiKey, displayEndpoint, endpointFingerprint, flyaiConfigPath,
|
|
11
|
+
isPlaceholderFlyaiKey, maskFlyaiKey, readFlyaiConfig, readFlyaiVerification,
|
|
12
|
+
removeFlyaiVerification, resolveFlyaiEndpoint, resolveFlyaiKey, saveFlyaiKey,
|
|
13
|
+
sha256Hex, writeFlyaiVerification,
|
|
14
|
+
} from '../capabilities/flyai-config.ts'
|
|
15
|
+
|
|
16
|
+
interface WebOptions {
|
|
17
|
+
stateRoot: string
|
|
18
|
+
homeDir?: string
|
|
19
|
+
env?: NodeJS.ProcessEnv
|
|
20
|
+
workspaceForSession: (sessionId: string) => Promise<string | undefined>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const json = (value: unknown, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } })
|
|
24
|
+
const rejection = (error: string, status = 400) => json({ ok: false, error }, status)
|
|
25
|
+
const BODY_LIMIT = 8192
|
|
26
|
+
|
|
27
|
+
async function readInput(request: Request): Promise<Record<string, unknown> | Response> {
|
|
28
|
+
if (request.headers.get('content-type')?.split(';')[0].trim() !== 'application/json') return rejection('需要 JSON 请求。', 415)
|
|
29
|
+
const reader = request.body?.getReader()
|
|
30
|
+
if (!reader) return rejection('请求为空。')
|
|
31
|
+
const chunks: Uint8Array[] = []
|
|
32
|
+
let bytes = 0
|
|
33
|
+
try {
|
|
34
|
+
while (true) {
|
|
35
|
+
const chunk = await reader.read()
|
|
36
|
+
if (chunk.done) break
|
|
37
|
+
bytes += chunk.value.byteLength
|
|
38
|
+
if (bytes > BODY_LIMIT) { await reader.cancel(); return rejection('请求过大。', 413) }
|
|
39
|
+
chunks.push(chunk.value)
|
|
40
|
+
}
|
|
41
|
+
const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
42
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : rejection('请求无效。')
|
|
43
|
+
} catch { return rejection('请求无效。') }
|
|
44
|
+
finally { reader.releaseLock() }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createGotryWebHandlers(options: WebOptions) {
|
|
48
|
+
const homeDir = options.homeDir ?? homedir(), env = options.env ?? process.env
|
|
49
|
+
const paths = { homeDir, env }, configPath = flyaiConfigPath(homeDir)
|
|
50
|
+
let mutations: Promise<unknown> = Promise.resolve()
|
|
51
|
+
const status = () => {
|
|
52
|
+
const current = resolveFlyaiKey(paths), endpoint = resolveFlyaiEndpoint(paths), receipt = readFlyaiVerification(homeDir)
|
|
53
|
+
const matches = current.key && receipt?.keySha256 === sha256Hex(current.key) && receipt.source === current.source
|
|
54
|
+
&& receipt.endpointFingerprint === endpointFingerprint(endpoint.url) && receipt.endpointDebug === endpoint.debug
|
|
55
|
+
return { ok: true, configured: Boolean(current.key), verified: Boolean(matches && receipt?.verdict === 'verified'),
|
|
56
|
+
writable: current.source !== 'env' && current.source !== 'env-debug' && process.platform !== 'win32', source: current.source,
|
|
57
|
+
endpoint: displayEndpoint(endpoint.url), endpointDebug: endpoint.debug,
|
|
58
|
+
...(matches && receipt ? { checkedAt: receipt.at, verdict: receipt.verdict } : {}) }
|
|
59
|
+
}
|
|
60
|
+
const bytesAt = () => existsSync(configPath) ? readFileSync(configPath, 'utf8') : undefined
|
|
61
|
+
async function mutate(input: Record<string, unknown>, signal: AbortSignal): Promise<Response> {
|
|
62
|
+
if (signal.aborted) return rejection('操作已取消。', 409)
|
|
63
|
+
if (!status().writable) return rejection('当前 Key 由环境变量提供或此平台不支持安全写入。', 409)
|
|
64
|
+
if (input.action === 'clear') {
|
|
65
|
+
const result = clearFlyaiKey(paths)
|
|
66
|
+
if (!result.ok) return rejection('无法清除配置,请检查本机配置文件及权限。', 409)
|
|
67
|
+
removeFlyaiVerification(homeDir)
|
|
68
|
+
return json(status())
|
|
69
|
+
}
|
|
70
|
+
const key = typeof input.key === 'string' ? input.key.trim() : ''
|
|
71
|
+
if (!key || key.length > 2048 || /[\r\n\0]/.test(key) || isPlaceholderFlyaiKey(key)) return rejection('请粘贴控制台中的真实 API Key。')
|
|
72
|
+
const before = readFlyaiConfig(configPath)
|
|
73
|
+
if (!before.ok) return rejection('原配置损坏,已保留原文件,请先修复。', 409)
|
|
74
|
+
const previousBytes = bytesAt(), endpoint = resolveFlyaiEndpoint(paths), fingerprint = endpointFingerprint(endpoint.url)
|
|
75
|
+
if (!fingerprint) return rejection('当前服务地址无效。', 409)
|
|
76
|
+
const verify = await verifyFlyaiCandidate(key, { env: { ...env, HOME: homeDir }, signal })
|
|
77
|
+
if (signal.aborted || verify.verdict !== 'hit') return json({ ok: false, verdict: signal.aborted ? 'cancelled' : verify.verdict,
|
|
78
|
+
error: signal.aborted ? '验证已取消,旧配置保留。' : verify.error ?? '验证失败,旧配置保留。' }, 422)
|
|
79
|
+
if (!status().writable || bytesAt() !== previousBytes || fingerprint !== endpointFingerprint(resolveFlyaiEndpoint(paths).url)) {
|
|
80
|
+
return rejection('配置在验证期间发生变化,请重新读取后再试。', 409)
|
|
81
|
+
}
|
|
82
|
+
const saved = saveFlyaiKey(key, paths)
|
|
83
|
+
if (!saved.ok) return rejection('安全保存失败,旧配置保留,请检查本机权限。', 409)
|
|
84
|
+
const receiptSaved = writeFlyaiVerification({ schema: 'gotry.flyai-verification.v1', source: 'config',
|
|
85
|
+
keySha256: sha256Hex(key), maskedKey: maskFlyaiKey(key), verdict: 'verified', endpointFingerprint: fingerprint,
|
|
86
|
+
endpointDebug: endpoint.debug, at: new Date().toISOString() }, homeDir)
|
|
87
|
+
if (!receiptSaved) {
|
|
88
|
+
const restored = previousBytes === undefined
|
|
89
|
+
? (() => { try { rmSync(configPath, { force: true }); return true } catch { return false } })()
|
|
90
|
+
: atomicWriteJson(configPath, before.data).ok
|
|
91
|
+
return rejection(restored ? '验证回执保存失败,旧配置已恢复。' : '验证回执保存失败,恢复旧配置失败,请检查本机文件。', 409)
|
|
92
|
+
}
|
|
93
|
+
return json(status())
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
async flyai(request: Request): Promise<Response> {
|
|
97
|
+
if (request.method === 'GET') return json(status())
|
|
98
|
+
if (request.method !== 'POST') return rejection('不支持此操作。', 405)
|
|
99
|
+
const input = await readInput(request)
|
|
100
|
+
if (input instanceof Response) return input
|
|
101
|
+
if (!['save', 'clear'].includes(String(input.action)) || Object.keys(input).some(key => key !== 'action' && key !== 'key')
|
|
102
|
+
|| (input.action === 'clear' && 'key' in input)) return rejection('请求无效。')
|
|
103
|
+
const operation = mutations.then(() => mutate(input, request.signal)).catch(() => rejection('操作失败,未能确认保存,请重新读取状态。', 500))
|
|
104
|
+
mutations = operation.then(() => undefined)
|
|
105
|
+
return operation
|
|
106
|
+
},
|
|
107
|
+
async artifacts(request: Request): Promise<Response> {
|
|
108
|
+
const query = new URL(request.url).searchParams
|
|
109
|
+
if ([...query.keys()].some(key => !['sessionId', 'search', 'offset', 'limit'].includes(key))) return rejection('查询字段无效。')
|
|
110
|
+
const sessionId = query.get('sessionId') ?? ''
|
|
111
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(sessionId)) return rejection('需要有效会话。')
|
|
112
|
+
const cwd = await options.workspaceForSession(sessionId)
|
|
113
|
+
if (!cwd || !isAbsolute(cwd)) return rejection('会话工作目录不可用。', 404)
|
|
114
|
+
try {
|
|
115
|
+
return json({ ok: true, ...await listArtifacts({ stateRoot: options.stateRoot, cwd,
|
|
116
|
+
handoffRoot: env.GOTRY_TURN_HANDOFF_ROOT ?? (options.stateRoot !== '.' ? options.stateRoot : cwd),
|
|
117
|
+
search: query.get('search') ?? undefined, offset: query.has('offset') ? Number(query.get('offset')) : undefined,
|
|
118
|
+
limit: query.has('limit') ? Number(query.get('limit')) : 20 }) })
|
|
119
|
+
} catch { return rejection('查询参数无效或产物暂时不可读取。') }
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function registerGotryWebApi(ctx: Context, stateRoot: string): void {
|
|
125
|
+
if (typeof ctx.inject !== 'function') return
|
|
126
|
+
ctx.inject(['connection', 'sessions'], scope => {
|
|
127
|
+
const connection = scope.get('connection') as HostConnectionHandle
|
|
128
|
+
const handlers = createGotryWebHandlers({ stateRoot, workspaceForSession: async sessionId => {
|
|
129
|
+
const id = sessionId as Parameters<typeof scope.sessions.get>[0]
|
|
130
|
+
const live = scope.sessions.get(id)?.header
|
|
131
|
+
const stored = live ? undefined : await scope.get('sessionPersistence')?.stat(sessionId)
|
|
132
|
+
return (live ?? stored?.header)?.cwd
|
|
133
|
+
} })
|
|
134
|
+
// DSH's streaming adapter always attaches a request body; GET cannot use
|
|
135
|
+
// it. Exact routes have one body mode, so reads and bounded writes differ.
|
|
136
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/flyai', methods: ['GET'], requestBody: 'buffered', fetch: handlers.flyai }))
|
|
137
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/flyai/write', methods: ['POST'], requestBody: 'streaming', fetch: handlers.flyai }))
|
|
138
|
+
scope.effect(() => connection.fetch.register({ path: '/api/gotry/artifacts', methods: ['GET'], requestBody: 'buffered', fetch: handlers.artifacts }))
|
|
139
|
+
})
|
|
140
|
+
}
|
package/ts/src/index.ts
CHANGED
|
@@ -59,6 +59,8 @@ import { generateItineraryDeckExport } from '../capabilities/itinerary-deck-expo
|
|
|
59
59
|
import { interpretEffect, declinedObservation, type EffectInterpreter } from '../capabilities/effect.ts'
|
|
60
60
|
import { customerSendCode, customerLogin } from '../capabilities/hbcli.ts'
|
|
61
61
|
import { FLYAI_POI_CATEGORIES, type FlyaiKind, type FlyaiQuery, type FlyaiResult } from '../capabilities/flyai.ts'
|
|
62
|
+
import { registerGotryWebApi } from './gotry-web-api.ts'
|
|
63
|
+
import { registerArtifactDeliveries } from './artifact-delivery.ts'
|
|
62
64
|
import { createFlyaiSetupTool } from './flyai-setup-tool.ts'
|
|
63
65
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.ts'
|
|
64
66
|
import { factsFromFlyai, factsFromHotel, factsFromSession, factsFromSessionTrain } from './bookable-facts.ts'
|
|
@@ -520,6 +522,10 @@ export function apply(ctx: Context, config: Config, seams: ApplyTestSeams = {}):
|
|
|
520
522
|
// 固定 policy 保证评测可复现,不走路由。GOTRY_HANDOFF_CHILD=1 是收集器
|
|
521
523
|
// 派生的后台规划会话:唯一出口 converge + 长 leash——前台转后台,后台
|
|
522
524
|
// 必须产出最终交付物,不得再次 handoff(否则无限递归)。
|
|
525
|
+
if (!rawBenchmarkEnvironmentConfigPath) {
|
|
526
|
+
registerGotryWebApi(ctx, config.stateRoot ?? '.')
|
|
527
|
+
registerArtifactDeliveries(ctx)
|
|
528
|
+
}
|
|
523
529
|
const handoffChild = process.env.GOTRY_HANDOFF_CHILD === '1'
|
|
524
530
|
installTurnDeadline(ctx, handoffChild
|
|
525
531
|
? { fixedPolicy: { softMs: 300_000, hardMs: 900_000, exit: 'converge' } }
|