pi-ptc-subagents 7.2.0 → 7.4.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/README.md +37 -3
- package/dist/index.js +829 -623
- package/dist/ptc/cli.js +61 -0
- package/dist/ptc/fs-bindings-DCGErhvt.js +423 -0
- package/dist/ptc/index.js +77 -0
- package/dist/ptc/worker.js +197 -0
- package/docs/routes/create-a-task.md +3 -0
- package/docs/routes/manage-a-task.md +57 -3
- package/package.json +9 -4
package/dist/ptc/cli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as runPtcProgram, t as createFsBindings } from "./fs-bindings-DCGErhvt.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
const USAGE = `用法:ptc <program.ts> [--workdir <dir>] [--timeout <ms>] [--json]
|
|
7
|
+
|
|
8
|
+
程序在运行格里跑(看不见 process / fetch / 动态 import),能调的只有文件四件套
|
|
9
|
+
(read / write / grep / glob / edit),且只在 workdir 内活动。`;
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
let program = "";
|
|
12
|
+
let workdir = process.cwd();
|
|
13
|
+
let timeoutMs;
|
|
14
|
+
let json = false;
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--json") json = true;
|
|
18
|
+
else if (arg === "--workdir" || arg === "-w") workdir = argv[++i] ?? workdir;
|
|
19
|
+
else if (arg === "--timeout") timeoutMs = Number(argv[++i]);
|
|
20
|
+
else if (arg.startsWith("-")) throw new Error(`未知参数 ${arg}`);
|
|
21
|
+
else if (program) throw new Error(`只能跑一个程序(多给了 ${arg})`);
|
|
22
|
+
else program = arg;
|
|
23
|
+
}
|
|
24
|
+
if (!program) throw new Error(USAGE);
|
|
25
|
+
if (timeoutMs !== void 0 && !Number.isFinite(timeoutMs)) throw new Error("--timeout 要一个毫秒数");
|
|
26
|
+
return {
|
|
27
|
+
program,
|
|
28
|
+
workdir: resolve(workdir),
|
|
29
|
+
timeoutMs,
|
|
30
|
+
json
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function isVoid(value) {
|
|
34
|
+
return typeof value === "object" && value !== null && "$void" in value;
|
|
35
|
+
}
|
|
36
|
+
async function main() {
|
|
37
|
+
const argv = process.argv.slice(2);
|
|
38
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
39
|
+
console.log(USAGE);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const options = parseArgs(argv);
|
|
43
|
+
const outcome = await runPtcProgram({
|
|
44
|
+
code: readFileSync(options.program, "utf8"),
|
|
45
|
+
cwd: options.workdir,
|
|
46
|
+
timeoutMs: options.timeoutMs,
|
|
47
|
+
bindings: createFsBindings(options.workdir)
|
|
48
|
+
});
|
|
49
|
+
for (const line of outcome.logs) console.error(line);
|
|
50
|
+
if (options.json) console.log(JSON.stringify(outcome, null, 2));
|
|
51
|
+
else if (outcome.status === "fulfilled") console.log(isVoid(outcome.value) ? "(程序无返回值)" : JSON.stringify(outcome.value, null, 2));
|
|
52
|
+
if (outcome.status !== "fulfilled") {
|
|
53
|
+
console.error(outcome.status === "canceled" ? outcome.reason : outcome.error);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
main().catch((error) => {
|
|
58
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
});
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,423 @@
|
|
|
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 };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { i as DEFAULT_LIMITS, n as runPtcProgram, r as spawnPtcWorker, t as createFsBindings } from "./fs-bindings-DCGErhvt.js";
|
|
2
|
+
// src/pool.ts
|
|
3
|
+
var WorkerPool = class {
|
|
4
|
+
#spawn;
|
|
5
|
+
#capacity;
|
|
6
|
+
#idle = [];
|
|
7
|
+
#waiters = [];
|
|
8
|
+
#retired = /* @__PURE__ */ new WeakSet();
|
|
9
|
+
#spawned = 0;
|
|
10
|
+
#draining = false;
|
|
11
|
+
constructor(spawn, capacity = DEFAULT_LIMITS.poolSize) {
|
|
12
|
+
this.#spawn = spawn;
|
|
13
|
+
this.#capacity = capacity;
|
|
14
|
+
}
|
|
15
|
+
acquire(timeoutMs = DEFAULT_LIMITS.poolAcquireTimeoutMs) {
|
|
16
|
+
const idle = this.#idle.pop();
|
|
17
|
+
if (idle) return Promise.resolve(idle);
|
|
18
|
+
if (this.#spawned < this.#capacity) {
|
|
19
|
+
this.#draining = false;
|
|
20
|
+
this.#spawned++;
|
|
21
|
+
return Promise.resolve(this.#track(this.#spawn()));
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
const index = this.#waiters.findIndex((w) => w.timer === timer);
|
|
26
|
+
if (index >= 0) this.#waiters.splice(index, 1);
|
|
27
|
+
reject(/* @__PURE__ */ new Error(`worker acquire timed out after ${timeoutMs}ms`));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
this.#waiters.push({
|
|
30
|
+
resolve,
|
|
31
|
+
timer
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
release(worker) {
|
|
36
|
+
if (this.#draining) {
|
|
37
|
+
this.#retire(worker);
|
|
38
|
+
worker.terminate();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const waiter = this.#waiters.shift();
|
|
42
|
+
if (waiter) {
|
|
43
|
+
clearTimeout(waiter.timer);
|
|
44
|
+
waiter.resolve(worker);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.#idle.push(worker);
|
|
48
|
+
}
|
|
49
|
+
/** 被杀/卡死/崩溃的 worker:收回名额并摘出空闲表(terminate 幂等) */
|
|
50
|
+
discard(worker) {
|
|
51
|
+
this.#retire(worker);
|
|
52
|
+
const index = this.#idle.indexOf(worker);
|
|
53
|
+
if (index >= 0) this.#idle.splice(index, 1);
|
|
54
|
+
worker.terminate();
|
|
55
|
+
}
|
|
56
|
+
drain() {
|
|
57
|
+
this.#draining = true;
|
|
58
|
+
for (const worker of this.#idle.splice(0)) {
|
|
59
|
+
this.#retire(worker);
|
|
60
|
+
worker.terminate();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
#track(worker) {
|
|
64
|
+
worker.once("exit", () => {
|
|
65
|
+
this.#retire(worker);
|
|
66
|
+
const index = this.#idle.indexOf(worker);
|
|
67
|
+
if (index >= 0) this.#idle.splice(index, 1);
|
|
68
|
+
});
|
|
69
|
+
return worker;
|
|
70
|
+
}
|
|
71
|
+
#retire(worker) {
|
|
72
|
+
if (this.#retired.has(worker)) return;
|
|
73
|
+
this.#retired.add(worker);
|
|
74
|
+
this.#spawned = Math.max(0, this.#spawned - 1);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export { DEFAULT_LIMITS, WorkerPool, createFsBindings, runPtcProgram, spawnPtcWorker };
|