@webskill/sdk 0.13.0 → 0.15.0
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/agent.d.ts +3 -3
- package/dist/agent.js +2442 -3
- package/dist/{dist-ExSQky4C.js → approval-BnLLCOG0.js} +277 -2934
- package/dist/browser.d.ts +327 -8
- package/dist/browser.js +1017 -51
- package/dist/{openUiLibrary-BKXW7Iwx-CWWBVOkE.js → dist-CTsxblSD.js} +496 -28
- package/dist/{echarts-De78wXqV.js → echarts-BE7oV_Dl.js} +1 -1
- package/dist/{env-8cY40DXB-CGnEVZby.js → env-Bj1MI2Ww.js} +1 -1
- package/dist/errors-BDZNpC13.js +22 -0
- package/dist/{eventTypes-FllCrX-Z-DNDeHWoG.js → eventTypes-Y07N8IAC.js} +14 -2
- package/dist/external-_ZRQe-V9.js +53 -0
- package/dist/governance.d.ts +15 -3
- package/dist/governance.js +86 -4
- package/dist/{index-iBm9tJL_.d.ts → index-CQYo4tcT.d.ts} +469 -10
- package/dist/{index-C3XdItd_.d.ts → index-CU2md8R1.d.ts} +26 -18
- package/dist/{index-DkvBhJQy.d.ts → index-Oc3H89TJ.d.ts} +204 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1001 -5
- package/dist/kind-DxgS8LM-.js +77 -0
- package/dist/linkedDocument-xNXF-z8n.js +94 -0
- package/dist/llm-B7lLH0ZI.js +1767 -0
- package/dist/mcp.d.ts +56 -3
- package/dist/mcp.js +75 -5
- package/dist/memoryArtifactStore-D6dLeY55.js +48 -0
- package/dist/miniChart-owdCdaw-.js +533 -0
- package/dist/node.d.ts +3 -3
- package/dist/node.js +110 -13
- package/dist/openUiLibrary-CY_ADoe0.js +30 -0
- package/dist/openUiSpecLang-DuOCdw9G.js +1849 -0
- package/dist/pathSecurity-B1owvJAF.js +65 -0
- package/dist/processSandboxEntry.js +5 -1
- package/dist/renderResult-D9Q-Vu2x.js +143 -0
- package/dist/{rolldown-runtime-BOF7iYI8.js → rolldown-runtime-BHkdefai.js} +1 -2
- package/dist/sandboxWorkerEntry.js +5 -1
- package/dist/{dist-Bev6i6Ip.js → skill-CAJMsLod.js} +44 -314
- package/dist/{skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts → skillVersionStore-B24Jjjry-C4zomhMZ.d.ts} +14 -2
- package/dist/surface-DVGiCmwq.js +145 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +64 -3
- package/dist/{types-DLctJep_-B5G4uk2u.d.ts → types-C3V6lAeO-BIf7UB2e.d.ts} +79 -4
- package/dist/types-TUydnxaj.js +78 -0
- package/dist/ui-react.d.ts +12 -5
- package/dist/ui-react.js +128548 -4729
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +11 -2
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +1254 -3
- package/dist/urlSafety-CiSuCJvX.js +65 -0
- package/dist/webSkillApi-DOOyz2G5.js +106 -0
- package/dist/{webskillLitCatalog-D_zCqeQF-C9lrvvMr.js → webskillLitCatalog-DSqVFQjK.js} +11 -3
- package/package.json +1 -1
- package/dist/catalogComponents-BgAJN0p8-CYEXSk45.js +0 -124384
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +0 -7787
- package/dist/dist-DqcL6jKO.js +0 -3509
- package/dist/dist-DxyxmXPU-DGC-dpXf.js +0 -5621
- package/dist/dist-GK6dtjRv.js +0 -1493
- package/dist/memoryArtifactStore-52Zn9npI-LbCQaqyx.js +0 -86
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +0 -31
- package/dist/testing-WPTyXQYt.js +0 -142
- package/dist/types-WovEf4ED-CZSDiiBU.js +0 -6215
|
@@ -1,1614 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
2
|
+
import { b as parseSkillMarkdown, i as buildCatalog, n as SkillDiscovery, r as SkillReader } from "./skill-CAJMsLod.js";
|
|
3
|
+
import { a as atomicWriteText, r as resolveInsideRoot, t as assertSafePathSegment } from "./pathSecurity-B1owvJAF.js";
|
|
4
|
+
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
5
|
+
import { c as findUnpairedToolCalls, d as partsToText, f as promptText, l as interruptedToolResult, p as textParts, u as sealToolCallPairs } from "./llm-B7lLH0ZI.js";
|
|
6
|
+
import { a as fromBase64, c as DEFAULT_MAX_DOCUMENT_BYTES, d as TEXT_BUDGETED_CONTENT_TYPES, i as UNSUPPORTED_DOCUMENT_MESSAGE, l as DEFAULT_MAX_DOCUMENT_TEXT_BYTES, n as READ_LINKED_DOCUMENT_TOOL_NAME, o as toBase64, r as SUPPORTED_DOCUMENT_MIME, t as READ_LINKED_DOCUMENT_TOOL } from "./linkedDocument-xNXF-z8n.js";
|
|
7
|
+
import { t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
|
|
8
|
+
import { i as isArtifactCardHidden, n as extractChartSpec, r as admitArtifactMetadata, t as buildRenderResult } from "./renderResult-D9Q-Vu2x.js";
|
|
9
|
+
import { t as extractUiSpecEvents } from "./surface-DVGiCmwq.js";
|
|
10
|
+
import { t as MemoryArtifactStore } from "./memoryArtifactStore-D6dLeY55.js";
|
|
3
11
|
|
|
4
|
-
//#region ../
|
|
5
|
-
/**
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
const because = options.reason === void 0 ? "" : ` (${options.reason})`;
|
|
9
|
-
return {
|
|
10
|
-
ok: false,
|
|
11
|
-
content: [],
|
|
12
|
-
error: {
|
|
13
|
-
code,
|
|
14
|
-
message: `Tool call "${call.name}" produced no result: the run ended before it finished${because}.`
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/** 尚无 `role:'tool'` 应答的工具调用,按消息顺序 */
|
|
19
|
-
function findUnpairedToolCalls(messages) {
|
|
20
|
-
const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
|
|
21
|
-
return messages.filter((m) => m.role === "assistant" && m.toolCalls?.length).flatMap((m) => m.toolCalls ?? []).filter((call) => !answered.has(call.id));
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 为每个未应答的工具调用补一条结构化的中断说明。
|
|
25
|
-
*
|
|
26
|
-
* 补齐消息紧跟在该 assistant 已有的 tool 兄弟之后——供应商装配按相邻块分组,
|
|
27
|
-
* 插到序列末尾会让它归属到别的 assistant 消息上。
|
|
28
|
-
* 本函数**不改写入参**,也不写盘(AC-11.8)。
|
|
29
|
-
*/
|
|
30
|
-
function sealToolCallPairs(messages, options = {}) {
|
|
31
|
-
const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
|
|
32
|
-
const code = options.code ?? "RUN_INTERRUPTED";
|
|
33
|
-
const sealed = [];
|
|
34
|
-
const out = [];
|
|
35
|
-
for (let i = 0; i < messages.length; i += 1) {
|
|
36
|
-
const message = messages[i];
|
|
37
|
-
out.push(message);
|
|
38
|
-
if (message.role !== "assistant" || !message.toolCalls?.length) continue;
|
|
39
|
-
const missing = message.toolCalls.filter((call) => !answered.has(call.id));
|
|
40
|
-
if (missing.length === 0) continue;
|
|
41
|
-
while (i + 1 < messages.length && messages[i + 1].role === "tool") {
|
|
42
|
-
out.push(messages[i + 1]);
|
|
43
|
-
i += 1;
|
|
44
|
-
}
|
|
45
|
-
for (const call of missing) {
|
|
46
|
-
out.push({
|
|
47
|
-
role: "tool",
|
|
48
|
-
toolCallId: call.id,
|
|
49
|
-
content: textParts(JSON.stringify(interruptedToolResult(call, {
|
|
50
|
-
...options,
|
|
51
|
-
code
|
|
52
|
-
})))
|
|
53
|
-
});
|
|
54
|
-
answered.add(call.id);
|
|
55
|
-
sealed.push({
|
|
56
|
-
callId: call.id,
|
|
57
|
-
toolName: call.name,
|
|
58
|
-
code
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
messages: sealed.length === 0 ? [...messages] : out,
|
|
64
|
-
sealed
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* $defs 必须与 type / properties 同级,位于每个工具 inputSchema 的根。
|
|
69
|
-
* 已验证:放进 properties.spec 内层会得到同样的 "Unsupported ref: #" ——
|
|
70
|
-
* Qwen / LM Studio 按当前 schema 上下文解析 $defs,不回溯根文档。
|
|
71
|
-
* 因此本模块只在**根**写入 $defs,不得改成就近放置。
|
|
72
|
-
*/
|
|
73
|
-
const DEFS_KEY = "$defs";
|
|
74
|
-
const SELF_REF = "#";
|
|
75
|
-
const NODE_NAME = "Node";
|
|
76
|
-
const COMPONENT_PREFIX = "Component_";
|
|
77
|
-
/** 值为「名称 → 子 schema」的映射。包内导出:vendorSchema 要用同一套下钻规则(两套会漂移)。 */
|
|
78
|
-
const SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
|
|
79
|
-
"properties",
|
|
80
|
-
"patternProperties",
|
|
81
|
-
"dependentSchemas",
|
|
82
|
-
"$defs",
|
|
83
|
-
"definitions"
|
|
84
|
-
]);
|
|
85
|
-
/** 值为「子 schema 数组」。 */
|
|
86
|
-
const SCHEMA_LIST_KEYS = /* @__PURE__ */ new Set([
|
|
87
|
-
"oneOf",
|
|
88
|
-
"anyOf",
|
|
89
|
-
"allOf",
|
|
90
|
-
"prefixItems"
|
|
91
|
-
]);
|
|
92
|
-
/** 值为单个子 schema(items 在 draft-07 遗留写法里也可能是数组)。 */
|
|
93
|
-
const SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
94
|
-
"items",
|
|
95
|
-
"additionalItems",
|
|
96
|
-
"additionalProperties",
|
|
97
|
-
"unevaluatedItems",
|
|
98
|
-
"unevaluatedProperties",
|
|
99
|
-
"contains",
|
|
100
|
-
"propertyNames",
|
|
101
|
-
"not",
|
|
102
|
-
"if",
|
|
103
|
-
"then",
|
|
104
|
-
"else"
|
|
105
|
-
]);
|
|
106
|
-
function isRecord$7(value) {
|
|
107
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
|
-
}
|
|
109
|
-
function isSelfRef(value) {
|
|
110
|
-
return isRecord$7(value) && value["$ref"] === SELF_REF;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* 按 JSON Schema 关键字下降到子 schema。
|
|
114
|
-
* 只走 schema 位置,`enum` / `const` / `default` 等数据位置一律不进——
|
|
115
|
-
* 那里出现的 `{ $ref: '#' }` 是字面数据而非引用,改写它会破坏语义。
|
|
116
|
-
*/
|
|
117
|
-
function forEachSubSchema(node, visit) {
|
|
118
|
-
for (const [key, value] of Object.entries(node)) {
|
|
119
|
-
if (SCHEMA_MAP_KEYS.has(key)) {
|
|
120
|
-
if (isRecord$7(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (SCHEMA_LIST_KEYS.has(key)) {
|
|
124
|
-
if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (SCHEMA_KEYS.has(key)) if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
|
|
128
|
-
else visit(value, [key]);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function pathKey(path) {
|
|
132
|
-
return path.join("\0");
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* 找出所有 `$ref: '#'` 所属的 schema resource 根。
|
|
136
|
-
* `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
|
|
137
|
-
*/
|
|
138
|
-
function collectSelfRefRoots(node, path, resourceRoot, out) {
|
|
139
|
-
if (!isRecord$7(node)) return;
|
|
140
|
-
if (isSelfRef(node)) {
|
|
141
|
-
out.set(pathKey(resourceRoot), resourceRoot);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const nextRoot = typeof node["$id"] === "string" && path.length > 0 ? path : resourceRoot;
|
|
145
|
-
forEachSubSchema(node, (child, childPath) => {
|
|
146
|
-
collectSelfRefRoots(child, [...path, ...childPath], nextRoot, out);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
function getAt(schema, path) {
|
|
150
|
-
let current = schema;
|
|
151
|
-
for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
|
|
152
|
-
else if (isRecord$7(current) && typeof segment === "string") current = current[segment];
|
|
153
|
-
else return void 0;
|
|
154
|
-
return current;
|
|
155
|
-
}
|
|
156
|
-
/** 返回把 `path` 处替换为 `replacement` 后的新对象;沿途节点浅拷贝,其余共享。 */
|
|
157
|
-
function setAt(schema, path, replacement) {
|
|
158
|
-
if (path.length === 0) return replacement;
|
|
159
|
-
const [head, ...rest] = path;
|
|
160
|
-
const child = getAt(schema, [head]);
|
|
161
|
-
const nextChild = rest.length === 0 ? replacement : setAt(child, rest, replacement);
|
|
162
|
-
if (Array.isArray(schema) && typeof head === "number") {
|
|
163
|
-
const copy = [...schema];
|
|
164
|
-
copy[head] = nextChild;
|
|
165
|
-
return copy;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
...schema,
|
|
169
|
-
[head]: nextChild
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* 深拷贝并把 `{ $ref: '#' }` 改写为指向根 `$defs` 中的具名节点。
|
|
174
|
-
* 同时剥掉非根位置的 `$id` / `$schema`:它们会重设 base URI,
|
|
175
|
-
* 正是严格解析器无法解析 `#` 的直接原因。
|
|
176
|
-
*/
|
|
177
|
-
function rewriteSelfRefs(node, target, depth) {
|
|
178
|
-
if (!isRecord$7(node)) return node;
|
|
179
|
-
if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
|
|
180
|
-
if (isSelfRef(node)) {
|
|
181
|
-
const rest = {};
|
|
182
|
-
for (const [key, value] of Object.entries(node)) if (key !== "$ref") rest[key] = value;
|
|
183
|
-
return {
|
|
184
|
-
$ref: `#/${DEFS_KEY}/${target}`,
|
|
185
|
-
...rest
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
const result = {};
|
|
189
|
-
for (const [key, value] of Object.entries(node)) {
|
|
190
|
-
if (key === "$id" || key === "$schema") continue;
|
|
191
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
|
|
192
|
-
const mapped = {};
|
|
193
|
-
for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
|
|
194
|
-
result[key] = mapped;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (SCHEMA_LIST_KEYS.has(key) || SCHEMA_KEYS.has(key)) {
|
|
198
|
-
result[key] = Array.isArray(value) ? value.map((child) => rewriteSelfRefs(child, target, depth + 1)) : rewriteSelfRefs(value, target, depth + 1);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
result[key] = value;
|
|
202
|
-
}
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
function uniqueName(base, taken) {
|
|
206
|
-
if (!taken.has(base)) return base;
|
|
207
|
-
const prefixed = `Portable${base}`;
|
|
208
|
-
if (!taken.has(prefixed)) return prefixed;
|
|
209
|
-
let index = 2;
|
|
210
|
-
while (taken.has(`${prefixed}_${index}`)) index += 1;
|
|
211
|
-
return `${prefixed}_${index}`;
|
|
212
|
-
}
|
|
213
|
-
function renameDefRefs(node, renames) {
|
|
214
|
-
if (Array.isArray(node)) return node.map((child) => renameDefRefs(child, renames));
|
|
215
|
-
if (!isRecord$7(node)) return node;
|
|
216
|
-
const out = {};
|
|
217
|
-
const prefix = `#/${DEFS_KEY}/`;
|
|
218
|
-
for (const [key, value] of Object.entries(node)) {
|
|
219
|
-
if (key === "$ref" && typeof value === "string" && value.startsWith(prefix)) {
|
|
220
|
-
const [head, ...tail] = value.slice(prefix.length).split("/");
|
|
221
|
-
const mapped = head === void 0 ? void 0 : renames.get(head);
|
|
222
|
-
out[key] = mapped === void 0 ? value : [`${prefix}${mapped}`, ...tail].join("/");
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
out[key] = renameDefRefs(value, renames);
|
|
226
|
-
}
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* 把非根位置的 `$defs` 搬到根。zod 每次 `toJSONSchema()` 各自产出 `$defs.__schemaN`,
|
|
231
|
-
* 嵌进大 schema 后其 `#/$defs/...` 从大根解析不到——提升即修复。重名时避让并改写引用。
|
|
232
|
-
*/
|
|
233
|
-
function hoistNestedDefs(schema) {
|
|
234
|
-
const rootDefs = isRecord$7(schema[DEFS_KEY]) ? { ...schema[DEFS_KEY] } : {};
|
|
235
|
-
const taken = new Set(Object.keys(rootDefs));
|
|
236
|
-
let hoisted = false;
|
|
237
|
-
function visit(node, isRoot) {
|
|
238
|
-
if (Array.isArray(node)) return node.map((child) => visit(child, false));
|
|
239
|
-
if (!isRecord$7(node)) return node;
|
|
240
|
-
const nested = !isRoot && isRecord$7(node[DEFS_KEY]) ? node[DEFS_KEY] : void 0;
|
|
241
|
-
const body = {};
|
|
242
|
-
for (const [key, value] of Object.entries(node)) {
|
|
243
|
-
if (key === DEFS_KEY && nested !== void 0) continue;
|
|
244
|
-
if (key === DEFS_KEY) {
|
|
245
|
-
body[key] = value;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
|
|
249
|
-
const mapped = {};
|
|
250
|
-
for (const [name, child] of Object.entries(value)) mapped[name] = visit(child, false);
|
|
251
|
-
body[key] = mapped;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
255
|
-
body[key] = value.map((child) => visit(child, false));
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (SCHEMA_KEYS.has(key)) {
|
|
259
|
-
body[key] = visit(value, false);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
body[key] = value;
|
|
263
|
-
}
|
|
264
|
-
if (nested === void 0) return body;
|
|
265
|
-
hoisted = true;
|
|
266
|
-
const renames = /* @__PURE__ */ new Map();
|
|
267
|
-
for (const name of Object.keys(nested)) {
|
|
268
|
-
const target = uniqueName(name, taken);
|
|
269
|
-
taken.add(target);
|
|
270
|
-
if (target !== name) renames.set(name, target);
|
|
271
|
-
}
|
|
272
|
-
for (const [name, def] of Object.entries(nested)) {
|
|
273
|
-
const moved = visit(def, false);
|
|
274
|
-
rootDefs[renames.get(name) ?? name] = renames.size > 0 ? renameDefRefs(moved, renames) : moved;
|
|
275
|
-
}
|
|
276
|
-
return renames.size > 0 ? renameDefRefs(body, renames) : body;
|
|
277
|
-
}
|
|
278
|
-
const out = visit(schema, true);
|
|
279
|
-
return hoisted ? {
|
|
280
|
-
...out,
|
|
281
|
-
[DEFS_KEY]: rootDefs
|
|
282
|
-
} : schema;
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* 把工具 inputSchema 重写为严格解析器可接受的 JSON Schema 2020-12 形态:
|
|
286
|
-
* 非根 `$defs` 提到根,自引用的 schema resource 提到根 `$defs`,
|
|
287
|
-
* 所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
|
|
288
|
-
*
|
|
289
|
-
* 幂等:不含自引用也不含嵌套 `$defs` 的 schema 原样返回。
|
|
290
|
-
*
|
|
291
|
-
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
|
|
292
|
-
* 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
|
|
293
|
-
*/
|
|
294
|
-
function toPortableToolSchema(input) {
|
|
295
|
-
const schema = hoistNestedDefs(input);
|
|
296
|
-
const roots = /* @__PURE__ */ new Map();
|
|
297
|
-
collectSelfRefRoots(schema, [], [], roots);
|
|
298
|
-
if (roots.size === 0) return schema;
|
|
299
|
-
if (roots.size > 1) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", "Tool schema contains self references in more than one schema resource; cannot produce a portable form");
|
|
300
|
-
const rootPath = [...roots.values()][0] ?? [];
|
|
301
|
-
const resource = getAt(schema, rootPath);
|
|
302
|
-
if (!isRecord$7(resource)) return schema;
|
|
303
|
-
const existingDefs = isRecord$7(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
|
|
304
|
-
const taken = new Set(Object.keys(existingDefs ?? {}));
|
|
305
|
-
const nodeName = uniqueName(NODE_NAME, taken);
|
|
306
|
-
taken.add(nodeName);
|
|
307
|
-
const added = {};
|
|
308
|
-
const branches = resource["oneOf"];
|
|
309
|
-
if (Array.isArray(branches)) {
|
|
310
|
-
const refs = branches.map((branch, index) => {
|
|
311
|
-
const name = uniqueName(`${COMPONENT_PREFIX}${index}`, taken);
|
|
312
|
-
taken.add(name);
|
|
313
|
-
added[name] = rewriteSelfRefs(branch, nodeName, 1);
|
|
314
|
-
return { $ref: `#/${DEFS_KEY}/${name}` };
|
|
315
|
-
});
|
|
316
|
-
const description = resource["description"];
|
|
317
|
-
added[nodeName] = typeof description === "string" ? {
|
|
318
|
-
description,
|
|
319
|
-
oneOf: refs
|
|
320
|
-
} : { oneOf: refs };
|
|
321
|
-
} else {
|
|
322
|
-
const body = {};
|
|
323
|
-
for (const [key, value] of Object.entries(resource)) if (key !== DEFS_KEY) body[key] = value;
|
|
324
|
-
added[nodeName] = rewriteSelfRefs(body, nodeName, 1);
|
|
325
|
-
}
|
|
326
|
-
return {
|
|
327
|
-
...setAt(schema, rootPath, { $ref: `#/${DEFS_KEY}/${nodeName}` }),
|
|
328
|
-
[DEFS_KEY]: {
|
|
329
|
-
...existingDefs ?? {},
|
|
330
|
-
...added
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
function isRecord$6(value) {
|
|
335
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* 与 `interaction/schemaToForm` 的 file 判定保持一致:JSON Schema 里二进制内容的
|
|
339
|
-
* 两种标准写法(`format: binary` / `contentEncoding: base64`)。
|
|
340
|
-
*/
|
|
341
|
-
function isModelUnfillable(node) {
|
|
342
|
-
return isRecord$6(node) && node["type"] === "string" && (node["format"] === "binary" || node["contentEncoding"] === "base64");
|
|
343
|
-
}
|
|
344
|
-
function stripNode(node) {
|
|
345
|
-
if (Array.isArray(node)) return node.map(stripNode);
|
|
346
|
-
if (!isRecord$6(node)) return node;
|
|
347
|
-
const out = {};
|
|
348
|
-
for (const [key, value] of Object.entries(node)) {
|
|
349
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
|
|
350
|
-
const mapped = {};
|
|
351
|
-
for (const [name, child] of Object.entries(value)) {
|
|
352
|
-
if (key === "properties" && isModelUnfillable(child)) continue;
|
|
353
|
-
mapped[name] = stripNode(child);
|
|
354
|
-
}
|
|
355
|
-
out[key] = mapped;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
359
|
-
out[key] = value.map(stripNode);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (SCHEMA_KEYS.has(key)) {
|
|
363
|
-
out[key] = stripNode(value);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
out[key] = value;
|
|
367
|
-
}
|
|
368
|
-
const required = out["required"];
|
|
369
|
-
const properties = out["properties"];
|
|
370
|
-
if (Array.isArray(required) && isRecord$6(properties)) out["required"] = required.filter((name) => typeof name !== "string" || name in properties);
|
|
371
|
-
return out;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* 把模型不可能自己产出的字段(本地文件二进制)从**出站**工具 schema 里剥离。
|
|
375
|
-
*
|
|
376
|
-
* 模型拿不到用户磁盘上的文件,留着这个字段只有两种下场:模型拒绝调用工具
|
|
377
|
-
* (实测 DeepSeek 直接回文本「请上传文件」,file-pick 永远触发不了),或编造一个
|
|
378
|
-
* 占位字符串把技能跑挂。剥掉后模型以缺参形式调用,引擎的 missing-params 链路
|
|
379
|
-
* 会弹 file-pick 让用户用系统文件选择器补这个字段。
|
|
380
|
-
*
|
|
381
|
-
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop——
|
|
382
|
-
* 引擎侧的 missing 校验看的必须是技能作者写的完整 inputSchema。
|
|
383
|
-
*
|
|
384
|
-
* 已知边界(0.10.0 复核注记):只剥 `properties` 里的 inline 二进制字段;
|
|
385
|
-
* 通过 `$ref` 指向 `$defs` 中二进制定义的字段仍会下发(剥掉会留悬空 $ref),
|
|
386
|
-
* 这类技能依旧可能拿不到 file-pick——需要完整修复时再补「剥 $def + 剥指向它的 $ref」。
|
|
387
|
-
*/
|
|
388
|
-
function stripModelUnfillableParams(schema) {
|
|
389
|
-
return stripNode(schema);
|
|
390
|
-
}
|
|
391
|
-
/** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
|
|
392
|
-
const BASE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
393
|
-
"type",
|
|
394
|
-
"description",
|
|
395
|
-
"title",
|
|
396
|
-
"default",
|
|
397
|
-
"enum",
|
|
398
|
-
"const",
|
|
399
|
-
"properties",
|
|
400
|
-
"required",
|
|
401
|
-
"items",
|
|
402
|
-
"anyOf",
|
|
403
|
-
"oneOf",
|
|
404
|
-
"minimum",
|
|
405
|
-
"maximum",
|
|
406
|
-
"minItems",
|
|
407
|
-
"maxItems",
|
|
408
|
-
"nullable",
|
|
409
|
-
"format"
|
|
410
|
-
]);
|
|
411
|
-
/** 宽松侧:保留 0.6.0 的透传行为,本版不主动收紧(没有实证之前收紧只会制造新的破坏) */
|
|
412
|
-
const PERMISSIVE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
413
|
-
...BASE_KEYWORDS,
|
|
414
|
-
"additionalProperties",
|
|
415
|
-
"patternProperties",
|
|
416
|
-
"dependentSchemas",
|
|
417
|
-
"definitions",
|
|
418
|
-
"$defs",
|
|
419
|
-
"$ref",
|
|
420
|
-
"$schema",
|
|
421
|
-
"$id",
|
|
422
|
-
"allOf",
|
|
423
|
-
"not",
|
|
424
|
-
"if",
|
|
425
|
-
"then",
|
|
426
|
-
"else",
|
|
427
|
-
"prefixItems",
|
|
428
|
-
"additionalItems",
|
|
429
|
-
"unevaluatedItems",
|
|
430
|
-
"unevaluatedProperties",
|
|
431
|
-
"contains",
|
|
432
|
-
"propertyNames",
|
|
433
|
-
"exclusiveMinimum",
|
|
434
|
-
"exclusiveMaximum",
|
|
435
|
-
"multipleOf",
|
|
436
|
-
"minLength",
|
|
437
|
-
"maxLength",
|
|
438
|
-
"pattern",
|
|
439
|
-
"uniqueItems",
|
|
440
|
-
"examples",
|
|
441
|
-
"readOnly",
|
|
442
|
-
"writeOnly",
|
|
443
|
-
"deprecated"
|
|
444
|
-
]);
|
|
445
|
-
const OPENAI_SCHEMA_PROFILE = {
|
|
446
|
-
id: "openai-compatible",
|
|
447
|
-
allowedKeywords: PERMISSIVE_KEYWORDS,
|
|
448
|
-
supportsRefs: true
|
|
449
|
-
};
|
|
450
|
-
const ANTHROPIC_SCHEMA_PROFILE = {
|
|
451
|
-
id: "anthropic",
|
|
452
|
-
allowedKeywords: PERMISSIVE_KEYWORDS,
|
|
453
|
-
supportsRefs: true
|
|
454
|
-
};
|
|
455
|
-
/**
|
|
456
|
-
* Google Gen AI 的 `FunctionDeclaration.parameters` 收到 `additionalProperties`
|
|
457
|
-
* 直接回 400 `Unknown name "additionalProperties"`,且不解析 `$ref` / `$defs`。
|
|
458
|
-
*
|
|
459
|
-
* 本 profile 描述的是 **`parameters` 这一字段**的能力,不是 Google 的能力上限:
|
|
460
|
-
* 完整 JSON Schema(含 `$ref` / `$defs`)走另一个字段 `parametersJsonSchema`。
|
|
461
|
-
* 把 `supportsRefs: false` 读成「Google 不支持引用」是错的,分流见 `googleGenAiClient.toGenAiTools`。
|
|
462
|
-
*/
|
|
463
|
-
const GOOGLE_SCHEMA_PROFILE = {
|
|
464
|
-
id: "google",
|
|
465
|
-
allowedKeywords: BASE_KEYWORDS,
|
|
466
|
-
supportsRefs: false
|
|
467
|
-
};
|
|
468
|
-
function isRecord$5(value) {
|
|
469
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
470
|
-
}
|
|
471
|
-
/** 只在 schema 位置上找 `$ref`:enum / const / default 里的同名键是字面数据,不是引用 */
|
|
472
|
-
function containsRef(node) {
|
|
473
|
-
if (Array.isArray(node)) return node.some(containsRef);
|
|
474
|
-
if (!isRecord$5(node)) return false;
|
|
475
|
-
if (typeof node["$ref"] === "string") return true;
|
|
476
|
-
for (const [key, value] of Object.entries(node)) {
|
|
477
|
-
if (SCHEMA_MAP_KEYS.has(key)) {
|
|
478
|
-
if (isRecord$5(value) && Object.values(value).some(containsRef)) return true;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
if (SCHEMA_LIST_KEYS.has(key)) {
|
|
482
|
-
if (Array.isArray(value) && value.some(containsRef)) return true;
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
if (SCHEMA_KEYS.has(key) && containsRef(value)) return true;
|
|
486
|
-
}
|
|
487
|
-
return false;
|
|
488
|
-
}
|
|
489
|
-
function sanitizeNode(node, profile) {
|
|
490
|
-
if (Array.isArray(node)) return node.map((item) => sanitizeNode(item, profile));
|
|
491
|
-
if (!isRecord$5(node)) return node;
|
|
492
|
-
const out = {};
|
|
493
|
-
for (const [key, value] of Object.entries(node)) {
|
|
494
|
-
if (!profile.allowedKeywords.has(key)) continue;
|
|
495
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$5(value)) {
|
|
496
|
-
const mapped = {};
|
|
497
|
-
for (const [name, child] of Object.entries(value)) mapped[name] = sanitizeNode(child, profile);
|
|
498
|
-
out[key] = mapped;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
502
|
-
out[key] = value.map((child) => sanitizeNode(child, profile));
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (SCHEMA_KEYS.has(key)) {
|
|
506
|
-
out[key] = sanitizeNode(value, profile);
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
out[key] = value;
|
|
510
|
-
}
|
|
511
|
-
return out;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* 按供应商 profile 清洗出站 schema。产出新对象,不修改入参(工具源可能复用同一个实例)。
|
|
515
|
-
*
|
|
516
|
-
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
|
|
517
|
-
* 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
|
|
518
|
-
*
|
|
519
|
-
* @throws WebSkillError `TOOL_SCHEMA_UNAVAILABLE` — schema 依赖该供应商不支持的引用
|
|
520
|
-
*/
|
|
521
|
-
function sanitizeForVendor(schema, profile, toolName) {
|
|
522
|
-
if (!profile.supportsRefs && containsRef(schema)) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool "${toolName}" requires $ref support that provider "${profile.id}" does not accept`);
|
|
523
|
-
return sanitizeNode(schema, profile);
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* schema 能否走 profile 描述的受限通道。与 `sanitizeForVendor` 同判据,但不抛错——
|
|
527
|
-
* 调用方需要的是「选哪条通道」而不是「失败」。
|
|
528
|
-
*/
|
|
529
|
-
function isExpressibleForVendor(schema, profile) {
|
|
530
|
-
return profile.supportsRefs || !containsRef(schema);
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* 放宽不可终止的递归环:Gemini 只接受「可选 / 可为 null / 可零长数组」的引用环。
|
|
534
|
-
* 对 `items` 直接是 `$ref` 的数组节点删去 `minItems`。
|
|
535
|
-
*
|
|
536
|
-
* **只作用于出站副本**:catalog 本体的校验判据不变。
|
|
537
|
-
*/
|
|
538
|
-
function relaxRefLoops(schema) {
|
|
539
|
-
return relaxNode(schema);
|
|
540
|
-
}
|
|
541
|
-
function relaxNode(node) {
|
|
542
|
-
if (Array.isArray(node)) return node.map(relaxNode);
|
|
543
|
-
if (!isRecord$5(node)) return node;
|
|
544
|
-
const out = {};
|
|
545
|
-
for (const [key, value] of Object.entries(node)) {
|
|
546
|
-
if (key === "minItems" && isRecord$5(node["items"]) && typeof node["items"]["$ref"] === "string") continue;
|
|
547
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$5(value)) {
|
|
548
|
-
const mapped = {};
|
|
549
|
-
for (const [name, child] of Object.entries(value)) mapped[name] = relaxNode(child);
|
|
550
|
-
out[key] = mapped;
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
554
|
-
out[key] = value.map(relaxNode);
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
if (SCHEMA_KEYS.has(key)) {
|
|
558
|
-
out[key] = relaxNode(value);
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
out[key] = value;
|
|
562
|
-
}
|
|
563
|
-
return out;
|
|
564
|
-
}
|
|
565
|
-
function createSseFrameReader() {
|
|
566
|
-
let buffer = "";
|
|
567
|
-
let data = [];
|
|
568
|
-
const dispatch = (out) => {
|
|
569
|
-
if (data.length === 0) return;
|
|
570
|
-
out.push(data.join("\n"));
|
|
571
|
-
data = [];
|
|
572
|
-
};
|
|
573
|
-
const consumeLine = (line, out) => {
|
|
574
|
-
if (line === "") {
|
|
575
|
-
dispatch(out);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (line.startsWith(":")) return;
|
|
579
|
-
const colon = line.indexOf(":");
|
|
580
|
-
if (colon < 0) return;
|
|
581
|
-
if (line.slice(0, colon) !== "data") return;
|
|
582
|
-
const value = line.slice(colon + 1);
|
|
583
|
-
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
584
|
-
};
|
|
585
|
-
return {
|
|
586
|
-
push(chunk) {
|
|
587
|
-
const out = [];
|
|
588
|
-
buffer += chunk;
|
|
589
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
590
|
-
let newline;
|
|
591
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
592
|
-
const line = buffer.slice(0, newline);
|
|
593
|
-
buffer = buffer.slice(newline + 1);
|
|
594
|
-
consumeLine(line, out);
|
|
595
|
-
}
|
|
596
|
-
return out;
|
|
597
|
-
},
|
|
598
|
-
flush() {
|
|
599
|
-
const out = [];
|
|
600
|
-
if (buffer !== "") {
|
|
601
|
-
const line = buffer;
|
|
602
|
-
buffer = "";
|
|
603
|
-
consumeLine(line, out);
|
|
604
|
-
}
|
|
605
|
-
dispatch(out);
|
|
606
|
-
return out;
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* 内联 `<think>` 段落切分器(内部模块,不进公开导出)。
|
|
612
|
-
*
|
|
613
|
-
* 本地部署的思考型模型(Qwen 等经 OpenAI 兼容端点)不下发 `reasoning_content`,
|
|
614
|
-
* 而是把推理直接写进 `content`,用 `<think>` … `</think>` 包起来。不切分的话
|
|
615
|
-
* 思考正文会当成回答正文流进界面、进消息历史、进落盘,既污染上下文也没法折叠。
|
|
616
|
-
*/
|
|
617
|
-
const OPEN = "<think>";
|
|
618
|
-
const CLOSE = "</think>";
|
|
619
|
-
/** buffer 是否为 tag 的真前缀(还不能判定,得再等) */
|
|
620
|
-
const isPartialPrefix = (buffer, tag) => buffer.length < tag.length && tag.startsWith(buffer);
|
|
621
|
-
function createThinkTagSplitter() {
|
|
622
|
-
let buffer = "";
|
|
623
|
-
let inThink = false;
|
|
624
|
-
const consume = () => {
|
|
625
|
-
let text = "";
|
|
626
|
-
let thinking = "";
|
|
627
|
-
for (;;) {
|
|
628
|
-
const tag = inThink ? CLOSE : OPEN;
|
|
629
|
-
const hit = buffer.indexOf(tag);
|
|
630
|
-
if (hit >= 0) {
|
|
631
|
-
const before = buffer.slice(0, hit);
|
|
632
|
-
if (inThink) thinking += before;
|
|
633
|
-
else text += before;
|
|
634
|
-
buffer = buffer.slice(hit + tag.length);
|
|
635
|
-
inThink = !inThink;
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
const start = buffer.lastIndexOf("<");
|
|
639
|
-
const keep = start >= 0 && isPartialPrefix(buffer.slice(start), tag) ? start : buffer.length;
|
|
640
|
-
const emit = buffer.slice(0, keep);
|
|
641
|
-
if (inThink) thinking += emit;
|
|
642
|
-
else text += emit;
|
|
643
|
-
buffer = buffer.slice(keep);
|
|
644
|
-
return {
|
|
645
|
-
text,
|
|
646
|
-
thinking
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
return {
|
|
651
|
-
push(delta) {
|
|
652
|
-
buffer += delta;
|
|
653
|
-
return consume();
|
|
654
|
-
},
|
|
655
|
-
flush() {
|
|
656
|
-
const rest = buffer;
|
|
657
|
-
buffer = "";
|
|
658
|
-
return inThink ? {
|
|
659
|
-
text: "",
|
|
660
|
-
thinking: rest
|
|
661
|
-
} : {
|
|
662
|
-
text: rest,
|
|
663
|
-
thinking: ""
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
/** 非流式整段切分:返回正文与思考正文(无 `<think>` 时 thinking 为 undefined) */
|
|
669
|
-
function splitThinkTags(content) {
|
|
670
|
-
if (!content.includes(OPEN)) return { text: content };
|
|
671
|
-
const splitter = createThinkTagSplitter();
|
|
672
|
-
const first = splitter.push(content);
|
|
673
|
-
const last = splitter.flush();
|
|
674
|
-
const thinking = first.thinking + last.thinking;
|
|
675
|
-
return {
|
|
676
|
-
text: first.text + last.text,
|
|
677
|
-
...thinking !== "" ? { thinking } : {}
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
|
|
681
|
-
/** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
|
|
682
|
-
const toOpenAiContent = (parts, where) => {
|
|
683
|
-
if (parts.every((p) => p.type === "text")) return partsToText(parts);
|
|
684
|
-
return parts.map((part) => {
|
|
685
|
-
switch (part.type) {
|
|
686
|
-
case "text": return {
|
|
687
|
-
type: "text",
|
|
688
|
-
text: part.text
|
|
689
|
-
};
|
|
690
|
-
case "image": return {
|
|
691
|
-
type: "image_url",
|
|
692
|
-
image_url: { url: dataUrl(part) }
|
|
693
|
-
};
|
|
694
|
-
case "file": return {
|
|
695
|
-
type: "file",
|
|
696
|
-
file: {
|
|
697
|
-
...part.name !== void 0 ? { filename: part.name } : {},
|
|
698
|
-
file_data: dataUrl(part)
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
default: return rejectUnsupportedPart(part, "OpenAI", where);
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
};
|
|
705
|
-
/** system / tool 消息只接受文本:这两类在 OpenAI 协议里没有多模态形态 */
|
|
706
|
-
const toOpenAiText = (parts, where) => {
|
|
707
|
-
const nonText = parts.find((p) => p.type !== "text");
|
|
708
|
-
if (nonText) rejectUnsupportedPart(nonText, "OpenAI", where);
|
|
709
|
-
return partsToText(parts);
|
|
710
|
-
};
|
|
711
|
-
const toOpenAiMessage = (msg) => {
|
|
712
|
-
if (msg.role === "tool") return {
|
|
713
|
-
role: "tool",
|
|
714
|
-
tool_call_id: msg.toolCallId,
|
|
715
|
-
content: toOpenAiText(msg.content, "tool")
|
|
716
|
-
};
|
|
717
|
-
if (msg.role === "system") return {
|
|
718
|
-
role: "system",
|
|
719
|
-
content: toOpenAiText(msg.content, "system")
|
|
720
|
-
};
|
|
721
|
-
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
722
|
-
const content = toOpenAiContent(msg.content, "assistant");
|
|
723
|
-
return {
|
|
724
|
-
role: "assistant",
|
|
725
|
-
content: content === "" ? null : content,
|
|
726
|
-
tool_calls: msg.toolCalls.map((call) => ({
|
|
727
|
-
id: call.id,
|
|
728
|
-
type: "function",
|
|
729
|
-
function: {
|
|
730
|
-
name: call.name,
|
|
731
|
-
arguments: JSON.stringify(call.arguments)
|
|
732
|
-
}
|
|
733
|
-
}))
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
return {
|
|
737
|
-
role: msg.role,
|
|
738
|
-
content: toOpenAiContent(msg.content, msg.role)
|
|
739
|
-
};
|
|
740
|
-
};
|
|
741
|
-
const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
742
|
-
type: "function",
|
|
743
|
-
function: {
|
|
744
|
-
name: tool.name,
|
|
745
|
-
...tool.description ? { description: tool.description } : {},
|
|
746
|
-
parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), OPENAI_SCHEMA_PROFILE, tool.name)
|
|
747
|
-
}
|
|
748
|
-
}));
|
|
749
|
-
const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
750
|
-
/** OpenAI chat completions 协议客户端(兼容端点通用) */
|
|
751
|
-
var OpenAiCompatibleClient = class {
|
|
752
|
-
#config;
|
|
753
|
-
#fetch;
|
|
754
|
-
constructor(config) {
|
|
755
|
-
this.#config = config;
|
|
756
|
-
this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
757
|
-
}
|
|
758
|
-
#requireConfig() {
|
|
759
|
-
const { baseUrl, apiKey, model } = this.#config;
|
|
760
|
-
if (!baseUrl || !apiKey || !model) throw new WebSkillError("LLM_UNAVAILABLE", "LLM client is not configured: baseUrl, apiKey and model are all required");
|
|
761
|
-
return {
|
|
762
|
-
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
763
|
-
apiKey,
|
|
764
|
-
model
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
async complete(input) {
|
|
768
|
-
const res = await this.#postChat(input, false);
|
|
769
|
-
let data;
|
|
770
|
-
try {
|
|
771
|
-
data = await res.json();
|
|
772
|
-
} catch (e) {
|
|
773
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage$2(e)}`, e);
|
|
774
|
-
}
|
|
775
|
-
return this.#parseResponse(data);
|
|
776
|
-
}
|
|
777
|
-
/** SSE 流式:fetch + ReadableStream 按行解析 data: 帧(Node/浏览器通用) */
|
|
778
|
-
async *stream(input) {
|
|
779
|
-
const res = await this.#postChat(input, true);
|
|
780
|
-
if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
|
|
781
|
-
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
782
|
-
const decoder = new TextDecoder();
|
|
783
|
-
const reader = res.body.getReader();
|
|
784
|
-
const frames = createSseFrameReader();
|
|
785
|
-
const think = createThinkTagSplitter();
|
|
786
|
-
let done = false;
|
|
787
|
-
let usage;
|
|
788
|
-
const handleFrame = function* (data) {
|
|
789
|
-
if (data === "[DONE]") {
|
|
790
|
-
done = true;
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
let chunk;
|
|
794
|
-
try {
|
|
795
|
-
chunk = JSON.parse(data);
|
|
796
|
-
} catch (e) {
|
|
797
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$2(e)}`, data);
|
|
798
|
-
}
|
|
799
|
-
const usageRaw = chunk.usage;
|
|
800
|
-
if (usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number")) usage = {
|
|
801
|
-
inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
|
|
802
|
-
outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
|
|
803
|
-
};
|
|
804
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
805
|
-
if (!delta) return;
|
|
806
|
-
if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") yield {
|
|
807
|
-
type: "thinking-delta",
|
|
808
|
-
delta: delta["reasoning_content"]
|
|
809
|
-
};
|
|
810
|
-
if (typeof delta["content"] === "string" && delta["content"] !== "") {
|
|
811
|
-
const split = think.push(delta["content"]);
|
|
812
|
-
if (split.thinking !== "") yield {
|
|
813
|
-
type: "thinking-delta",
|
|
814
|
-
delta: split.thinking
|
|
815
|
-
};
|
|
816
|
-
if (split.text !== "") yield {
|
|
817
|
-
type: "text-delta",
|
|
818
|
-
delta: split.text
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
const toolDeltas = delta["tool_calls"];
|
|
822
|
-
for (const td of toolDeltas ?? []) {
|
|
823
|
-
const index = td.index ?? 0;
|
|
824
|
-
const acc = toolCallsByIndex.get(index) ?? {
|
|
825
|
-
id: "",
|
|
826
|
-
name: "",
|
|
827
|
-
arguments: ""
|
|
828
|
-
};
|
|
829
|
-
if (td.id) acc.id = td.id;
|
|
830
|
-
if (td.function?.name) acc.name = td.function.name;
|
|
831
|
-
if (td.function?.arguments) acc.arguments += td.function.arguments;
|
|
832
|
-
toolCallsByIndex.set(index, acc);
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
try {
|
|
836
|
-
for (;;) {
|
|
837
|
-
const { value, done: readerDone } = await reader.read();
|
|
838
|
-
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
839
|
-
for (const payload of payloads) {
|
|
840
|
-
yield* handleFrame(payload);
|
|
841
|
-
if (done) break;
|
|
842
|
-
}
|
|
843
|
-
if (done || readerDone) break;
|
|
844
|
-
}
|
|
845
|
-
} finally {
|
|
846
|
-
reader.releaseLock();
|
|
847
|
-
}
|
|
848
|
-
const tail = think.flush();
|
|
849
|
-
if (tail.thinking !== "") yield {
|
|
850
|
-
type: "thinking-delta",
|
|
851
|
-
delta: tail.thinking
|
|
852
|
-
};
|
|
853
|
-
if (tail.text !== "") yield {
|
|
854
|
-
type: "text-delta",
|
|
855
|
-
delta: tail.text
|
|
856
|
-
};
|
|
857
|
-
if (toolCallsByIndex.size > 0) yield {
|
|
858
|
-
type: "tool-calls",
|
|
859
|
-
toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
|
|
860
|
-
let args = {};
|
|
861
|
-
let parseError;
|
|
862
|
-
try {
|
|
863
|
-
args = JSON.parse(acc.arguments || "{}");
|
|
864
|
-
} catch (e) {
|
|
865
|
-
parseError = e instanceof Error ? e.message : String(e);
|
|
866
|
-
}
|
|
867
|
-
return {
|
|
868
|
-
id: acc.id || `call-${index}`,
|
|
869
|
-
name: acc.name,
|
|
870
|
-
arguments: args,
|
|
871
|
-
...parseError ? { argumentsParseError: parseError } : {}
|
|
872
|
-
};
|
|
873
|
-
})
|
|
874
|
-
};
|
|
875
|
-
yield {
|
|
876
|
-
type: "done",
|
|
877
|
-
...usage ? { usage } : {}
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
async #postChat(input, stream) {
|
|
881
|
-
const { baseUrl, apiKey, model } = this.#requireConfig();
|
|
882
|
-
validateLlmMessages(input.messages);
|
|
883
|
-
const body = {
|
|
884
|
-
model: input.model ?? model,
|
|
885
|
-
messages: input.messages.map(toOpenAiMessage)
|
|
886
|
-
};
|
|
887
|
-
if (input.tools?.length) {
|
|
888
|
-
body["tools"] = toOpenAiTools(input.tools);
|
|
889
|
-
body["tool_choice"] = "auto";
|
|
890
|
-
}
|
|
891
|
-
if (input.temperature !== void 0) body["temperature"] = input.temperature;
|
|
892
|
-
if (stream) {
|
|
893
|
-
body["stream"] = true;
|
|
894
|
-
body["stream_options"] = { include_usage: true };
|
|
895
|
-
}
|
|
896
|
-
let res;
|
|
897
|
-
try {
|
|
898
|
-
res = await this.#fetch(`${baseUrl}/chat/completions`, {
|
|
899
|
-
method: "POST",
|
|
900
|
-
headers: {
|
|
901
|
-
"content-type": "application/json",
|
|
902
|
-
authorization: `Bearer ${apiKey}`
|
|
903
|
-
},
|
|
904
|
-
body: JSON.stringify(body),
|
|
905
|
-
signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
|
|
906
|
-
});
|
|
907
|
-
} catch (e) {
|
|
908
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage$2(e)}`, e);
|
|
909
|
-
}
|
|
910
|
-
if (!res.ok) {
|
|
911
|
-
const detail = await res.text().catch(() => "");
|
|
912
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
913
|
-
}
|
|
914
|
-
return res;
|
|
915
|
-
}
|
|
916
|
-
/** 轻量探测(GET /models),集成测试据此决定 skip */
|
|
917
|
-
async checkAvailability() {
|
|
918
|
-
try {
|
|
919
|
-
const { baseUrl, apiKey } = this.#requireConfig();
|
|
920
|
-
return (await this.#fetch(`${baseUrl}/models`, { headers: { authorization: `Bearer ${apiKey}` } })).ok;
|
|
921
|
-
} catch {
|
|
922
|
-
return false;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
#parseResponse(data) {
|
|
926
|
-
try {
|
|
927
|
-
const choice = data.choices?.[0]?.message;
|
|
928
|
-
if (!choice) throw new Error("response has no choices[0].message");
|
|
929
|
-
const toolCalls = choice["tool_calls"]?.map((tc) => {
|
|
930
|
-
let args = {};
|
|
931
|
-
let parseError;
|
|
932
|
-
try {
|
|
933
|
-
args = JSON.parse(tc.function?.arguments ?? "{}");
|
|
934
|
-
} catch (e) {
|
|
935
|
-
parseError = e instanceof Error ? e.message : String(e);
|
|
936
|
-
}
|
|
937
|
-
return {
|
|
938
|
-
id: tc.id,
|
|
939
|
-
name: tc.function?.name ?? "",
|
|
940
|
-
arguments: args,
|
|
941
|
-
...parseError ? { argumentsParseError: parseError } : {}
|
|
942
|
-
};
|
|
943
|
-
});
|
|
944
|
-
const raw = choice["content"];
|
|
945
|
-
const split = typeof raw === "string" ? splitThinkTags(raw) : {
|
|
946
|
-
text: "",
|
|
947
|
-
thinking: void 0
|
|
948
|
-
};
|
|
949
|
-
const content = split.text;
|
|
950
|
-
const declared = choice["reasoning_content"];
|
|
951
|
-
const thinking = typeof declared === "string" && declared !== "" ? declared : split.thinking;
|
|
952
|
-
const usageRaw = data.usage;
|
|
953
|
-
const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
|
|
954
|
-
inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
|
|
955
|
-
outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
|
|
956
|
-
} : void 0;
|
|
957
|
-
return {
|
|
958
|
-
content: content !== "" ? textParts(content) : void 0,
|
|
959
|
-
toolCalls: toolCalls?.length ? toolCalls : void 0,
|
|
960
|
-
...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
|
|
961
|
-
...usage ? { usage } : {},
|
|
962
|
-
raw: data
|
|
963
|
-
};
|
|
964
|
-
} catch (e) {
|
|
965
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage$2(e)}`, data);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
};
|
|
969
|
-
const ANTHROPIC_VERSION = "2023-06-01";
|
|
970
|
-
const errorMessage$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
971
|
-
/** parts → Anthropic content blocks(image/document 均为 base64 source) */
|
|
972
|
-
const toAnthropicBlocks = (parts, where) => parts.map((part) => {
|
|
973
|
-
switch (part.type) {
|
|
974
|
-
case "text": return {
|
|
975
|
-
type: "text",
|
|
976
|
-
text: part.text
|
|
977
|
-
};
|
|
978
|
-
case "image": return {
|
|
979
|
-
type: "image",
|
|
980
|
-
source: {
|
|
981
|
-
type: "base64",
|
|
982
|
-
media_type: part.mimeType,
|
|
983
|
-
data: part.data
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
case "file": return {
|
|
987
|
-
type: "document",
|
|
988
|
-
source: {
|
|
989
|
-
type: "base64",
|
|
990
|
-
media_type: part.mimeType,
|
|
991
|
-
data: part.data
|
|
992
|
-
}
|
|
993
|
-
};
|
|
994
|
-
default: return rejectUnsupportedPart(part, "Anthropic", where);
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
/** system 是独立的字符串参数,无多模态形态 */
|
|
998
|
-
const toAnthropicSystemText = (parts) => {
|
|
999
|
-
const nonText = parts.find((p) => p.type !== "text");
|
|
1000
|
-
if (nonText) rejectUnsupportedPart(nonText, "Anthropic", "system");
|
|
1001
|
-
return partsToText(parts);
|
|
1002
|
-
};
|
|
1003
|
-
/** LlmMessage 序列 → system 独立参数 + user/assistant 消息(tool 结果合并进 user 消息 tool_result blocks) */
|
|
1004
|
-
function toAnthropicMessages(messages) {
|
|
1005
|
-
const system = messages.filter((m) => m.role === "system").map((m) => toAnthropicSystemText(m.content)).join("\n");
|
|
1006
|
-
const out = [];
|
|
1007
|
-
for (const msg of messages) {
|
|
1008
|
-
if (msg.role === "system") continue;
|
|
1009
|
-
if (msg.role === "tool") {
|
|
1010
|
-
const block = {
|
|
1011
|
-
type: "tool_result",
|
|
1012
|
-
tool_use_id: msg.toolCallId ?? "",
|
|
1013
|
-
content: toAnthropicBlocks(msg.content, "tool")
|
|
1014
|
-
};
|
|
1015
|
-
const last = out.at(-1);
|
|
1016
|
-
if (last && last.role === "user" && Array.isArray(last.content)) last.content.push(block);
|
|
1017
|
-
else out.push({
|
|
1018
|
-
role: "user",
|
|
1019
|
-
content: [block]
|
|
1020
|
-
});
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
1024
|
-
const content = toAnthropicBlocks(msg.content, "assistant");
|
|
1025
|
-
for (const call of msg.toolCalls) content.push({
|
|
1026
|
-
type: "tool_use",
|
|
1027
|
-
id: call.id,
|
|
1028
|
-
name: call.name,
|
|
1029
|
-
input: call.arguments
|
|
1030
|
-
});
|
|
1031
|
-
out.push({
|
|
1032
|
-
role: "assistant",
|
|
1033
|
-
content
|
|
1034
|
-
});
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
out.push({
|
|
1038
|
-
role: msg.role,
|
|
1039
|
-
content: toAnthropicBlocks(msg.content, msg.role)
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
return system === "" ? { messages: out } : {
|
|
1043
|
-
system,
|
|
1044
|
-
messages: out
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
const toAnthropicTools = (tools) => tools.map((tool) => ({
|
|
1048
|
-
name: tool.name,
|
|
1049
|
-
...tool.description ? { description: tool.description } : {},
|
|
1050
|
-
input_schema: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), ANTHROPIC_SCHEMA_PROFILE, tool.name)
|
|
1051
|
-
}));
|
|
1052
|
-
/** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
|
|
1053
|
-
var AnthropicClient = class {
|
|
1054
|
-
#config;
|
|
1055
|
-
#fetch;
|
|
1056
|
-
constructor(config) {
|
|
1057
|
-
this.#config = config;
|
|
1058
|
-
this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
1059
|
-
}
|
|
1060
|
-
#baseUrl() {
|
|
1061
|
-
return (this.#config.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
1062
|
-
}
|
|
1063
|
-
#headers() {
|
|
1064
|
-
return {
|
|
1065
|
-
"content-type": "application/json",
|
|
1066
|
-
"x-api-key": this.#config.apiKey,
|
|
1067
|
-
"anthropic-version": ANTHROPIC_VERSION,
|
|
1068
|
-
...this.#config.dangerouslyAllowDirectBrowserAccess ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
async complete(input) {
|
|
1072
|
-
const res = await this.#post(input, false);
|
|
1073
|
-
let data;
|
|
1074
|
-
try {
|
|
1075
|
-
data = await res.json();
|
|
1076
|
-
} catch (e) {
|
|
1077
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage$1(e)}`, e);
|
|
1078
|
-
}
|
|
1079
|
-
return this.#parseResponse(data);
|
|
1080
|
-
}
|
|
1081
|
-
/** SSE 流式:text_delta → text-delta;input_json_delta 按 block index 聚合 → tool-calls */
|
|
1082
|
-
async *stream(input) {
|
|
1083
|
-
const res = await this.#post(input, true);
|
|
1084
|
-
if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
|
|
1085
|
-
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
1086
|
-
const decoder = new TextDecoder();
|
|
1087
|
-
const reader = res.body.getReader();
|
|
1088
|
-
const frames = createSseFrameReader();
|
|
1089
|
-
let inputTokens;
|
|
1090
|
-
let outputTokens;
|
|
1091
|
-
const handleFrame = function* (data) {
|
|
1092
|
-
let chunk;
|
|
1093
|
-
try {
|
|
1094
|
-
chunk = JSON.parse(data);
|
|
1095
|
-
} catch (e) {
|
|
1096
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$1(e)}`, data);
|
|
1097
|
-
}
|
|
1098
|
-
const type = chunk["type"];
|
|
1099
|
-
if (type === "error") throw new WebSkillError("LLM_REQUEST_FAILED", `LLM stream error: ${chunk["error"]?.message ?? data}`);
|
|
1100
|
-
if (type === "message_start") {
|
|
1101
|
-
const usage = chunk["message"]?.["usage"];
|
|
1102
|
-
if (typeof usage?.["input_tokens"] === "number") inputTokens = usage["input_tokens"];
|
|
1103
|
-
if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (type === "message_delta") {
|
|
1107
|
-
const usage = chunk["usage"];
|
|
1108
|
-
if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
if (type === "content_block_start") {
|
|
1112
|
-
const block = chunk["content_block"];
|
|
1113
|
-
if (block?.["type"] === "tool_use") {
|
|
1114
|
-
const index = typeof chunk["index"] === "number" ? chunk["index"] : 0;
|
|
1115
|
-
toolCallsByIndex.set(index, {
|
|
1116
|
-
id: typeof block["id"] === "string" ? block["id"] : "",
|
|
1117
|
-
name: typeof block["name"] === "string" ? block["name"] : "",
|
|
1118
|
-
arguments: ""
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
if (type === "content_block_delta") {
|
|
1124
|
-
const index = typeof chunk["index"] === "number" ? chunk["index"] : 0;
|
|
1125
|
-
const delta = chunk["delta"];
|
|
1126
|
-
if (delta?.["type"] === "text_delta" && typeof delta["text"] === "string" && delta["text"] !== "") {
|
|
1127
|
-
yield {
|
|
1128
|
-
type: "text-delta",
|
|
1129
|
-
delta: delta["text"]
|
|
1130
|
-
};
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
if (delta?.["type"] === "thinking_delta" && typeof delta["thinking"] === "string" && delta["thinking"] !== "") {
|
|
1134
|
-
yield {
|
|
1135
|
-
type: "thinking-delta",
|
|
1136
|
-
delta: delta["thinking"]
|
|
1137
|
-
};
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
if (delta?.["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
|
|
1141
|
-
const acc = toolCallsByIndex.get(index) ?? {
|
|
1142
|
-
id: "",
|
|
1143
|
-
name: "",
|
|
1144
|
-
arguments: ""
|
|
1145
|
-
};
|
|
1146
|
-
acc.arguments += delta["partial_json"];
|
|
1147
|
-
toolCallsByIndex.set(index, acc);
|
|
1148
|
-
}
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1152
|
-
try {
|
|
1153
|
-
for (;;) {
|
|
1154
|
-
const { value, done: readerDone } = await reader.read();
|
|
1155
|
-
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
1156
|
-
for (const payload of payloads) yield* handleFrame(payload);
|
|
1157
|
-
if (readerDone) break;
|
|
1158
|
-
}
|
|
1159
|
-
} finally {
|
|
1160
|
-
reader.releaseLock();
|
|
1161
|
-
}
|
|
1162
|
-
if (toolCallsByIndex.size > 0) yield {
|
|
1163
|
-
type: "tool-calls",
|
|
1164
|
-
toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
|
|
1165
|
-
let args = {};
|
|
1166
|
-
let parseError;
|
|
1167
|
-
try {
|
|
1168
|
-
args = JSON.parse(acc.arguments || "{}");
|
|
1169
|
-
} catch (e) {
|
|
1170
|
-
parseError = e instanceof Error ? e.message : String(e);
|
|
1171
|
-
}
|
|
1172
|
-
return {
|
|
1173
|
-
id: acc.id || `call-${index}`,
|
|
1174
|
-
name: acc.name,
|
|
1175
|
-
arguments: args,
|
|
1176
|
-
...parseError ? { argumentsParseError: parseError } : {}
|
|
1177
|
-
};
|
|
1178
|
-
})
|
|
1179
|
-
};
|
|
1180
|
-
const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
1181
|
-
inputTokens: inputTokens ?? 0,
|
|
1182
|
-
outputTokens: outputTokens ?? 0
|
|
1183
|
-
} : void 0;
|
|
1184
|
-
yield {
|
|
1185
|
-
type: "done",
|
|
1186
|
-
...usage ? { usage } : {}
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
async #post(input, stream) {
|
|
1190
|
-
validateLlmMessages(input.messages);
|
|
1191
|
-
const { system, messages } = toAnthropicMessages(input.messages);
|
|
1192
|
-
const body = {
|
|
1193
|
-
model: input.model ?? this.#config.model,
|
|
1194
|
-
max_tokens: this.#config.maxTokens ?? 4096,
|
|
1195
|
-
messages
|
|
1196
|
-
};
|
|
1197
|
-
if (system !== void 0) body["system"] = system;
|
|
1198
|
-
if (input.tools?.length) body["tools"] = toAnthropicTools(input.tools);
|
|
1199
|
-
const thinkingBudget = this.#config.thinkingBudgetTokens;
|
|
1200
|
-
if (thinkingBudget !== void 0 && Number.isInteger(thinkingBudget) && thinkingBudget > 0) body["thinking"] = {
|
|
1201
|
-
type: "enabled",
|
|
1202
|
-
budget_tokens: thinkingBudget
|
|
1203
|
-
};
|
|
1204
|
-
else if (input.temperature !== void 0) body["temperature"] = input.temperature;
|
|
1205
|
-
if (stream) body["stream"] = true;
|
|
1206
|
-
let res;
|
|
1207
|
-
try {
|
|
1208
|
-
res = await this.#fetch(`${this.#baseUrl()}/v1/messages`, {
|
|
1209
|
-
method: "POST",
|
|
1210
|
-
headers: this.#headers(),
|
|
1211
|
-
body: JSON.stringify(body),
|
|
1212
|
-
signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
|
|
1213
|
-
});
|
|
1214
|
-
} catch (e) {
|
|
1215
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage$1(e)}`, e);
|
|
1216
|
-
}
|
|
1217
|
-
if (!res.ok) {
|
|
1218
|
-
const detail = await res.text().catch(() => "");
|
|
1219
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
1220
|
-
}
|
|
1221
|
-
return res;
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* 真实调用路径探测(FR-27.1):`POST /v1/messages` 最小请求(max_tokens=1),200 即可达。
|
|
1225
|
-
* Anthropic 兼容端点(如 LM Studio 的 /anthropic 层)不实现 `GET /v1/models`,
|
|
1226
|
-
* 用 /models 探测会把「能对话」误判为「不可达」——checkAvailability 的语义从
|
|
1227
|
-
* 「/models 可达」改为「能完成一次真实调用」。
|
|
1228
|
-
*/
|
|
1229
|
-
async checkAvailability() {
|
|
1230
|
-
try {
|
|
1231
|
-
return (await this.#fetch(`${this.#baseUrl()}/v1/messages`, {
|
|
1232
|
-
method: "POST",
|
|
1233
|
-
headers: this.#headers(),
|
|
1234
|
-
body: JSON.stringify({
|
|
1235
|
-
model: this.#config.model,
|
|
1236
|
-
max_tokens: 1,
|
|
1237
|
-
messages: [{
|
|
1238
|
-
role: "user",
|
|
1239
|
-
content: "ping"
|
|
1240
|
-
}]
|
|
1241
|
-
})
|
|
1242
|
-
})).ok;
|
|
1243
|
-
} catch {
|
|
1244
|
-
return false;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
#parseResponse(data) {
|
|
1248
|
-
try {
|
|
1249
|
-
const blocks = data.content;
|
|
1250
|
-
if (!Array.isArray(blocks)) throw new Error("response has no content blocks");
|
|
1251
|
-
const text = blocks.filter((b) => b["type"] === "text").map((b) => String(b["text"] ?? "")).join("");
|
|
1252
|
-
const thinking = blocks.filter((b) => b["type"] === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
|
|
1253
|
-
const toolCalls = blocks.filter((b) => b["type"] === "tool_use").map((b) => ({
|
|
1254
|
-
id: typeof b["id"] === "string" ? b["id"] : "",
|
|
1255
|
-
name: typeof b["name"] === "string" ? b["name"] : "",
|
|
1256
|
-
arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
|
|
1257
|
-
}));
|
|
1258
|
-
const usageRaw = data.usage;
|
|
1259
|
-
const usage = typeof usageRaw?.["input_tokens"] === "number" || typeof usageRaw?.["output_tokens"] === "number" ? {
|
|
1260
|
-
inputTokens: typeof usageRaw["input_tokens"] === "number" ? usageRaw["input_tokens"] : 0,
|
|
1261
|
-
outputTokens: typeof usageRaw["output_tokens"] === "number" ? usageRaw["output_tokens"] : 0
|
|
1262
|
-
} : void 0;
|
|
1263
|
-
return {
|
|
1264
|
-
content: text === "" ? void 0 : textParts(text),
|
|
1265
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1266
|
-
...thinking === "" ? {} : { thinking },
|
|
1267
|
-
...usage ? { usage } : {},
|
|
1268
|
-
raw: data
|
|
1269
|
-
};
|
|
1270
|
-
} catch (e) {
|
|
1271
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage$1(e)}`, data);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
};
|
|
1275
|
-
const errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
1276
|
-
/** usageMetadata → LlmTokenUsage(promptTokenCount / candidatesTokenCount;无字段返回 undefined) */
|
|
1277
|
-
function parseGenAiUsage(data) {
|
|
1278
|
-
const usage = data?.usageMetadata;
|
|
1279
|
-
if (!usage) return void 0;
|
|
1280
|
-
const input = usage["promptTokenCount"];
|
|
1281
|
-
const output = usage["candidatesTokenCount"];
|
|
1282
|
-
if (typeof input !== "number" && typeof output !== "number") return void 0;
|
|
1283
|
-
return {
|
|
1284
|
-
inputTokens: typeof input === "number" ? input : 0,
|
|
1285
|
-
outputTokens: typeof output === "number" ? output : 0
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
/** parts → GenAI parts(二进制统一走 inlineData) */
|
|
1289
|
-
const toGenAiParts = (parts, where) => parts.map((part) => {
|
|
1290
|
-
switch (part.type) {
|
|
1291
|
-
case "text": return { text: part.text };
|
|
1292
|
-
case "image":
|
|
1293
|
-
case "file": return { inlineData: {
|
|
1294
|
-
mimeType: part.mimeType,
|
|
1295
|
-
data: part.data
|
|
1296
|
-
} };
|
|
1297
|
-
default: return rejectUnsupportedPart(part, "Google GenAI", where);
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
/** systemInstruction 与 functionResponse 只接受文本 */
|
|
1301
|
-
const toGenAiText = (parts, where) => {
|
|
1302
|
-
const nonText = parts.find((p) => p.type !== "text");
|
|
1303
|
-
if (nonText) rejectUnsupportedPart(nonText, "Google GenAI", where);
|
|
1304
|
-
return partsToText(parts);
|
|
1305
|
-
};
|
|
1306
|
-
/** tool 结果文本包装为 functionResponse.response 对象 */
|
|
1307
|
-
function responseObject(content) {
|
|
1308
|
-
try {
|
|
1309
|
-
const parsed = JSON.parse(content);
|
|
1310
|
-
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
1311
|
-
} catch {}
|
|
1312
|
-
return { result: content };
|
|
1313
|
-
}
|
|
1314
|
-
/** LlmMessage 序列 → systemInstruction + contents(tool 结果合并进 user 消息 functionResponse parts) */
|
|
1315
|
-
function toGenAiContents(messages) {
|
|
1316
|
-
const system = messages.filter((m) => m.role === "system").map((m) => toGenAiText(m.content, "system")).join("\n");
|
|
1317
|
-
const nameByCallId = /* @__PURE__ */ new Map();
|
|
1318
|
-
for (const msg of messages) if (msg.role === "assistant") for (const call of msg.toolCalls ?? []) nameByCallId.set(call.id, call.name);
|
|
1319
|
-
const out = [];
|
|
1320
|
-
for (const msg of messages) {
|
|
1321
|
-
if (msg.role === "system") continue;
|
|
1322
|
-
if (msg.role === "tool") {
|
|
1323
|
-
const callId = msg.toolCallId ?? "";
|
|
1324
|
-
const part = { functionResponse: {
|
|
1325
|
-
name: nameByCallId.get(callId) ?? callId,
|
|
1326
|
-
response: responseObject(toGenAiText(msg.content, "tool"))
|
|
1327
|
-
} };
|
|
1328
|
-
const last = out.at(-1);
|
|
1329
|
-
if (last && last.role === "user") last.parts.push(part);
|
|
1330
|
-
else out.push({
|
|
1331
|
-
role: "user",
|
|
1332
|
-
parts: [part]
|
|
1333
|
-
});
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (msg.role === "assistant") {
|
|
1337
|
-
const parts = toGenAiParts(msg.content, "assistant");
|
|
1338
|
-
for (const call of msg.toolCalls ?? []) {
|
|
1339
|
-
const signature = call.vendor?.["thoughtSignature"];
|
|
1340
|
-
parts.push({
|
|
1341
|
-
functionCall: {
|
|
1342
|
-
name: call.name,
|
|
1343
|
-
args: call.arguments
|
|
1344
|
-
},
|
|
1345
|
-
...typeof signature === "string" ? { thoughtSignature: signature } : {}
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
out.push({
|
|
1349
|
-
role: "model",
|
|
1350
|
-
parts: parts.length > 0 ? parts : [{ text: "" }]
|
|
1351
|
-
});
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
out.push({
|
|
1355
|
-
role: "user",
|
|
1356
|
-
parts: toGenAiParts(msg.content, msg.role)
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
return system === "" ? { contents: out } : {
|
|
1360
|
-
systemInstruction: { parts: [{ text: system }] },
|
|
1361
|
-
contents: out
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => {
|
|
1365
|
-
const portable = toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema));
|
|
1366
|
-
return {
|
|
1367
|
-
name: tool.name,
|
|
1368
|
-
...tool.description ? { description: tool.description } : {},
|
|
1369
|
-
...isExpressibleForVendor(portable, GOOGLE_SCHEMA_PROFILE) ? { parameters: sanitizeForVendor(portable, GOOGLE_SCHEMA_PROFILE, tool.name) } : { parametersJsonSchema: relaxRefLoops(portable) }
|
|
1370
|
-
};
|
|
1371
|
-
}) }];
|
|
1372
|
-
/** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
|
|
1373
|
-
var GoogleGenAiClient = class {
|
|
1374
|
-
#config;
|
|
1375
|
-
#fetch;
|
|
1376
|
-
constructor(config) {
|
|
1377
|
-
this.#config = config;
|
|
1378
|
-
this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
1379
|
-
}
|
|
1380
|
-
#baseUrl() {
|
|
1381
|
-
return (this.#config.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
|
|
1382
|
-
}
|
|
1383
|
-
async complete(input) {
|
|
1384
|
-
const res = await this.#post(input, false);
|
|
1385
|
-
let data;
|
|
1386
|
-
try {
|
|
1387
|
-
data = await res.json();
|
|
1388
|
-
} catch (e) {
|
|
1389
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage(e)}`, e);
|
|
1390
|
-
}
|
|
1391
|
-
const parts = this.#responseParts(data);
|
|
1392
|
-
const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
|
|
1393
|
-
const toolCalls = this.#functionCalls(parts);
|
|
1394
|
-
const usage = parseGenAiUsage(data);
|
|
1395
|
-
return {
|
|
1396
|
-
content: text === "" ? void 0 : textParts(text),
|
|
1397
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1398
|
-
...usage ? { usage } : {},
|
|
1399
|
-
raw: data
|
|
1400
|
-
};
|
|
1401
|
-
}
|
|
1402
|
-
/** SSE 流式:chunk.parts text → text-delta;functionCall 聚合 → tool-calls */
|
|
1403
|
-
async *stream(input) {
|
|
1404
|
-
const res = await this.#post(input, true);
|
|
1405
|
-
if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
|
|
1406
|
-
const toolCalls = [];
|
|
1407
|
-
const decoder = new TextDecoder();
|
|
1408
|
-
const reader = res.body.getReader();
|
|
1409
|
-
const frames = createSseFrameReader();
|
|
1410
|
-
let usage;
|
|
1411
|
-
const handleFrame = function* (data) {
|
|
1412
|
-
let chunk;
|
|
1413
|
-
try {
|
|
1414
|
-
chunk = JSON.parse(data);
|
|
1415
|
-
} catch (e) {
|
|
1416
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
|
|
1417
|
-
}
|
|
1418
|
-
usage = parseGenAiUsage(chunk) ?? usage;
|
|
1419
|
-
const parts = chunk.candidates?.[0]?.content?.parts;
|
|
1420
|
-
for (const part of parts ?? []) {
|
|
1421
|
-
if (typeof part["text"] === "string" && part["text"] !== "") {
|
|
1422
|
-
yield {
|
|
1423
|
-
type: "text-delta",
|
|
1424
|
-
delta: part["text"]
|
|
1425
|
-
};
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
const call = part["functionCall"];
|
|
1429
|
-
if (call) {
|
|
1430
|
-
const signature = part["thoughtSignature"];
|
|
1431
|
-
toolCalls.push({
|
|
1432
|
-
id: `call-${toolCalls.length}`,
|
|
1433
|
-
name: call.name ?? "",
|
|
1434
|
-
arguments: call.args ?? {},
|
|
1435
|
-
...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1440
|
-
try {
|
|
1441
|
-
for (;;) {
|
|
1442
|
-
const { value, done: readerDone } = await reader.read();
|
|
1443
|
-
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
1444
|
-
for (const payload of payloads) yield* handleFrame(payload);
|
|
1445
|
-
if (readerDone) break;
|
|
1446
|
-
}
|
|
1447
|
-
} finally {
|
|
1448
|
-
reader.releaseLock();
|
|
1449
|
-
}
|
|
1450
|
-
if (toolCalls.length > 0) yield {
|
|
1451
|
-
type: "tool-calls",
|
|
1452
|
-
toolCalls
|
|
1453
|
-
};
|
|
1454
|
-
yield {
|
|
1455
|
-
type: "done",
|
|
1456
|
-
...usage ? { usage } : {}
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
async #post(input, stream) {
|
|
1460
|
-
const model = input.model ?? this.#config.model;
|
|
1461
|
-
const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
|
|
1462
|
-
validateLlmMessages(input.messages);
|
|
1463
|
-
const { systemInstruction, contents } = toGenAiContents(input.messages);
|
|
1464
|
-
const body = { contents };
|
|
1465
|
-
if (systemInstruction) body["systemInstruction"] = systemInstruction;
|
|
1466
|
-
if (input.tools?.length) body["tools"] = toGenAiTools(input.tools);
|
|
1467
|
-
if (input.temperature !== void 0) body["generationConfig"] = { temperature: input.temperature };
|
|
1468
|
-
let res;
|
|
1469
|
-
try {
|
|
1470
|
-
res = await this.#fetch(`${this.#baseUrl()}/v1beta/models/${encodeURIComponent(model)}${action}`, {
|
|
1471
|
-
method: "POST",
|
|
1472
|
-
headers: {
|
|
1473
|
-
"content-type": "application/json",
|
|
1474
|
-
"x-goog-api-key": this.#config.apiKey
|
|
1475
|
-
},
|
|
1476
|
-
body: JSON.stringify(body),
|
|
1477
|
-
signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
|
|
1478
|
-
});
|
|
1479
|
-
} catch (e) {
|
|
1480
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage(e)}`, e);
|
|
1481
|
-
}
|
|
1482
|
-
if (!res.ok) {
|
|
1483
|
-
const detail = await res.text().catch(() => "");
|
|
1484
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
1485
|
-
}
|
|
1486
|
-
return res;
|
|
1487
|
-
}
|
|
1488
|
-
/** 轻量探测(GET /v1beta/models),集成测试据此决定 skip */
|
|
1489
|
-
async checkAvailability() {
|
|
1490
|
-
try {
|
|
1491
|
-
return (await this.#fetch(`${this.#baseUrl()}/v1beta/models`, { headers: { "x-goog-api-key": this.#config.apiKey } })).ok;
|
|
1492
|
-
} catch {
|
|
1493
|
-
return false;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
#responseParts(data) {
|
|
1497
|
-
try {
|
|
1498
|
-
const parts = data.candidates?.[0]?.content?.parts;
|
|
1499
|
-
if (!Array.isArray(parts)) throw new Error("response has no candidates[0].content.parts");
|
|
1500
|
-
return parts;
|
|
1501
|
-
} catch (e) {
|
|
1502
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage(e)}`, data);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
#functionCalls(parts) {
|
|
1506
|
-
const out = [];
|
|
1507
|
-
for (const part of parts) {
|
|
1508
|
-
const call = part["functionCall"];
|
|
1509
|
-
if (call) {
|
|
1510
|
-
const signature = part["thoughtSignature"];
|
|
1511
|
-
out.push({
|
|
1512
|
-
id: `call-${out.length}`,
|
|
1513
|
-
name: call.name ?? "",
|
|
1514
|
-
arguments: call.args ?? {},
|
|
1515
|
-
...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
return out;
|
|
1520
|
-
}
|
|
1521
|
-
};
|
|
1522
|
-
/** v5 { inputTokens, outputTokens } 与 v4 { promptTokens, completionTokens } 两种形状都接 */
|
|
1523
|
-
function fromVercelUsage(raw) {
|
|
1524
|
-
const u = raw ?? {};
|
|
1525
|
-
const input = u["inputTokens"] ?? u["promptTokens"];
|
|
1526
|
-
const output = u["outputTokens"] ?? u["completionTokens"];
|
|
1527
|
-
if (typeof input !== "number" && typeof output !== "number") return void 0;
|
|
1528
|
-
return {
|
|
1529
|
-
inputTokens: typeof input === "number" ? input : 0,
|
|
1530
|
-
outputTokens: typeof output === "number" ? output : 0
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
function toVercelToolSpecs(tools) {
|
|
1534
|
-
const out = {};
|
|
1535
|
-
for (const tool of tools) out[tool.name] = {
|
|
1536
|
-
...tool.description ? { description: tool.description } : {},
|
|
1537
|
-
inputSchema: tool.inputSchema
|
|
1538
|
-
};
|
|
1539
|
-
return out;
|
|
1540
|
-
}
|
|
1541
|
-
/**
|
|
1542
|
-
* 归一化供应商思考内容:v5 的 `reasoningText`(字符串)与 v4 的 `reasoning`
|
|
1543
|
-
* (Array<{ text; type }>)都能落到同一条 `LlmResponse.thinking`。
|
|
1544
|
-
*/
|
|
1545
|
-
function normalizeThinking(value) {
|
|
1546
|
-
if (typeof value === "string") return value !== "" ? value : void 0;
|
|
1547
|
-
if (!Array.isArray(value)) return void 0;
|
|
1548
|
-
const text = value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
|
|
1549
|
-
return text !== "" ? text : void 0;
|
|
1550
|
-
}
|
|
1551
|
-
/** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
|
|
1552
|
-
function fromVercelResult(result) {
|
|
1553
|
-
const r = result;
|
|
1554
|
-
const toolCalls = (r.toolCalls ?? []).map((tc, i) => {
|
|
1555
|
-
const args = tc.input ?? tc.args ?? {};
|
|
1556
|
-
return {
|
|
1557
|
-
id: typeof tc.toolCallId === "string" ? tc.toolCallId : `call-${i}`,
|
|
1558
|
-
name: typeof tc.toolName === "string" ? tc.toolName : "",
|
|
1559
|
-
arguments: typeof args === "object" && args !== null ? args : {}
|
|
1560
|
-
};
|
|
1561
|
-
});
|
|
1562
|
-
const thinking = normalizeThinking(r.reasoningText ?? r.reasoning);
|
|
1563
|
-
const usage = fromVercelUsage(r.usage);
|
|
1564
|
-
return {
|
|
1565
|
-
content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
|
|
1566
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1567
|
-
...thinking !== void 0 ? { thinking } : {},
|
|
1568
|
-
...usage ? { usage } : {},
|
|
1569
|
-
raw: result
|
|
1570
|
-
};
|
|
12
|
+
//#region ../core/src/render/xmlRenderer.ts
|
|
13
|
+
/** 转义 XML 五个特殊字符:& < > " ' */
|
|
14
|
+
function escapeXml(text) {
|
|
15
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1571
16
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
case "reasoning-delta": {
|
|
1587
|
-
const delta = p["text"] ?? p["delta"];
|
|
1588
|
-
return typeof delta === "string" && delta !== "" ? {
|
|
1589
|
-
type: "thinking-delta",
|
|
1590
|
-
delta
|
|
1591
|
-
} : void 0;
|
|
1592
|
-
}
|
|
1593
|
-
case "tool-call": return {
|
|
1594
|
-
type: "tool-calls",
|
|
1595
|
-
toolCalls: [{
|
|
1596
|
-
id: typeof p["toolCallId"] === "string" ? p["toolCallId"] : "call-0",
|
|
1597
|
-
name: typeof p["toolName"] === "string" ? p["toolName"] : "",
|
|
1598
|
-
arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
|
|
1599
|
-
}]
|
|
1600
|
-
};
|
|
1601
|
-
case "finish": {
|
|
1602
|
-
const usage = fromVercelUsage(p["totalUsage"] ?? p["usage"]);
|
|
1603
|
-
return {
|
|
1604
|
-
type: "done",
|
|
1605
|
-
...usage ? { usage } : {}
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
default: return;
|
|
1609
|
-
}
|
|
17
|
+
function renderAvailableSkillsXml(catalog) {
|
|
18
|
+
const lines = ["<available_skills>"];
|
|
19
|
+
for (const skill of catalog.entries) {
|
|
20
|
+
lines.push(" <skill>");
|
|
21
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
22
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
23
|
+
if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
|
|
24
|
+
if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
|
|
25
|
+
lines.push(` <source>${escapeXml(skill.source)}</source>`);
|
|
26
|
+
lines.push(" </skill>");
|
|
27
|
+
}
|
|
28
|
+
lines.push("</available_skills>");
|
|
29
|
+
return lines.join("\n");
|
|
1610
30
|
}
|
|
1611
|
-
const
|
|
31
|
+
const xmlRenderer = {
|
|
32
|
+
format: "xml",
|
|
33
|
+
render: renderAvailableSkillsXml
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region ../runtime/src/routing/progressiveRouter.ts
|
|
38
|
+
const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
|
|
1612
39
|
|
|
1613
40
|
Skills are disclosed progressively: the catalog below only shows metadata. To use a skill:
|
|
1614
41
|
1. Call the "read_skill_file" tool to load the skill's SKILL.md instructions. Reading SKILL.md activates the skill's script tools, which are named "<skillName>__<scriptName>".
|
|
@@ -1623,74 +50,13 @@ var ProgressiveRouter = class {
|
|
|
1623
50
|
return {
|
|
1624
51
|
strategy: "progressive",
|
|
1625
52
|
catalog,
|
|
1626
|
-
systemPrompt: `${INSTRUCTIONS
|
|
53
|
+
systemPrompt: `${INSTRUCTIONS}\n\n${renderAvailableSkillsXml(catalog)}`
|
|
1627
54
|
};
|
|
1628
55
|
}
|
|
1629
56
|
};
|
|
1630
|
-
const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
|
|
1631
|
-
The full instructions of every available skill are injected below. Follow them directly.
|
|
1632
|
-
Script tools are named "<skillName>__<scriptName>".
|
|
1633
|
-
When the task is complete, answer in plain text without calling any tool.
|
|
1634
57
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
var FullDisclosureRouter = class {
|
|
1638
|
-
#discovery;
|
|
1639
|
-
constructor(discovery) {
|
|
1640
|
-
this.#discovery = discovery;
|
|
1641
|
-
}
|
|
1642
|
-
async route(catalog) {
|
|
1643
|
-
const sections = [];
|
|
1644
|
-
for (const entry of catalog.entries) {
|
|
1645
|
-
const doc = await this.#discovery.loadDocument(entry.name);
|
|
1646
|
-
sections.push(`<skill name="${entry.name}">\n${doc.body}\n</skill>`);
|
|
1647
|
-
}
|
|
1648
|
-
return {
|
|
1649
|
-
strategy: "full-disclosure",
|
|
1650
|
-
catalog,
|
|
1651
|
-
systemPrompt: `${INSTRUCTIONS}\n\n${sections.join("\n\n")}`
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
};
|
|
1655
|
-
/**
|
|
1656
|
-
* 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
|
|
1657
|
-
*
|
|
1658
|
-
* 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
|
|
1659
|
-
* 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
|
|
1660
|
-
* 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
|
|
1661
|
-
* @experimental
|
|
1662
|
-
*/
|
|
1663
|
-
const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
|
|
1664
|
-
/**
|
|
1665
|
-
* `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
|
|
1666
|
-
* provider 的限额算的是请求载荷。
|
|
1667
|
-
*
|
|
1668
|
-
* 2026-08-14 查证的三家上限:
|
|
1669
|
-
* | provider | 上限 | 出处 |
|
|
1670
|
-
* | --------- | --------------------------------------- | ----------------------- |
|
|
1671
|
-
* | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
|
|
1672
|
-
* | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
|
|
1673
|
-
* | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
|
|
1674
|
-
*
|
|
1675
|
-
* 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
|
|
1676
|
-
* 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
|
|
1677
|
-
* 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
|
|
1678
|
-
* 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
|
|
1679
|
-
*/
|
|
1680
|
-
const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
|
|
1681
|
-
/**
|
|
1682
|
-
* `document-text` 分片的上限(FR-23.4)。
|
|
1683
|
-
*
|
|
1684
|
-
* 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
|
|
1685
|
-
* 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
|
|
1686
|
-
* 给历史消息与模型的回答留下其余。
|
|
1687
|
-
*/
|
|
1688
|
-
const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
|
|
1689
|
-
/**
|
|
1690
|
-
* 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
|
|
1691
|
-
* 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
|
|
1692
|
-
*/
|
|
1693
|
-
const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region ../runtime/src/tools/toolResolver.ts
|
|
1694
60
|
/**
|
|
1695
61
|
* 工具名解析规则(单一实现,Agent 循环使用):
|
|
1696
62
|
* 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
|
|
@@ -1744,6 +110,9 @@ function toLlmToolSpec(tool) {
|
|
|
1744
110
|
}
|
|
1745
111
|
};
|
|
1746
112
|
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region ../runtime/src/tools/skillScripts.ts
|
|
1747
116
|
/** 工具名规则:技能**目录名**加双下划线加脚本名,与 frontmatter 的 name 无关 */
|
|
1748
117
|
function scriptToolName(skillName, scriptName) {
|
|
1749
118
|
return `${skillName}__${scriptName}`;
|
|
@@ -1790,43 +159,9 @@ function formatSkillScriptManifest(descriptors) {
|
|
|
1790
159
|
if (descriptors.length === 0) return "";
|
|
1791
160
|
return `\n\nAvailable scripts in this skill (from directory listing, authoritative):\n${descriptors.map((item) => item.toolName === void 0 ? `- ${item.path} → not exposed as a tool (${item.unexposedReason ?? "no loadable definition"})` : `- ${item.path} → tool "${item.toolName}" (input schema: ${SCHEMA_SOURCE_TEXT[item.schemaSource]})`).join("\n")}`;
|
|
1792
161
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
* 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
|
|
1797
|
-
* 产出只有文本型分片:脚本侧没有下发图像的通道(图像只来自页面取像)。
|
|
1798
|
-
*/
|
|
1799
|
-
function normalizeToolContent(value) {
|
|
1800
|
-
if (Array.isArray(value)) return value.map((item) => {
|
|
1801
|
-
if (typeof item === "object" && item !== null && typeof item.type === "string" && MCP_CONTENT_RE.test(item.type)) {
|
|
1802
|
-
const { type, ...rest } = item;
|
|
1803
|
-
if (type === "json") return {
|
|
1804
|
-
type: "json",
|
|
1805
|
-
data: rest["data"] ?? rest
|
|
1806
|
-
};
|
|
1807
|
-
return {
|
|
1808
|
-
type: "text",
|
|
1809
|
-
text: typeof rest["text"] === "string" ? rest["text"] : JSON.stringify(rest)
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
return {
|
|
1813
|
-
type: "text",
|
|
1814
|
-
text: typeof item === "string" ? item : JSON.stringify(item)
|
|
1815
|
-
};
|
|
1816
|
-
});
|
|
1817
|
-
if (typeof value === "string") return [{
|
|
1818
|
-
type: "text",
|
|
1819
|
-
text: value
|
|
1820
|
-
}];
|
|
1821
|
-
if (value === void 0 || value === null) return [{
|
|
1822
|
-
type: "text",
|
|
1823
|
-
text: ""
|
|
1824
|
-
}];
|
|
1825
|
-
return [{
|
|
1826
|
-
type: "text",
|
|
1827
|
-
text: typeof value === "object" ? JSON.stringify(value) : String(value)
|
|
1828
|
-
}];
|
|
1829
|
-
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region ../runtime/src/tools/readSkillFileTool.ts
|
|
1830
165
|
const READ_SKILL_FILE_TOOL_NAME = "read_skill_file";
|
|
1831
166
|
const READ_SKILL_FILE_INPUT_SCHEMA = {
|
|
1832
167
|
type: "object",
|
|
@@ -1849,6 +184,9 @@ const READ_SKILL_FILE_TOOL = {
|
|
|
1849
184
|
inputSchema: READ_SKILL_FILE_INPUT_SCHEMA,
|
|
1850
185
|
source: "builtin"
|
|
1851
186
|
};
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region ../runtime/src/tools/askUserTool.ts
|
|
1852
190
|
const ASK_USER_TOOL_NAME = "ask_user";
|
|
1853
191
|
/** 一次 `ask_user` 能收的字段数上限(FR-11.3 裁决值),可由 `AgentLoopConfig` 覆盖 */
|
|
1854
192
|
const ASK_USER_MAX_FIELDS = 20;
|
|
@@ -1938,78 +276,9 @@ const ASK_USER_TOOL = {
|
|
|
1938
276
|
inputSchema: ASK_USER_INPUT_SCHEMA,
|
|
1939
277
|
source: "builtin"
|
|
1940
278
|
};
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
* metadata 是能力桥上第一个**开放结构**的入参,且来自沙箱内的技能脚本。
|
|
1945
|
-
* 校验落在 `createScriptContext.writeArtifact`——它是 in-process 与三个沙箱执行器
|
|
1946
|
-
* 唯一的汇聚点,在此抛错才能带上正确的桥请求 id 回给脚本(DV-10)。
|
|
1947
|
-
*/
|
|
1948
|
-
/** 序列化后的字节上限;`index.json` 是整覆盖原子写,无上限等于廉价的放大面 */
|
|
1949
|
-
const ARTIFACT_METADATA_MAX_BYTES = 4096;
|
|
1950
|
-
/** 原型污染键;JSON 解析产生的同名键是**自有键**,可被 getOwnPropertyNames 查出 */
|
|
1951
|
-
const FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
1952
|
-
"__proto__",
|
|
1953
|
-
"constructor",
|
|
1954
|
-
"prototype"
|
|
1955
|
-
]);
|
|
1956
|
-
function isPlainObject$1(value) {
|
|
1957
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1958
|
-
const proto = Object.getPrototypeOf(value);
|
|
1959
|
-
return proto === Object.prototype || proto === null;
|
|
1960
|
-
}
|
|
1961
|
-
/** 函数声明而非箭头函数赋值:只有前者能让 TS 的控制流分析认到 never 终止 */
|
|
1962
|
-
function reject$1(message) {
|
|
1963
|
-
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
1964
|
-
}
|
|
1965
|
-
/**
|
|
1966
|
-
* 递归拒绝原型污染键与不可序列化值。
|
|
1967
|
-
* `JSON.stringify` 会**静默丢弃**函数与 symbol,而静默正是本需求明令禁止的形态,
|
|
1968
|
-
* 所以必须在序列化之前显式查一遍。
|
|
1969
|
-
*/
|
|
1970
|
-
function assertSerializableShape(value, path, seen) {
|
|
1971
|
-
const kind = typeof value;
|
|
1972
|
-
if (kind === "function" || kind === "symbol" || kind === "bigint") reject$1(`Artifact metadata value at "${path}" is not JSON-serializable (${kind})`);
|
|
1973
|
-
if (kind !== "object" || value === null) return;
|
|
1974
|
-
const container = value;
|
|
1975
|
-
if (seen.has(container)) reject$1(`Artifact metadata contains a circular reference at "${path}"`);
|
|
1976
|
-
seen.add(container);
|
|
1977
|
-
if (Array.isArray(value)) value.forEach((item, index) => assertSerializableShape(item, `${path}[${index}]`, seen));
|
|
1978
|
-
else {
|
|
1979
|
-
if (!isPlainObject$1(value)) reject$1(`Artifact metadata value at "${path}" must be a plain object or array`);
|
|
1980
|
-
for (const key of Object.getOwnPropertyNames(value)) {
|
|
1981
|
-
if (FORBIDDEN_KEYS.has(key)) reject$1(`Artifact metadata must not contain the reserved key "${key}" (at "${path}")`);
|
|
1982
|
-
assertSerializableShape(value[key], `${path}.${key}`, seen);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
seen.delete(container);
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* 准入校验。通过后返回 `JSON.parse(JSON.stringify(...))` 的副本:
|
|
1989
|
-
* in-process 路径由此获得与 JSON stdio 沙箱路径完全相同的语义,
|
|
1990
|
-
* 而不是依赖某条传输链路碰巧做了序列化。
|
|
1991
|
-
*/
|
|
1992
|
-
function admitArtifactMetadata(value) {
|
|
1993
|
-
if (!isPlainObject$1(value)) reject$1("Artifact metadata must be a plain object");
|
|
1994
|
-
assertSerializableShape(value, "metadata", /* @__PURE__ */ new Set());
|
|
1995
|
-
let json;
|
|
1996
|
-
try {
|
|
1997
|
-
json = JSON.stringify(value);
|
|
1998
|
-
} catch (error) {
|
|
1999
|
-
reject$1(`Artifact metadata is not JSON-serializable: ${error.message}`);
|
|
2000
|
-
}
|
|
2001
|
-
const bytes = new TextEncoder().encode(json).length;
|
|
2002
|
-
if (bytes > 4096) reject$1(`Artifact metadata is too large: ${bytes} bytes exceeds the ${ARTIFACT_METADATA_MAX_BYTES} byte limit`);
|
|
2003
|
-
return JSON.parse(json);
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* 「这份产物不出自动下载卡」的判读。**严格 `=== false`**:
|
|
2007
|
-
* 隐式真值转换会让写错的键值静默生效,而隐藏是不可见的失败。
|
|
2008
|
-
* `buildRenderResult` 与 agentLoop 的去重集合共用本函数,两处判据不得分叉。
|
|
2009
|
-
*/
|
|
2010
|
-
function isArtifactCardHidden(artifact) {
|
|
2011
|
-
return artifact.metadata?.["resultCard"] === false;
|
|
2012
|
-
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region ../runtime/src/tools/scriptContext.ts
|
|
2013
282
|
/**
|
|
2014
283
|
* 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
|
|
2015
284
|
* 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
|
|
@@ -2033,7 +302,7 @@ function resolveSkillDir(skillRoot, dir, relativePath) {
|
|
|
2033
302
|
* 四个显式能力,不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
|
|
2034
303
|
*/
|
|
2035
304
|
function createScriptContext(deps) {
|
|
2036
|
-
const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
|
|
305
|
+
const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, uploadFiles, onWarning, onArtifactCreated } = deps;
|
|
2037
306
|
const readFs = fs.withRoot?.(skillRoot) ?? fs;
|
|
2038
307
|
return {
|
|
2039
308
|
skillName,
|
|
@@ -2067,210 +336,17 @@ function createScriptContext(deps) {
|
|
|
2067
336
|
},
|
|
2068
337
|
...confirm ? { confirm } : {},
|
|
2069
338
|
...fetchData ? { fetchData } : {},
|
|
339
|
+
...deps.documentSurface ? { documentSurface: true } : {},
|
|
340
|
+
...uploadFiles ? {
|
|
341
|
+
listUploadFiles: async () => [...await uploadFiles.list()],
|
|
342
|
+
readUploadFile: (id) => uploadFiles.read(id)
|
|
343
|
+
} : {},
|
|
2070
344
|
...onWarning ? { onWarning } : {}
|
|
2071
345
|
};
|
|
2072
346
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
* 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
|
|
2077
|
-
*/
|
|
2078
|
-
function extractChartSpec(data) {
|
|
2079
|
-
if (!isRecord$4(data)) return void 0;
|
|
2080
|
-
const raw = data["$chart"];
|
|
2081
|
-
if (!isRecord$4(raw)) return void 0;
|
|
2082
|
-
const { kind, labels, series } = raw;
|
|
2083
|
-
if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
|
|
2084
|
-
if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
|
|
2085
|
-
if (!Array.isArray(series)) return void 0;
|
|
2086
|
-
const validSeries = [];
|
|
2087
|
-
for (const item of series) {
|
|
2088
|
-
if (!isRecord$4(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
|
|
2089
|
-
const name = item["name"];
|
|
2090
|
-
validSeries.push({
|
|
2091
|
-
...typeof name === "string" ? { name } : {},
|
|
2092
|
-
data: item["data"]
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
const title = raw["title"];
|
|
2096
|
-
return {
|
|
2097
|
-
kind,
|
|
2098
|
-
labels,
|
|
2099
|
-
series: validSeries,
|
|
2100
|
-
...typeof title === "string" ? { title } : {}
|
|
2101
|
-
};
|
|
2102
|
-
}
|
|
2103
|
-
/**
|
|
2104
|
-
* 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
|
|
2105
|
-
* LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
|
|
2106
|
-
* 注入 output block 时一并记下它的下标(S7),供消费方按来源去重。
|
|
2107
|
-
*/
|
|
2108
|
-
function buildRenderResult(run, output, renderBlocks = []) {
|
|
2109
|
-
const blocks = [...renderBlocks];
|
|
2110
|
-
let outputBlockIndex;
|
|
2111
|
-
if (output.trim() !== "") {
|
|
2112
|
-
outputBlockIndex = blocks.length;
|
|
2113
|
-
blocks.push({
|
|
2114
|
-
type: "markdown",
|
|
2115
|
-
text: output
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
for (const artifact of run.artifacts) {
|
|
2119
|
-
if (isArtifactCardHidden(artifact)) continue;
|
|
2120
|
-
blocks.push({
|
|
2121
|
-
type: "file",
|
|
2122
|
-
path: artifact.path,
|
|
2123
|
-
...artifact.mimeType ? { mimeType: artifact.mimeType } : {},
|
|
2124
|
-
size: artifact.size
|
|
2125
|
-
});
|
|
2126
|
-
}
|
|
2127
|
-
return {
|
|
2128
|
-
runId: run.id,
|
|
2129
|
-
summary: run.terminationReason,
|
|
2130
|
-
blocks,
|
|
2131
|
-
artifacts: run.artifacts,
|
|
2132
|
-
...outputBlockIndex === void 0 ? {} : { outputBlockIndex }
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
const MAX_SURFACE_BYTES = 256 * 1024;
|
|
2136
|
-
const MAX_ACTIONS = 32;
|
|
2137
|
-
const MAX_NODES = 2e3;
|
|
2138
|
-
const MAX_NODE_DEPTH = 32;
|
|
2139
|
-
const MAX_JSON_DEPTH = 16;
|
|
2140
|
-
const MAX_PATCH_OPERATIONS = 128;
|
|
2141
|
-
const actionIntents = /* @__PURE__ */ new Set([
|
|
2142
|
-
"submit",
|
|
2143
|
-
"cancel",
|
|
2144
|
-
"select",
|
|
2145
|
-
"download",
|
|
2146
|
-
"refresh"
|
|
2147
|
-
]);
|
|
2148
|
-
function isRecord$3(value) {
|
|
2149
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2150
|
-
}
|
|
2151
|
-
function reject(message) {
|
|
2152
|
-
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
2153
|
-
}
|
|
2154
|
-
function requireString(value, name) {
|
|
2155
|
-
if (typeof value !== "string" || value.trim() === "") reject(`${name} must be a non-empty string`);
|
|
2156
|
-
}
|
|
2157
|
-
function requireNonNegativeInteger(value, name) {
|
|
2158
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) reject(`${name} must be a non-negative integer`);
|
|
2159
|
-
}
|
|
2160
|
-
function isJsonValue(value, depth = 0) {
|
|
2161
|
-
if (depth > MAX_JSON_DEPTH || value === void 0 || typeof value === "function" || typeof value === "symbol") return false;
|
|
2162
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
2163
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
2164
|
-
if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
|
|
2165
|
-
if (!isRecord$3(value)) return false;
|
|
2166
|
-
return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Runtime 只做结构校验;组件名白名单与 props schema 属于 catalog 语义,
|
|
2170
|
-
* 留在 `@webskill/ui`——runtime 不得为了校验反向依赖 ui。
|
|
2171
|
-
*/
|
|
2172
|
-
function assertNode(value, path, depth, counter) {
|
|
2173
|
-
if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
|
|
2174
|
-
if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
|
|
2175
|
-
if (!isRecord$3(value)) reject(`${path} must be an object`);
|
|
2176
|
-
requireString(value["component"], `${path}.component`);
|
|
2177
|
-
if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
|
|
2178
|
-
if (value["props"] !== void 0 && (!isRecord$3(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
|
|
2179
|
-
const children = value["children"];
|
|
2180
|
-
if (children === void 0) return;
|
|
2181
|
-
if (!Array.isArray(children)) reject(`${path}.children must be an array`);
|
|
2182
|
-
children.forEach((child, index) => assertNode(child, `${path}.children[${index}]`, depth + 1, counter));
|
|
2183
|
-
}
|
|
2184
|
-
function assertActions(value) {
|
|
2185
|
-
if (value === void 0) return;
|
|
2186
|
-
if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
|
|
2187
|
-
for (const action of value) {
|
|
2188
|
-
if (!isRecord$3(action)) reject("A surface action must be an object");
|
|
2189
|
-
requireString(action["id"], "Surface action ID");
|
|
2190
|
-
const intent = action["intent"];
|
|
2191
|
-
if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
|
|
2192
|
-
if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
|
|
2193
|
-
if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
/** Validates the allowlisted, data-only node tree accepted by a UI surface renderer. @experimental */
|
|
2197
|
-
function validateUiSpecNode(value) {
|
|
2198
|
-
assertNode(value, "root", 0, { nodes: 0 });
|
|
2199
|
-
if (!isJsonValue(value)) reject("A UI spec tree must contain JSON data only");
|
|
2200
|
-
if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI spec tree exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
|
|
2201
|
-
return structuredClone(value);
|
|
2202
|
-
}
|
|
2203
|
-
function assertPatch(value) {
|
|
2204
|
-
if (!isRecord$3(value)) reject("A surface patch operation must be an object");
|
|
2205
|
-
if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
|
|
2206
|
-
requireString(value["path"], "Surface patch path");
|
|
2207
|
-
if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
|
|
2208
|
-
if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
|
|
2209
|
-
}
|
|
2210
|
-
/** Validates an individual event in the framework-neutral surface stream. @experimental */
|
|
2211
|
-
function validateUiSpecEvent(value) {
|
|
2212
|
-
if (!isRecord$3(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
|
|
2213
|
-
if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
|
|
2214
|
-
switch (value["type"]) {
|
|
2215
|
-
case "open":
|
|
2216
|
-
requireString(value["id"], "UI surface ID");
|
|
2217
|
-
assertActions(value["actions"]);
|
|
2218
|
-
return {
|
|
2219
|
-
type: "open",
|
|
2220
|
-
...value["runId"] ? { runId: value["runId"] } : {},
|
|
2221
|
-
id: value["id"],
|
|
2222
|
-
node: validateUiSpecNode(value["node"]),
|
|
2223
|
-
...value["actions"] ? { actions: structuredClone(value["actions"]) } : {}
|
|
2224
|
-
};
|
|
2225
|
-
case "patch":
|
|
2226
|
-
requireString(value["id"], "Surface patch ID");
|
|
2227
|
-
requireNonNegativeInteger(value["revision"], "Surface patch revision");
|
|
2228
|
-
if (!Array.isArray(value["operations"]) || value["operations"].length === 0) reject("A surface patch requires operations");
|
|
2229
|
-
if (value["operations"].length > MAX_PATCH_OPERATIONS) reject(`A surface patch must contain at most ${MAX_PATCH_OPERATIONS} operations`);
|
|
2230
|
-
for (const operation of value["operations"]) assertPatch(operation);
|
|
2231
|
-
return {
|
|
2232
|
-
type: "patch",
|
|
2233
|
-
...value["runId"] ? { runId: value["runId"] } : {},
|
|
2234
|
-
id: value["id"],
|
|
2235
|
-
revision: value["revision"],
|
|
2236
|
-
operations: structuredClone(value["operations"])
|
|
2237
|
-
};
|
|
2238
|
-
case "complete":
|
|
2239
|
-
requireString(value["id"], "Surface completion ID");
|
|
2240
|
-
requireNonNegativeInteger(value["revision"], "Surface completion revision");
|
|
2241
|
-
return {
|
|
2242
|
-
type: "complete",
|
|
2243
|
-
...value["runId"] ? { runId: value["runId"] } : {},
|
|
2244
|
-
id: value["id"],
|
|
2245
|
-
revision: value["revision"]
|
|
2246
|
-
};
|
|
2247
|
-
case "error":
|
|
2248
|
-
requireString(value["id"], "Surface error ID");
|
|
2249
|
-
requireString(value["code"], "Surface error code");
|
|
2250
|
-
requireString(value["message"], "Surface error message");
|
|
2251
|
-
return {
|
|
2252
|
-
type: "error",
|
|
2253
|
-
...value["runId"] ? { runId: value["runId"] } : {},
|
|
2254
|
-
id: value["id"],
|
|
2255
|
-
code: value["code"],
|
|
2256
|
-
message: value["message"]
|
|
2257
|
-
};
|
|
2258
|
-
case "cancel":
|
|
2259
|
-
requireString(value["id"], "Surface cancellation ID");
|
|
2260
|
-
return {
|
|
2261
|
-
type: "cancel",
|
|
2262
|
-
...value["runId"] ? { runId: value["runId"] } : {},
|
|
2263
|
-
id: value["id"]
|
|
2264
|
-
};
|
|
2265
|
-
default: return reject("UI surface event type is invalid");
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
/** Extracts validated surface stream events from structured tool output. @experimental */
|
|
2269
|
-
function extractUiSpecEvents(data) {
|
|
2270
|
-
if (!isRecord$3(data) || data["$surface"] === void 0) return [];
|
|
2271
|
-
const raw = data["$surface"];
|
|
2272
|
-
return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
|
|
2273
|
-
}
|
|
347
|
+
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region ../runtime/src/interaction/schemaToForm.ts
|
|
2274
350
|
/**
|
|
2275
351
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
2276
352
|
* providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
|
|
@@ -2309,6 +385,9 @@ function mapFieldType(prop) {
|
|
|
2309
385
|
default: return "textarea";
|
|
2310
386
|
}
|
|
2311
387
|
}
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region ../runtime/src/lifecycle/eventBus.ts
|
|
2312
391
|
/** 生命周期事件总线:只读观测,支持按阶段或通配订阅 */
|
|
2313
392
|
var EventBus = class {
|
|
2314
393
|
#listeners = /* @__PURE__ */ new Map();
|
|
@@ -2349,6 +428,9 @@ var EventBus = class {
|
|
|
2349
428
|
}
|
|
2350
429
|
}
|
|
2351
430
|
};
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region ../runtime/src/memory/serializingMemoryStore.ts
|
|
2352
434
|
/**
|
|
2353
435
|
* per-scope 串行化 MemoryStore 装饰器:同一 scope 的 get/set/delete/list
|
|
2354
436
|
* 按 promise 链串行(并发 run 的 read-modify-write 不丢计数)。
|
|
@@ -2392,6 +474,9 @@ var SerializingMemoryStore = class {
|
|
|
2392
474
|
return this.#serialize(scope, () => fn(this.#inner));
|
|
2393
475
|
}
|
|
2394
476
|
};
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../runtime/src/profile/types.ts
|
|
2395
480
|
/** @experimental */
|
|
2396
481
|
const DEFAULT_USER_PROFILE_LIMITS = {
|
|
2397
482
|
recordLimit: 500,
|
|
@@ -2406,6 +491,9 @@ const EMPTY_USER_PROFILE = {
|
|
|
2406
491
|
entries: [],
|
|
2407
492
|
refinedAt: 0
|
|
2408
493
|
};
|
|
494
|
+
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region ../runtime/src/profile/inject.ts
|
|
2409
497
|
/** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
|
|
2410
498
|
const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
|
|
2411
499
|
/**
|
|
@@ -2436,6 +524,9 @@ function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAU
|
|
|
2436
524
|
while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
|
|
2437
525
|
return kept === 0 ? "" : render(kept);
|
|
2438
526
|
}
|
|
527
|
+
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region ../runtime/src/profile/records.ts
|
|
2439
530
|
/** `user:{id}` scope 下的行为记录 key @experimental */
|
|
2440
531
|
const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
|
|
2441
532
|
/** `user:{id}` scope 下的画像 key @experimental */
|
|
@@ -2557,59 +648,26 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
|
2557
648
|
for (const record of [...records].reverse()) take(record);
|
|
2558
649
|
return [...picked.values()].sort((a, b) => a.at - b.at);
|
|
2559
650
|
}
|
|
651
|
+
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region ../runtime/src/routing/plainChatPrompt.ts
|
|
2560
654
|
/**
|
|
2561
655
|
* 无工具模型的系统提示(0.6.0 FR-14.3)。
|
|
2562
656
|
* 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
|
|
2563
657
|
*/
|
|
2564
658
|
const PLAIN_CHAT_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's question directly and concisely. You have no tools available; do not describe or simulate tool calls.";
|
|
659
|
+
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region ../runtime/src/routing/onDemandToolsHint.ts
|
|
2565
662
|
/**
|
|
2566
663
|
* 按需披露的系统提示(分册 19 / FR-19.7)。
|
|
2567
664
|
* 只在本 run 确实存在按需工具时追加——没有按需工具却说「有些工具没列出来」,
|
|
2568
665
|
* 是在教模型怀疑一个完整的工具表。
|
|
2569
666
|
*/
|
|
2570
667
|
const ON_DEMAND_TOOLS_HINT = "Some tools are disclosed on demand and are not listed above. If a capability you need seems missing, look for a related skill in the catalog and read its SKILL.md first — activating it may reveal additional tools. Do not conclude the capability does not exist.";
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
* 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
|
|
2575
|
-
*/
|
|
2576
|
-
const SUPPORTED_DOCUMENT_MIME = {
|
|
2577
|
-
pdf: "application/pdf",
|
|
2578
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2579
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
2580
|
-
};
|
|
2581
|
-
/** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
|
|
2582
|
-
const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}), Excel (${SUPPORTED_DOCUMENT_MIME.xlsx}) and plain text documents can be read.`;
|
|
2583
|
-
/**
|
|
2584
|
-
* 内建工具:读页面链接指向的文档。
|
|
2585
|
-
*
|
|
2586
|
-
* **始终注册**(只要宿主装配了 reader):docx / xlsx 与纯文本这条路对所有模型成立。
|
|
2587
|
-
* PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
|
|
2588
|
-
* 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
|
|
2589
|
-
*/
|
|
2590
|
-
const READ_LINKED_DOCUMENT_TOOL = {
|
|
2591
|
-
name: READ_LINKED_DOCUMENT_TOOL_NAME,
|
|
2592
|
-
description: "Read a document linked from the current page. Pass the exact \"href\" from a perceive_page result. Cross-origin documents need the user to confirm each time.",
|
|
2593
|
-
inputSchema: {
|
|
2594
|
-
type: "object",
|
|
2595
|
-
properties: { url: {
|
|
2596
|
-
type: "string",
|
|
2597
|
-
description: "The href of a link that appeared in a perceive_page result."
|
|
2598
|
-
} },
|
|
2599
|
-
required: ["url"]
|
|
2600
|
-
},
|
|
2601
|
-
source: "builtin"
|
|
2602
|
-
};
|
|
2603
|
-
/** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
|
|
2604
|
-
function toBase64(bytes) {
|
|
2605
|
-
let binary = "";
|
|
2606
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2607
|
-
return btoa(binary);
|
|
2608
|
-
}
|
|
2609
|
-
/** `toBase64` 的逆;分片里的文档要交回抽取器时用(0.13.0 FR-22.3) */
|
|
2610
|
-
function fromBase64(data) {
|
|
2611
|
-
return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));
|
|
2612
|
-
}
|
|
668
|
+
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region ../runtime/src/trace/traceRecorder.ts
|
|
2613
671
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
2614
672
|
var TraceRecorder = class {
|
|
2615
673
|
#runId;
|
|
@@ -2636,6 +694,9 @@ var TraceRecorder = class {
|
|
|
2636
694
|
return [...this.#events];
|
|
2637
695
|
}
|
|
2638
696
|
};
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region ../runtime/src/trace/todoMarker.ts
|
|
2639
700
|
const isRecord$2 = (v) => typeof v === "object" && v !== null;
|
|
2640
701
|
/** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
|
|
2641
702
|
const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -2666,6 +727,9 @@ function extractTodoTraceEvents(data) {
|
|
|
2666
727
|
}
|
|
2667
728
|
return events;
|
|
2668
729
|
}
|
|
730
|
+
|
|
731
|
+
//#endregion
|
|
732
|
+
//#region ../runtime/src/skillGeneration/candidateMarker.ts
|
|
2669
733
|
const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
2670
734
|
/**
|
|
2671
735
|
* `$skillCandidate` 约定的形状校验:JSON content 的 data 含合法 `$skillCandidate` 键 → 候选载荷。
|
|
@@ -2685,6 +749,9 @@ function extractSkillCandidate(data) {
|
|
|
2685
749
|
name
|
|
2686
750
|
};
|
|
2687
751
|
}
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region ../runtime/src/engine/limits.ts
|
|
2688
755
|
/**
|
|
2689
756
|
* Agent loop 的运行上限默认值。**全仓唯一来源**(S8)。
|
|
2690
757
|
*
|
|
@@ -2706,6 +773,9 @@ const DEFAULT_LOOP_LIMITS = {
|
|
|
2706
773
|
* @stable
|
|
2707
774
|
*/
|
|
2708
775
|
const MAX_TOOL_STEP_ARG_BYTES = 32768;
|
|
776
|
+
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region ../runtime/src/engine/snapshot.ts
|
|
2709
779
|
const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
2710
780
|
/** @experimental */
|
|
2711
781
|
function isUnsupportedRunSnapshot(entry) {
|
|
@@ -2751,7 +821,7 @@ var FsRunSnapshotStore = class {
|
|
|
2751
821
|
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
2752
822
|
}
|
|
2753
823
|
const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
|
|
2754
|
-
if (schemaVersion !== 3) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version 3. The file was kept for read-only inspection.`);
|
|
824
|
+
if (schemaVersion !== 3) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version ${3}. The file was kept for read-only inspection.`);
|
|
2755
825
|
if (snapshot.runId !== runId) {
|
|
2756
826
|
await this.#fs.remove(path).catch(() => void 0);
|
|
2757
827
|
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
@@ -2803,6 +873,9 @@ var FsRunSnapshotStore = class {
|
|
|
2803
873
|
} catch {}
|
|
2804
874
|
}
|
|
2805
875
|
};
|
|
876
|
+
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region ../runtime/src/engine/toolAccess.ts
|
|
2806
879
|
/**
|
|
2807
880
|
* LLM 可见名 → 作者在 SKILL.md 里写的形态。
|
|
2808
881
|
* `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
|
|
@@ -2872,6 +945,9 @@ function isNamedByActivatedSkill(state, llmToolName) {
|
|
|
2872
945
|
}
|
|
2873
946
|
return false;
|
|
2874
947
|
}
|
|
948
|
+
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region ../runtime/src/engine/toolStepRedaction.ts
|
|
2875
951
|
/**
|
|
2876
952
|
* 参数敏感标注关键字(分册 30 / FR-30.3)。
|
|
2877
953
|
*
|
|
@@ -2935,6 +1011,9 @@ function redactToolStepArgs(args, schema, trust) {
|
|
|
2935
1011
|
redacted
|
|
2936
1012
|
};
|
|
2937
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region ../runtime/src/engine/agentLoop.ts
|
|
2938
1017
|
const MAX_SURFACE_PATCHES_PER_SECOND = 240;
|
|
2939
1018
|
/**
|
|
2940
1019
|
* 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
|
|
@@ -2959,6 +1038,17 @@ const MODEL_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
2959
1038
|
"image/webp",
|
|
2960
1039
|
"image/gif"
|
|
2961
1040
|
]);
|
|
1041
|
+
/**
|
|
1042
|
+
* 数据源清单里的出处标注(0.14.0 分册 21,FR-21.6)。
|
|
1043
|
+
*
|
|
1044
|
+
* 措辞必须说出「这段说明来自页面、可能不准确」:`description` 已在候选层剥掉换行与控制字符,
|
|
1045
|
+
* 它无法把自己伪装成上下文里的新一行指令——**剥换行与这句标注是一对,只做一件都不够**。
|
|
1046
|
+
*/
|
|
1047
|
+
const DATA_SOURCE_PROVENANCE_NOTE = {
|
|
1048
|
+
"": "",
|
|
1049
|
+
"page-declared": " [declared by the current site; this description comes from the page and may be inaccurate]",
|
|
1050
|
+
"tool-projection": " [projected from a tool the current site registered; this description comes from the page and may be inaccurate]"
|
|
1051
|
+
};
|
|
2962
1052
|
/** 分片是不是 PDF;mime 允许带参数与大小写差异,与 FR-11.1 的判定同口径 */
|
|
2963
1053
|
const isPdfPart = (part) => part.type === "file" && part.mimeType.split(";")[0]?.trim().toLowerCase() === SUPPORTED_DOCUMENT_MIME.pdf;
|
|
2964
1054
|
/**
|
|
@@ -4008,7 +2098,7 @@ var AgentLoop = class {
|
|
|
4008
2098
|
const source = resolution.kind === "script" ? void 0 : (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
4009
2099
|
if (source === void 0 && !this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
4010
2100
|
else if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
4011
|
-
else if (source) result = await source.call(call.name, call.arguments);
|
|
2101
|
+
else if (source) result = await source.call(call.name, call.arguments, { runId: state.runId });
|
|
4012
2102
|
else {
|
|
4013
2103
|
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
4014
2104
|
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
@@ -4789,7 +2879,9 @@ var AgentLoop = class {
|
|
|
4789
2879
|
}
|
|
4790
2880
|
const head = text.slice(0, Math.floor(max * .6));
|
|
4791
2881
|
const tail = text.slice(-Math.floor(max * .3));
|
|
4792
|
-
|
|
2882
|
+
const omitted = text.length - head.length - tail.length;
|
|
2883
|
+
state.trace.record("run.warning", { message: `Tool result for "${call.name}" exceeded ${String(max)} chars and was truncated in the middle; ${String(omitted)} chars omitted (${note}).` });
|
|
2884
|
+
return `${head}\n...[truncated ${omitted} chars; ${note}]...\n${tail}`;
|
|
4793
2885
|
}
|
|
4794
2886
|
/** SkillStateGuard 判定(无注入默认全放行;仅显式 false 拦截) */
|
|
4795
2887
|
async #guardDenied(kind, skillName) {
|
|
@@ -4858,6 +2950,7 @@ var AgentLoop = class {
|
|
|
4858
2950
|
if (!root) return "";
|
|
4859
2951
|
let allowedTools;
|
|
4860
2952
|
let dependencies = [];
|
|
2953
|
+
let declaredSources = [];
|
|
4861
2954
|
try {
|
|
4862
2955
|
const { metadata } = parseSkillMarkdown(skillMdText ?? await this.#deps.fs.readText(`${root}/SKILL.md`));
|
|
4863
2956
|
const rawDeps = metadata["dependencies"];
|
|
@@ -4868,6 +2961,9 @@ var AgentLoop = class {
|
|
|
4868
2961
|
state.skillAllowedTools.set(skillName, allowedTools);
|
|
4869
2962
|
this.#warnUnmatchedToolPatterns(state, skillName, allowedTools);
|
|
4870
2963
|
} else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
2964
|
+
const rawSources = metadata["data-sources"];
|
|
2965
|
+
if (rawSources !== void 0) if (Array.isArray(rawSources)) declaredSources = rawSources.filter((e) => typeof e === "string");
|
|
2966
|
+
else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "data-sources" metadata entry; ignored` });
|
|
4871
2967
|
} catch (e) {
|
|
4872
2968
|
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
|
|
4873
2969
|
}
|
|
@@ -4903,6 +2999,7 @@ var AgentLoop = class {
|
|
|
4903
2999
|
}
|
|
4904
3000
|
}
|
|
4905
3001
|
let note = (loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "") + formatSkillScriptManifest(scripts);
|
|
3002
|
+
note += this.#dataSourceNotice(skillName, declaredSources, state);
|
|
4906
3003
|
for (const dep of dependencies) {
|
|
4907
3004
|
if (state.activated.has(dep)) continue;
|
|
4908
3005
|
if (!this.#deps.skillIndex.has(dep)) {
|
|
@@ -4914,6 +3011,26 @@ var AgentLoop = class {
|
|
|
4914
3011
|
return note;
|
|
4915
3012
|
}
|
|
4916
3013
|
/**
|
|
3014
|
+
* 0.14.0 分册 19:技能声明的源在本宿主缺失时,把「缺哪些 / 有哪些」写进激活回执与 trace。
|
|
3015
|
+
* 宿主没注入清单时一个字不说——那是「未知」,不是「没有」。
|
|
3016
|
+
*
|
|
3017
|
+
* 0.14.0 分册 21(DV-7):「有哪些」由 id 列表升级为 `<id> — <description>`,
|
|
3018
|
+
* 并对非宿主声明的条目附出处标注。`description` 可能来自页面,
|
|
3019
|
+
* 因此标注必须与它同时出现——只写说明不写出处,等于让站点以宿主口吻讲话。
|
|
3020
|
+
*/
|
|
3021
|
+
#dataSourceNotice(skillName, declared, state) {
|
|
3022
|
+
const declaredSources = this.#deps.dataSources;
|
|
3023
|
+
const available = typeof declaredSources === "function" ? declaredSources() : declaredSources;
|
|
3024
|
+
if (declared.length === 0 || available === void 0) return "";
|
|
3025
|
+
const have = new Set(available.map((source) => source.id));
|
|
3026
|
+
const missing = declared.filter((id) => !have.has(id));
|
|
3027
|
+
if (missing.length === 0) return "";
|
|
3028
|
+
const listed = available.map((source) => `\n- ${source.id} — ${source.description}${DATA_SOURCE_PROVENANCE_NOTE[source.provenance ?? ""] ?? ""}`).join("");
|
|
3029
|
+
const message = `Skill "${skillName}" declares data source(s) this host does not provide: ${missing.join(", ")}. Available here:${listed || " (none)"}\nDo not retry them or invent their values; tell the user which data source is missing.`;
|
|
3030
|
+
state.trace.record("run.warning", { message });
|
|
3031
|
+
return `\n\n${message}`;
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
4917
3034
|
* D2 Schema 兜底链(同一 run 内激活时只算一次):
|
|
4918
3035
|
* 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
|
|
4919
3036
|
* 返回值即最终来源,进技能清单告诉模型该信谁(FR-18.1)。
|
|
@@ -5009,6 +3126,8 @@ var AgentLoop = class {
|
|
|
5009
3126
|
runId: state.runId,
|
|
5010
3127
|
confirm: (message) => this.#confirm(message, state),
|
|
5011
3128
|
...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
|
|
3129
|
+
...this.#deps.documentSurface ? { documentSurface: true } : {},
|
|
3130
|
+
...this.#deps.uploadFiles ? { uploadFiles: this.#uploadFilePort(this.#deps.uploadFiles, skillName, state) } : {},
|
|
5012
3131
|
onWarning: (message) => state.trace.record("run.warning", { message }),
|
|
5013
3132
|
onArtifactCreated: (artifact) => createdIds.add(artifact.id)
|
|
5014
3133
|
});
|
|
@@ -5106,6 +3225,71 @@ var AgentLoop = class {
|
|
|
5106
3225
|
#nextInteractionId(state) {
|
|
5107
3226
|
return `int-${++state.interactionSeq}`;
|
|
5108
3227
|
}
|
|
3228
|
+
/**
|
|
3229
|
+
* 把宿主端口包成「每次读都要人点头」的形态再交给脚本上下文。
|
|
3230
|
+
* 四个执行器共用这一份,避免各自实现一套把关而出现口径差异。
|
|
3231
|
+
*/
|
|
3232
|
+
#uploadFilePort(source, skillName, state) {
|
|
3233
|
+
return {
|
|
3234
|
+
list: () => source.list(),
|
|
3235
|
+
read: async (id) => {
|
|
3236
|
+
const file = (await source.list()).find((f) => f.id === id);
|
|
3237
|
+
if (!file) throw new WebSkillError("UPLOAD_FILE_NOT_FOUND", `Upload file "${id}" is not part of the message that started this run`);
|
|
3238
|
+
const budget = this.#deps.maxUploadFileBytes;
|
|
3239
|
+
if (budget !== void 0 && file.size > budget) throw new WebSkillError("UPLOAD_FILE_TOO_LARGE", `Upload file "${file.name}" is ${file.size} bytes, over the ${budget} byte limit; it was not read`);
|
|
3240
|
+
const approved = await this.#consentUploadFile(file, skillName, state);
|
|
3241
|
+
await this.#auditUploadFile(file, skillName, approved, state);
|
|
3242
|
+
if (!approved) throw new WebSkillError("TOOL_DENIED", `The user declined to share "${file.name}" with skill "${skillName}"`);
|
|
3243
|
+
return source.read(id);
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* 与 `#confirm` 分开是刻意的:那条通路在没接 UI 桥、桥故障或 confirmations=auto-approve
|
|
3249
|
+
* 时都会自动放行,而本能力的「需批准」是恒定的(分册 17 D-17-3),只能失败即拒。
|
|
3250
|
+
*/
|
|
3251
|
+
async #consentUploadFile(file, skillName, state) {
|
|
3252
|
+
if (!this.#deps.uiBridge) {
|
|
3253
|
+
state.trace.record("run.warning", { message: "No UiBridge configured; upload file read denied" });
|
|
3254
|
+
return false;
|
|
3255
|
+
}
|
|
3256
|
+
try {
|
|
3257
|
+
return await this.#interact(state, {
|
|
3258
|
+
type: "confirm",
|
|
3259
|
+
id: this.#nextInteractionId(state),
|
|
3260
|
+
message: `Let skill "${skillName}" read the file you uploaded: ${file.name} (${file.contentType}, ${file.size} bytes)?`,
|
|
3261
|
+
defaultValue: false
|
|
3262
|
+
}, {
|
|
3263
|
+
kind: "confirm",
|
|
3264
|
+
uploadFile: file.id
|
|
3265
|
+
}) === true;
|
|
3266
|
+
} catch (e) {
|
|
3267
|
+
if (e instanceof BridgeRequestError) {
|
|
3268
|
+
state.trace.record("run.warning", { message: `Upload file consent bridge failed, denied: ${e.message}` });
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
throw e;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
/** 只写文件名 / 类型 / 字节数与是否被拒,**不写内容**(FR-17.7) */
|
|
3275
|
+
async #auditUploadFile(file, skillName, approved, state) {
|
|
3276
|
+
try {
|
|
3277
|
+
await this.#deps.uploadFileAudit?.append({
|
|
3278
|
+
type: "upload.file.read",
|
|
3279
|
+
target: skillName,
|
|
3280
|
+
data: {
|
|
3281
|
+
runId: state.runId,
|
|
3282
|
+
fileId: file.id,
|
|
3283
|
+
fileName: file.name,
|
|
3284
|
+
contentType: file.contentType,
|
|
3285
|
+
bytes: file.size,
|
|
3286
|
+
denied: !approved
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
} catch (e) {
|
|
3290
|
+
state.trace.record("run.warning", { message: `Upload file audit failed: ${messageOf(e)}` });
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
5109
3293
|
async #memoryGet(scope, key, state) {
|
|
5110
3294
|
try {
|
|
5111
3295
|
return await this.#deps.memory?.get(scope, key);
|
|
@@ -5276,15 +3460,9 @@ var AgentLoop = class {
|
|
|
5276
3460
|
};
|
|
5277
3461
|
}
|
|
5278
3462
|
};
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
*/
|
|
5283
|
-
function mergeCatalogEntries(localEntries, providerEntries) {
|
|
5284
|
-
const byName = new Map(localEntries.map((e) => [e.name, e]));
|
|
5285
|
-
for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
5286
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
5287
|
-
}
|
|
3463
|
+
|
|
3464
|
+
//#endregion
|
|
3465
|
+
//#region ../runtime/src/engine/runtime.ts
|
|
5288
3466
|
/**
|
|
5289
3467
|
* session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
|
|
5290
3468
|
* 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
|
|
@@ -5362,11 +3540,12 @@ var WebSkillRuntime = class {
|
|
|
5362
3540
|
return {
|
|
5363
3541
|
id: sessionId,
|
|
5364
3542
|
createdAt,
|
|
5365
|
-
run: (prompt) => {
|
|
3543
|
+
run: (prompt, runOptions = {}) => {
|
|
5366
3544
|
const result = queue.then(async () => {
|
|
5367
3545
|
const r = await this.run(prompt, {
|
|
5368
3546
|
sessionId,
|
|
5369
|
-
history
|
|
3547
|
+
history,
|
|
3548
|
+
...runOptions
|
|
5370
3549
|
});
|
|
5371
3550
|
history = trimSessionHistory(r.messages.slice(1), maxHistory);
|
|
5372
3551
|
return r;
|
|
@@ -5428,11 +3607,16 @@ var WebSkillRuntime = class {
|
|
|
5428
3607
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
5429
3608
|
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
5430
3609
|
fetchData: this.#deps.fetchData,
|
|
3610
|
+
dataSources: this.#deps.dataSources,
|
|
5431
3611
|
linkedDocuments: this.#deps.linkedDocuments,
|
|
5432
3612
|
docxExtractor: this.#deps.docxExtractor,
|
|
5433
3613
|
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
5434
3614
|
pdfExtractor: this.#deps.pdfExtractor,
|
|
5435
|
-
documentAudit: this.#deps.documentAudit
|
|
3615
|
+
documentAudit: this.#deps.documentAudit,
|
|
3616
|
+
documentSurface: this.#deps.documentSurface,
|
|
3617
|
+
uploadFileAudit: this.#deps.uploadFileAudit,
|
|
3618
|
+
maxUploadFileBytes: this.#deps.maxUploadFileBytes,
|
|
3619
|
+
uploadFiles: options.uploadFiles
|
|
5436
3620
|
}, this.#deps.config);
|
|
5437
3621
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
5438
3622
|
this.#loops.set(runId, loop);
|
|
@@ -5556,176 +3740,35 @@ var WebSkillRuntime = class {
|
|
|
5556
3740
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
5557
3741
|
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
5558
3742
|
fetchData: this.#deps.fetchData,
|
|
3743
|
+
dataSources: this.#deps.dataSources,
|
|
5559
3744
|
linkedDocuments: this.#deps.linkedDocuments,
|
|
5560
3745
|
docxExtractor: this.#deps.docxExtractor,
|
|
5561
3746
|
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
5562
|
-
pdfExtractor: this.#deps.pdfExtractor,
|
|
5563
|
-
documentAudit: this.#deps.documentAudit
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
* run 以 cancelled(RUN_CANCELLED)终止;未找到活跃 run 返回 false。
|
|
5575
|
-
* 取消在下一个中断点生效(LLM complete/stream;交互等待不强制中断)。
|
|
5576
|
-
*/
|
|
5577
|
-
cancel(runId) {
|
|
5578
|
-
return this.#loops.get(runId)?.cancel(runId) ?? false;
|
|
5579
|
-
}
|
|
5580
|
-
};
|
|
5581
|
-
/**
|
|
5582
|
-
* navigator.webskill 门面装配层:零新业务逻辑,全部直转
|
|
5583
|
-
* SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
|
|
5584
|
-
* @stable
|
|
5585
|
-
*/
|
|
5586
|
-
function createWebSkillApi(deps) {
|
|
5587
|
-
const { fs, roots } = deps;
|
|
5588
|
-
let runtime;
|
|
5589
|
-
const discoveries = /* @__PURE__ */ new Map();
|
|
5590
|
-
const discoveryFor = (paths) => {
|
|
5591
|
-
const key = paths.join("");
|
|
5592
|
-
let discovery = discoveries.get(key);
|
|
5593
|
-
if (!discovery) {
|
|
5594
|
-
discovery = new SkillDiscovery(fs, paths);
|
|
5595
|
-
discoveries.set(key, discovery);
|
|
5596
|
-
}
|
|
5597
|
-
return discovery;
|
|
5598
|
-
};
|
|
5599
|
-
const lazyRuntime = () => {
|
|
5600
|
-
runtime ??= new WebSkillRuntime({
|
|
5601
|
-
fs,
|
|
5602
|
-
roots,
|
|
5603
|
-
llm: deps.llm,
|
|
5604
|
-
...deps.executor ? { executor: deps.executor } : {},
|
|
5605
|
-
...deps.artifactStore ? { artifactStore: deps.artifactStore } : {},
|
|
5606
|
-
...deps.loopConfig ? { config: deps.loopConfig } : {}
|
|
5607
|
-
});
|
|
5608
|
-
return runtime;
|
|
5609
|
-
};
|
|
5610
|
-
const requireManager = () => {
|
|
5611
|
-
if (!deps.skillManager) throw new WebSkillError("TOOL_UNSUPPORTED", "No skill manager is configured; install/uninstall is unavailable");
|
|
5612
|
-
return deps.skillManager;
|
|
5613
|
-
};
|
|
5614
|
-
return {
|
|
5615
|
-
async discover(path) {
|
|
5616
|
-
const { entries } = await discoveryFor([path]).discover();
|
|
5617
|
-
return buildCatalog(entries);
|
|
5618
|
-
},
|
|
5619
|
-
async read(name) {
|
|
5620
|
-
const { entries } = await discoveryFor(roots).discover();
|
|
5621
|
-
const entry = entries.find((e) => e.name === name);
|
|
5622
|
-
if (!entry) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${name}`);
|
|
5623
|
-
const skillMdPath = `${entry.root}/SKILL.md`;
|
|
5624
|
-
const { metadata, body } = parseSkillMarkdown(await fs.readText(skillMdPath));
|
|
5625
|
-
return {
|
|
5626
|
-
metadata,
|
|
5627
|
-
body,
|
|
5628
|
-
location: {
|
|
5629
|
-
skillRoot: entry.root,
|
|
5630
|
-
skillMdPath,
|
|
5631
|
-
source: entry.source
|
|
5632
|
-
}
|
|
5633
|
-
};
|
|
5634
|
-
},
|
|
5635
|
-
validate(path) {
|
|
5636
|
-
return validateSkills(fs, [path]);
|
|
5637
|
-
},
|
|
5638
|
-
async run(prompt) {
|
|
5639
|
-
return (await lazyRuntime().run(prompt)).run;
|
|
5640
|
-
},
|
|
5641
|
-
async install(url) {
|
|
5642
|
-
const manifest = await requireManager().install(sourceFromUrl(url));
|
|
5643
|
-
return {
|
|
5644
|
-
name: manifest.name,
|
|
5645
|
-
...manifest.version !== void 0 ? { version: manifest.version } : {},
|
|
5646
|
-
installedAt: manifest.installedAt,
|
|
5647
|
-
integrity: manifest.integrity
|
|
5648
|
-
};
|
|
5649
|
-
},
|
|
5650
|
-
async uninstall(name) {
|
|
5651
|
-
await requireManager().uninstall(name);
|
|
5652
|
-
}
|
|
5653
|
-
};
|
|
5654
|
-
}
|
|
5655
|
-
/** install URL 分派:http(s) .zip → http;.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);
|
|
5656
|
-
* 其余 http(s) URL → INSTALL_FAILED(不静默当 local);非 URL → local */
|
|
5657
|
-
function sourceFromUrl(url) {
|
|
5658
|
-
const bare = url.split("?")[0] ?? url;
|
|
5659
|
-
if (/^https?:\/\//.test(url)) {
|
|
5660
|
-
if (bare.endsWith(".zip")) return {
|
|
5661
|
-
type: "http",
|
|
5662
|
-
url
|
|
5663
|
-
};
|
|
5664
|
-
if (bare.endsWith(".git")) return {
|
|
5665
|
-
type: "git",
|
|
5666
|
-
url
|
|
5667
|
-
};
|
|
5668
|
-
throw new WebSkillError("INSTALL_FAILED", `Unsupported install URL "${url}" (supported: http(s) URLs ending in .zip or .git, or local paths)`);
|
|
5669
|
-
}
|
|
5670
|
-
if (bare.endsWith(".git")) return {
|
|
5671
|
-
type: "git",
|
|
5672
|
-
url
|
|
5673
|
-
};
|
|
5674
|
-
return {
|
|
5675
|
-
type: "local",
|
|
5676
|
-
path: url
|
|
5677
|
-
};
|
|
5678
|
-
}
|
|
5679
|
-
/** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
|
|
5680
|
-
var HookRunner = class {
|
|
5681
|
-
#hooks = /* @__PURE__ */ new Map();
|
|
5682
|
-
/** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
|
|
5683
|
-
timeoutMs;
|
|
5684
|
-
failOnHookError;
|
|
5685
|
-
onWarning;
|
|
5686
|
-
constructor(options = {}) {
|
|
5687
|
-
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
5688
|
-
this.failOnHookError = options.failOnHookError ?? false;
|
|
5689
|
-
this.onWarning = options.onWarning;
|
|
5690
|
-
}
|
|
5691
|
-
register(phase, hook) {
|
|
5692
|
-
const key = phase;
|
|
5693
|
-
const list = this.#hooks.get(key) ?? [];
|
|
5694
|
-
list.push(hook);
|
|
5695
|
-
this.#hooks.set(key, list);
|
|
5696
|
-
return this;
|
|
3747
|
+
pdfExtractor: this.#deps.pdfExtractor,
|
|
3748
|
+
documentAudit: this.#deps.documentAudit,
|
|
3749
|
+
documentSurface: this.#deps.documentSurface,
|
|
3750
|
+
uploadFileAudit: this.#deps.uploadFileAudit,
|
|
3751
|
+
maxUploadFileBytes: this.#deps.maxUploadFileBytes
|
|
3752
|
+
}, this.#deps.config);
|
|
3753
|
+
this.#loops.set(runId, loop);
|
|
3754
|
+
try {
|
|
3755
|
+
return await loop.resume(snapshot);
|
|
3756
|
+
} finally {
|
|
3757
|
+
this.#loops.delete(runId);
|
|
3758
|
+
}
|
|
5697
3759
|
}
|
|
5698
3760
|
/**
|
|
5699
|
-
*
|
|
5700
|
-
*
|
|
3761
|
+
* 取消进行中的 run(chatbot Stop 按钮等):触发该 run 的 AbortController,
|
|
3762
|
+
* run 以 cancelled(RUN_CANCELLED)终止;未找到活跃 run 返回 false。
|
|
3763
|
+
* 取消在下一个中断点生效(LLM complete/stream;交互等待不强制中断)。
|
|
5701
3764
|
*/
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
|
|
5705
|
-
return counts;
|
|
5706
|
-
}
|
|
5707
|
-
async run(phase, ctx) {
|
|
5708
|
-
const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
|
|
5709
|
-
for (const hook of hooks) try {
|
|
5710
|
-
await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
|
|
5711
|
-
} catch (e) {
|
|
5712
|
-
if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf(e), e);
|
|
5713
|
-
this.onWarning?.(messageOf(e));
|
|
5714
|
-
}
|
|
5715
|
-
}
|
|
5716
|
-
#withTimeout(promise, phase) {
|
|
5717
|
-
return new Promise((resolve, reject) => {
|
|
5718
|
-
const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
|
|
5719
|
-
promise.then((v) => {
|
|
5720
|
-
clearTimeout(timer);
|
|
5721
|
-
resolve(v);
|
|
5722
|
-
}, (e) => {
|
|
5723
|
-
clearTimeout(timer);
|
|
5724
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
5725
|
-
});
|
|
5726
|
-
});
|
|
3765
|
+
cancel(runId) {
|
|
3766
|
+
return this.#loops.get(runId)?.cancel(runId) ?? false;
|
|
5727
3767
|
}
|
|
5728
3768
|
};
|
|
3769
|
+
|
|
3770
|
+
//#endregion
|
|
3771
|
+
//#region ../runtime/src/memory/fsMemoryStore.ts
|
|
5729
3772
|
const encode = (s) => encodeURIComponent(s);
|
|
5730
3773
|
/**
|
|
5731
3774
|
* 基于 FileSystemProvider 的 MemoryStore:
|
|
@@ -5808,191 +3851,10 @@ var FsMemoryStore = class {
|
|
|
5808
3851
|
for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
|
|
5809
3852
|
}
|
|
5810
3853
|
};
|
|
5811
|
-
/**
|
|
5812
|
-
* 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
|
|
5813
|
-
* `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
|
|
5814
|
-
*/
|
|
5815
|
-
const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
|
|
5816
|
-
|
|
5817
|
-
Return ONLY a JSON object with this shape, and nothing else:
|
|
5818
|
-
|
|
5819
|
-
{
|
|
5820
|
-
"entries": [
|
|
5821
|
-
{ "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
|
|
5822
|
-
]
|
|
5823
|
-
}
|
|
5824
3854
|
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
- Use "high" only when several records agree. A single record is "low".
|
|
5829
|
-
- Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
|
|
5830
|
-
- Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
|
|
5831
|
-
/** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
|
|
5832
|
-
function toRecordDigests(records) {
|
|
5833
|
-
return records.map((record) => ({
|
|
5834
|
-
kind: record.kind,
|
|
5835
|
-
at: new Date(record.at).toISOString(),
|
|
5836
|
-
value: record.value,
|
|
5837
|
-
...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
|
|
5838
|
-
...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
|
|
5839
|
-
...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
|
|
5840
|
-
...record.scene.options !== void 0 ? { options: record.scene.options } : {},
|
|
5841
|
-
...record.confirmed === true ? { confirmed: true } : {}
|
|
5842
|
-
}));
|
|
5843
|
-
}
|
|
5844
|
-
/** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
|
|
5845
|
-
function parseEntries(content, now) {
|
|
5846
|
-
const text = promptText(content ?? []);
|
|
5847
|
-
const start = text.indexOf("{");
|
|
5848
|
-
const end = text.lastIndexOf("}");
|
|
5849
|
-
if (start < 0 || end <= start) return [];
|
|
5850
|
-
let parsed;
|
|
5851
|
-
try {
|
|
5852
|
-
parsed = JSON.parse(text.slice(start, end + 1));
|
|
5853
|
-
} catch {
|
|
5854
|
-
return [];
|
|
5855
|
-
}
|
|
5856
|
-
const raw = parsed?.entries;
|
|
5857
|
-
return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
|
|
5858
|
-
updatedAt: now,
|
|
5859
|
-
...item
|
|
5860
|
-
})) : []);
|
|
5861
|
-
}
|
|
5862
|
-
/**
|
|
5863
|
-
* 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
|
|
5864
|
-
* 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
|
|
5865
|
-
* 在这里吞掉会让手动提炼入口无法报错。
|
|
5866
|
-
* @experimental
|
|
5867
|
-
*/
|
|
5868
|
-
async function refineUserProfile(input) {
|
|
5869
|
-
const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
5870
|
-
const now = (input.now ?? Date.now)();
|
|
5871
|
-
const current = input.profile ?? EMPTY_USER_PROFILE;
|
|
5872
|
-
const sampled = sampleBehaviorRecords(input.records, limits);
|
|
5873
|
-
if (sampled.length === 0) return current;
|
|
5874
|
-
const existing = current.entries.map((entry) => ({
|
|
5875
|
-
id: entry.id,
|
|
5876
|
-
text: entry.text
|
|
5877
|
-
}));
|
|
5878
|
-
const entries = parseEntries((await input.llm.complete({
|
|
5879
|
-
...input.model !== void 0 ? { model: input.model } : {},
|
|
5880
|
-
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
5881
|
-
messages: [{
|
|
5882
|
-
role: "system",
|
|
5883
|
-
content: [{
|
|
5884
|
-
type: "text",
|
|
5885
|
-
text: USER_PROFILE_REFINE_PROMPT
|
|
5886
|
-
}]
|
|
5887
|
-
}, {
|
|
5888
|
-
role: "user",
|
|
5889
|
-
content: [{
|
|
5890
|
-
type: "text",
|
|
5891
|
-
text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
|
|
5892
|
-
}]
|
|
5893
|
-
}]
|
|
5894
|
-
})).content, now);
|
|
5895
|
-
if (entries.length === 0) return current;
|
|
5896
|
-
return mergeProfileEntries(current, entries, {
|
|
5897
|
-
now,
|
|
5898
|
-
limits
|
|
5899
|
-
});
|
|
5900
|
-
}
|
|
5901
|
-
/** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
|
|
5902
|
-
const USER_PROFILE_EXPORT_VERSION = 1;
|
|
5903
|
-
/** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
|
|
5904
|
-
const CREDENTIAL_MARKERS = [
|
|
5905
|
-
"apikey",
|
|
5906
|
-
"secret",
|
|
5907
|
-
"token",
|
|
5908
|
-
"password",
|
|
5909
|
-
"passphrase",
|
|
5910
|
-
"credential",
|
|
5911
|
-
"privatekey"
|
|
5912
|
-
];
|
|
5913
|
-
const isCredentialKey = (key) => {
|
|
5914
|
-
const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
|
|
5915
|
-
return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
|
|
5916
|
-
};
|
|
5917
|
-
function findCredentialKey(value) {
|
|
5918
|
-
if (Array.isArray(value)) {
|
|
5919
|
-
for (const item of value) {
|
|
5920
|
-
const hit = findCredentialKey(item);
|
|
5921
|
-
if (hit !== void 0) return hit;
|
|
5922
|
-
}
|
|
5923
|
-
return;
|
|
5924
|
-
}
|
|
5925
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
5926
|
-
for (const [key, child] of Object.entries(value)) {
|
|
5927
|
-
if (isCredentialKey(key)) return key;
|
|
5928
|
-
const hit = findCredentialKey(child);
|
|
5929
|
-
if (hit !== void 0) return hit;
|
|
5930
|
-
}
|
|
5931
|
-
}
|
|
5932
|
-
/**
|
|
5933
|
-
* 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
|
|
5934
|
-
* @experimental
|
|
5935
|
-
*/
|
|
5936
|
-
function exportUserProfile(profile, options) {
|
|
5937
|
-
const excluded = new Set(options.excludeIds ?? []);
|
|
5938
|
-
const result = {
|
|
5939
|
-
kind: "webskill.user-profile",
|
|
5940
|
-
version: 1,
|
|
5941
|
-
origin: options.origin,
|
|
5942
|
-
exportedAt: (options.now ?? Date.now)(),
|
|
5943
|
-
entries: profile.entries.filter((entry) => !excluded.has(entry.id))
|
|
5944
|
-
};
|
|
5945
|
-
const leaked = findCredentialKey(result);
|
|
5946
|
-
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
|
|
5947
|
-
return result;
|
|
5948
|
-
}
|
|
5949
|
-
/** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
|
|
5950
|
-
function parseUserProfileExport(raw) {
|
|
5951
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
|
|
5952
|
-
const record = raw;
|
|
5953
|
-
if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
|
|
5954
|
-
if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
|
|
5955
|
-
const leaked = findCredentialKey(record);
|
|
5956
|
-
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
|
|
5957
|
-
const entries = readProfileEntries(record["entries"]);
|
|
5958
|
-
if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
|
|
5959
|
-
return {
|
|
5960
|
-
kind: "webskill.user-profile",
|
|
5961
|
-
version: 1,
|
|
5962
|
-
origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
|
|
5963
|
-
exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
|
|
5964
|
-
entries
|
|
5965
|
-
};
|
|
5966
|
-
}
|
|
5967
|
-
function diffUserProfile(current, incoming) {
|
|
5968
|
-
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
5969
|
-
const added = [];
|
|
5970
|
-
const updated = [];
|
|
5971
|
-
for (const entry of incoming.entries) {
|
|
5972
|
-
const existing = byId.get(entry.id);
|
|
5973
|
-
if (existing === void 0) added.push(entry);
|
|
5974
|
-
else if (existing.text !== entry.text) updated.push(entry);
|
|
5975
|
-
}
|
|
5976
|
-
return {
|
|
5977
|
-
added,
|
|
5978
|
-
updated
|
|
5979
|
-
};
|
|
5980
|
-
}
|
|
5981
|
-
/** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
|
|
5982
|
-
function applyUserProfileImport(current, incoming, options = {}) {
|
|
5983
|
-
const now = (options.now ?? Date.now)();
|
|
5984
|
-
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
5985
|
-
for (const entry of incoming.entries) byId.set(entry.id, {
|
|
5986
|
-
...entry,
|
|
5987
|
-
updatedAt: now
|
|
5988
|
-
});
|
|
5989
|
-
return {
|
|
5990
|
-
version: 1,
|
|
5991
|
-
entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
|
|
5992
|
-
refinedAt: current.refinedAt
|
|
5993
|
-
};
|
|
5994
|
-
}
|
|
5995
|
-
const INDEX_FILE$1 = "index.json";
|
|
3855
|
+
//#endregion
|
|
3856
|
+
//#region ../runtime/src/artifacts/fsArtifactStore.ts
|
|
3857
|
+
const INDEX_FILE = "index.json";
|
|
5996
3858
|
/**
|
|
5997
3859
|
* 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
|
|
5998
3860
|
* 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
|
|
@@ -6026,7 +3888,7 @@ var FsArtifactStore = class {
|
|
|
6026
3888
|
}
|
|
6027
3889
|
async listArtifacts(runId) {
|
|
6028
3890
|
assertSafePathSegment(runId, "runId");
|
|
6029
|
-
const indexPath = `${this.#root}/${runId}/${INDEX_FILE
|
|
3891
|
+
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
6030
3892
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
6031
3893
|
const raw = await this.#fs.readText(indexPath);
|
|
6032
3894
|
try {
|
|
@@ -6059,10 +3921,13 @@ var FsArtifactStore = class {
|
|
|
6059
3921
|
metadata: input.metadata
|
|
6060
3922
|
};
|
|
6061
3923
|
const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
|
|
6062
|
-
await atomicWriteText(this.#fs, `${this.#root}/${input.runId}/${INDEX_FILE
|
|
3924
|
+
await atomicWriteText(this.#fs, `${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
|
|
6063
3925
|
return artifact;
|
|
6064
3926
|
}
|
|
6065
3927
|
};
|
|
3928
|
+
|
|
3929
|
+
//#endregion
|
|
3930
|
+
//#region ../runtime/src/sandbox/bridgeProtocol.ts
|
|
6066
3931
|
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
6067
3932
|
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
6068
3933
|
/** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
|
|
@@ -6096,6 +3961,15 @@ function parseBridgeRequest(data) {
|
|
|
6096
3961
|
id,
|
|
6097
3962
|
message: data["message"]
|
|
6098
3963
|
} : void 0;
|
|
3964
|
+
case "listUploadFiles": return {
|
|
3965
|
+
kind,
|
|
3966
|
+
id
|
|
3967
|
+
};
|
|
3968
|
+
case "readUploadFile": return isNonEmptyString(data["fileId"]) ? {
|
|
3969
|
+
kind,
|
|
3970
|
+
id,
|
|
3971
|
+
fileId: data["fileId"]
|
|
3972
|
+
} : void 0;
|
|
6099
3973
|
case "fetchData": {
|
|
6100
3974
|
if (!isNonEmptyString(data["sourceId"])) return void 0;
|
|
6101
3975
|
const params = data["params"];
|
|
@@ -6120,6 +3994,14 @@ function bridgeError(id, code, message) {
|
|
|
6120
3994
|
};
|
|
6121
3995
|
}
|
|
6122
3996
|
/**
|
|
3997
|
+
* 上传文件能力缺席时四个执行器共用的说辞(分册 17 FR-17.6):
|
|
3998
|
+
* 讲清「宿主没接 / 开关没开」,与「用户拒了这一次」(TOOL_DENIED)分得开。
|
|
3999
|
+
*/
|
|
4000
|
+
const UPLOAD_FILES_UNAVAILABLE = "Capability \"uploadFiles\" is unavailable: this host did not wire access to the files you uploaded, or the sandbox setting for it is off. This is not the same as the read being declined.";
|
|
4001
|
+
|
|
4002
|
+
//#endregion
|
|
4003
|
+
//#region ../runtime/src/sandbox/networkPolicy.ts
|
|
4004
|
+
/**
|
|
6123
4005
|
* 判定 URL 是否被策略放行:
|
|
6124
4006
|
* - 'deny-all'(默认)全拒;'allow-all' 全放
|
|
6125
4007
|
* - { allow } 条目支持三种写法:
|
|
@@ -6180,6 +4062,9 @@ function networkUrlHost(url) {
|
|
|
6180
4062
|
function networkPolicyLibSource() {
|
|
6181
4063
|
return `var isNetworkAllowed = ${isNetworkAllowed.toString()};\nvar networkUrlHost = ${networkUrlHost.toString()};`;
|
|
6182
4064
|
}
|
|
4065
|
+
|
|
4066
|
+
//#endregion
|
|
4067
|
+
//#region ../runtime/src/sandbox/errorCodes.ts
|
|
6183
4068
|
/** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
|
|
6184
4069
|
const WHITELIST = /* @__PURE__ */ new Set([
|
|
6185
4070
|
"FS_NOT_FOUND",
|
|
@@ -6241,6 +4126,9 @@ function normalizeToolError(code, message) {
|
|
|
6241
4126
|
message: `${message} [original code: ${String(code)}]`
|
|
6242
4127
|
};
|
|
6243
4128
|
}
|
|
4129
|
+
|
|
4130
|
+
//#endregion
|
|
4131
|
+
//#region ../runtime/src/sandbox/approval.ts
|
|
6244
4132
|
/**
|
|
6245
4133
|
* Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
|
|
6246
4134
|
* 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
|
|
@@ -6291,551 +4179,6 @@ var CapabilityApproval = class CapabilityApproval {
|
|
|
6291
4179
|
return "allowed";
|
|
6292
4180
|
}
|
|
6293
4181
|
};
|
|
6294
|
-
/**
|
|
6295
|
-
* 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
|
|
6296
|
-
* 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
|
|
6297
|
-
* @stable
|
|
6298
|
-
*/
|
|
6299
|
-
function summarizeRunUsage(trace) {
|
|
6300
|
-
let llmCalls = 0;
|
|
6301
|
-
let inputTokens = 0;
|
|
6302
|
-
let outputTokens = 0;
|
|
6303
|
-
let hasUsage = false;
|
|
6304
|
-
for (const event of trace) {
|
|
6305
|
-
if (event.type === "llm.request") {
|
|
6306
|
-
llmCalls += 1;
|
|
6307
|
-
continue;
|
|
6308
|
-
}
|
|
6309
|
-
if (event.type !== "llm.usage") continue;
|
|
6310
|
-
const input = event.data?.["inputTokens"];
|
|
6311
|
-
const output = event.data?.["outputTokens"];
|
|
6312
|
-
const hasInput = typeof input === "number" && Number.isFinite(input);
|
|
6313
|
-
const hasOutput = typeof output === "number" && Number.isFinite(output);
|
|
6314
|
-
if (hasInput) inputTokens += input;
|
|
6315
|
-
if (hasOutput) outputTokens += output;
|
|
6316
|
-
if (hasInput || hasOutput) hasUsage = true;
|
|
6317
|
-
}
|
|
6318
|
-
return {
|
|
6319
|
-
llmCalls,
|
|
6320
|
-
...hasUsage ? {
|
|
6321
|
-
inputTokens,
|
|
6322
|
-
outputTokens
|
|
6323
|
-
} : {}
|
|
6324
|
-
};
|
|
6325
|
-
}
|
|
6326
|
-
const RUN_TRACE_SCHEMA_VERSION = 1;
|
|
6327
|
-
/** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
|
|
6328
|
-
function extractEndReason(events) {
|
|
6329
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
6330
|
-
const event = events[i];
|
|
6331
|
-
if (event.type === "run.completed" || event.type === "run.cancelled" || event.type === "run.failed") {
|
|
6332
|
-
const reason = event.data?.["reason"];
|
|
6333
|
-
return typeof reason === "string" ? reason : void 0;
|
|
6334
|
-
}
|
|
6335
|
-
}
|
|
6336
|
-
}
|
|
6337
|
-
function summarize(trace) {
|
|
6338
|
-
const endReason = extractEndReason(trace.events);
|
|
6339
|
-
const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
|
|
6340
|
-
const runUsage = summarizeRunUsage(trace.events);
|
|
6341
|
-
return {
|
|
6342
|
-
runId: trace.runId,
|
|
6343
|
-
startedAt: trace.startedAt,
|
|
6344
|
-
status: trace.status,
|
|
6345
|
-
activeSkills: trace.activeSkills,
|
|
6346
|
-
eventCount: trace.events.length,
|
|
6347
|
-
turnCount: trace.events.filter((e) => e.type === "llm.request").length,
|
|
6348
|
-
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
6349
|
-
...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
|
|
6350
|
-
...endReason !== void 0 ? { endReason } : {},
|
|
6351
|
-
...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {},
|
|
6352
|
-
...runUsage.inputTokens !== void 0 ? { usage: {
|
|
6353
|
-
inputTokens: runUsage.inputTokens,
|
|
6354
|
-
outputTokens: runUsage.outputTokens ?? 0
|
|
6355
|
-
} } : {}
|
|
6356
|
-
};
|
|
6357
|
-
}
|
|
6358
|
-
function parseTraceFile(raw, path) {
|
|
6359
|
-
let data;
|
|
6360
|
-
try {
|
|
6361
|
-
data = JSON.parse(raw);
|
|
6362
|
-
} catch {
|
|
6363
|
-
return;
|
|
6364
|
-
}
|
|
6365
|
-
if (typeof data !== "object" || data === null) return void 0;
|
|
6366
|
-
if (typeof data.runId !== "string" || typeof data.startedAt !== "string") return void 0;
|
|
6367
|
-
const schemaVersion = typeof data.schemaVersion === "number" ? data.schemaVersion : 0;
|
|
6368
|
-
if (schemaVersion > 1) throw new WebSkillError("RUN_TRACE_INCOMPATIBLE", `Run trace "${path}" declares schemaVersion ${schemaVersion}, which is newer than the supported 1`);
|
|
6369
|
-
return {
|
|
6370
|
-
schemaVersion,
|
|
6371
|
-
runId: data.runId,
|
|
6372
|
-
...typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {},
|
|
6373
|
-
startedAt: data.startedAt,
|
|
6374
|
-
...typeof data.endedAt === "string" ? { endedAt: data.endedAt } : {},
|
|
6375
|
-
status: typeof data.status === "string" ? data.status : "unknown",
|
|
6376
|
-
activeSkills: Array.isArray(data.activeSkills) ? data.activeSkills.filter((s) => typeof s === "string") : [],
|
|
6377
|
-
events: Array.isArray(data.events) ? data.events : []
|
|
6378
|
-
};
|
|
6379
|
-
}
|
|
6380
|
-
const INDEX_FILE = "index.jsonl";
|
|
6381
|
-
/**
|
|
6382
|
-
* `FileSystemProvider` 后端:`<root>/<runId>.json` 存全量 trace,
|
|
6383
|
-
* `<root>/index.jsonl` 是 append-only 的摘要旁路索引。
|
|
6384
|
-
*
|
|
6385
|
-
* 索引存在的唯一理由是**读次数**:0.0.1 的实现为了拼一份列表要整读每个 trace
|
|
6386
|
-
* 文件,点一次指标再来一遍。有了索引,`list()` 与 `metrics()` 各只读一个文件。
|
|
6387
|
-
*
|
|
6388
|
-
* 索引与 trace 目录发散(写完 trace 崩在追加前)的检测不引入新的读放大:
|
|
6389
|
-
* 目录列举本来就要做,比较 `*.json` 个数与索引行数即可;不等则整体重建。
|
|
6390
|
-
* @stable
|
|
6391
|
-
*/
|
|
6392
|
-
var FsRunTraceStore = class {
|
|
6393
|
-
#root;
|
|
6394
|
-
#fs;
|
|
6395
|
-
#onError;
|
|
6396
|
-
#onIndexError;
|
|
6397
|
-
constructor(deps) {
|
|
6398
|
-
this.#root = deps.root.replace(/\/+$/, "");
|
|
6399
|
-
this.#fs = deps.fs;
|
|
6400
|
-
this.#onError = deps.onError ?? ((error, run) => {
|
|
6401
|
-
console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
|
|
6402
|
-
});
|
|
6403
|
-
this.#onIndexError = deps.onIndexError ?? ((error, run) => {
|
|
6404
|
-
console.warn(`Run trace "${run.id}" was saved but its index entry could not be appended: ${messageOf(error)}. The index will be rebuilt on the next read.`);
|
|
6405
|
-
});
|
|
6406
|
-
}
|
|
6407
|
-
#path(runId) {
|
|
6408
|
-
return resolveInsideRoot(this.#root, `${runId}.json`);
|
|
6409
|
-
}
|
|
6410
|
-
async put(run) {
|
|
6411
|
-
const trace = {
|
|
6412
|
-
schemaVersion: 1,
|
|
6413
|
-
runId: run.id,
|
|
6414
|
-
sessionId: run.sessionId,
|
|
6415
|
-
startedAt: run.startedAt,
|
|
6416
|
-
...run.endedAt !== void 0 ? { endedAt: run.endedAt } : {},
|
|
6417
|
-
status: run.status === "completed" || run.status === "cancelled" ? run.status : "failed",
|
|
6418
|
-
activeSkills: run.activeSkillNames,
|
|
6419
|
-
events: run.trace
|
|
6420
|
-
};
|
|
6421
|
-
try {
|
|
6422
|
-
await atomicWriteText(this.#fs, this.#path(run.id), JSON.stringify(trace, null, 2));
|
|
6423
|
-
} catch (e) {
|
|
6424
|
-
this.#onError(e, run);
|
|
6425
|
-
return;
|
|
6426
|
-
}
|
|
6427
|
-
try {
|
|
6428
|
-
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
6429
|
-
} catch (e) {
|
|
6430
|
-
this.#onIndexError(e, run);
|
|
6431
|
-
}
|
|
6432
|
-
}
|
|
6433
|
-
async get(runId) {
|
|
6434
|
-
assertSafePathSegment(runId, "run id");
|
|
6435
|
-
const path = this.#path(runId);
|
|
6436
|
-
let raw;
|
|
6437
|
-
try {
|
|
6438
|
-
raw = await this.#fs.readText(path);
|
|
6439
|
-
} catch (e) {
|
|
6440
|
-
if (e instanceof WebSkillError) throw e;
|
|
6441
|
-
throw new WebSkillError("FS_NOT_FOUND", `Trace not found for run ${JSON.stringify(runId)}: ${path}`);
|
|
6442
|
-
}
|
|
6443
|
-
const trace = parseTraceFile(raw, path);
|
|
6444
|
-
if (!trace) throw new WebSkillError("VALIDATION_FAILED", `Trace file is corrupted or malformed: ${path}`);
|
|
6445
|
-
return trace;
|
|
6446
|
-
}
|
|
6447
|
-
async list(filter = {}) {
|
|
6448
|
-
const { summaries } = await this.#readIndex();
|
|
6449
|
-
const matched = applyFilter(summaries, filter);
|
|
6450
|
-
const offset = Math.max(0, filter.offset ?? 0);
|
|
6451
|
-
const sliced = matched.slice(offset);
|
|
6452
|
-
return filter.limit !== void 0 ? sliced.slice(0, Math.max(0, filter.limit)) : sliced;
|
|
6453
|
-
}
|
|
6454
|
-
async metrics(filter = {}) {
|
|
6455
|
-
const { summaries, skipped } = await this.#readIndex();
|
|
6456
|
-
const runs = applyFilter(summaries, filter);
|
|
6457
|
-
const succeeded = runs.filter((r) => r.status === "completed").length;
|
|
6458
|
-
const failedRuns = runs.filter((r) => r.status === "failed");
|
|
6459
|
-
const durations = runs.map((r) => r.durationMs).filter((ms) => ms !== void 0);
|
|
6460
|
-
return {
|
|
6461
|
-
totalRuns: runs.length,
|
|
6462
|
-
succeeded,
|
|
6463
|
-
failed: failedRuns.length,
|
|
6464
|
-
successRate: runs.length === 0 ? 0 : succeeded / runs.length,
|
|
6465
|
-
avgTurns: runs.length === 0 ? 0 : runs.reduce((sum, r) => sum + r.turnCount, 0) / runs.length,
|
|
6466
|
-
skippedRuns: skipped,
|
|
6467
|
-
avgDurationMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
6468
|
-
recentFailures: failedRuns.slice(0, 5)
|
|
6469
|
-
};
|
|
6470
|
-
}
|
|
6471
|
-
/** 健康路径恒为「一次目录列举 + 一次索引读」,与 run 数无关。 */
|
|
6472
|
-
async #readIndex() {
|
|
6473
|
-
if (!await this.#fs.exists(this.#root)) return {
|
|
6474
|
-
summaries: [],
|
|
6475
|
-
skipped: 0
|
|
6476
|
-
};
|
|
6477
|
-
const traceCount = (await this.#fs.list(this.#root)).filter((f) => f.type === "file" && f.path.endsWith(".json")).length;
|
|
6478
|
-
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
6479
|
-
let lines = [];
|
|
6480
|
-
if (await this.#fs.exists(indexPath)) lines = (await this.#fs.readText(indexPath)).split("\n").filter((line) => line.trim() !== "");
|
|
6481
|
-
if (lines.length !== traceCount) return {
|
|
6482
|
-
summaries: await this.#rebuildIndex(),
|
|
6483
|
-
skipped: 0
|
|
6484
|
-
};
|
|
6485
|
-
const summaries = [];
|
|
6486
|
-
let skipped = 0;
|
|
6487
|
-
for (const line of lines) try {
|
|
6488
|
-
summaries.push(JSON.parse(line));
|
|
6489
|
-
} catch {
|
|
6490
|
-
skipped += 1;
|
|
6491
|
-
}
|
|
6492
|
-
if (skipped > 0) return {
|
|
6493
|
-
summaries: await this.#rebuildIndex(),
|
|
6494
|
-
skipped
|
|
6495
|
-
};
|
|
6496
|
-
return {
|
|
6497
|
-
summaries: sortByStartedAtDesc(summaries),
|
|
6498
|
-
skipped: 0
|
|
6499
|
-
};
|
|
6500
|
-
}
|
|
6501
|
-
/** 异常路径,允许 O(N):整读全部 trace 文件后用 temp + rename 原子替换索引 */
|
|
6502
|
-
async #rebuildIndex() {
|
|
6503
|
-
const summaries = [];
|
|
6504
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
6505
|
-
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
6506
|
-
let trace;
|
|
6507
|
-
try {
|
|
6508
|
-
trace = parseTraceFile(await this.#fs.readText(entry.path), entry.path);
|
|
6509
|
-
} catch {
|
|
6510
|
-
continue;
|
|
6511
|
-
}
|
|
6512
|
-
if (!trace) continue;
|
|
6513
|
-
summaries.push(summarize(trace));
|
|
6514
|
-
}
|
|
6515
|
-
const sorted = sortByStartedAtDesc(summaries);
|
|
6516
|
-
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
6517
|
-
const tempPath = `${this.#root}/${INDEX_FILE}.rebuilding`;
|
|
6518
|
-
try {
|
|
6519
|
-
await this.#fs.writeText(tempPath, sorted.map((s) => `${JSON.stringify(s)}\n`).join(""));
|
|
6520
|
-
await this.#fs.rename(tempPath, indexPath);
|
|
6521
|
-
} catch {
|
|
6522
|
-
await this.#fs.remove(tempPath).catch(() => void 0);
|
|
6523
|
-
}
|
|
6524
|
-
return sorted;
|
|
6525
|
-
}
|
|
6526
|
-
};
|
|
6527
|
-
/** ISO 时间戳可按字典序比较 */
|
|
6528
|
-
function sortByStartedAtDesc(summaries) {
|
|
6529
|
-
return [...summaries].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
6530
|
-
}
|
|
6531
|
-
function applyFilter(summaries, filter) {
|
|
6532
|
-
let runs = summaries;
|
|
6533
|
-
if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
|
|
6534
|
-
if (filter.sessionId !== void 0 && filter.sessionId !== "") runs = runs.filter((r) => r.sessionId === filter.sessionId);
|
|
6535
|
-
const q = filter.search?.trim().toLowerCase();
|
|
6536
|
-
if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
|
|
6537
|
-
return runs;
|
|
6538
|
-
}
|
|
6539
|
-
/**
|
|
6540
|
-
* 从 run 的 trace 推导终态工具调用列表。
|
|
6541
|
-
*
|
|
6542
|
-
* 消费者过去要靠自己遍历 trace 才能拿到这份列表,而遍历 trace 的同一段代码
|
|
6543
|
-
* 又顺手承担了「补发漏掉的 live 事件」的职责——两件事绑在一起,谁也删不掉。
|
|
6544
|
-
* 幂等归 runtime(见 `AgentLoop` 的 per-run 已发集合)之后,
|
|
6545
|
-
* 推导终态列表就是纯函数,独立导出。
|
|
6546
|
-
* @stable
|
|
6547
|
-
*/
|
|
6548
|
-
function summarizeToolCalls(run) {
|
|
6549
|
-
const startedAtByCallId = /* @__PURE__ */ new Map();
|
|
6550
|
-
for (const event of run.trace) {
|
|
6551
|
-
if (event.type !== "tool.started") continue;
|
|
6552
|
-
const callId = event.data?.["callId"];
|
|
6553
|
-
if (typeof callId === "string" && !startedAtByCallId.has(callId)) startedAtByCallId.set(callId, event.ts);
|
|
6554
|
-
}
|
|
6555
|
-
const calls = [];
|
|
6556
|
-
for (const event of run.trace) {
|
|
6557
|
-
if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
|
|
6558
|
-
const name = event.data?.["name"];
|
|
6559
|
-
const callId = event.data?.["callId"];
|
|
6560
|
-
if (typeof name !== "string" || typeof callId !== "string") continue;
|
|
6561
|
-
const args = event.data?.["args"];
|
|
6562
|
-
const durationMs = event.data?.["durationMs"];
|
|
6563
|
-
const errorCode = event.data?.["code"];
|
|
6564
|
-
const startedAt = startedAtByCallId.get(callId);
|
|
6565
|
-
calls.push({
|
|
6566
|
-
callId,
|
|
6567
|
-
name,
|
|
6568
|
-
status: event.type === "tool.completed" ? "completed" : "failed",
|
|
6569
|
-
...typeof args === "string" ? { args } : {},
|
|
6570
|
-
...typeof durationMs === "number" ? { durationMs } : {},
|
|
6571
|
-
...typeof errorCode === "string" ? { errorCode } : {},
|
|
6572
|
-
...startedAt !== void 0 ? { startedAt } : {},
|
|
6573
|
-
...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
|
|
6574
|
-
});
|
|
6575
|
-
}
|
|
6576
|
-
return calls;
|
|
6577
|
-
}
|
|
6578
|
-
const SESSION_SCHEMA_VERSION = 1;
|
|
6579
|
-
/** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
|
|
6580
|
-
const FS_SESSION_PAGE_SIZE = 50;
|
|
6581
|
-
const toMeta = (record) => ({
|
|
6582
|
-
id: record.id,
|
|
6583
|
-
createdAt: record.createdAt,
|
|
6584
|
-
...record.title !== void 0 ? { title: record.title } : {},
|
|
6585
|
-
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
6586
|
-
...record.archived === true ? { archived: true } : {},
|
|
6587
|
-
...record.modelId !== void 0 ? { modelId: record.modelId } : {},
|
|
6588
|
-
messageCount: record.messages.length
|
|
6589
|
-
});
|
|
6590
|
-
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6591
|
-
const baseName = (path) => path.split("/").pop() ?? path;
|
|
6592
|
-
/**
|
|
6593
|
-
* 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
|
|
6594
|
-
* 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
|
|
6595
|
-
* 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
|
|
6596
|
-
*/
|
|
6597
|
-
function takeTailPage(all, options, what) {
|
|
6598
|
-
const limit = options.limit ?? 50;
|
|
6599
|
-
if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
|
|
6600
|
-
let end = all.length;
|
|
6601
|
-
if (options.cursor !== void 0) {
|
|
6602
|
-
end = Number(options.cursor);
|
|
6603
|
-
if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
|
|
6604
|
-
}
|
|
6605
|
-
const start = Math.max(0, end - limit);
|
|
6606
|
-
return {
|
|
6607
|
-
items: all.slice(start, end),
|
|
6608
|
-
...start > 0 ? { nextCursor: String(start) } : {}
|
|
6609
|
-
};
|
|
6610
|
-
}
|
|
6611
|
-
/**
|
|
6612
|
-
* 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
|
|
6613
|
-
* 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
|
|
6614
|
-
*/
|
|
6615
|
-
function parseSessionFile(raw, path) {
|
|
6616
|
-
let parsed;
|
|
6617
|
-
try {
|
|
6618
|
-
parsed = JSON.parse(raw);
|
|
6619
|
-
} catch (e) {
|
|
6620
|
-
throw new WebSkillError("VALIDATION_FAILED", `Session file is not valid JSON: ${path}`, e);
|
|
6621
|
-
}
|
|
6622
|
-
if (typeof parsed !== "object" || parsed === null) throw new WebSkillError("VALIDATION_FAILED", `Session file is not an object: ${path}`);
|
|
6623
|
-
const file = parsed;
|
|
6624
|
-
const schemaVersion = typeof file.schemaVersion === "number" ? file.schemaVersion : 0;
|
|
6625
|
-
if (schemaVersion > 1) throw new WebSkillError("SESSION_INCOMPATIBLE", `Session file at ${path} has schemaVersion ${schemaVersion}, but this build understands at most 1`);
|
|
6626
|
-
if (typeof file.id !== "string" || typeof file.createdAt !== "string") throw new WebSkillError("VALIDATION_FAILED", `Session file is missing "id" or "createdAt": ${path}`);
|
|
6627
|
-
return {
|
|
6628
|
-
schemaVersion,
|
|
6629
|
-
id: file.id,
|
|
6630
|
-
createdAt: file.createdAt,
|
|
6631
|
-
...typeof file.title === "string" ? { title: file.title } : {},
|
|
6632
|
-
...file.titleLocked === true ? { titleLocked: true } : {},
|
|
6633
|
-
...file.archived === true ? { archived: true } : {},
|
|
6634
|
-
...typeof file.modelId === "string" ? { modelId: file.modelId } : {},
|
|
6635
|
-
messages: Array.isArray(file.messages) ? file.messages : [],
|
|
6636
|
-
messageCount: Array.isArray(file.messages) ? file.messages.length : 0
|
|
6637
|
-
};
|
|
6638
|
-
}
|
|
6639
|
-
/**
|
|
6640
|
-
* `FileSystemProvider` 后端的会话存储:`<root>/<id>.json`。
|
|
6641
|
-
*
|
|
6642
|
-
* 所有变更操作按 id 串到一条 promise 链上:会话文件是整读整写的,
|
|
6643
|
-
* 两个并发 `appendMessages` 若都先读后写,后写的会覆盖先写的那条消息。
|
|
6644
|
-
*/
|
|
6645
|
-
var FsSessionStore = class {
|
|
6646
|
-
#root;
|
|
6647
|
-
#fs;
|
|
6648
|
-
/** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
|
|
6649
|
-
#writes = /* @__PURE__ */ new Map();
|
|
6650
|
-
/** FR-11.4:损坏文件诊断只报一次,避免 list 轮询刷屏 */
|
|
6651
|
-
#warnedDataLoss = false;
|
|
6652
|
-
constructor(deps) {
|
|
6653
|
-
this.#root = deps.root.replace(/\/+$/, "");
|
|
6654
|
-
this.#fs = deps.fs;
|
|
6655
|
-
}
|
|
6656
|
-
#path(id) {
|
|
6657
|
-
assertSafePathSegment(id, "session id");
|
|
6658
|
-
return resolveInsideRoot(this.#root, `${id}.json`);
|
|
6659
|
-
}
|
|
6660
|
-
/** 把 mutation 排到该 id 的队尾;前一个失败不阻塞后一个 */
|
|
6661
|
-
async #serialize(id, work) {
|
|
6662
|
-
const next = (this.#writes.get(id) ?? Promise.resolve()).catch(() => void 0).then(work);
|
|
6663
|
-
this.#writes.set(id, next);
|
|
6664
|
-
try {
|
|
6665
|
-
return await next;
|
|
6666
|
-
} finally {
|
|
6667
|
-
if (this.#writes.get(id) === next) this.#writes.delete(id);
|
|
6668
|
-
}
|
|
6669
|
-
}
|
|
6670
|
-
async #write(record) {
|
|
6671
|
-
const file = {
|
|
6672
|
-
schemaVersion: 1,
|
|
6673
|
-
id: record.id,
|
|
6674
|
-
createdAt: record.createdAt,
|
|
6675
|
-
...record.title !== void 0 ? { title: record.title } : {},
|
|
6676
|
-
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
6677
|
-
...record.archived === true ? { archived: true } : {},
|
|
6678
|
-
...record.modelId !== void 0 ? { modelId: record.modelId } : {},
|
|
6679
|
-
messages: record.messages
|
|
6680
|
-
};
|
|
6681
|
-
await atomicWriteText(this.#fs, this.#path(record.id), JSON.stringify(file, null, 2));
|
|
6682
|
-
}
|
|
6683
|
-
async #require(id) {
|
|
6684
|
-
const record = await this.get(id);
|
|
6685
|
-
if (!record) throw new WebSkillError("FS_NOT_FOUND", `Session "${id}" not found under ${this.#root}`);
|
|
6686
|
-
return record;
|
|
6687
|
-
}
|
|
6688
|
-
async list(options = {}) {
|
|
6689
|
-
if (!await this.#fs.exists(this.#root)) return {
|
|
6690
|
-
items: [],
|
|
6691
|
-
source: "missing-root",
|
|
6692
|
-
skipped: []
|
|
6693
|
-
};
|
|
6694
|
-
const metas = [];
|
|
6695
|
-
const skipped = [];
|
|
6696
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
6697
|
-
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
6698
|
-
let record;
|
|
6699
|
-
try {
|
|
6700
|
-
record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
|
|
6701
|
-
} catch {
|
|
6702
|
-
skipped.push(baseName(entry.path));
|
|
6703
|
-
continue;
|
|
6704
|
-
}
|
|
6705
|
-
if (record.archived === true && options.includeArchived !== true) continue;
|
|
6706
|
-
metas.push(toMeta(record));
|
|
6707
|
-
}
|
|
6708
|
-
if (skipped.length > 0 && !this.#warnedDataLoss) {
|
|
6709
|
-
this.#warnedDataLoss = true;
|
|
6710
|
-
console.warn(`[webskill] ${skipped.length} session file(s) under ${this.#root} are unreadable (possible data loss from an interrupted write): ${skipped.join(", ")}`);
|
|
6711
|
-
}
|
|
6712
|
-
metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
6713
|
-
return {
|
|
6714
|
-
...takeTailPage(metas, options, "session"),
|
|
6715
|
-
source: "ok",
|
|
6716
|
-
skipped
|
|
6717
|
-
};
|
|
6718
|
-
}
|
|
6719
|
-
/**
|
|
6720
|
-
* 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
|
|
6721
|
-
* 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
|
|
6722
|
-
*/
|
|
6723
|
-
async listMessages(id, options = {}) {
|
|
6724
|
-
return takeTailPage((await this.#require(id)).messages, options, "message");
|
|
6725
|
-
}
|
|
6726
|
-
async get(id) {
|
|
6727
|
-
const path = this.#path(id);
|
|
6728
|
-
if (!await this.#fs.exists(path)) return void 0;
|
|
6729
|
-
return parseSessionFile(await this.#fs.readText(path), path);
|
|
6730
|
-
}
|
|
6731
|
-
async create(init = {}) {
|
|
6732
|
-
const record = {
|
|
6733
|
-
schemaVersion: 1,
|
|
6734
|
-
id: init.id ?? newSessionId(),
|
|
6735
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6736
|
-
...init.title !== void 0 ? { title: init.title } : {},
|
|
6737
|
-
messages: [],
|
|
6738
|
-
messageCount: 0
|
|
6739
|
-
};
|
|
6740
|
-
return this.#serialize(record.id, async () => {
|
|
6741
|
-
await this.#write(record);
|
|
6742
|
-
return toMeta(record);
|
|
6743
|
-
});
|
|
6744
|
-
}
|
|
6745
|
-
async appendMessages(id, messages) {
|
|
6746
|
-
if (messages.length === 0) return;
|
|
6747
|
-
await this.#serialize(id, async () => {
|
|
6748
|
-
const record = await this.#require(id);
|
|
6749
|
-
record.messages.push(...messages);
|
|
6750
|
-
await this.#write(record);
|
|
6751
|
-
});
|
|
6752
|
-
}
|
|
6753
|
-
async replaceMessages(id, messages) {
|
|
6754
|
-
await this.#serialize(id, async () => {
|
|
6755
|
-
const record = await this.#require(id);
|
|
6756
|
-
record.messages = [...messages];
|
|
6757
|
-
await this.#write(record);
|
|
6758
|
-
});
|
|
6759
|
-
}
|
|
6760
|
-
async setTitle(id, title, options = {}) {
|
|
6761
|
-
await this.#serialize(id, async () => {
|
|
6762
|
-
const record = await this.#require(id);
|
|
6763
|
-
record.title = title;
|
|
6764
|
-
if (options.lock === true) record.titleLocked = true;
|
|
6765
|
-
await this.#write(record);
|
|
6766
|
-
});
|
|
6767
|
-
}
|
|
6768
|
-
async setArchived(id, archived) {
|
|
6769
|
-
await this.#serialize(id, async () => {
|
|
6770
|
-
const record = await this.#require(id);
|
|
6771
|
-
record.archived = archived;
|
|
6772
|
-
await this.#write(record);
|
|
6773
|
-
});
|
|
6774
|
-
}
|
|
6775
|
-
async setModel(id, modelId) {
|
|
6776
|
-
await this.#serialize(id, async () => {
|
|
6777
|
-
const record = await this.#require(id);
|
|
6778
|
-
if (modelId === void 0) delete record.modelId;
|
|
6779
|
-
else record.modelId = modelId;
|
|
6780
|
-
await this.#write(record);
|
|
6781
|
-
});
|
|
6782
|
-
}
|
|
6783
|
-
async getMeta(id) {
|
|
6784
|
-
const record = await this.get(id);
|
|
6785
|
-
return record === void 0 ? void 0 : toMeta(record);
|
|
6786
|
-
}
|
|
6787
|
-
async delete(id) {
|
|
6788
|
-
await this.#serialize(id, async () => {
|
|
6789
|
-
const path = this.#path(id);
|
|
6790
|
-
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
6791
|
-
});
|
|
6792
|
-
}
|
|
6793
|
-
};
|
|
6794
|
-
/**
|
|
6795
|
-
* 按会话分文件的 append-only JSONL 留存。
|
|
6796
|
-
*
|
|
6797
|
-
* 分文件维度取会话而非 run:读取侧唯一的查询就是「这个会话的全部步骤」,
|
|
6798
|
-
* 按 run 分会让一次生成要开 N 个文件。
|
|
6799
|
-
*/
|
|
6800
|
-
var FsToolStepStore = class {
|
|
6801
|
-
#root;
|
|
6802
|
-
#fs;
|
|
6803
|
-
#onError;
|
|
6804
|
-
constructor(deps) {
|
|
6805
|
-
this.#root = deps.root.replace(/\/+$/, "");
|
|
6806
|
-
this.#fs = deps.fs;
|
|
6807
|
-
this.#onError = deps.onError ?? ((error, record) => {
|
|
6808
|
-
console.warn(`Failed to persist tool step for call "${record.callId}": ${messageOf(error)}`);
|
|
6809
|
-
});
|
|
6810
|
-
}
|
|
6811
|
-
#path(sessionId) {
|
|
6812
|
-
assertSafePathSegment(sessionId, "session id");
|
|
6813
|
-
return resolveInsideRoot(this.#root, `${sessionId}.jsonl`);
|
|
6814
|
-
}
|
|
6815
|
-
async append(record) {
|
|
6816
|
-
const path = this.#path(record.sessionId);
|
|
6817
|
-
try {
|
|
6818
|
-
await this.#fs.appendText(path, `${JSON.stringify(record)}\n`);
|
|
6819
|
-
} catch (e) {
|
|
6820
|
-
this.#onError(e, record);
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
async listBySession(sessionId) {
|
|
6824
|
-
const path = this.#path(sessionId);
|
|
6825
|
-
if (!await this.#fs.exists(path)) return [];
|
|
6826
|
-
const raw = await this.#fs.readText(path);
|
|
6827
|
-
const records = [];
|
|
6828
|
-
for (const line of raw.split("\n")) {
|
|
6829
|
-
if (line.trim() === "") continue;
|
|
6830
|
-
try {
|
|
6831
|
-
records.push(JSON.parse(line));
|
|
6832
|
-
} catch {
|
|
6833
|
-
continue;
|
|
6834
|
-
}
|
|
6835
|
-
}
|
|
6836
|
-
return records;
|
|
6837
|
-
}
|
|
6838
|
-
};
|
|
6839
4182
|
|
|
6840
4183
|
//#endregion
|
|
6841
|
-
export {
|
|
4184
|
+
export { scriptToolName as $, readUserProfile as A, createScriptContext as B, TraceRecorder as C, mergeProfileEntries as D, appendBehaviorRecords as E, DEFAULT_USER_PROFILE_LIMITS as F, ASK_USER_TOOL_NAME as G, ASK_USER_INPUT_SCHEMA as H, EMPTY_USER_PROFILE as I, READ_SKILL_FILE_TOOL_NAME as J, READ_SKILL_FILE_INPUT_SCHEMA as K, SerializingMemoryStore as L, USER_PROFILE_NO_INVENTION_RULE as M, USER_PROFILE_PROMPT_HEADER as N, readBehaviorRecords as O, renderUserProfileContext as P, schemaSourceLabel as Q, EventBus as R, extractTodoTraceEvents as S, USER_PROFILE_KEY as T, ASK_USER_MAX_FIELDS as U, ASK_USER_FIELD_TYPES as V, ASK_USER_TOOL as W, formatSkillScriptManifest as X, ALLOWED_TOOLS_EXCLUSION_REASON as Y, listSkillScripts as Z, RUN_SNAPSHOT_SCHEMA_VERSION as _, networkPolicyLibSource as a, xmlRenderer as at, MAX_TOOL_STEP_ARG_BYTES as b, bridgeError as c, FsMemoryStore as d, resolveToolName as et, WebSkillRuntime as f, FsRunSnapshotStore as g, redactToolStepArgs as h, isNetworkAllowed as i, renderAvailableSkillsXml as it, sampleBehaviorRecords as j, readProfileEntries as k, parseBridgeRequest as l, SENSITIVE_ANNOTATION as m, normalizeErrorCode as n, ProgressiveRouter as nt, networkUrlHost as o, AgentLoop as p, READ_SKILL_FILE_TOOL as q, normalizeToolError as r, escapeXml as rt, UPLOAD_FILES_UNAVAILABLE as s, CapabilityApproval as t, toLlmToolSpec as tt, FsArtifactStore as u, isUnsupportedRunSnapshot as v, BEHAVIOR_RECORDS_KEY as w, extractSkillCandidate as x, DEFAULT_LOOP_LIMITS as y, schemaToForm as z };
|