pi-ptc-subagents 7.7.0 → 7.8.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/CHANGELOG.md +30 -0
- package/dist/index.js +42 -6060
- package/dist/ptc/cli.js +3 -58
- package/dist/ptc/index.js +1 -77
- package/dist/ptc-engine.js +3 -0
- package/dist/worker.js +3 -145
- package/package.json +6 -6
- package/dist/ptc/fs-bindings-DCGErhvt.js +0 -423
- package/dist/ptc/worker.js +0 -197
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import { Worker } from "node:worker_threads";
|
|
2
|
-
import { existsSync, globSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { basename, dirname, resolve, sep } from "node:path";
|
|
5
|
-
// src/limits.ts
|
|
6
|
-
const DEFAULT_LIMITS = {
|
|
7
|
-
timeoutMs: 12e4,
|
|
8
|
-
maxTimeoutMs: 6e5,
|
|
9
|
-
graceMs: 3e3,
|
|
10
|
-
maxOutputBytes: 67108864,
|
|
11
|
-
maxPendingCalls: 128,
|
|
12
|
-
maxParallelSubCalls: 10,
|
|
13
|
-
poolSize: 4,
|
|
14
|
-
poolAcquireTimeoutMs: 3e4
|
|
15
|
-
};
|
|
16
|
-
function effectiveTimeoutMs(requested) {
|
|
17
|
-
if (requested === void 0) return DEFAULT_LIMITS.timeoutMs;
|
|
18
|
-
return Math.min(Math.max(requested, 1e3), DEFAULT_LIMITS.maxTimeoutMs);
|
|
19
|
-
}
|
|
20
|
-
// src/sub-calls.ts
|
|
21
|
-
const PREVIEW_MAX_CHARS = 60;
|
|
22
|
-
var SubCallTracker = class {
|
|
23
|
-
#records = /* @__PURE__ */ new Map();
|
|
24
|
-
#order = [];
|
|
25
|
-
start(callId, name, args) {
|
|
26
|
-
let preview = "";
|
|
27
|
-
try {
|
|
28
|
-
preview = JSON.stringify(args) ?? "";
|
|
29
|
-
} catch {
|
|
30
|
-
preview = String(args);
|
|
31
|
-
}
|
|
32
|
-
this.#records.set(callId, {
|
|
33
|
-
callId,
|
|
34
|
-
name,
|
|
35
|
-
argsPreview: preview.slice(0, PREVIEW_MAX_CHARS),
|
|
36
|
-
status: "running",
|
|
37
|
-
startedAt: performance.now()
|
|
38
|
-
});
|
|
39
|
-
this.#order.push(callId);
|
|
40
|
-
}
|
|
41
|
-
end(callId, status) {
|
|
42
|
-
const record = this.#records.get(callId);
|
|
43
|
-
if (!record) return;
|
|
44
|
-
record.status = status;
|
|
45
|
-
record.durationMs = Math.round(performance.now() - record.startedAt);
|
|
46
|
-
}
|
|
47
|
-
snapshot() {
|
|
48
|
-
return this.#order.map((callId) => this.#records.get(callId));
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
// src/worker-source.ts
|
|
52
|
-
function buildWorkerUrl(from) {
|
|
53
|
-
const built = new URL("./worker.js", from);
|
|
54
|
-
if (existsSync(built)) return built;
|
|
55
|
-
return new URL("../src/worker-entry.ts", from);
|
|
56
|
-
}
|
|
57
|
-
// src/dispatcher.ts
|
|
58
|
-
const WORKER_ENV_ALLOW_LIST = [
|
|
59
|
-
"PATH",
|
|
60
|
-
"PATHEXT",
|
|
61
|
-
"SYSTEMROOT",
|
|
62
|
-
"WINDIR",
|
|
63
|
-
"TEMP",
|
|
64
|
-
"TMP"
|
|
65
|
-
];
|
|
66
|
-
function workerEnv() {
|
|
67
|
-
return Object.fromEntries(WORKER_ENV_ALLOW_LIST.filter((key) => key in process.env).map((key) => [key, process.env[key]]));
|
|
68
|
-
}
|
|
69
|
-
function spawnPtcWorker() {
|
|
70
|
-
return new Worker(buildWorkerUrl(import.meta.url), {
|
|
71
|
-
env: workerEnv(),
|
|
72
|
-
execArgv: [],
|
|
73
|
-
resourceLimits: {
|
|
74
|
-
maxOldGenerationSizeMb: 512,
|
|
75
|
-
maxYoungGenerationSizeMb: 64
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
async function runPtcProgram(request) {
|
|
80
|
-
const timeoutMs = effectiveTimeoutMs(request.timeoutMs);
|
|
81
|
-
const cwd = request.cwd ?? process.cwd();
|
|
82
|
-
const worker = request.pool ? await request.pool.acquire() : spawnPtcWorker();
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
const runId = crypto.randomUUID();
|
|
85
|
-
const logs = [];
|
|
86
|
-
const images = [];
|
|
87
|
-
const subCalls = new SubCallTracker();
|
|
88
|
-
let pendingCalls = 0;
|
|
89
|
-
let runningCalls = 0;
|
|
90
|
-
const callQueue = [];
|
|
91
|
-
const controllers = /* @__PURE__ */ new Set();
|
|
92
|
-
let settled = false;
|
|
93
|
-
let waitingReset = false;
|
|
94
|
-
let resetTimer;
|
|
95
|
-
const send = (frame) => worker.postMessage(frame);
|
|
96
|
-
const finish = (outcome) => {
|
|
97
|
-
if (settled) return;
|
|
98
|
-
settled = true;
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
callQueue.length = 0;
|
|
101
|
-
for (const abort of controllers) abort.abort();
|
|
102
|
-
const withExtras = {
|
|
103
|
-
...outcome,
|
|
104
|
-
subCalls: subCalls.snapshot()
|
|
105
|
-
};
|
|
106
|
-
if (withExtras.status === "fulfilled" && images.length > 0) withExtras.images = images;
|
|
107
|
-
resolve(withExtras);
|
|
108
|
-
if (request.pool) {
|
|
109
|
-
waitingReset = true;
|
|
110
|
-
resetTimer = setTimeout(() => {
|
|
111
|
-
if (!waitingReset) return;
|
|
112
|
-
waitingReset = false;
|
|
113
|
-
request.pool.discard(worker);
|
|
114
|
-
}, DEFAULT_LIMITS.graceMs);
|
|
115
|
-
resetTimer.unref();
|
|
116
|
-
send({ kind: "reset" });
|
|
117
|
-
} else worker.terminate();
|
|
118
|
-
};
|
|
119
|
-
const timer = setTimeout(() => {
|
|
120
|
-
send({
|
|
121
|
-
kind: "cancel",
|
|
122
|
-
id: runId,
|
|
123
|
-
reason: "timeout"
|
|
124
|
-
});
|
|
125
|
-
finish({
|
|
126
|
-
status: "canceled",
|
|
127
|
-
reason: `timeout after ${timeoutMs}ms`,
|
|
128
|
-
logs
|
|
129
|
-
});
|
|
130
|
-
}, timeoutMs);
|
|
131
|
-
const onMessage = (frame) => {
|
|
132
|
-
if (settled && frame.kind !== "ready") return;
|
|
133
|
-
if (frame.kind === "ready") {
|
|
134
|
-
if (waitingReset) {
|
|
135
|
-
waitingReset = false;
|
|
136
|
-
if (resetTimer) clearTimeout(resetTimer);
|
|
137
|
-
worker.off("message", onMessage);
|
|
138
|
-
worker.off("error", onError);
|
|
139
|
-
worker.off("exit", onExit);
|
|
140
|
-
request.pool.release(worker);
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (frame.runId !== runId) return;
|
|
145
|
-
if (frame.kind === "log") {
|
|
146
|
-
logs.push(frame.text);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (frame.kind === "call") {
|
|
150
|
-
if (pendingCalls >= DEFAULT_LIMITS.maxPendingCalls) {
|
|
151
|
-
send({
|
|
152
|
-
kind: "result",
|
|
153
|
-
callId: frame.callId,
|
|
154
|
-
ok: false,
|
|
155
|
-
error: `maxPendingCalls ${DEFAULT_LIMITS.maxPendingCalls} reached`
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
pendingCalls++;
|
|
160
|
-
subCalls.start(frame.callId, frame.name, frame.args);
|
|
161
|
-
const execute = async () => {
|
|
162
|
-
if (settled) return;
|
|
163
|
-
const controller = new AbortController();
|
|
164
|
-
controllers.add(controller);
|
|
165
|
-
try {
|
|
166
|
-
const binding = request.bindings[frame.name];
|
|
167
|
-
if (!binding) {
|
|
168
|
-
subCalls.end(frame.callId, "rejected");
|
|
169
|
-
send({
|
|
170
|
-
kind: "result",
|
|
171
|
-
callId: frame.callId,
|
|
172
|
-
ok: false,
|
|
173
|
-
error: `unknown binding ${frame.name}`
|
|
174
|
-
});
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const value = await binding(frame.args, {
|
|
178
|
-
cwd,
|
|
179
|
-
emitImage: (image) => images.push(image),
|
|
180
|
-
signal: controller.signal
|
|
181
|
-
});
|
|
182
|
-
subCalls.end(frame.callId, "ok");
|
|
183
|
-
send({
|
|
184
|
-
kind: "result",
|
|
185
|
-
callId: frame.callId,
|
|
186
|
-
ok: true,
|
|
187
|
-
value: value === void 0 ? null : value
|
|
188
|
-
});
|
|
189
|
-
} catch (error) {
|
|
190
|
-
subCalls.end(frame.callId, "error");
|
|
191
|
-
send({
|
|
192
|
-
kind: "result",
|
|
193
|
-
callId: frame.callId,
|
|
194
|
-
ok: false,
|
|
195
|
-
error: error instanceof Error ? error.message : String(error)
|
|
196
|
-
});
|
|
197
|
-
} finally {
|
|
198
|
-
controllers.delete(controller);
|
|
199
|
-
pendingCalls--;
|
|
200
|
-
runningCalls--;
|
|
201
|
-
const next = callQueue.shift();
|
|
202
|
-
if (next) {
|
|
203
|
-
runningCalls++;
|
|
204
|
-
next();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
if (runningCalls < DEFAULT_LIMITS.maxParallelSubCalls) {
|
|
209
|
-
runningCalls++;
|
|
210
|
-
execute();
|
|
211
|
-
} else callQueue.push(execute);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (frame.kind === "settled") {
|
|
215
|
-
const outcome = frame.outcome;
|
|
216
|
-
if (JSON.stringify({
|
|
217
|
-
logs: outcome.logs,
|
|
218
|
-
value: outcome.status === "fulfilled" ? outcome.value : void 0
|
|
219
|
-
}).length > DEFAULT_LIMITS.maxOutputBytes) {
|
|
220
|
-
finish({
|
|
221
|
-
status: "rejected",
|
|
222
|
-
error: `output exceeds ${DEFAULT_LIMITS.maxOutputBytes} bytes budget`,
|
|
223
|
-
logs
|
|
224
|
-
});
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
finish(outcome);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
worker.on("message", onMessage);
|
|
231
|
-
const onError = (error) => void finish({
|
|
232
|
-
status: "rejected",
|
|
233
|
-
error: `worker error: ${error instanceof Error ? error.message : String(error)}`,
|
|
234
|
-
logs
|
|
235
|
-
});
|
|
236
|
-
const onExit = (code) => {
|
|
237
|
-
if (!settled) finish({
|
|
238
|
-
status: "rejected",
|
|
239
|
-
error: `worker exited early with code ${code}`,
|
|
240
|
-
logs
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
worker.on("error", onError);
|
|
244
|
-
worker.on("exit", onExit);
|
|
245
|
-
send({
|
|
246
|
-
kind: "init",
|
|
247
|
-
surface: request.surface ?? "run_code",
|
|
248
|
-
bindingNames: Object.keys(request.bindings)
|
|
249
|
-
});
|
|
250
|
-
send({
|
|
251
|
-
kind: "run",
|
|
252
|
-
id: runId,
|
|
253
|
-
code: request.code,
|
|
254
|
-
args: request.args ?? {}
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
// src/fs-bindings.ts
|
|
259
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
260
|
-
const SKIP_SUFFIXES = /* @__PURE__ */ new Set([
|
|
261
|
-
".png",
|
|
262
|
-
".jpg",
|
|
263
|
-
".jpeg",
|
|
264
|
-
".gif",
|
|
265
|
-
".webp",
|
|
266
|
-
".ico",
|
|
267
|
-
".zip",
|
|
268
|
-
".gz",
|
|
269
|
-
".tgz",
|
|
270
|
-
".pdf",
|
|
271
|
-
".woff",
|
|
272
|
-
".woff2",
|
|
273
|
-
".map"
|
|
274
|
-
]);
|
|
275
|
-
const GREP_MAX_RESULTS = 500;
|
|
276
|
-
const GLOB_MAX_RESULTS = 1e3;
|
|
277
|
-
const READ_MAX_BYTES = 2097152;
|
|
278
|
-
const WRITE_MAX_BYTES = 2097152;
|
|
279
|
-
function strArg(args, key) {
|
|
280
|
-
const value = args?.[key];
|
|
281
|
-
if (typeof value !== "string" || value.length === 0) throw new Error(`${key} 必须是非空字符串`);
|
|
282
|
-
return value;
|
|
283
|
-
}
|
|
284
|
-
function jailed(workdirReal, p) {
|
|
285
|
-
const abs = resolve(workdirReal, p);
|
|
286
|
-
let real;
|
|
287
|
-
try {
|
|
288
|
-
real = realpathSync(abs);
|
|
289
|
-
} catch {
|
|
290
|
-
real = resolve(realpathSync(dirname(abs)), basename(abs));
|
|
291
|
-
}
|
|
292
|
-
if (real !== workdirReal && !real.startsWith(workdirReal + sep)) throw new Error(`path escapes workdir: ${p}`);
|
|
293
|
-
return real;
|
|
294
|
-
}
|
|
295
|
-
function* walk(dir) {
|
|
296
|
-
let entries;
|
|
297
|
-
try {
|
|
298
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
299
|
-
} catch {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
for (const entry of entries) if (entry.isDirectory()) {
|
|
303
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
304
|
-
yield* walk(resolve(dir, entry.name));
|
|
305
|
-
} else if (entry.isFile()) yield resolve(dir, entry.name);
|
|
306
|
-
}
|
|
307
|
-
function readPlainText(file) {
|
|
308
|
-
const stat = statSync(file);
|
|
309
|
-
const dot = file.lastIndexOf(".");
|
|
310
|
-
const suffix = dot === -1 ? "" : file.slice(dot).toLowerCase();
|
|
311
|
-
if (stat.size > READ_MAX_BYTES || SKIP_SUFFIXES.has(suffix)) return void 0;
|
|
312
|
-
const text = readFileSync(file, "utf8");
|
|
313
|
-
return text.includes("�") ? void 0 : text;
|
|
314
|
-
}
|
|
315
|
-
function createFsBindings(workdir, onCall) {
|
|
316
|
-
let workdirReal;
|
|
317
|
-
try {
|
|
318
|
-
workdirReal = realpathSync(resolve(workdir));
|
|
319
|
-
} catch {
|
|
320
|
-
throw new Error(`workdir 不存在或不可达: ${workdir}(workdir 传用户工作区绝对路径)`);
|
|
321
|
-
}
|
|
322
|
-
const table = {
|
|
323
|
-
read: async (args) => {
|
|
324
|
-
const real = jailed(workdirReal, strArg(args, "path"));
|
|
325
|
-
const stat = statSync(real);
|
|
326
|
-
if (!stat.isFile()) throw new Error(`not a file: ${strArg(args, "path")}`);
|
|
327
|
-
if (stat.size > READ_MAX_BYTES) throw new Error(`file too large (${stat.size} bytes > ${READ_MAX_BYTES})`);
|
|
328
|
-
return readFileSync(real, "utf8");
|
|
329
|
-
},
|
|
330
|
-
write: async (args) => {
|
|
331
|
-
const p = strArg(args, "path");
|
|
332
|
-
const content = strArg(args, "content");
|
|
333
|
-
const bytes = Buffer.byteLength(content, "utf8");
|
|
334
|
-
if (bytes > WRITE_MAX_BYTES) throw new Error(`content too large (${bytes} bytes > ${WRITE_MAX_BYTES} bytes)`);
|
|
335
|
-
const abs = resolve(workdirReal, p);
|
|
336
|
-
const parentAbs = dirname(abs);
|
|
337
|
-
if (!existsSync(parentAbs)) throw new Error(`parent dir does not exist: ${dirname(p)}(write 不隐式建目录)`);
|
|
338
|
-
const parentReal = jailed(workdirReal, parentAbs);
|
|
339
|
-
const real = resolve(parentReal, basename(abs));
|
|
340
|
-
const tmp = `${real}.ptc-${randomUUID()}`;
|
|
341
|
-
writeFileSync(tmp, content, "utf8");
|
|
342
|
-
renameSync(tmp, real);
|
|
343
|
-
return { written: real.slice(workdirReal.length + 1) };
|
|
344
|
-
},
|
|
345
|
-
grep: async (args) => {
|
|
346
|
-
const pattern = strArg(args, "pattern");
|
|
347
|
-
const scope = typeof args?.path === "string" ? jailed(workdirReal, strArg(args, "path")) : workdirReal;
|
|
348
|
-
let regex;
|
|
349
|
-
try {
|
|
350
|
-
regex = new RegExp(pattern);
|
|
351
|
-
} catch {
|
|
352
|
-
throw new Error(`invalid pattern: ${pattern}`);
|
|
353
|
-
}
|
|
354
|
-
const hits = [];
|
|
355
|
-
const files = statSync(scope).isFile() ? [scope] : walk(scope);
|
|
356
|
-
outer: for (const file of files) {
|
|
357
|
-
let text;
|
|
358
|
-
try {
|
|
359
|
-
text = readPlainText(file);
|
|
360
|
-
} catch {
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (text === void 0) continue;
|
|
364
|
-
const lines = text.split("\n");
|
|
365
|
-
for (let i = 0; i < lines.length; i++) if (regex.test(lines[i])) {
|
|
366
|
-
hits.push(`${file.slice(workdirReal.length + 1)}:${i + 1}: ${lines[i]}`);
|
|
367
|
-
if (hits.length >= GREP_MAX_RESULTS) {
|
|
368
|
-
hits.push(`…(已截断:达到 ${GREP_MAX_RESULTS} 条上限)`);
|
|
369
|
-
break outer;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return hits;
|
|
374
|
-
},
|
|
375
|
-
glob: async (args) => {
|
|
376
|
-
const pattern = strArg(args, "pattern");
|
|
377
|
-
let found;
|
|
378
|
-
try {
|
|
379
|
-
found = globSync(pattern, { cwd: workdirReal });
|
|
380
|
-
} catch {
|
|
381
|
-
throw new Error(`invalid glob: ${pattern}`);
|
|
382
|
-
}
|
|
383
|
-
return found.slice(0, GLOB_MAX_RESULTS);
|
|
384
|
-
},
|
|
385
|
-
edit: async (args) => {
|
|
386
|
-
const real = jailed(workdirReal, strArg(args, "path"));
|
|
387
|
-
const oldString = strArg(args, "old_string");
|
|
388
|
-
const newString = strArg(args, "new_string");
|
|
389
|
-
const text = readFileSync(real, "utf8");
|
|
390
|
-
const count = text.split(oldString).length - 1;
|
|
391
|
-
if (count === 0) throw new Error("old_string not found");
|
|
392
|
-
if (count > 1) throw new Error(`old_string matches ${count} places — must be unique`);
|
|
393
|
-
const tmp = `${real}.ptc-${randomUUID()}`;
|
|
394
|
-
writeFileSync(tmp, text.replace(oldString, newString), "utf8");
|
|
395
|
-
renameSync(tmp, real);
|
|
396
|
-
return { edited: real.slice(workdirReal.length + 1) };
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
if (!onCall) return table;
|
|
400
|
-
return {
|
|
401
|
-
read: (a) => {
|
|
402
|
-
onCall();
|
|
403
|
-
return table.read(a);
|
|
404
|
-
},
|
|
405
|
-
write: (a) => {
|
|
406
|
-
onCall();
|
|
407
|
-
return table.write(a);
|
|
408
|
-
},
|
|
409
|
-
grep: (a) => {
|
|
410
|
-
onCall();
|
|
411
|
-
return table.grep(a);
|
|
412
|
-
},
|
|
413
|
-
glob: (a) => {
|
|
414
|
-
onCall();
|
|
415
|
-
return table.glob(a);
|
|
416
|
-
},
|
|
417
|
-
edit: (a) => {
|
|
418
|
-
onCall();
|
|
419
|
-
return table.edit(a);
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
export { DEFAULT_LIMITS as i, runPtcProgram as n, spawnPtcWorker as r, createFsBindings as t };
|
package/dist/ptc/worker.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { stripTypeScriptTypes } from "node:module";
|
|
2
|
-
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
-
import vm from "node:vm";
|
|
4
|
-
// src/compile.ts
|
|
5
|
-
function compileProgram(source) {
|
|
6
|
-
return `"use strict";\n${stripTypeScriptTypes(`async function __ptcProgram() {\n${source}\n}`, { mode: "strip" })}\n;globalThis.__ptcProgram = __ptcProgram;`;
|
|
7
|
-
}
|
|
8
|
-
// src/worker-main.ts
|
|
9
|
-
const CELL_PREAMBLE = `"use strict";
|
|
10
|
-
(function () {
|
|
11
|
-
var __bridge = globalThis.__ptcCall;
|
|
12
|
-
var __emit = globalThis.__ptcConsole;
|
|
13
|
-
var __args = JSON.parse(globalThis.__ptcArgsJson);
|
|
14
|
-
// contextified 全局属性不可 delete(vm 限制)但可写——置 undefined 即脱手,桥只剩序言闭包可达
|
|
15
|
-
globalThis.__ptcCall = undefined;
|
|
16
|
-
globalThis.__ptcConsole = undefined;
|
|
17
|
-
globalThis.__ptcArgsJson = undefined;
|
|
18
|
-
function __ptcFmt(v) {
|
|
19
|
-
if (typeof v === "string") return v;
|
|
20
|
-
try { var s = JSON.stringify(v); return s === undefined ? String(v) : s; } catch (e) { return String(v); }
|
|
21
|
-
}
|
|
22
|
-
function __ptcJoin(values) {
|
|
23
|
-
var out = [];
|
|
24
|
-
for (var i = 0; i < values.length; i++) out.push(__ptcFmt(values[i]));
|
|
25
|
-
return out.join(" ");
|
|
26
|
-
}
|
|
27
|
-
var __ptcTools = {};
|
|
28
|
-
__PTC_NAMES__.forEach(function (name) {
|
|
29
|
-
__ptcTools[name] = function (args) {
|
|
30
|
-
return new Promise(function (resolve, reject) {
|
|
31
|
-
__bridge(name, JSON.stringify(args === undefined ? null : args)).then(function (json) {
|
|
32
|
-
var value;
|
|
33
|
-
try { value = JSON.parse(json); } catch (e) { reject(e); return; }
|
|
34
|
-
resolve(value);
|
|
35
|
-
}, reject);
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
globalThis.tools = Object.freeze(__ptcTools);
|
|
40
|
-
globalThis.console = Object.freeze({
|
|
41
|
-
log: function () { __emit("log", __ptcJoin(arguments)); },
|
|
42
|
-
info: function () { __emit("info", __ptcJoin(arguments)); },
|
|
43
|
-
warn: function () { __emit("warn", __ptcJoin(arguments)); },
|
|
44
|
-
error: function () { __emit("error", __ptcJoin(arguments)); },
|
|
45
|
-
debug: function () { __emit("debug", __ptcJoin(arguments)); }
|
|
46
|
-
});
|
|
47
|
-
globalThis.args = Object.freeze(__args);
|
|
48
|
-
if (__PTC_WORKFLOW__) {
|
|
49
|
-
globalThis.workflow = Object.freeze({
|
|
50
|
-
log: function () { __emit("log", __ptcJoin(arguments)); },
|
|
51
|
-
phase: function (name) { __emit("log", "— phase: " + name + " —"); },
|
|
52
|
-
parallel: function (items) { return Promise.allSettled(items.map(function (run) { return run(); })); },
|
|
53
|
-
pipeline: function (items, step) {
|
|
54
|
-
var results = [];
|
|
55
|
-
return items.reduce(function (chain, item, index) {
|
|
56
|
-
return chain.then(function () { return Promise.resolve(step(item, index)).then(function (r) { results.push(r); }); });
|
|
57
|
-
}, Promise.resolve()).then(function () { return results; });
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
})();
|
|
62
|
-
`;
|
|
63
|
-
function workerMain(deps) {
|
|
64
|
-
let bindingNames = [];
|
|
65
|
-
let surface = "run_code";
|
|
66
|
-
let pending = /* @__PURE__ */ new Map();
|
|
67
|
-
let nextCallId = 1;
|
|
68
|
-
let currentRunId = null;
|
|
69
|
-
let logs = [];
|
|
70
|
-
deps.onFrame((frame) => {
|
|
71
|
-
if (frame.kind === "init") {
|
|
72
|
-
bindingNames = frame.bindingNames;
|
|
73
|
-
surface = frame.surface;
|
|
74
|
-
deps.post({ kind: "ready" });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (frame.kind === "reset") {
|
|
78
|
-
currentRunId = null;
|
|
79
|
-
for (const { reject } of pending.values()) reject(/* @__PURE__ */ new Error("run reset:worker 被复用"));
|
|
80
|
-
pending = /* @__PURE__ */ new Map();
|
|
81
|
-
deps.post({ kind: "ready" });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (frame.kind === "result") {
|
|
85
|
-
const entry = pending.get(frame.callId);
|
|
86
|
-
if (!entry) return;
|
|
87
|
-
pending.delete(frame.callId);
|
|
88
|
-
if (frame.ok) {
|
|
89
|
-
let json;
|
|
90
|
-
try {
|
|
91
|
-
json = JSON.stringify(frame.value ?? null);
|
|
92
|
-
} catch {
|
|
93
|
-
json = void 0;
|
|
94
|
-
}
|
|
95
|
-
if (json === void 0) entry.reject(/* @__PURE__ */ new Error("binding 返回值不可 JSON 序列化(如循环引用)"));
|
|
96
|
-
else entry.resolve(json);
|
|
97
|
-
} else entry.reject(new Error(frame.error));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (frame.kind === "cancel") return;
|
|
101
|
-
run(frame.code, frame.id, frame.args);
|
|
102
|
-
});
|
|
103
|
-
function preamble() {
|
|
104
|
-
return CELL_PREAMBLE.replace(/__PTC_NAMES__/g, () => JSON.stringify(bindingNames)).replace(/__PTC_WORKFLOW__/g, () => surface === "workflow" ? "true" : "false");
|
|
105
|
-
}
|
|
106
|
-
async function run(code, runId, args) {
|
|
107
|
-
currentRunId = runId;
|
|
108
|
-
logs = [];
|
|
109
|
-
const postLog = (level, text) => {
|
|
110
|
-
logs.push(text);
|
|
111
|
-
deps.post({
|
|
112
|
-
kind: "log",
|
|
113
|
-
runId,
|
|
114
|
-
level,
|
|
115
|
-
text
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
const outcome = await execute(code, vm.createContext({
|
|
119
|
-
__ptcArgsJson: JSON.stringify(args ?? {}),
|
|
120
|
-
__ptcCall(name, argsJson) {
|
|
121
|
-
if (currentRunId === null) return Promise.reject(/* @__PURE__ */ new Error("run 已结束:reset 后不再接受工具调用"));
|
|
122
|
-
let parsed = null;
|
|
123
|
-
try {
|
|
124
|
-
parsed = JSON.parse(argsJson);
|
|
125
|
-
} catch {
|
|
126
|
-
return Promise.reject(/* @__PURE__ */ new Error("工具参数不是合法 JSON"));
|
|
127
|
-
}
|
|
128
|
-
const call = new Promise((resolve, reject) => {
|
|
129
|
-
const callId = nextCallId++;
|
|
130
|
-
pending.set(callId, {
|
|
131
|
-
resolve,
|
|
132
|
-
reject
|
|
133
|
-
});
|
|
134
|
-
deps.post({
|
|
135
|
-
kind: "call",
|
|
136
|
-
runId: currentRunId ?? "",
|
|
137
|
-
callId,
|
|
138
|
-
name,
|
|
139
|
-
args: parsed
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
call.catch(() => {});
|
|
143
|
-
return call;
|
|
144
|
-
},
|
|
145
|
-
__ptcConsole(level, text) {
|
|
146
|
-
postLog(level, String(text));
|
|
147
|
-
}
|
|
148
|
-
}));
|
|
149
|
-
deps.post({
|
|
150
|
-
kind: "settled",
|
|
151
|
-
runId,
|
|
152
|
-
outcome
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
function errorMessage(error) {
|
|
156
|
-
if (error instanceof Error) return error.message;
|
|
157
|
-
if (error && typeof error === "object" && typeof error.message === "string") return error.message;
|
|
158
|
-
return String(error);
|
|
159
|
-
}
|
|
160
|
-
async function execute(code, context) {
|
|
161
|
-
try {
|
|
162
|
-
vm.runInContext(preamble(), context, { filename: "ptc_preamble.js" });
|
|
163
|
-
vm.runInContext(compileProgram(code), context, { filename: "run_code.js" });
|
|
164
|
-
const value = await context.__ptcProgram.call(context);
|
|
165
|
-
if (value === void 0) return {
|
|
166
|
-
status: "fulfilled",
|
|
167
|
-
value: { $void: true },
|
|
168
|
-
logs
|
|
169
|
-
};
|
|
170
|
-
const json = JSON.stringify(value);
|
|
171
|
-
if (json === void 0) return {
|
|
172
|
-
status: "rejected",
|
|
173
|
-
error: "程序返回值不可序列化",
|
|
174
|
-
logs
|
|
175
|
-
};
|
|
176
|
-
return {
|
|
177
|
-
status: "fulfilled",
|
|
178
|
-
value: JSON.parse(json),
|
|
179
|
-
logs
|
|
180
|
-
};
|
|
181
|
-
} catch (error) {
|
|
182
|
-
return {
|
|
183
|
-
status: "rejected",
|
|
184
|
-
error: errorMessage(error),
|
|
185
|
-
logs
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// src/worker-entry.ts
|
|
191
|
-
if (!parentPort) throw new Error("worker-entry 只能在 worker 线程里加载");
|
|
192
|
-
Object.freeze(workerData);
|
|
193
|
-
workerMain({
|
|
194
|
-
post: (frame) => parentPort.postMessage(frame),
|
|
195
|
-
onFrame: (handler) => parentPort.on("message", (frame) => handler(frame))
|
|
196
|
-
});
|
|
197
|
-
export {};
|