@trim21/personal-pi-extensions 0.1.654 → 0.1.658
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/package.json +4 -2
- package/src/codemode/declarations.ts +103 -0
- package/src/codemode/prelude.ts +298 -0
- package/src/codemode/protocol.ts +156 -0
- package/src/codemode/sandbox.ts +192 -0
- package/src/codemode/source.ts +91 -0
- package/src/codemode/tool.ts +353 -0
- package/src/codemode/wasm.ts +20 -0
- package/src/codemode/worker.js +580 -0
- package/src/codemode/worker.ts +279 -0
- package/src/index.ts +13 -0
- package/src/lib/tool-bus.ts +7 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [opencode 工具集](#opencode-工具集) | 小写 `read`/`edit`/`write`/`grep`/`glob`/`bash`/`todowrite`/`question` |
|
|
12
12
|
| [Claude Code 工具集](#claude-code-工具集) | 大写 `Read`/`Edit`/`Write`/`Grep`/`Glob`/`Bash`/`TodoWrite`/`AskUserQuestion` |
|
|
13
13
|
| [工具启用配置](#工具启用配置) | `personalExtensions`:按模型选文件 IO 工具集、通配禁用/启用工具 |
|
|
14
|
+
| [codemode](#codemode) | 模型写一段 JS,在 QuickJS 沙箱里批量/串行调用其它工具,只有脚本输出进上下文 |
|
|
14
15
|
| [bwrap](#bwrap) | 基于 bubblewrap 的 OS 级沙箱(内置两套工具集):文件系统隔离 + 多档网络策略 |
|
|
15
16
|
| [写保护(内置)](#写保护内置) | 写工具内置:限制文件写入在 workspace 内,外部写入需审批 |
|
|
16
17
|
| [LSP(内置)](#lsp内置) | 文件工具内置 LSP 诊断 + `lsp-rename`/`lsp-inspect`/`lsp-find-definition`/`lsp-find-reference` |
|
|
@@ -26,6 +27,30 @@
|
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
30
|
+
## codemode
|
|
31
|
+
|
|
32
|
+
`codemode` 让模型写一段 JavaScript(作为 async 函数体,`await` 与 `return` 都可用),在
|
|
33
|
+
进程内的 worker 线程里用 QuickJS wasm VM 执行。VM 里没有 node、文件系统、网络、timer
|
|
34
|
+
或模块加载,脚本唯一的出口是 `tools.<name>(args)`。
|
|
35
|
+
|
|
36
|
+
- **嵌套调用经工具总线**:`tools.Read({ file_path })` 最终执行的是 `Read` 工具自己的
|
|
37
|
+
`execute`,所以工具的审批照常生效(写工作区外会弹 write-guard,Bash 沙箱外执行会弹
|
|
38
|
+
自己的提权确认)。codemode 不额外加确认层:脚本里连发十次写操作就是十次工具自己的
|
|
39
|
+
审批(需要审批的那些)。
|
|
40
|
+
- **可调用集合** = 总线上实际注册的工具减去 `codemode` 自身,执行时再与当前 active
|
|
41
|
+
列表求交,所以 `personalExtensions.disabledTools`、pi 的 `defaultTools` / `--tools`、
|
|
42
|
+
子代理的工具白名单都同样约束脚本。
|
|
43
|
+
- **只有脚本输出进上下文**:`text(value)` / `console.log(...)` 与 `return` 值进入工具结果,
|
|
44
|
+
中间的工具调用与它们的返回内容不会(也不在会话记录里留下工具调用条目)。
|
|
45
|
+
- **脚本接口**:`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console.*` /
|
|
46
|
+
`store(key, value)` / `load(key)`;首行可选 `// @options: {"timeout_ms": 30000,
|
|
47
|
+
"max_output_tokens": 10000}`。默认没有超时,靠调用方中止或 `timeout_ms` 结束。
|
|
48
|
+
- **store** 写在会话的自定义 entry 上(跟随分支),输出超过 `max_output_tokens` 时头尾
|
|
49
|
+
截断并把全文落到 `$TMPDIR/pi-codemode-*.txt`。
|
|
50
|
+
- **构建**:worker 入口是 esbuild 产物 `src/codemode/worker.js`(随仓库提交,
|
|
51
|
+
`pnpm run build:codemode-worker` 重新生成,pre-commit 会自动跑);`quickjs-wasi` 的
|
|
52
|
+
wasm 在注册工具时编译一次,之后每次执行复用。
|
|
53
|
+
|
|
29
54
|
## 工具启用配置
|
|
30
55
|
|
|
31
56
|
所有工具由单一扩展入口 `src/index.ts` 注册(`package.json` 的 `pi.extensions` 只列
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.658",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"sandbox": "jiti bin/sandbox.ts",
|
|
28
28
|
"aft-search": "jiti bin/aft-search.ts",
|
|
29
29
|
"build:holder": "esbuild src/bwrap/holder.ts --format=esm --target=node24 --outfile=src/bwrap/holder.js",
|
|
30
|
-
"prepare": "husky"
|
|
30
|
+
"prepare": "husky",
|
|
31
|
+
"build:codemode-worker": "esbuild src/codemode/worker.ts --bundle --packages=external --format=esm --target=node24 --outfile=src/codemode/worker.js"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"@earendil-works/pi-agent-core": ">=0.84.1",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"lodash-es": "^4.18.1",
|
|
97
98
|
"minimatch": "^10.2.6",
|
|
98
99
|
"octokit": "^5.0.5",
|
|
100
|
+
"quickjs-wasi": "^3.6.2",
|
|
99
101
|
"turndown": "^7.2.0",
|
|
100
102
|
"undici": "^8.0.0",
|
|
101
103
|
"vscode-languageserver-protocol": "^3.18.4",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把工具的参数 schema 渲染成脚本侧的 TypeScript 声明:写进 codemode 的工具描述里,
|
|
3
|
+
* 模型据此知道脚本里能写 `await tools.Read({ file_path: "..." })`。
|
|
4
|
+
*
|
|
5
|
+
* 只处理本仓库工具实际用到的形状(object / array / string / number / boolean /
|
|
6
|
+
* union / enum),其余退化成 `unknown`——宁可少给类型,也不要编出错类型。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ScriptTool } from "./protocol.js";
|
|
10
|
+
|
|
11
|
+
interface ToolLike {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
parameters?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type JsonSchema = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
function isSchema(value: unknown): value is JsonSchema {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function literal(value: unknown): string {
|
|
24
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 工具名作为属性访问:非法标识符用引号形式。 */
|
|
28
|
+
function propertyName(name: string): string {
|
|
29
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderType(schema: unknown, indent: string): string {
|
|
33
|
+
if (!isSchema(schema)) {
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
37
|
+
return schema.enum.map((value) => literal(value)).join(" | ");
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
40
|
+
return schema.anyOf.map((option) => renderType(option, indent)).join(" | ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (schema.type) {
|
|
44
|
+
case "string": {
|
|
45
|
+
return "string";
|
|
46
|
+
}
|
|
47
|
+
case "number":
|
|
48
|
+
case "integer": {
|
|
49
|
+
return "number";
|
|
50
|
+
}
|
|
51
|
+
case "boolean": {
|
|
52
|
+
return "boolean";
|
|
53
|
+
}
|
|
54
|
+
case "null": {
|
|
55
|
+
return "null";
|
|
56
|
+
}
|
|
57
|
+
case "array": {
|
|
58
|
+
return `Array<${renderType(schema.items, indent)}>`;
|
|
59
|
+
}
|
|
60
|
+
case "object": {
|
|
61
|
+
const properties = isSchema(schema.properties) ? schema.properties : {};
|
|
62
|
+
const required = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
|
|
63
|
+
const entries = Object.entries(properties);
|
|
64
|
+
if (entries.length === 0) {
|
|
65
|
+
return "Record<string, unknown>";
|
|
66
|
+
}
|
|
67
|
+
const lines = entries.map(([key, value]) => {
|
|
68
|
+
const optional = required.has(key) ? "" : "?";
|
|
69
|
+
return `${indent} ${propertyName(key)}${optional}: ${renderType(value, `${indent} `)};`;
|
|
70
|
+
});
|
|
71
|
+
return `{\n${lines.join("\n")}\n${indent}}`;
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
return "unknown";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 单个工具在脚本里的签名(`tools` 的一个成员)。 */
|
|
80
|
+
function renderToolMember(tool: ToolLike): string {
|
|
81
|
+
const summary = tool.description?.split("\n", 1)[0]?.trim();
|
|
82
|
+
const doc = summary ? `/** ${summary} */\n ` : "";
|
|
83
|
+
return `${doc}${propertyName(tool.name)}(args: ${renderType(tool.parameters, " ")}): Promise<unknown>;`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 给脚本用的工具清单(名字 + 说明)。 */
|
|
87
|
+
export function toScriptTools(tools: readonly ToolLike[]): ScriptTool[] {
|
|
88
|
+
return tools.map((tool) => ({ name: tool.name, description: tool.description }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 渲染脚本侧的声明:`declare const tools: {...}` 与全局辅助函数。 */
|
|
92
|
+
export function renderDeclarations(tools: readonly ToolLike[]): string {
|
|
93
|
+
const members = tools.map((tool) => ` ${renderToolMember(tool)}`).join("\n");
|
|
94
|
+
return [
|
|
95
|
+
`declare const tools: {\n${members}\n};`,
|
|
96
|
+
"declare const ALL_TOOLS: Array<{ name: string; description?: string }>;",
|
|
97
|
+
"declare function text(value: unknown): void;",
|
|
98
|
+
"declare function image(value: unknown): void;",
|
|
99
|
+
"declare function exit(): void;",
|
|
100
|
+
"declare function store(key: string, value: unknown): void;",
|
|
101
|
+
"declare function load(key: string): unknown;",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codemode 脚本侧的 prelude:在 QuickJS VM 里先于脚本求值,构建脚本能看到的全部
|
|
3
|
+
* 能力(`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console` / `store` /
|
|
4
|
+
* `load`),并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
|
|
5
|
+
*
|
|
6
|
+
* 值与参数过桥时都是 JSON 文本,本侧负责 parse/stringify;异步调用用一个 pending
|
|
7
|
+
* 表把 id 映射到 promise,由宿主在结果到达时 settle。
|
|
8
|
+
*
|
|
9
|
+
* 求值结果是一个函数 `(bridge, toolsJson, storeJson) => { settle, run, stalled }`。
|
|
10
|
+
* `bridge(kind, a, b, c)`:
|
|
11
|
+
* - `"call"`(id, name, argsJson)
|
|
12
|
+
* - `"output"`("text", text)/("image", data, mimeType)
|
|
13
|
+
* - `"done"`(ok, valueJsonOrErrorJson, writesJson)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** `store()` 单个值的 JSON 上限(字符数)。 */
|
|
17
|
+
export const MAX_STORE_VALUE_CHARS = 256 * 1024;
|
|
18
|
+
/** 全部 store 值的 JSON 上限(字符数)。 */
|
|
19
|
+
export const MAX_STORE_TOTAL_CHARS = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
const IMAGE_HELPER_EXPECTS =
|
|
22
|
+
"image expects a non-empty image URL string, an object with image_url, or a raw MCP image block";
|
|
23
|
+
|
|
24
|
+
export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
|
|
25
|
+
"use strict";
|
|
26
|
+
const stringify = JSON.stringify;
|
|
27
|
+
const parse = JSON.parse;
|
|
28
|
+
const promiseThen = Promise.prototype.then;
|
|
29
|
+
const ErrorCtor = Error;
|
|
30
|
+
const TypeErrorCtor = TypeError;
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
let nextId = 1;
|
|
33
|
+
let finished = false;
|
|
34
|
+
// exit() 用它解开脚本栈:成功结果已经报过了。
|
|
35
|
+
const EXIT = Object.freeze({});
|
|
36
|
+
|
|
37
|
+
function done(ok, payload, writes) {
|
|
38
|
+
if (finished) return;
|
|
39
|
+
finished = true;
|
|
40
|
+
bridge("done", ok, payload, writes);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function serialize(value) {
|
|
44
|
+
return value === undefined ? undefined : stringify(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// QuickJS 的 stack 只有帧,没有 "Name: message" 头;补上并去掉本 prelude 的帧,
|
|
48
|
+
// 读起来与 Node 的报错一致。
|
|
49
|
+
function errorText(error) {
|
|
50
|
+
const head = error.message ? error.name + ": " + error.message : String(error.name);
|
|
51
|
+
const frames =
|
|
52
|
+
typeof error.stack === "string"
|
|
53
|
+
? error.stack.split("\n").filter((line) => line.trim() && !line.includes("codemode-prelude.js"))
|
|
54
|
+
: [];
|
|
55
|
+
return [head, ...frames].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function format(value) {
|
|
59
|
+
if (typeof value === "string") return value;
|
|
60
|
+
if (value instanceof ErrorCtor) return errorText(value);
|
|
61
|
+
try {
|
|
62
|
+
const json = stringify(value);
|
|
63
|
+
return json === undefined ? String(value) : json;
|
|
64
|
+
} catch {
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function describeError(error) {
|
|
70
|
+
if (error instanceof ErrorCtor) {
|
|
71
|
+
return stringify({ name: error.name, message: error.message, stack: errorText(error) });
|
|
72
|
+
}
|
|
73
|
+
return stringify({ message: format(error) });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function caller(name) {
|
|
77
|
+
return (args) =>
|
|
78
|
+
new Promise((resolve, reject) => {
|
|
79
|
+
let json;
|
|
80
|
+
try {
|
|
81
|
+
json = serialize(args);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
reject(error);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const id = nextId++;
|
|
87
|
+
pending.set(id, { resolve, reject });
|
|
88
|
+
bridge("call", id, name, json);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tools = Object.create(null);
|
|
93
|
+
const allTools = [];
|
|
94
|
+
for (const { name, jsName, description } of parse(toolsJson)) {
|
|
95
|
+
const fn = caller(name);
|
|
96
|
+
// 两个名字归一化成同一个标识符时,第一个赢
|
|
97
|
+
if (!(jsName in tools)) {
|
|
98
|
+
tools[jsName] = fn;
|
|
99
|
+
allTools.push(Object.freeze({ name: jsName, description }));
|
|
100
|
+
}
|
|
101
|
+
if (!(name in tools)) tools[name] = fn;
|
|
102
|
+
}
|
|
103
|
+
Object.freeze(tools);
|
|
104
|
+
Object.freeze(allTools);
|
|
105
|
+
|
|
106
|
+
// key -> JSON 文本;容量按 key 与 JSON 的字符数计
|
|
107
|
+
const stored = new Map(Object.entries(parse(storeJson)));
|
|
108
|
+
const writes = new Map();
|
|
109
|
+
let storedChars = 0;
|
|
110
|
+
for (const [key, json] of stored) storedChars += key.length + json.length;
|
|
111
|
+
|
|
112
|
+
function checkKey(name, key) {
|
|
113
|
+
if (typeof key !== "string") throw new TypeErrorCtor(name + "() key must be a string");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function store(key, value) {
|
|
117
|
+
checkKey("store", key);
|
|
118
|
+
const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
|
|
119
|
+
if (value === undefined) {
|
|
120
|
+
stored.delete(key);
|
|
121
|
+
storedChars -= previous;
|
|
122
|
+
writes.set(key, undefined);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let json;
|
|
126
|
+
try {
|
|
127
|
+
json = stringify(value);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
|
|
130
|
+
}
|
|
131
|
+
if (json === undefined) {
|
|
132
|
+
throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable");
|
|
133
|
+
}
|
|
134
|
+
if (json.length > ${MAX_STORE_VALUE_CHARS}) {
|
|
135
|
+
throw new RangeError("store(" + stringify(key) + ") value exceeds ${MAX_STORE_VALUE_CHARS} characters of JSON");
|
|
136
|
+
}
|
|
137
|
+
const next = storedChars - previous + key.length + json.length;
|
|
138
|
+
if (next > ${MAX_STORE_TOTAL_CHARS}) {
|
|
139
|
+
throw new RangeError("store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON");
|
|
140
|
+
}
|
|
141
|
+
stored.set(key, json);
|
|
142
|
+
storedChars = next;
|
|
143
|
+
writes.set(key, json);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function load(key) {
|
|
147
|
+
checkKey("load", key);
|
|
148
|
+
const json = stored.get(key);
|
|
149
|
+
return json === undefined ? undefined : parse(json);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function serializeWrites() {
|
|
153
|
+
const entries = [];
|
|
154
|
+
for (const [key, json] of writes) entries.push(json === undefined ? [key] : [key, json]);
|
|
155
|
+
return stringify(entries);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Object.defineProperty(globalThis, "store", { value: store, enumerable: true });
|
|
159
|
+
Object.defineProperty(globalThis, "load", { value: load, enumerable: true });
|
|
160
|
+
|
|
161
|
+
// 原始值转字符串,其余 JSON 化
|
|
162
|
+
function outputText(value) {
|
|
163
|
+
if (value === undefined || value === null || (typeof value !== "object" && typeof value !== "function")) {
|
|
164
|
+
return String(value);
|
|
165
|
+
}
|
|
166
|
+
const json = stringify(value);
|
|
167
|
+
return json === undefined ? String(value) : json;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function text(value) {
|
|
171
|
+
let rendered;
|
|
172
|
+
try {
|
|
173
|
+
rendered = outputText(value);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw new TypeErrorCtor(error instanceof ErrorCtor ? error.message : String(error));
|
|
176
|
+
}
|
|
177
|
+
if (!finished) bridge("output", "text", rendered);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function imageUrl(value) {
|
|
181
|
+
if (typeof value === "string") return value;
|
|
182
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
183
|
+
throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
184
|
+
}
|
|
185
|
+
if (value.image_url !== undefined) {
|
|
186
|
+
if (typeof value.image_url !== "string") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
187
|
+
return value.image_url;
|
|
188
|
+
}
|
|
189
|
+
if (typeof value.type !== "string") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
190
|
+
if (value.type !== "image") {
|
|
191
|
+
throw new TypeErrorCtor('image only accepts MCP image blocks, got "' + value.type + '"');
|
|
192
|
+
}
|
|
193
|
+
if (typeof value.data !== "string" || value.data === "") throw new TypeErrorCtor("image expected MCP image data");
|
|
194
|
+
if (value.data.toLowerCase().startsWith("data:")) return value.data;
|
|
195
|
+
const mimeType = typeof value.mimeType === "string" && value.mimeType ? value.mimeType : "application/octet-stream";
|
|
196
|
+
return "data:" + mimeType + ";base64," + value.data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function image(value) {
|
|
200
|
+
const url = imageUrl(value);
|
|
201
|
+
if (url === "") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
202
|
+
const colon = url.indexOf(":");
|
|
203
|
+
const scheme = colon === -1 ? "" : url.slice(0, colon).toLowerCase();
|
|
204
|
+
if (scheme === "http" || scheme === "https") {
|
|
205
|
+
throw new TypeErrorCtor("remote image URLs are not supported in tool outputs. Pass a base64 data URI instead");
|
|
206
|
+
}
|
|
207
|
+
const comma = url.indexOf(",");
|
|
208
|
+
const header = comma === -1 ? [] : url.slice(colon + 1, comma).split(";");
|
|
209
|
+
if (scheme !== "data" || comma === -1 || header.slice(1).every((part) => part.toLowerCase() !== "base64")) {
|
|
210
|
+
throw new TypeErrorCtor("invalid image output. Pass a base64 data URI instead");
|
|
211
|
+
}
|
|
212
|
+
if (!finished) bridge("output", "image", url.slice(comma + 1), header[0] || "application/octet-stream");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function exit() {
|
|
216
|
+
let writesJson;
|
|
217
|
+
try {
|
|
218
|
+
writesJson = serializeWrites();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
done(false, describeError(error));
|
|
221
|
+
throw EXIT;
|
|
222
|
+
}
|
|
223
|
+
done(true, undefined, writesJson);
|
|
224
|
+
throw EXIT;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const console = {};
|
|
228
|
+
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
229
|
+
console[level] = (...args) => {
|
|
230
|
+
if (!finished) bridge("output", "text", args.map(format).join(" "));
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
Object.freeze(console);
|
|
234
|
+
|
|
235
|
+
Object.defineProperty(globalThis, "tools", { value: tools, enumerable: true });
|
|
236
|
+
Object.defineProperty(globalThis, "ALL_TOOLS", { value: allTools, enumerable: true });
|
|
237
|
+
Object.defineProperty(globalThis, "console", { value: console, enumerable: true });
|
|
238
|
+
Object.defineProperty(globalThis, "text", { value: text, enumerable: true });
|
|
239
|
+
Object.defineProperty(globalThis, "image", { value: image, enumerable: true });
|
|
240
|
+
Object.defineProperty(globalThis, "exit", { value: exit, enumerable: true });
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
settle(id, ok, payload) {
|
|
244
|
+
const entry = pending.get(id);
|
|
245
|
+
if (!entry) return;
|
|
246
|
+
pending.delete(id);
|
|
247
|
+
if (!ok) {
|
|
248
|
+
entry.reject(new ErrorCtor(payload));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let value;
|
|
252
|
+
try {
|
|
253
|
+
value = payload === undefined ? undefined : parse(payload);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
entry.reject(error);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
entry.resolve(value);
|
|
259
|
+
},
|
|
260
|
+
run(fn) {
|
|
261
|
+
let promise;
|
|
262
|
+
try {
|
|
263
|
+
promise = fn(tools, console);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
done(false, describeError(error));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
promiseThen.call(
|
|
269
|
+
promise,
|
|
270
|
+
(value) => {
|
|
271
|
+
let json;
|
|
272
|
+
try {
|
|
273
|
+
json = serialize(value);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
done(false, describeError(error));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
done(true, json, serializeWrites());
|
|
279
|
+
},
|
|
280
|
+
(error) => {
|
|
281
|
+
done(false, describeError(error));
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
stalled() {
|
|
286
|
+
if (finished || pending.size > 0) return false;
|
|
287
|
+
done(
|
|
288
|
+
false,
|
|
289
|
+
stringify({
|
|
290
|
+
name: "Error",
|
|
291
|
+
message:
|
|
292
|
+
"The script is waiting on a promise that can never settle: no tool call is pending, and timers do not exist here.",
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
return true;
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
})`;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codemode 主线程 ↔ worker 线程的消息类型(结构化克隆,不是字符串协议)。
|
|
3
|
+
*
|
|
4
|
+
* 每次执行起一个新的 worker:主线程把注册时编好的 wasm 模块连同脚本、可调用工具、
|
|
5
|
+
* 初始 store 一起发过去,worker 里建 QuickJS VM 跑脚本,脚本的嵌套调用与输出回传,
|
|
6
|
+
* 主线程执行工具后把结果发回去。超时/中止由主线程 `worker.terminate()` 负责,因此
|
|
7
|
+
* 不需要取消消息。
|
|
8
|
+
*
|
|
9
|
+
* worker 边界两侧都做 typebox 校验:worker 里跑的是模型写的代码,回传的消息同样
|
|
10
|
+
* 当外部输入对待。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type TSchema, Type } from "typebox";
|
|
14
|
+
import { Value } from "typebox/value";
|
|
15
|
+
|
|
16
|
+
// ── 消息 schema ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const outputItemSchema = Type.Union([
|
|
19
|
+
Type.Object({ type: Type.Literal("text"), text: Type.String() }),
|
|
20
|
+
Type.Object({ type: Type.Literal("image"), data: Type.String(), mimeType: Type.String() }),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const toolDeclSchema = Type.Object({
|
|
24
|
+
name: Type.String(),
|
|
25
|
+
description: Type.Optional(Type.String()),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const startSchema = Type.Object({
|
|
29
|
+
t: Type.Literal("start"),
|
|
30
|
+
code: Type.String(),
|
|
31
|
+
tools: Type.Array(toolDeclSchema),
|
|
32
|
+
store: Type.Record(Type.String(), Type.Unknown()),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const resultSchema = Type.Union([
|
|
36
|
+
Type.Object({
|
|
37
|
+
t: Type.Literal("result"),
|
|
38
|
+
id: Type.Number(),
|
|
39
|
+
ok: Type.Literal(true),
|
|
40
|
+
value: Type.Unknown(),
|
|
41
|
+
}),
|
|
42
|
+
Type.Object({
|
|
43
|
+
t: Type.Literal("result"),
|
|
44
|
+
id: Type.Number(),
|
|
45
|
+
ok: Type.Literal(false),
|
|
46
|
+
error: Type.String(),
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const callSchema = Type.Object({
|
|
51
|
+
t: Type.Literal("call"),
|
|
52
|
+
id: Type.Number(),
|
|
53
|
+
name: Type.String(),
|
|
54
|
+
args: Type.Unknown(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const outputSchema = Type.Object({
|
|
58
|
+
t: Type.Literal("output"),
|
|
59
|
+
items: Type.Array(outputItemSchema),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const scriptErrorSchema = Type.Object({
|
|
63
|
+
kind: Type.Union([
|
|
64
|
+
Type.Literal("script"),
|
|
65
|
+
Type.Literal("timeout"),
|
|
66
|
+
Type.Literal("aborted"),
|
|
67
|
+
Type.Literal("sandbox"),
|
|
68
|
+
]),
|
|
69
|
+
name: Type.Optional(Type.String()),
|
|
70
|
+
message: Type.String(),
|
|
71
|
+
stack: Type.Optional(Type.String()),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const storeWritesSchema = Type.Object({
|
|
75
|
+
set: Type.Record(Type.String(), Type.Unknown()),
|
|
76
|
+
delete: Type.Array(Type.String()),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const doneSchema = Type.Union([
|
|
80
|
+
Type.Object({
|
|
81
|
+
t: Type.Literal("done"),
|
|
82
|
+
ok: Type.Literal(true),
|
|
83
|
+
value: Type.Unknown(),
|
|
84
|
+
writes: storeWritesSchema,
|
|
85
|
+
}),
|
|
86
|
+
Type.Object({
|
|
87
|
+
t: Type.Literal("done"),
|
|
88
|
+
ok: Type.Literal(false),
|
|
89
|
+
error: scriptErrorSchema,
|
|
90
|
+
writes: storeWritesSchema,
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const hostMessageSchema = Type.Union([startSchema, resultSchema]);
|
|
95
|
+
const workerMessageSchema = Type.Union([callSchema, outputSchema, doneSchema]);
|
|
96
|
+
|
|
97
|
+
/** worker 启动数据:注册时编译好的 wasm 模块(结构化克隆可以带它跨线程)。 */
|
|
98
|
+
export interface WorkerBootstrap {
|
|
99
|
+
wasm: object;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 类型 ─────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export type CodemodeOutputItem =
|
|
105
|
+
{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
|
|
106
|
+
|
|
107
|
+
export type ScriptErrorKind = "script" | "timeout" | "aborted" | "sandbox";
|
|
108
|
+
|
|
109
|
+
export interface ScriptError {
|
|
110
|
+
kind: ScriptErrorKind;
|
|
111
|
+
name?: string;
|
|
112
|
+
message: string;
|
|
113
|
+
stack?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 脚本侧的 store 写入:`set` 是覆写的键值,`delete` 是被删除的键。 */
|
|
117
|
+
export interface StoreWrites {
|
|
118
|
+
set: Record<string, unknown>;
|
|
119
|
+
delete: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ScriptTool {
|
|
123
|
+
name: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type HostMessage =
|
|
128
|
+
| { t: "start"; code: string; tools: ScriptTool[]; store: Record<string, unknown> }
|
|
129
|
+
| { t: "result"; id: number; ok: true; value: unknown }
|
|
130
|
+
| { t: "result"; id: number; ok: false; error: string };
|
|
131
|
+
|
|
132
|
+
export type WorkerMessage =
|
|
133
|
+
| { t: "call"; id: number; name: string; args: unknown }
|
|
134
|
+
| { t: "output"; items: CodemodeOutputItem[] }
|
|
135
|
+
| { t: "done"; ok: true; value: unknown; writes: StoreWrites }
|
|
136
|
+
| { t: "done"; ok: false; error: ScriptError; writes: StoreWrites };
|
|
137
|
+
|
|
138
|
+
export type DecodeResult<T> = { ok: true; frame: T } | { ok: false; error: string };
|
|
139
|
+
|
|
140
|
+
function decode<T>(schema: TSchema, value: unknown): DecodeResult<T> {
|
|
141
|
+
if (!Value.Check(schema, value)) {
|
|
142
|
+
const preview = JSON.stringify(value).slice(0, 200);
|
|
143
|
+
return { ok: false, error: `unexpected message shape: ${preview}` };
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, frame: value as T };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 校验主线程发给 worker 的消息。 */
|
|
149
|
+
export function decodeHostMessage(value: unknown): DecodeResult<HostMessage> {
|
|
150
|
+
return decode<HostMessage>(hostMessageSchema, value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 校验 worker 发回主线程的消息。 */
|
|
154
|
+
export function decodeWorkerMessage(value: unknown): DecodeResult<WorkerMessage> {
|
|
155
|
+
return decode<WorkerMessage>(workerMessageSchema, value);
|
|
156
|
+
}
|