@gt-fe/eap-runtime 0.0.29 → 0.0.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/dist/api/handlers/invoke.handler.js +9 -5
- package/dist/api/handlers/stream.handler.js +15 -4
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/user-root.d.ts +8 -0
- package/dist/common/user-root.js +19 -0
- package/dist/graph/adapters/langgraph.adapter.js +193 -134
- package/dist/graph/adapters/skill-read.js +13 -4
- package/dist/graph/contracts/executable-graph.d.ts +7 -1
- package/dist/graph/hitl/hitl-handler.js +6 -0
- package/dist/graph/nodes/tool-node.d.ts +4 -1
- package/dist/graph/nodes/tool-node.js +15 -1
- package/dist/graph/trace-span.d.ts +15 -1
- package/dist/graph/trace-span.js +69 -1
- package/dist/registry/deployment-snapshot.d.ts +1 -1
- package/dist/registry/deployment-snapshot.js +6 -3
- package/dist/registry/skill-tree.d.ts +1 -1
- package/dist/registry/skill-tree.js +11 -6
- package/dist/runtime/agent-runtime.js +36 -5
- package/dist/session/persist-turn-messages.d.ts +13 -6
- package/dist/session/persist-turn-messages.js +4 -0
- package/dist/sse/events/tool-events.d.ts +29 -0
- package/dist/tool/harness-command.d.ts +1 -1
- package/dist/tool/harness-command.js +8 -7
- package/dist/tool/harness-primitives.d.ts +3 -3
- package/dist/tool/harness-primitives.js +185 -60
- package/dist/tool/index.d.ts +2 -0
- package/dist/tool/index.js +2 -0
- package/dist/tool/inject-public-artifact-links.d.ts +32 -0
- package/dist/tool/inject-public-artifact-links.js +142 -0
- package/dist/tool/static-artifact-url.d.ts +66 -0
- package/dist/tool/static-artifact-url.js +561 -0
- package/dist/tool/system-prompt-builder.js +8 -4
- package/dist/tool/tool-event-emitter.d.ts +2 -1
- package/dist/tool/tool-event-emitter.js +30 -18
- package/dist/tool/tool-executor.interface.d.ts +8 -1
- package/dist/tool/tool-result-injector.d.ts +3 -3
- package/dist/tool/tool-result-injector.js +7 -3
- package/package.json +2 -2
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
|
-
* 写:`users/<userId>/`(含用户自己的 skills
|
|
5
|
+
* 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
|
|
6
6
|
* 读:写根 + 已装配 `preinstall/tools`、`preinstall/skills`。
|
|
7
|
-
* 命令:cwd
|
|
7
|
+
* 命令:cwd 默认 `files/`。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
|
|
8
8
|
* 开发态 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。在 `createDevRuntime` /
|
|
9
9
|
* `createToolExecutor` 时钉进执行器 extraRoots;SDK `createAgent` 构造完会恢复 process.env,
|
|
10
10
|
* 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { constants as fsConstants, existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
14
|
-
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
14
|
+
import { chmod, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { MISSING_USER_DIR, userRoot } from '../common/user-root.js';
|
|
17
|
+
import { MAX_SNAPSHOT_ENUM, MAX_SNAPSHOT_URLS, PUBLIC_FILE_MODE, ensureUserSlots, maxWriteBytesFromEnv, publishFromFiles, resolveUserProductPath, snapshotCandidates, walkFilesTree, } from './static-artifact-url.js';
|
|
16
18
|
import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
|
|
17
19
|
import { ensureInterpreterShims, formatSpawnError, killProcessTree, resolveShellSpawn, sandboxEnv, spawnOptionsFor, substituteSkillDir, } from './harness-command.js';
|
|
18
20
|
import { expandSkillScriptRefs, isSkillEntryFile } from './skill-script-refs.js';
|
|
21
|
+
/** 超时杀进程或 spawn 失败后等 `close`;未到则最多再等此时长再快照。 */
|
|
22
|
+
const SPAWN_CLOSE_GRACE_MS = 1000;
|
|
19
23
|
function builtinConfig() {
|
|
20
24
|
return resolveBuiltinToolsConfig(process.env);
|
|
21
25
|
}
|
|
@@ -66,7 +70,7 @@ function harnessDefinitions() {
|
|
|
66
70
|
manifest: {
|
|
67
71
|
id: 'builtin:write_file',
|
|
68
72
|
name: 'write_file',
|
|
69
|
-
description: '
|
|
73
|
+
description: '把 UTF-8 文本整份写入当前用户的 files/(装配槽用 skills/ 前缀)。没有该文件则创建;已有文件会整份换成 content,不是改某一行。只要在末尾加内容请用 append_to_file。不能写 public/ 或 work/,不能写预装 tools/skills,不能写 .assembly.json 等平台文件。png/xlsx/docx/zip/pdf 等二进制请用 execute_command 写。成功时工具结果是 JSON:{"ok":true,"path":"工作稿原名","bytesWritten":字节数,"url":"公开副本链接,未配基址则无此字段"}。路径越权或禁写文件名会失败。',
|
|
70
74
|
...common,
|
|
71
75
|
},
|
|
72
76
|
inputSchema: {
|
|
@@ -77,11 +81,11 @@ function harnessDefinitions() {
|
|
|
77
81
|
path: {
|
|
78
82
|
type: 'string',
|
|
79
83
|
minLength: 1,
|
|
80
|
-
description: '
|
|
84
|
+
description: '相对 files/ 的路径,例如 notes/todo.md 或 page/index.html。装配槽用 skills/ 前缀。不能写预装 tools/skills、public/、work/。',
|
|
81
85
|
},
|
|
82
86
|
content: {
|
|
83
87
|
type: 'string',
|
|
84
|
-
description: '
|
|
88
|
+
description: '文件的完整新内容(UTF-8 文本)。会覆盖原文件全部内容。',
|
|
85
89
|
},
|
|
86
90
|
},
|
|
87
91
|
},
|
|
@@ -92,7 +96,7 @@ function harnessDefinitions() {
|
|
|
92
96
|
manifest: {
|
|
93
97
|
id: 'builtin:append_to_file',
|
|
94
98
|
name: 'append_to_file',
|
|
95
|
-
description: '
|
|
99
|
+
description: '在当前用户 files/ 的文件末尾追加 UTF-8 文本,不覆盖已有内容。没有该文件则创建。不要用它整份改写文件,那是 write_file。不能写 public/ 或 work/,不能写预装 tools/skills,不能写 .assembly.json 等平台文件。成功时工具结果是 JSON:{"ok":true,"path":"工作稿原名","bytesWritten":本次追加字节数,"url":"公开副本链接,未配基址则无此字段"}。路径越权或禁写文件名会失败。',
|
|
96
100
|
...common,
|
|
97
101
|
},
|
|
98
102
|
inputSchema: {
|
|
@@ -103,11 +107,11 @@ function harnessDefinitions() {
|
|
|
103
107
|
path: {
|
|
104
108
|
type: 'string',
|
|
105
109
|
minLength: 1,
|
|
106
|
-
description: '
|
|
110
|
+
description: '相对 files/ 的路径,例如 notes/todo.md。装配槽用 skills/ 前缀。不能写预装 tools/skills、public/、work/。',
|
|
107
111
|
},
|
|
108
112
|
content: {
|
|
109
113
|
type: 'string',
|
|
110
|
-
description: '
|
|
114
|
+
description: '接到文件末尾的 UTF-8 文本。不会删除或替换原有内容。',
|
|
111
115
|
},
|
|
112
116
|
},
|
|
113
117
|
},
|
|
@@ -118,7 +122,7 @@ function harnessDefinitions() {
|
|
|
118
122
|
manifest: {
|
|
119
123
|
id: 'builtin:execute_command',
|
|
120
124
|
name: 'execute_command',
|
|
121
|
-
description: `用本机 shell
|
|
125
|
+
description: `用本机 shell 在用户 files/ 执行命令(Windows cmd.exe,其它 /bin/sh)。技能包只读,用 EAP_SKILL_DIR 加包内相对路径跑文件(不限 scripts/)。python/python3、node/nodejs、sh/bash 经 shim。默认超时 ${DEFAULT_BUILTIN_TIMEOUT_MS}ms,可用 timeoutMs 延长,上限 ${MAX_BUILTIN_TIMEOUT_MS}ms。不继承密钥。HOME/TMPDIR 在 work/。子进程 close 后(含超时杀进程)把 files/ 里新的或有改动的公开类型文件做成 public/ 时间戳副本。`,
|
|
122
126
|
...common,
|
|
123
127
|
},
|
|
124
128
|
inputSchema: {
|
|
@@ -126,7 +130,7 @@ function harnessDefinitions() {
|
|
|
126
130
|
required: ['command'],
|
|
127
131
|
properties: {
|
|
128
132
|
command: { type: 'string' },
|
|
129
|
-
cwd: { type: 'string', description: '
|
|
133
|
+
cwd: { type: 'string', description: 'files/ 内的子目录,可选;省略则 files/。不能是 work/、public/ 或预装技能/工具包' },
|
|
130
134
|
timeoutMs: {
|
|
131
135
|
type: 'integer',
|
|
132
136
|
description: `超时毫秒。省略则 ${DEFAULT_BUILTIN_TIMEOUT_MS}。上限 ${MAX_BUILTIN_TIMEOUT_MS}。`,
|
|
@@ -151,19 +155,10 @@ export function mergeHarnessPrimitives(loadedTools) {
|
|
|
151
155
|
export function assembleLoadedTools(loadedTools) {
|
|
152
156
|
return mergeHarnessPrimitives(loadedTools);
|
|
153
157
|
}
|
|
154
|
-
const MISSING_USER_DIR = '缺少用户目录,拒绝该操作';
|
|
155
158
|
const PROTECTED_WRITE_NAMES = new Set(['.assembly.json', 'assembly.json', '.ready']);
|
|
156
|
-
/** `
|
|
159
|
+
/** `dirname(agentDir)/users/<userId>`(写根;含用户 skills) */
|
|
157
160
|
export function resolveUserDir(agentDir, userId) {
|
|
158
|
-
|
|
159
|
-
const uid = userId.trim();
|
|
160
|
-
if (!dir || !uid) {
|
|
161
|
-
throw new Error(MISSING_USER_DIR);
|
|
162
|
-
}
|
|
163
|
-
if (uid === '.' || uid === '..' || /[\\/]/.test(uid)) {
|
|
164
|
-
throw new Error('非法用户标识');
|
|
165
|
-
}
|
|
166
|
-
return path.resolve(dir, 'users', uid);
|
|
161
|
+
return userRoot(agentDir, userId);
|
|
167
162
|
}
|
|
168
163
|
export function collectToolBundlePaths(loadedTools) {
|
|
169
164
|
const roots = [];
|
|
@@ -270,6 +265,17 @@ async function writeFileNoFollow(target, content, append = false) {
|
|
|
270
265
|
await handle.close();
|
|
271
266
|
}
|
|
272
267
|
}
|
|
268
|
+
function maxWriteBytes(config) {
|
|
269
|
+
const n = config.maxWriteBytes;
|
|
270
|
+
if (typeof n === 'number' && Number.isFinite(n) && n > 0)
|
|
271
|
+
return Math.floor(n);
|
|
272
|
+
return maxWriteBytesFromEnv();
|
|
273
|
+
}
|
|
274
|
+
function toolFileFromPublished(p) {
|
|
275
|
+
if (!p.url)
|
|
276
|
+
return undefined;
|
|
277
|
+
return { fileName: p.fileName, fileType: p.fileType, size: p.size, url: p.url };
|
|
278
|
+
}
|
|
273
279
|
function isInsideRoot(candidate, root) {
|
|
274
280
|
const rel = path.relative(root, candidate);
|
|
275
281
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
@@ -473,10 +479,27 @@ export class HarnessPrimitiveExecutor {
|
|
|
473
479
|
const filePath = String(request.arguments.path ?? '');
|
|
474
480
|
const workdir = this.workdirFor(request);
|
|
475
481
|
const located = locateSkillRelativeRead(filePath, workdir, request.skillDir, this.extraRootsFor(request));
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
482
|
+
const userBase = userReadBase(workdir);
|
|
483
|
+
let target;
|
|
484
|
+
if (path.isAbsolute(filePath.trim())) {
|
|
485
|
+
target = resolveJailedPath(filePath, this.rootsFor(request), { mustExist: true });
|
|
486
|
+
}
|
|
487
|
+
else if (located.relativeBase && located.relativeBase !== userBase) {
|
|
488
|
+
target = resolveJailedPath(located.path, this.rootsFor(request), {
|
|
489
|
+
mustExist: true,
|
|
490
|
+
relativeBase: located.relativeBase,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else if (workdir) {
|
|
494
|
+
const product = resolveUserProductPath(filePath, workdir, 'read');
|
|
495
|
+
target = resolveJailedPath(product.abs, this.rootsFor(request), { mustExist: true });
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
target = resolveJailedPath(located.path, this.rootsFor(request), {
|
|
499
|
+
mustExist: true,
|
|
500
|
+
relativeBase: located.relativeBase,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
480
503
|
const buf = await readFile(target);
|
|
481
504
|
const config = builtinConfig();
|
|
482
505
|
const lines = buf.toString('utf8').split('\n');
|
|
@@ -513,38 +536,75 @@ export class HarnessPrimitiveExecutor {
|
|
|
513
536
|
const filePath = String(request.arguments.path ?? '');
|
|
514
537
|
const content = String(request.arguments.content ?? '');
|
|
515
538
|
const workdir = this.requireWorkdir(request);
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
539
|
+
await ensureUserSlots(workdir);
|
|
540
|
+
const resolved = resolveUserProductPath(filePath, workdir, 'write');
|
|
541
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(resolved.abs))) {
|
|
542
|
+
throw new Error(`禁止写入平台文件:${path.basename(resolved.abs)}`);
|
|
543
|
+
}
|
|
544
|
+
const incomingBytes = Buffer.byteLength(content, 'utf8');
|
|
545
|
+
const limit = maxWriteBytes(builtinConfig());
|
|
546
|
+
if (append) {
|
|
547
|
+
const existing = await stat(resolved.abs)
|
|
548
|
+
.then((s) => s.size)
|
|
549
|
+
.catch(() => 0);
|
|
550
|
+
if (existing + incomingBytes > limit) {
|
|
551
|
+
throw new Error(`文件超过 ${limit} 字节上限`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (incomingBytes > limit) {
|
|
555
|
+
throw new Error(`文件超过 ${limit} 字节上限`);
|
|
521
556
|
}
|
|
522
|
-
await mkdir(path.dirname(
|
|
523
|
-
const recast =
|
|
524
|
-
if (PROTECTED_WRITE_NAMES.has(path.basename(recast))) {
|
|
525
|
-
throw new Error(`禁止写入平台文件:${path.basename(recast)}`);
|
|
557
|
+
await mkdir(path.dirname(resolved.abs), { recursive: true });
|
|
558
|
+
const recast = resolveUserProductPath(filePath, workdir, 'write');
|
|
559
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(recast.abs))) {
|
|
560
|
+
throw new Error(`禁止写入平台文件:${path.basename(recast.abs)}`);
|
|
526
561
|
}
|
|
527
|
-
const bytesWritten = await writeFileNoFollow(recast, content, append);
|
|
528
|
-
|
|
562
|
+
const bytesWritten = await writeFileNoFollow(recast.abs, content, append);
|
|
563
|
+
await chmod(recast.abs, PUBLIC_FILE_MODE).catch(() => undefined);
|
|
564
|
+
const payload = {
|
|
565
|
+
ok: true,
|
|
566
|
+
path: recast.jsonPath,
|
|
567
|
+
bytesWritten,
|
|
568
|
+
};
|
|
569
|
+
let file;
|
|
570
|
+
if (recast.slot === 'files' && recast.relFromFiles) {
|
|
571
|
+
const published = await publishFromFiles(workdir, [recast.relFromFiles]);
|
|
572
|
+
const first = published[0];
|
|
573
|
+
if (first?.url) {
|
|
574
|
+
payload.url = first.url;
|
|
575
|
+
file = toolFileFromPublished(first);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return { ...ok(request, JSON.stringify(payload)), ...(file ? { file } : {}) };
|
|
529
579
|
}
|
|
530
580
|
async executeCommand(request) {
|
|
531
581
|
const command = substituteSkillDir(String(request.arguments.command ?? '').trim(), request.skillDir);
|
|
532
582
|
if (!command)
|
|
533
583
|
return fail(request, 'command 不能为空');
|
|
534
584
|
const workdir = this.requireWorkdir(request);
|
|
535
|
-
await
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
585
|
+
const slots = await ensureUserSlots(workdir);
|
|
586
|
+
let cwd;
|
|
587
|
+
if (request.arguments.cwd != null && String(request.arguments.cwd).trim() !== '') {
|
|
588
|
+
const resolvedCwd = resolveUserProductPath(String(request.arguments.cwd), workdir, 'cwd');
|
|
589
|
+
if (resolvedCwd.slot !== 'files') {
|
|
590
|
+
throw new Error(`路径超出允许目录:${String(request.arguments.cwd)}`);
|
|
591
|
+
}
|
|
592
|
+
cwd = resolvedCwd.abs;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
cwd = slots.filesDir;
|
|
596
|
+
}
|
|
597
|
+
await mkdir(cwd, { recursive: true });
|
|
541
598
|
const config = builtinConfig();
|
|
542
599
|
const timeoutMs = clampBuiltinTimeout(request.arguments.timeoutMs, config);
|
|
543
600
|
const maxOutput = config.maxOutputBytes;
|
|
601
|
+
const writeLimit = maxWriteBytes(config);
|
|
602
|
+
const before = await walkFilesTree(slots.filesDir);
|
|
544
603
|
const shell = resolveShellSpawn(command);
|
|
545
|
-
const shimDir = ensureInterpreterShims(
|
|
546
|
-
const env = sandboxEnv(workdir, cwd, config.envAllowlist, shimDir, request.skillDir);
|
|
547
|
-
|
|
604
|
+
const shimDir = ensureInterpreterShims(slots.workDir);
|
|
605
|
+
const env = sandboxEnv(workdir, cwd, config.envAllowlist, shimDir, request.skillDir, slots.workDir);
|
|
606
|
+
// 子进程 close 后再快照 files/(超时先杀进程再等 close;close 未到则最多再等 SPAWN_CLOSE_GRACE_MS)。
|
|
607
|
+
const output = await new Promise((resolve) => {
|
|
548
608
|
const child = spawn(shell.file, shell.args, {
|
|
549
609
|
cwd,
|
|
550
610
|
env,
|
|
@@ -555,9 +615,36 @@ export class HarnessPrimitiveExecutor {
|
|
|
555
615
|
let stderr = Buffer.alloc(0);
|
|
556
616
|
let stdoutTruncated = false;
|
|
557
617
|
let stderrTruncated = false;
|
|
618
|
+
let settled = false;
|
|
619
|
+
let timedOut = false;
|
|
620
|
+
let spawnError;
|
|
621
|
+
let closeFallback;
|
|
622
|
+
const finish = (value) => {
|
|
623
|
+
if (settled)
|
|
624
|
+
return;
|
|
625
|
+
settled = true;
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
if (closeFallback !== undefined)
|
|
628
|
+
clearTimeout(closeFallback);
|
|
629
|
+
resolve(value);
|
|
630
|
+
};
|
|
631
|
+
const snapshotStdio = () => ({
|
|
632
|
+
stdout: stdout.toString('utf8'),
|
|
633
|
+
stderr: stderr.toString('utf8'),
|
|
634
|
+
stdoutTruncated,
|
|
635
|
+
stderrTruncated,
|
|
636
|
+
});
|
|
637
|
+
const waitForCloseThenFinish = () => {
|
|
638
|
+
if (closeFallback !== undefined)
|
|
639
|
+
clearTimeout(closeFallback);
|
|
640
|
+
closeFallback = setTimeout(() => {
|
|
641
|
+
finish({ ...snapshotStdio(), code: null, timedOut, spawnError });
|
|
642
|
+
}, SPAWN_CLOSE_GRACE_MS);
|
|
643
|
+
};
|
|
558
644
|
const timer = setTimeout(() => {
|
|
645
|
+
timedOut = true;
|
|
559
646
|
killProcessTree(child);
|
|
560
|
-
|
|
647
|
+
waitForCloseThenFinish();
|
|
561
648
|
}, timeoutMs);
|
|
562
649
|
child.stdout?.on('data', (chunk) => {
|
|
563
650
|
if (stdout.length >= maxOutput) {
|
|
@@ -578,33 +665,71 @@ export class HarnessPrimitiveExecutor {
|
|
|
578
665
|
stderrTruncated = true;
|
|
579
666
|
});
|
|
580
667
|
child.on('error', (err) => {
|
|
668
|
+
spawnError = formatSpawnError(err, shell.file);
|
|
581
669
|
clearTimeout(timer);
|
|
582
|
-
|
|
670
|
+
waitForCloseThenFinish();
|
|
583
671
|
});
|
|
584
672
|
child.on('close', (code) => {
|
|
585
|
-
|
|
586
|
-
resolve({
|
|
587
|
-
stdout: stdout.toString('utf8'),
|
|
588
|
-
stderr: stderr.toString('utf8'),
|
|
589
|
-
code,
|
|
590
|
-
stdoutTruncated,
|
|
591
|
-
stderrTruncated,
|
|
592
|
-
});
|
|
673
|
+
finish({ ...snapshotStdio(), code, timedOut, spawnError });
|
|
593
674
|
});
|
|
594
675
|
});
|
|
595
|
-
const
|
|
676
|
+
const after = await walkFilesTree(slots.filesDir);
|
|
677
|
+
const { rels, skippedOversize } = snapshotCandidates(before.entries, after.entries, writeLimit);
|
|
678
|
+
const limited = rels.slice(0, MAX_SNAPSHOT_URLS);
|
|
679
|
+
const published = limited.length > 0 ? await publishFromFiles(workdir, limited) : [];
|
|
680
|
+
const publicFiles = published.map((p) => ({
|
|
681
|
+
path: p.sourceRel ?? p.fileName,
|
|
682
|
+
fileName: p.fileName,
|
|
683
|
+
size: p.size,
|
|
684
|
+
...(p.url ? { url: p.url } : {}),
|
|
685
|
+
}));
|
|
686
|
+
const notes = [];
|
|
687
|
+
if (before.truncated || after.truncated) {
|
|
688
|
+
notes.push(`快照枚举达到 ${MAX_SNAPSHOT_ENUM} 上限`);
|
|
689
|
+
}
|
|
690
|
+
if (skippedOversize > 0) {
|
|
691
|
+
notes.push(`跳过 ${skippedOversize} 个超过大小上限的文件`);
|
|
692
|
+
}
|
|
693
|
+
if (rels.length > MAX_SNAPSHOT_URLS) {
|
|
694
|
+
notes.push(`快照 url 最多 ${MAX_SNAPSHOT_URLS} 个`);
|
|
695
|
+
}
|
|
696
|
+
const withUrl = published
|
|
697
|
+
.map(toolFileFromPublished)
|
|
698
|
+
.filter((f) => Boolean(f));
|
|
699
|
+
const file = withUrl[0];
|
|
700
|
+
const files = withUrl.slice(1);
|
|
701
|
+
const ended = output.timedOut
|
|
702
|
+
? `命令超时(${timeoutMs}ms)`
|
|
703
|
+
: output.spawnError
|
|
704
|
+
? output.spawnError.message
|
|
705
|
+
: `exit ${output.code ?? -1}`;
|
|
706
|
+
const commandBody = [
|
|
596
707
|
output.stdout.trimEnd(),
|
|
597
708
|
output.stdoutTruncated ? `[stdout truncated at ${maxOutput} bytes]` : '',
|
|
598
709
|
output.stderr.trim() ? `stderr:\n${output.stderr.trimEnd()}` : '',
|
|
599
710
|
output.stderrTruncated ? `[stderr truncated at ${maxOutput} bytes]` : '',
|
|
600
|
-
|
|
711
|
+
ended,
|
|
712
|
+
...notes,
|
|
601
713
|
]
|
|
602
714
|
.filter(Boolean)
|
|
603
715
|
.join('\n');
|
|
716
|
+
const body = publicFiles.length > 0
|
|
717
|
+
? `${JSON.stringify({ publicFiles })}\n${commandBody}`
|
|
718
|
+
: commandBody;
|
|
719
|
+
const extra = {
|
|
720
|
+
...(file ? { file } : {}),
|
|
721
|
+
...(files.length > 0 ? { files } : {}),
|
|
722
|
+
};
|
|
723
|
+
if (output.spawnError) {
|
|
724
|
+
return { ...fail(request, output.spawnError.message), content: body, ...extra };
|
|
725
|
+
}
|
|
726
|
+
if (output.timedOut) {
|
|
727
|
+
return { ...fail(request, `命令超时(${timeoutMs}ms)`), content: body, ...extra };
|
|
728
|
+
}
|
|
604
729
|
if (output.code !== 0) {
|
|
605
|
-
return { ...fail(request, `命令退出码 ${output.code}`), content: body };
|
|
730
|
+
return { ...fail(request, `命令退出码 ${output.code}`), content: body, ...extra };
|
|
606
731
|
}
|
|
607
|
-
return ok(request, body);
|
|
732
|
+
return { ...ok(request, body), ...extra };
|
|
608
733
|
}
|
|
609
734
|
}
|
|
610
735
|
export function withHarnessPrimitives(inner, options) {
|
package/dist/tool/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_
|
|
|
15
15
|
export { ToolExecutionRouter, type DeploymentContext, type ToolExecutionRouterOptions, } from './tool-execution-router.js';
|
|
16
16
|
export { createToolExecutor, EmptyInstallStore, type CreateToolExecutorOptions, } from './create-tool-executor.js';
|
|
17
17
|
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';
|
|
18
|
+
export { injectPublicArtifactLinks, collectPublicArtifactLinks, applyInjectedAssistant, } from './inject-public-artifact-links.js';
|
|
19
|
+
export { publishFromFiles, ensureUserSlots, publicArtifactUrl, PUBLIC_EXTENSIONS, } from './static-artifact-url.js';
|
|
18
20
|
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
19
21
|
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
20
22
|
export { ToolResultInjector, type ToolMessage, } from './tool-result-injector.js';
|
package/dist/tool/index.js
CHANGED
|
@@ -14,6 +14,8 @@ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_
|
|
|
14
14
|
export { ToolExecutionRouter, } from './tool-execution-router.js';
|
|
15
15
|
export { createToolExecutor, EmptyInstallStore, } from './create-tool-executor.js';
|
|
16
16
|
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, } from './harness-primitives.js';
|
|
17
|
+
export { injectPublicArtifactLinks, collectPublicArtifactLinks, applyInjectedAssistant, } from './inject-public-artifact-links.js';
|
|
18
|
+
export { publishFromFiles, ensureUserSlots, publicArtifactUrl, PUBLIC_EXTENSIONS, } from './static-artifact-url.js';
|
|
17
19
|
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
18
20
|
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
19
21
|
export { ToolResultInjector, } from './tool-result-injector.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本轮带 url 的公开副本 → 助手正文末尾 GFM 链接。
|
|
3
|
+
* stream / streamResumeHitl / stream.handler / invoke.handler 共用。
|
|
4
|
+
*/
|
|
5
|
+
export type ArtifactLink = {
|
|
6
|
+
fileName: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
export type ToolResultLike = {
|
|
10
|
+
content?: unknown;
|
|
11
|
+
file?: {
|
|
12
|
+
fileName?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
};
|
|
15
|
+
files?: Array<{
|
|
16
|
+
fileName?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
export declare function mergeArtifactLinks(...groups: ArtifactLink[][]): ArtifactLink[];
|
|
21
|
+
export declare function collectPublicArtifactLinks(results: ToolResultLike[]): ArtifactLink[];
|
|
22
|
+
export declare function collectPublicArtifactLinksFromEvent(eventType: string, payload: unknown, bucket: ArtifactLink[]): void;
|
|
23
|
+
export declare function toolResultHasArtifacts(result: ToolResultLike): boolean;
|
|
24
|
+
export declare function applyInjectedAssistant<T extends {
|
|
25
|
+
role?: string;
|
|
26
|
+
content: string;
|
|
27
|
+
toolCalls?: unknown[];
|
|
28
|
+
}>(messages: T[], injected: string, links: ArtifactLink[]): T[];
|
|
29
|
+
export declare function injectPublicArtifactLinks(visibleText: string, artifacts: ArtifactLink[]): {
|
|
30
|
+
injected: string;
|
|
31
|
+
suffix: string;
|
|
32
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本轮带 url 的公开副本 → 助手正文末尾 GFM 链接。
|
|
3
|
+
* stream / streamResumeHitl / stream.handler / invoke.handler 共用。
|
|
4
|
+
*/
|
|
5
|
+
function escapeMdLinkText(text) {
|
|
6
|
+
return text.replace(/\\/g, '\\\\').replace(/([\[\]()])/g, '\\$1');
|
|
7
|
+
}
|
|
8
|
+
function addLink(out, seen, fileName, url) {
|
|
9
|
+
const href = url.trim();
|
|
10
|
+
if (!href || seen.has(href))
|
|
11
|
+
return;
|
|
12
|
+
seen.add(href);
|
|
13
|
+
out.push({ fileName: fileName.trim() || href, url: href });
|
|
14
|
+
}
|
|
15
|
+
function parseContentLinks(content, out, seen) {
|
|
16
|
+
if (typeof content !== 'string' || !content.trim())
|
|
17
|
+
return;
|
|
18
|
+
const first = content.split('\n', 1)[0] ?? '';
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(first);
|
|
21
|
+
if (Array.isArray(parsed.publicFiles)) {
|
|
22
|
+
for (const item of parsed.publicFiles) {
|
|
23
|
+
if (!item || typeof item !== 'object')
|
|
24
|
+
continue;
|
|
25
|
+
const row = item;
|
|
26
|
+
if (typeof row.url === 'string') {
|
|
27
|
+
const name = (typeof row.fileName === 'string' && row.fileName) ||
|
|
28
|
+
(typeof row.path === 'string' && row.path) ||
|
|
29
|
+
'file';
|
|
30
|
+
addLink(out, seen, name, row.url);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.ok === true && typeof parsed.url === 'string') {
|
|
36
|
+
const name = typeof parsed.path === 'string' ? parsed.path : 'file';
|
|
37
|
+
addLink(out, seen, name, parsed.url);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 非 JSON 行忽略
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function mergeArtifactLinks(...groups) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
for (const item of group)
|
|
49
|
+
addLink(out, seen, item.fileName, item.url);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export function collectPublicArtifactLinks(results) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const result of results) {
|
|
57
|
+
if (result.file?.url) {
|
|
58
|
+
addLink(out, seen, result.file.fileName ?? result.file.url, result.file.url);
|
|
59
|
+
}
|
|
60
|
+
for (const f of result.files ?? []) {
|
|
61
|
+
if (f?.url)
|
|
62
|
+
addLink(out, seen, f.fileName ?? f.url, f.url);
|
|
63
|
+
}
|
|
64
|
+
parseContentLinks(result.content, out, seen);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export function collectPublicArtifactLinksFromEvent(eventType, payload, bucket) {
|
|
69
|
+
if (!payload || typeof payload !== 'object')
|
|
70
|
+
return;
|
|
71
|
+
const p = payload;
|
|
72
|
+
const seen = new Set(bucket.map((l) => l.url));
|
|
73
|
+
if (eventType === 'file.created' && typeof p.url === 'string') {
|
|
74
|
+
addLink(bucket, seen, typeof p.fileName === 'string' ? p.fileName : p.url, p.url);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (eventType === 'tool.result' || eventType === 'tool.error') {
|
|
78
|
+
const extra = collectPublicArtifactLinks([
|
|
79
|
+
{ content: p.content, file: p.file, files: p.files },
|
|
80
|
+
]);
|
|
81
|
+
for (const link of extra)
|
|
82
|
+
addLink(bucket, seen, link.fileName, link.url);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function toolResultHasArtifacts(result) {
|
|
86
|
+
if (result.file?.url)
|
|
87
|
+
return true;
|
|
88
|
+
if ((result.files ?? []).some((f) => Boolean(f?.url)))
|
|
89
|
+
return true;
|
|
90
|
+
if (typeof result.content !== 'string' || !result.content.trim())
|
|
91
|
+
return false;
|
|
92
|
+
const first = result.content.split('\n', 1)[0] ?? '';
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(first);
|
|
95
|
+
if (Array.isArray(parsed.publicFiles) && parsed.publicFiles.length > 0)
|
|
96
|
+
return true;
|
|
97
|
+
if (parsed.ok === true && typeof parsed.url === 'string' && parsed.url)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
export function applyInjectedAssistant(messages, injected, links) {
|
|
106
|
+
if (!injected || links.length === 0)
|
|
107
|
+
return messages;
|
|
108
|
+
let idx = -1;
|
|
109
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
110
|
+
const m = messages[i];
|
|
111
|
+
if (m.role !== 'assistant')
|
|
112
|
+
continue;
|
|
113
|
+
if (!m.content.trim())
|
|
114
|
+
continue;
|
|
115
|
+
idx = i;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (idx >= 0) {
|
|
119
|
+
const copy = messages.slice();
|
|
120
|
+
copy[idx] = { ...copy[idx], content: injected };
|
|
121
|
+
return copy;
|
|
122
|
+
}
|
|
123
|
+
return [...messages, { role: 'assistant', content: injected }];
|
|
124
|
+
}
|
|
125
|
+
export function injectPublicArtifactLinks(visibleText, artifacts) {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const lines = [];
|
|
128
|
+
for (const item of artifacts) {
|
|
129
|
+
const url = item.url.trim();
|
|
130
|
+
if (!url || seen.has(url))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(url);
|
|
133
|
+
if (visibleText.includes(url))
|
|
134
|
+
continue;
|
|
135
|
+
lines.push(`[${escapeMdLinkText(item.fileName)}](${url})`);
|
|
136
|
+
}
|
|
137
|
+
if (lines.length === 0)
|
|
138
|
+
return { injected: visibleText, suffix: '' };
|
|
139
|
+
const block = lines.join('\n');
|
|
140
|
+
const suffix = visibleText ? `\n\n${block}` : block;
|
|
141
|
+
return { injected: `${visibleText}${suffix}`, suffix };
|
|
142
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户产物公开副本:files/ 工作稿 → public/ 时间戳 copy;拼 Hosting /static url。
|
|
3
|
+
* @see docs/design/user-artifact-delivery.md
|
|
4
|
+
*/
|
|
5
|
+
/** 与 contracts `DEFAULT_BUILTIN_MAX_WRITE_BYTES` 同值;本地编译不依赖未发版 contracts */
|
|
6
|
+
export declare const DEFAULT_MAX_WRITE_BYTES: number;
|
|
7
|
+
export declare const PUBLIC_DIR_MODE = 493;
|
|
8
|
+
export declare const PUBLIC_FILE_MODE = 420;
|
|
9
|
+
export declare const MAX_SNAPSHOT_ENUM = 5000;
|
|
10
|
+
export declare const MAX_SNAPSHOT_URLS = 20;
|
|
11
|
+
export declare const MAX_PUBLIC_NAME_ATTEMPTS = 8;
|
|
12
|
+
export declare const PUBLIC_EXTENSIONS: Set<string>;
|
|
13
|
+
export type ArtifactEnv = NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
14
|
+
export type PublishedArtifact = {
|
|
15
|
+
fileName: string;
|
|
16
|
+
fileType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
url?: string;
|
|
19
|
+
absPath: string;
|
|
20
|
+
/** 工作稿相对 `files/` 的路径 */
|
|
21
|
+
sourceRel?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function staticBaseUrlFromEnv(env?: ArtifactEnv): string | undefined;
|
|
24
|
+
export declare function preinstallRootFromEnv(env?: ArtifactEnv): string | undefined;
|
|
25
|
+
export declare function encodePublicRel(rel: string): string;
|
|
26
|
+
export declare function isPublicUserArtifactRel(rel: string): boolean;
|
|
27
|
+
export declare function publicArtifactUrl(absPath: string, env?: ArtifactEnv): string | undefined;
|
|
28
|
+
export declare function publicTimeSuffix(now?: Date, withMs?: boolean): string;
|
|
29
|
+
export declare function stripPublicSuffix(name: string): string;
|
|
30
|
+
export declare function extensionOf(name: string): string;
|
|
31
|
+
export declare function isPublicExtension(name: string): boolean;
|
|
32
|
+
export declare function isSnapshotJunkName(name: string): boolean;
|
|
33
|
+
export declare function mkdirChmod(dir: string, mode?: number): Promise<void>;
|
|
34
|
+
/** 四槽 `skills/` `work/` `files/` `public/`,目录 0755(与 Hosting `ensureUserWorkspace` 对齐)。 */
|
|
35
|
+
export declare function ensureUserSlots(userDir: string): Promise<{
|
|
36
|
+
filesDir: string;
|
|
37
|
+
workDir: string;
|
|
38
|
+
publicDir: string;
|
|
39
|
+
skillsDir: string;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function allocPublicBasename(publicDir: string, srcFile: string, originalBasename: string): Promise<string | undefined>;
|
|
42
|
+
export declare function allocPublicDir(publicDir: string, srcDir: string, originalDirName: string): Promise<string | undefined>;
|
|
43
|
+
export declare function publishFromFiles(userDir: string, relFromFiles: string[], env?: ArtifactEnv): Promise<PublishedArtifact[]>;
|
|
44
|
+
export type FilesWalkEntry = {
|
|
45
|
+
rel: string;
|
|
46
|
+
size: number;
|
|
47
|
+
mtimeMs: number;
|
|
48
|
+
ctimeMs: number;
|
|
49
|
+
};
|
|
50
|
+
export declare function walkFilesTree(filesDir: string): Promise<{
|
|
51
|
+
entries: Map<string, FilesWalkEntry>;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function snapshotCandidates(before: Map<string, FilesWalkEntry>, after: Map<string, FilesWalkEntry>, maxWriteBytes?: number): {
|
|
55
|
+
rels: string[];
|
|
56
|
+
skippedOversize: number;
|
|
57
|
+
};
|
|
58
|
+
export type UserProductSlot = 'files' | 'skills';
|
|
59
|
+
export type ResolvedUserProductPath = {
|
|
60
|
+
abs: string;
|
|
61
|
+
jsonPath: string;
|
|
62
|
+
slot: UserProductSlot;
|
|
63
|
+
relFromFiles?: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function resolveUserProductPath(requested: string, userDir: string, kind: 'write' | 'cwd' | 'read'): ResolvedUserProductPath;
|
|
66
|
+
export declare function maxWriteBytesFromEnv(env?: ArtifactEnv): number;
|