@snack-kit/porygon 0.1.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 +490 -0
- package/dist/index.d.ts +715 -0
- package/dist/index.js +1963 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
// src/porygon.ts
|
|
2
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3
|
+
|
|
4
|
+
// src/config/defaults.ts
|
|
5
|
+
var DEFAULT_CONFIG = {
|
|
6
|
+
defaultBackend: "claude",
|
|
7
|
+
backends: {},
|
|
8
|
+
defaults: {
|
|
9
|
+
timeoutMs: 3e5,
|
|
10
|
+
// 5 minutes
|
|
11
|
+
maxTurns: 50
|
|
12
|
+
},
|
|
13
|
+
proxy: void 0
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/config/schema.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/errors/index.ts
|
|
20
|
+
var PorygonError = class extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message, options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.name = "PorygonError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var AdapterNotFoundError = class extends PorygonError {
|
|
29
|
+
constructor(backend, options) {
|
|
30
|
+
super("ADAPTER_NOT_FOUND", `Adapter not found: ${backend}`, options);
|
|
31
|
+
this.name = "AdapterNotFoundError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var AdapterNotAvailableError = class extends PorygonError {
|
|
35
|
+
constructor(backend, options) {
|
|
36
|
+
super("ADAPTER_NOT_AVAILABLE", `Adapter not available: ${backend}`, options);
|
|
37
|
+
this.name = "AdapterNotAvailableError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var AdapterIncompatibleError = class extends PorygonError {
|
|
41
|
+
constructor(backend, version, options) {
|
|
42
|
+
super(
|
|
43
|
+
"ADAPTER_INCOMPATIBLE",
|
|
44
|
+
`Adapter incompatible: ${backend} version ${version}`,
|
|
45
|
+
options
|
|
46
|
+
);
|
|
47
|
+
this.name = "AdapterIncompatibleError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var SessionNotFoundError = class extends PorygonError {
|
|
51
|
+
constructor(sessionId, options) {
|
|
52
|
+
super("SESSION_NOT_FOUND", `Session not found: ${sessionId}`, options);
|
|
53
|
+
this.name = "SessionNotFoundError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var InterceptorRejectedError = class extends PorygonError {
|
|
57
|
+
constructor(reason, options) {
|
|
58
|
+
super("INTERCEPTOR_REJECTED", `Interceptor rejected: ${reason}`, options);
|
|
59
|
+
this.name = "InterceptorRejectedError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var AgentExecutionError = class extends PorygonError {
|
|
63
|
+
constructor(message, options) {
|
|
64
|
+
super("AGENT_EXECUTION_ERROR", message, options);
|
|
65
|
+
this.name = "AgentExecutionError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var AgentTimeoutError = class extends PorygonError {
|
|
69
|
+
constructor(timeoutMs, options) {
|
|
70
|
+
super("AGENT_TIMEOUT", `Agent timed out after ${timeoutMs}ms`, options);
|
|
71
|
+
this.name = "AgentTimeoutError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var ConfigValidationError = class extends PorygonError {
|
|
75
|
+
constructor(message, options) {
|
|
76
|
+
super("CONFIG_VALIDATION_ERROR", message, options);
|
|
77
|
+
this.name = "ConfigValidationError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/config/schema.ts
|
|
82
|
+
var ProxyConfigSchema = z.object({
|
|
83
|
+
url: z.string().url(),
|
|
84
|
+
noProxy: z.string().optional()
|
|
85
|
+
});
|
|
86
|
+
var BackendConfigSchema = z.object({
|
|
87
|
+
model: z.string().optional(),
|
|
88
|
+
appendSystemPrompt: z.string().optional(),
|
|
89
|
+
proxy: ProxyConfigSchema.optional(),
|
|
90
|
+
cwd: z.string().optional(),
|
|
91
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
92
|
+
});
|
|
93
|
+
var PorygonConfigSchema = z.object({
|
|
94
|
+
defaultBackend: z.string().optional(),
|
|
95
|
+
backends: z.record(z.string(), BackendConfigSchema).optional(),
|
|
96
|
+
defaults: z.object({
|
|
97
|
+
appendSystemPrompt: z.string().optional(),
|
|
98
|
+
timeoutMs: z.number().positive().optional(),
|
|
99
|
+
maxTurns: z.number().int().positive().optional()
|
|
100
|
+
}).optional(),
|
|
101
|
+
proxy: ProxyConfigSchema.optional()
|
|
102
|
+
});
|
|
103
|
+
function validateConfig(config) {
|
|
104
|
+
const result = PorygonConfigSchema.safeParse(config);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
const issues = result.error.issues.map((issue) => ({
|
|
107
|
+
path: issue.path.join("."),
|
|
108
|
+
message: issue.message
|
|
109
|
+
}));
|
|
110
|
+
throw new ConfigValidationError(
|
|
111
|
+
`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return result.data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/config/config-loader.ts
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
function deepMerge(target, source) {
|
|
122
|
+
const result = { ...target };
|
|
123
|
+
for (const key of Object.keys(source)) {
|
|
124
|
+
const sourceVal = source[key];
|
|
125
|
+
const targetVal = result[key];
|
|
126
|
+
if (sourceVal === void 0) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
|
|
130
|
+
result[key] = deepMerge(
|
|
131
|
+
targetVal,
|
|
132
|
+
sourceVal
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
result[key] = sourceVal;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
function loadEnvOverrides() {
|
|
141
|
+
const config = {};
|
|
142
|
+
const defaultBackend = process.env["PORYGON_DEFAULT_BACKEND"];
|
|
143
|
+
if (defaultBackend) {
|
|
144
|
+
config.defaultBackend = defaultBackend;
|
|
145
|
+
}
|
|
146
|
+
const proxyUrl = process.env["PORYGON_PROXY_URL"];
|
|
147
|
+
if (proxyUrl) {
|
|
148
|
+
config.proxy = {
|
|
149
|
+
url: proxyUrl,
|
|
150
|
+
noProxy: process.env["PORYGON_PROXY_NO_PROXY"]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return config;
|
|
154
|
+
}
|
|
155
|
+
var ConfigLoader = class {
|
|
156
|
+
/**
|
|
157
|
+
* 加载并合并配置
|
|
158
|
+
* @param userConfig - 用户传入的配置(可选)
|
|
159
|
+
* @returns 校验通过的最终配置
|
|
160
|
+
* @throws ConfigValidationError 校验失败时抛出
|
|
161
|
+
*/
|
|
162
|
+
static load(userConfig) {
|
|
163
|
+
let merged = { ...DEFAULT_CONFIG };
|
|
164
|
+
const envOverrides = loadEnvOverrides();
|
|
165
|
+
merged = deepMerge(merged, envOverrides);
|
|
166
|
+
if (userConfig) {
|
|
167
|
+
merged = deepMerge(merged, userConfig);
|
|
168
|
+
}
|
|
169
|
+
return validateConfig(merged);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/interceptor/interceptor.ts
|
|
174
|
+
var InterceptorManager = class {
|
|
175
|
+
inputInterceptors = [];
|
|
176
|
+
outputInterceptors = [];
|
|
177
|
+
/**
|
|
178
|
+
* 注册拦截器
|
|
179
|
+
* @param direction - 拦截方向
|
|
180
|
+
* @param fn - 拦截器函数
|
|
181
|
+
* @returns 取消注册的函数
|
|
182
|
+
*/
|
|
183
|
+
use(direction, fn) {
|
|
184
|
+
const list = direction === "input" ? this.inputInterceptors : this.outputInterceptors;
|
|
185
|
+
list.push(fn);
|
|
186
|
+
return () => {
|
|
187
|
+
const idx = list.indexOf(fn);
|
|
188
|
+
if (idx !== -1) {
|
|
189
|
+
list.splice(idx, 1);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 执行输入拦截器流水线
|
|
195
|
+
* @param text - 原始输入文本
|
|
196
|
+
* @param context - 拦截器上下文(不含 direction)
|
|
197
|
+
* @returns 处理后的文本
|
|
198
|
+
* @throws InterceptorRejectedError 拦截器返回 false 时抛出
|
|
199
|
+
*/
|
|
200
|
+
async processInput(text, context) {
|
|
201
|
+
return this.runPipeline(text, "input", context, this.inputInterceptors);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 执行输出拦截器流水线
|
|
205
|
+
* @param text - 原始输出文本
|
|
206
|
+
* @param context - 拦截器上下文(不含 direction)
|
|
207
|
+
* @returns 处理后的文本
|
|
208
|
+
* @throws InterceptorRejectedError 拦截器返回 false 时抛出
|
|
209
|
+
*/
|
|
210
|
+
async processOutput(text, context) {
|
|
211
|
+
return this.runPipeline(text, "output", context, this.outputInterceptors);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 执行拦截器流水线
|
|
215
|
+
*/
|
|
216
|
+
async runPipeline(text, direction, context, interceptors) {
|
|
217
|
+
const fullContext = { ...context, direction };
|
|
218
|
+
let current = text;
|
|
219
|
+
for (const fn of interceptors) {
|
|
220
|
+
const result = await fn(current, fullContext);
|
|
221
|
+
if (result === false) {
|
|
222
|
+
throw new InterceptorRejectedError(direction);
|
|
223
|
+
}
|
|
224
|
+
if (typeof result === "string") {
|
|
225
|
+
current = result;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return current;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/process/process-handle.ts
|
|
233
|
+
import { spawn } from "child_process";
|
|
234
|
+
import { EventEmitter } from "events";
|
|
235
|
+
import { createInterface } from "readline";
|
|
236
|
+
var GRACE_PERIOD_MS = 5e3;
|
|
237
|
+
var EphemeralProcess = class {
|
|
238
|
+
childProcess = null;
|
|
239
|
+
aborted = false;
|
|
240
|
+
/**
|
|
241
|
+
* 执行命令并收集输出。支持 AbortController 进行取消操作。
|
|
242
|
+
* @param options 进程启动选项
|
|
243
|
+
* @param abortSignal 可选的中止信号
|
|
244
|
+
* @returns 进程执行结果
|
|
245
|
+
*/
|
|
246
|
+
async execute(options, abortSignal) {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
if (abortSignal?.aborted) {
|
|
249
|
+
reject(new Error("Process aborted before start"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.aborted = false;
|
|
253
|
+
const child = spawn(options.command, options.args, {
|
|
254
|
+
cwd: options.cwd,
|
|
255
|
+
env: options.env ? { ...process.env, ...options.env } : void 0,
|
|
256
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
257
|
+
});
|
|
258
|
+
this.childProcess = child;
|
|
259
|
+
const stdoutChunks = [];
|
|
260
|
+
const stderrChunks = [];
|
|
261
|
+
child.stdout?.on("data", (chunk) => {
|
|
262
|
+
stdoutChunks.push(chunk);
|
|
263
|
+
});
|
|
264
|
+
child.stderr?.on("data", (chunk) => {
|
|
265
|
+
stderrChunks.push(chunk);
|
|
266
|
+
});
|
|
267
|
+
let timeoutTimer;
|
|
268
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
269
|
+
timeoutTimer = setTimeout(() => {
|
|
270
|
+
this.terminate();
|
|
271
|
+
reject(new Error(`Process timed out after ${options.timeoutMs}ms`));
|
|
272
|
+
}, options.timeoutMs);
|
|
273
|
+
}
|
|
274
|
+
const onAbort = () => {
|
|
275
|
+
this.terminate();
|
|
276
|
+
reject(new Error("Process aborted"));
|
|
277
|
+
};
|
|
278
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
279
|
+
child.on("error", (err) => {
|
|
280
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
281
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
282
|
+
this.childProcess = null;
|
|
283
|
+
reject(err);
|
|
284
|
+
});
|
|
285
|
+
child.on("close", (code, signal) => {
|
|
286
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
287
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
288
|
+
this.childProcess = null;
|
|
289
|
+
resolve({
|
|
290
|
+
exitCode: code,
|
|
291
|
+
signal,
|
|
292
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
293
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8")
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 以流式方式执行命令,逐行输出 stdout 内容。
|
|
300
|
+
* @param options 进程启动选项
|
|
301
|
+
* @param abortSignal 可选的中止信号
|
|
302
|
+
*/
|
|
303
|
+
async *executeStreaming(options, abortSignal) {
|
|
304
|
+
if (abortSignal?.aborted) {
|
|
305
|
+
throw new Error("Process aborted before start");
|
|
306
|
+
}
|
|
307
|
+
this.aborted = false;
|
|
308
|
+
const child = spawn(options.command, options.args, {
|
|
309
|
+
cwd: options.cwd,
|
|
310
|
+
env: options.env ?? void 0,
|
|
311
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
312
|
+
});
|
|
313
|
+
this.childProcess = child;
|
|
314
|
+
let timeoutTimer;
|
|
315
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
316
|
+
timeoutTimer = setTimeout(() => {
|
|
317
|
+
this.terminate();
|
|
318
|
+
}, options.timeoutMs);
|
|
319
|
+
}
|
|
320
|
+
const onAbort = () => {
|
|
321
|
+
this.terminate();
|
|
322
|
+
};
|
|
323
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
324
|
+
const stderrChunks = [];
|
|
325
|
+
child.stderr?.on("data", (chunk) => {
|
|
326
|
+
stderrChunks.push(chunk);
|
|
327
|
+
});
|
|
328
|
+
let exitCode = null;
|
|
329
|
+
const exitPromise = new Promise((resolve) => {
|
|
330
|
+
child.on("close", (code) => {
|
|
331
|
+
exitCode = code;
|
|
332
|
+
resolve();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
const rl = createInterface({ input: child.stdout });
|
|
336
|
+
let yieldedLines = 0;
|
|
337
|
+
try {
|
|
338
|
+
for await (const line of rl) {
|
|
339
|
+
yieldedLines++;
|
|
340
|
+
yield line;
|
|
341
|
+
}
|
|
342
|
+
await exitPromise;
|
|
343
|
+
if (exitCode !== 0 && yieldedLines === 0 && !this.aborted) {
|
|
344
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
345
|
+
throw new Error(
|
|
346
|
+
stderr || `Process exited with code ${exitCode}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
} finally {
|
|
350
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
351
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
352
|
+
rl.close();
|
|
353
|
+
this.childProcess = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** 获取底层子进程的 PID */
|
|
357
|
+
get pid() {
|
|
358
|
+
return this.childProcess?.pid;
|
|
359
|
+
}
|
|
360
|
+
/** 终止进程:先发送 SIGTERM,超时后发送 SIGKILL */
|
|
361
|
+
terminate() {
|
|
362
|
+
if (!this.childProcess || this.aborted) return;
|
|
363
|
+
this.aborted = true;
|
|
364
|
+
this.childProcess.kill("SIGTERM");
|
|
365
|
+
const ref = this.childProcess;
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
if (!ref.killed) {
|
|
368
|
+
ref.kill("SIGKILL");
|
|
369
|
+
}
|
|
370
|
+
}, GRACE_PERIOD_MS);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
var PersistentProcess = class extends EventEmitter {
|
|
374
|
+
process = null;
|
|
375
|
+
restartCount = 0;
|
|
376
|
+
stopped = false;
|
|
377
|
+
healthCheckTimer = null;
|
|
378
|
+
options;
|
|
379
|
+
constructor(options) {
|
|
380
|
+
super();
|
|
381
|
+
this.options = options;
|
|
382
|
+
}
|
|
383
|
+
/** 启动持久进程 */
|
|
384
|
+
async start() {
|
|
385
|
+
this.stopped = false;
|
|
386
|
+
const child = spawn(this.options.command, this.options.args, {
|
|
387
|
+
cwd: this.options.cwd,
|
|
388
|
+
env: this.options.env ? { ...process.env, ...this.options.env } : void 0,
|
|
389
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
390
|
+
});
|
|
391
|
+
this.process = child;
|
|
392
|
+
child.on("error", (err) => {
|
|
393
|
+
this.emit("error", err);
|
|
394
|
+
});
|
|
395
|
+
child.on("exit", (code) => {
|
|
396
|
+
this.process = null;
|
|
397
|
+
this.stopHealthCheck();
|
|
398
|
+
this.handleCrash(code);
|
|
399
|
+
});
|
|
400
|
+
if (this.options.healthCheckFn && this.options.healthCheckIntervalMs > 0) {
|
|
401
|
+
this.healthCheckTimer = setInterval(() => {
|
|
402
|
+
this.options.healthCheckFn().catch((err) => {
|
|
403
|
+
this.emit("healthCheckFailed", err);
|
|
404
|
+
});
|
|
405
|
+
}, this.options.healthCheckIntervalMs);
|
|
406
|
+
}
|
|
407
|
+
this.emit("started", child.pid);
|
|
408
|
+
}
|
|
409
|
+
/** 处理进程崩溃,按指数退避策略重启 */
|
|
410
|
+
handleCrash(code) {
|
|
411
|
+
if (this.stopped) return;
|
|
412
|
+
this.emit("exit", code);
|
|
413
|
+
if (this.restartCount < this.options.maxRestarts) {
|
|
414
|
+
const delay = this.options.restartIntervalMs * Math.pow(2, this.restartCount);
|
|
415
|
+
this.restartCount++;
|
|
416
|
+
this.emit("restart", this.restartCount);
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
if (!this.stopped) {
|
|
419
|
+
this.start().catch((err) => {
|
|
420
|
+
this.emit("fatal", err);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}, delay);
|
|
424
|
+
} else {
|
|
425
|
+
this.emit(
|
|
426
|
+
"fatal",
|
|
427
|
+
new Error(
|
|
428
|
+
`Max restarts (${this.options.maxRestarts}) exceeded, last exit code: ${code}`
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** 停止健康检查定时器 */
|
|
434
|
+
stopHealthCheck() {
|
|
435
|
+
if (this.healthCheckTimer) {
|
|
436
|
+
clearInterval(this.healthCheckTimer);
|
|
437
|
+
this.healthCheckTimer = null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** 停止持久进程 */
|
|
441
|
+
async stop() {
|
|
442
|
+
this.stopped = true;
|
|
443
|
+
this.stopHealthCheck();
|
|
444
|
+
if (this.process) {
|
|
445
|
+
const child = this.process;
|
|
446
|
+
child.kill("SIGTERM");
|
|
447
|
+
await new Promise((resolve) => {
|
|
448
|
+
const timer = setTimeout(() => {
|
|
449
|
+
if (!child.killed) {
|
|
450
|
+
child.kill("SIGKILL");
|
|
451
|
+
}
|
|
452
|
+
resolve();
|
|
453
|
+
}, GRACE_PERIOD_MS);
|
|
454
|
+
child.on("exit", () => {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
resolve();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
this.process = null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/** 获取底层子进程的 PID */
|
|
463
|
+
get pid() {
|
|
464
|
+
return this.process?.pid;
|
|
465
|
+
}
|
|
466
|
+
/** 当前进程是否正在运行 */
|
|
467
|
+
get isRunning() {
|
|
468
|
+
return this.process !== null && !this.process.killed;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// src/process/process-manager.ts
|
|
473
|
+
var ProcessManager = class _ProcessManager {
|
|
474
|
+
ephemeral = /* @__PURE__ */ new Map();
|
|
475
|
+
persistent = /* @__PURE__ */ new Map();
|
|
476
|
+
/** 全局静态标记,确保信号处理器只注册一次 */
|
|
477
|
+
static cleanupRegistered = false;
|
|
478
|
+
/** 所有活跃的 ProcessManager 实例(弱引用避免阻止 GC) */
|
|
479
|
+
static instances = /* @__PURE__ */ new Set();
|
|
480
|
+
constructor() {
|
|
481
|
+
_ProcessManager.instances.add(this);
|
|
482
|
+
_ProcessManager.registerCleanup();
|
|
483
|
+
}
|
|
484
|
+
/** 注册进程退出清理钩子(仅清理资源,不劫持宿主退出行为) */
|
|
485
|
+
static registerCleanup() {
|
|
486
|
+
if (_ProcessManager.cleanupRegistered) return;
|
|
487
|
+
_ProcessManager.cleanupRegistered = true;
|
|
488
|
+
const cleanup = () => {
|
|
489
|
+
for (const instance of _ProcessManager.instances) {
|
|
490
|
+
instance.terminateAllSync();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
process.on("exit", cleanup);
|
|
494
|
+
process.on("SIGTERM", cleanup);
|
|
495
|
+
process.on("SIGINT", cleanup);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 创建一次性进程实例并注册到管理器
|
|
499
|
+
* @param sessionId 会话标识
|
|
500
|
+
* @returns 一次性进程实例
|
|
501
|
+
*/
|
|
502
|
+
createEphemeral(sessionId) {
|
|
503
|
+
const proc = new EphemeralProcess();
|
|
504
|
+
this.ephemeral.set(sessionId, proc);
|
|
505
|
+
return proc;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* 从管理器中移除一次性进程
|
|
509
|
+
* @param sessionId 会话标识
|
|
510
|
+
*/
|
|
511
|
+
removeEphemeral(sessionId) {
|
|
512
|
+
this.ephemeral.delete(sessionId);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 启动或获取持久进程
|
|
516
|
+
* @param backend 后端标识
|
|
517
|
+
* @param options 持久进程选项
|
|
518
|
+
* @returns 持久进程实例
|
|
519
|
+
*/
|
|
520
|
+
async startPersistent(backend, options) {
|
|
521
|
+
const existing = this.persistent.get(backend);
|
|
522
|
+
if (existing?.isRunning) return existing;
|
|
523
|
+
const proc = new PersistentProcess(options);
|
|
524
|
+
this.persistent.set(backend, proc);
|
|
525
|
+
await proc.start();
|
|
526
|
+
return proc;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* 获取指定后端的持久进程
|
|
530
|
+
* @param backend 后端标识
|
|
531
|
+
*/
|
|
532
|
+
getPersistent(backend) {
|
|
533
|
+
return this.persistent.get(backend);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* 获取指定会话的一次性进程
|
|
537
|
+
* @param sessionId 会话标识
|
|
538
|
+
*/
|
|
539
|
+
getEphemeral(sessionId) {
|
|
540
|
+
return this.ephemeral.get(sessionId);
|
|
541
|
+
}
|
|
542
|
+
/** 异步终止所有托管进程 */
|
|
543
|
+
async terminateAll() {
|
|
544
|
+
const promises = [];
|
|
545
|
+
for (const [, proc] of this.ephemeral) {
|
|
546
|
+
proc.terminate();
|
|
547
|
+
}
|
|
548
|
+
for (const [, proc] of this.persistent) {
|
|
549
|
+
promises.push(proc.stop());
|
|
550
|
+
}
|
|
551
|
+
this.ephemeral.clear();
|
|
552
|
+
await Promise.allSettled(promises);
|
|
553
|
+
this.persistent.clear();
|
|
554
|
+
}
|
|
555
|
+
/** 同步终止所有托管进程(用于 exit 钩子) */
|
|
556
|
+
terminateAllSync() {
|
|
557
|
+
for (const [, proc] of this.ephemeral) {
|
|
558
|
+
proc.terminate();
|
|
559
|
+
}
|
|
560
|
+
for (const [, proc] of this.persistent) {
|
|
561
|
+
const pid = proc.pid;
|
|
562
|
+
if (pid) {
|
|
563
|
+
try {
|
|
564
|
+
process.kill(pid, "SIGTERM");
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 销毁实例,从全局实例列表中移除
|
|
572
|
+
*/
|
|
573
|
+
destroy() {
|
|
574
|
+
_ProcessManager.instances.delete(this);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/session/session-manager.ts
|
|
579
|
+
var SessionManager = class {
|
|
580
|
+
cache = /* @__PURE__ */ new Map();
|
|
581
|
+
adapters = /* @__PURE__ */ new Map();
|
|
582
|
+
/**
|
|
583
|
+
* 注册适配器用于会话管理
|
|
584
|
+
* @param backend 后端标识
|
|
585
|
+
* @param adapter 适配器实例
|
|
586
|
+
*/
|
|
587
|
+
registerAdapter(backend, adapter) {
|
|
588
|
+
this.adapters.set(backend, adapter);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* 注销适配器
|
|
592
|
+
* @param backend 后端标识
|
|
593
|
+
*/
|
|
594
|
+
unregisterAdapter(backend) {
|
|
595
|
+
this.adapters.delete(backend);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 获取指定后端的适配器,不存在时抛出错误
|
|
599
|
+
* @param backend 后端标识
|
|
600
|
+
*/
|
|
601
|
+
getAdapter(backend) {
|
|
602
|
+
const adapter = this.adapters.get(backend);
|
|
603
|
+
if (!adapter) {
|
|
604
|
+
throw new AdapterNotFoundError(backend);
|
|
605
|
+
}
|
|
606
|
+
return adapter;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 列出指定后端的会话,结果会写入缓存
|
|
610
|
+
* @param backend 后端标识
|
|
611
|
+
* @param options 查询选项
|
|
612
|
+
*/
|
|
613
|
+
async list(backend, options) {
|
|
614
|
+
const adapter = this.getAdapter(backend);
|
|
615
|
+
const sessions = await adapter.listSessions(options);
|
|
616
|
+
for (const session of sessions) {
|
|
617
|
+
this.cache.set(session.sessionId, session);
|
|
618
|
+
}
|
|
619
|
+
return sessions;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 从缓存中获取会话信息
|
|
623
|
+
* @param sessionId 会话 ID
|
|
624
|
+
*/
|
|
625
|
+
get(sessionId) {
|
|
626
|
+
return this.cache.get(sessionId);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 恢复会话,委托适配器以 resume 标志执行查询
|
|
630
|
+
* @param backend 后端标识
|
|
631
|
+
* @param sessionId 要恢复的会话 ID
|
|
632
|
+
* @param prompt 提示词
|
|
633
|
+
*/
|
|
634
|
+
async *resume(backend, sessionId, prompt) {
|
|
635
|
+
const adapter = this.getAdapter(backend);
|
|
636
|
+
yield* adapter.query({ prompt, resume: sessionId });
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 从缓存中移除指定会话
|
|
640
|
+
* @param sessionId 会话 ID
|
|
641
|
+
*/
|
|
642
|
+
evict(sessionId) {
|
|
643
|
+
this.cache.delete(sessionId);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 清空全部会话缓存
|
|
647
|
+
*/
|
|
648
|
+
clearCache() {
|
|
649
|
+
this.cache.clear();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/adapters/claude/index.ts
|
|
654
|
+
import { readFile, writeFile } from "fs/promises";
|
|
655
|
+
import { homedir } from "os";
|
|
656
|
+
import { join } from "path";
|
|
657
|
+
import { randomUUID } from "crypto";
|
|
658
|
+
|
|
659
|
+
// src/adapters/base.ts
|
|
660
|
+
function satisfiesRange(version, range) {
|
|
661
|
+
const parseVersion = (v) => {
|
|
662
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
663
|
+
if (!match) return null;
|
|
664
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
665
|
+
};
|
|
666
|
+
const trimmed = range.trim();
|
|
667
|
+
if (trimmed.startsWith(">=")) {
|
|
668
|
+
const rangeVersion2 = parseVersion(trimmed.slice(2));
|
|
669
|
+
const currentVersion2 = parseVersion(version);
|
|
670
|
+
if (!rangeVersion2 || !currentVersion2) return false;
|
|
671
|
+
for (let i = 0; i < 3; i++) {
|
|
672
|
+
if (currentVersion2[i] > rangeVersion2[i]) return true;
|
|
673
|
+
if (currentVersion2[i] < rangeVersion2[i]) return false;
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
if (trimmed.startsWith("<=")) {
|
|
678
|
+
const rangeVersion2 = parseVersion(trimmed.slice(2));
|
|
679
|
+
const currentVersion2 = parseVersion(version);
|
|
680
|
+
if (!rangeVersion2 || !currentVersion2) return false;
|
|
681
|
+
for (let i = 0; i < 3; i++) {
|
|
682
|
+
if (currentVersion2[i] < rangeVersion2[i]) return true;
|
|
683
|
+
if (currentVersion2[i] > rangeVersion2[i]) return false;
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
const rangeVersion = parseVersion(trimmed);
|
|
688
|
+
const currentVersion = parseVersion(version);
|
|
689
|
+
if (!rangeVersion || !currentVersion) return false;
|
|
690
|
+
return currentVersion[0] === rangeVersion[0] && currentVersion[1] === rangeVersion[1] && currentVersion[2] === rangeVersion[2];
|
|
691
|
+
}
|
|
692
|
+
var AbstractAgentAdapter = class {
|
|
693
|
+
/** 活跃的 AbortController 映射,按 sessionId 索引 */
|
|
694
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
695
|
+
processManager;
|
|
696
|
+
config;
|
|
697
|
+
constructor(processManager, config) {
|
|
698
|
+
this.processManager = processManager;
|
|
699
|
+
this.config = config;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* 默认兼容性检查:获取当前版本并与 testedVersionRange 进行比较
|
|
703
|
+
*/
|
|
704
|
+
async checkCompatibility() {
|
|
705
|
+
const warnings = [];
|
|
706
|
+
let version;
|
|
707
|
+
try {
|
|
708
|
+
version = await this.getVersion();
|
|
709
|
+
} catch {
|
|
710
|
+
return {
|
|
711
|
+
version: "unknown",
|
|
712
|
+
supported: false,
|
|
713
|
+
warnings: [`\u65E0\u6CD5\u83B7\u53D6 ${this.backend} \u7248\u672C\u4FE1\u606F`]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const capabilities = this.getCapabilities();
|
|
717
|
+
const supported = satisfiesRange(version, capabilities.testedVersionRange);
|
|
718
|
+
if (!supported) {
|
|
719
|
+
warnings.push(
|
|
720
|
+
`${this.backend} \u7248\u672C ${version} \u4E0D\u5728\u6D4B\u8BD5\u8303\u56F4 ${capabilities.testedVersionRange} \u5185\uFF0C\u53EF\u80FD\u5B58\u5728\u517C\u5BB9\u6027\u95EE\u9898`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return { version, supported, warnings };
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 中止指定会话的执行
|
|
727
|
+
* @param sessionId 会话标识
|
|
728
|
+
*/
|
|
729
|
+
abort(sessionId) {
|
|
730
|
+
const controller = this.abortControllers.get(sessionId);
|
|
731
|
+
if (controller) {
|
|
732
|
+
controller.abort();
|
|
733
|
+
this.abortControllers.delete(sessionId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* 创建并注册 AbortController
|
|
738
|
+
* @param sessionId 会话标识
|
|
739
|
+
* @returns AbortController 实例
|
|
740
|
+
*/
|
|
741
|
+
createAbortController(sessionId) {
|
|
742
|
+
const existing = this.abortControllers.get(sessionId);
|
|
743
|
+
if (existing) {
|
|
744
|
+
existing.abort();
|
|
745
|
+
}
|
|
746
|
+
const controller = new AbortController();
|
|
747
|
+
this.abortControllers.set(sessionId, controller);
|
|
748
|
+
return controller;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* 清理指定会话的 AbortController
|
|
752
|
+
* @param sessionId 会话标识
|
|
753
|
+
*/
|
|
754
|
+
removeAbortController(sessionId) {
|
|
755
|
+
this.abortControllers.delete(sessionId);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/adapters/claude/types.ts
|
|
760
|
+
function isSystemEvent(event) {
|
|
761
|
+
if (typeof event !== "object" || event === null) return false;
|
|
762
|
+
return event["type"] === "system";
|
|
763
|
+
}
|
|
764
|
+
function isAssistantEvent(event) {
|
|
765
|
+
if (typeof event !== "object" || event === null) return false;
|
|
766
|
+
const obj = event;
|
|
767
|
+
return obj["type"] === "assistant" && typeof obj["message"] === "object" && obj["message"] !== null;
|
|
768
|
+
}
|
|
769
|
+
function isStreamEvent(event) {
|
|
770
|
+
if (typeof event !== "object" || event === null) return false;
|
|
771
|
+
const obj = event;
|
|
772
|
+
return obj["type"] === "stream_event" && typeof obj["event"] === "object" && obj["event"] !== null;
|
|
773
|
+
}
|
|
774
|
+
function isResultEvent(event) {
|
|
775
|
+
if (typeof event !== "object" || event === null) return false;
|
|
776
|
+
const obj = event;
|
|
777
|
+
return obj["type"] === "result" && "result" in obj;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/adapters/claude/message-mapper.ts
|
|
781
|
+
function mapClaudeEvent(event, sessionId) {
|
|
782
|
+
const timestamp = Date.now();
|
|
783
|
+
const baseFields = { timestamp, sessionId, raw: event };
|
|
784
|
+
if (isSystemEvent(event)) {
|
|
785
|
+
return {
|
|
786
|
+
...baseFields,
|
|
787
|
+
type: "system",
|
|
788
|
+
model: event.model,
|
|
789
|
+
tools: event.tools,
|
|
790
|
+
cwd: event.cwd
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (isAssistantEvent(event)) {
|
|
794
|
+
const content = event.message.content;
|
|
795
|
+
return mapAssistantContent(content, baseFields);
|
|
796
|
+
}
|
|
797
|
+
if (isStreamEvent(event)) {
|
|
798
|
+
const delta = event.event.delta;
|
|
799
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
800
|
+
return {
|
|
801
|
+
...baseFields,
|
|
802
|
+
type: "stream_chunk",
|
|
803
|
+
text: delta.text
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
if (isResultEvent(event)) {
|
|
809
|
+
return {
|
|
810
|
+
...baseFields,
|
|
811
|
+
type: "result",
|
|
812
|
+
text: event.result,
|
|
813
|
+
costUsd: event.cost_usd,
|
|
814
|
+
durationMs: event.duration_ms,
|
|
815
|
+
inputTokens: event.input_tokens,
|
|
816
|
+
outputTokens: event.output_tokens
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function mapAssistantContent(blocks, baseFields) {
|
|
822
|
+
if (!blocks || blocks.length === 0) return null;
|
|
823
|
+
const textParts = [];
|
|
824
|
+
let firstToolUse = null;
|
|
825
|
+
for (const block of blocks) {
|
|
826
|
+
if (block.type === "text" && block.text) {
|
|
827
|
+
textParts.push(block.text);
|
|
828
|
+
}
|
|
829
|
+
if (block.type === "tool_use" && !firstToolUse) {
|
|
830
|
+
firstToolUse = { name: block.name, input: block.input };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (textParts.length > 0) {
|
|
834
|
+
return {
|
|
835
|
+
...baseFields,
|
|
836
|
+
type: "assistant",
|
|
837
|
+
text: textParts.join("\n")
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (firstToolUse) {
|
|
841
|
+
return {
|
|
842
|
+
...baseFields,
|
|
843
|
+
type: "tool_use",
|
|
844
|
+
toolName: firstToolUse.name,
|
|
845
|
+
input: firstToolUse.input
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/adapters/claude/index.ts
|
|
852
|
+
var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
853
|
+
var CLAUDE_MODELS = [
|
|
854
|
+
{ id: "sonnet", name: "Sonnet", provider: "anthropic" },
|
|
855
|
+
{ id: "opus", name: "Opus", provider: "anthropic" },
|
|
856
|
+
{ id: "haiku", name: "Haiku", provider: "anthropic" }
|
|
857
|
+
];
|
|
858
|
+
var ClaudeAdapter = class extends AbstractAgentAdapter {
|
|
859
|
+
backend = "claude";
|
|
860
|
+
/**
|
|
861
|
+
* 检查 Claude CLI 是否可用
|
|
862
|
+
*/
|
|
863
|
+
async isAvailable() {
|
|
864
|
+
try {
|
|
865
|
+
const proc = new EphemeralProcess();
|
|
866
|
+
const result = await proc.execute({
|
|
867
|
+
command: "which",
|
|
868
|
+
args: ["claude"]
|
|
869
|
+
});
|
|
870
|
+
return result.exitCode === 0;
|
|
871
|
+
} catch {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* 获取 Claude CLI 版本号
|
|
877
|
+
*/
|
|
878
|
+
async getVersion() {
|
|
879
|
+
const proc = new EphemeralProcess();
|
|
880
|
+
const result = await proc.execute({
|
|
881
|
+
command: "claude",
|
|
882
|
+
args: ["--version"]
|
|
883
|
+
});
|
|
884
|
+
if (result.exitCode !== 0) {
|
|
885
|
+
throw new Error(`\u83B7\u53D6 Claude \u7248\u672C\u5931\u8D25: ${result.stderr}`);
|
|
886
|
+
}
|
|
887
|
+
const output = result.stdout.trim();
|
|
888
|
+
const match = /(\d+\.\d+\.\d+)/.exec(output);
|
|
889
|
+
if (!match) {
|
|
890
|
+
throw new Error(`\u65E0\u6CD5\u89E3\u6790 Claude \u7248\u672C\u53F7: ${output}`);
|
|
891
|
+
}
|
|
892
|
+
return match[1];
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* 获取 Claude 适配器能力声明
|
|
896
|
+
*/
|
|
897
|
+
getCapabilities() {
|
|
898
|
+
return {
|
|
899
|
+
features: /* @__PURE__ */ new Set([
|
|
900
|
+
"streaming",
|
|
901
|
+
"session-resume",
|
|
902
|
+
"system-prompt",
|
|
903
|
+
"tool-restriction",
|
|
904
|
+
"mcp",
|
|
905
|
+
"subagents",
|
|
906
|
+
"worktree"
|
|
907
|
+
]),
|
|
908
|
+
outputFormats: ["text", "json", "stream-json"],
|
|
909
|
+
testedVersionRange: ">=1.0.0"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* 向 Claude 发送提示并以流式方式接收响应
|
|
914
|
+
* @param request 提示请求参数
|
|
915
|
+
*/
|
|
916
|
+
async *query(request) {
|
|
917
|
+
const sessionId = request.resume ?? randomUUID();
|
|
918
|
+
const controller = this.createAbortController(sessionId);
|
|
919
|
+
const proc = this.processManager.createEphemeral(sessionId);
|
|
920
|
+
try {
|
|
921
|
+
const args = this.buildArgs(request);
|
|
922
|
+
const cwd = request.cwd ?? this.config?.cwd;
|
|
923
|
+
const cleanEnv = {};
|
|
924
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
925
|
+
if (k !== "CLAUDECODE" && v !== void 0) {
|
|
926
|
+
cleanEnv[k] = v;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const proxyUrl = this.config?.proxy?.url;
|
|
930
|
+
if (proxyUrl) {
|
|
931
|
+
this.applyProxyEnv(cleanEnv, proxyUrl, this.config?.proxy?.noProxy);
|
|
932
|
+
}
|
|
933
|
+
if (request.envVars) {
|
|
934
|
+
for (const [k, v] of Object.entries(request.envVars)) {
|
|
935
|
+
if (v) cleanEnv[k] = v;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const streamOptions = {
|
|
939
|
+
command: "claude",
|
|
940
|
+
args,
|
|
941
|
+
...cwd ? { cwd } : {},
|
|
942
|
+
env: cleanEnv,
|
|
943
|
+
timeoutMs: request.timeoutMs
|
|
944
|
+
};
|
|
945
|
+
const cmdStr = ["claude", ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
|
|
946
|
+
const debugCmd = cwd ? `cd ${JSON.stringify(cwd)} && ${cmdStr}` : cmdStr;
|
|
947
|
+
yield {
|
|
948
|
+
type: "system",
|
|
949
|
+
timestamp: Date.now(),
|
|
950
|
+
sessionId,
|
|
951
|
+
cwd,
|
|
952
|
+
raw: { debug_command: debugCmd }
|
|
953
|
+
};
|
|
954
|
+
for await (const line of proc.executeStreaming(
|
|
955
|
+
streamOptions,
|
|
956
|
+
controller.signal
|
|
957
|
+
)) {
|
|
958
|
+
const trimmed = line.trim();
|
|
959
|
+
if (!trimmed) continue;
|
|
960
|
+
let parsed;
|
|
961
|
+
try {
|
|
962
|
+
parsed = JSON.parse(trimmed);
|
|
963
|
+
} catch {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const message = mapClaudeEvent(parsed, sessionId);
|
|
967
|
+
if (message) {
|
|
968
|
+
yield message;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} finally {
|
|
972
|
+
this.removeAbortController(sessionId);
|
|
973
|
+
this.processManager.removeEphemeral(sessionId);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* 列出 Claude 会话
|
|
978
|
+
* @param options 查询选项
|
|
979
|
+
*/
|
|
980
|
+
async listSessions(options) {
|
|
981
|
+
const proc = new EphemeralProcess();
|
|
982
|
+
const args = ["sessions", "list", "--output", "json"];
|
|
983
|
+
if (options?.limit) {
|
|
984
|
+
args.push("--limit", String(options.limit));
|
|
985
|
+
}
|
|
986
|
+
const result = await proc.execute({
|
|
987
|
+
command: "claude",
|
|
988
|
+
args,
|
|
989
|
+
cwd: options?.cwd
|
|
990
|
+
});
|
|
991
|
+
if (result.exitCode !== 0) {
|
|
992
|
+
throw new Error(`\u83B7\u53D6 Claude \u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${result.stderr}`);
|
|
993
|
+
}
|
|
994
|
+
const output = result.stdout.trim();
|
|
995
|
+
if (!output) return [];
|
|
996
|
+
let rawSessions;
|
|
997
|
+
try {
|
|
998
|
+
rawSessions = JSON.parse(output);
|
|
999
|
+
} catch {
|
|
1000
|
+
throw new Error(`\u89E3\u6790 Claude \u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${output.slice(0, 200)}`);
|
|
1001
|
+
}
|
|
1002
|
+
if (!Array.isArray(rawSessions)) return [];
|
|
1003
|
+
return rawSessions.map((session) => {
|
|
1004
|
+
const s = session;
|
|
1005
|
+
return {
|
|
1006
|
+
sessionId: String(s["id"] ?? s["session_id"] ?? ""),
|
|
1007
|
+
backend: this.backend,
|
|
1008
|
+
summary: typeof s["summary"] === "string" ? s["summary"] : void 0,
|
|
1009
|
+
lastModified: typeof s["last_modified"] === "number" ? s["last_modified"] : typeof s["last_modified"] === "string" ? new Date(s["last_modified"]).getTime() : Date.now(),
|
|
1010
|
+
cwd: typeof s["cwd"] === "string" ? s["cwd"] : void 0,
|
|
1011
|
+
metadata: s
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* 获取 Claude 可用模型列表。
|
|
1017
|
+
* Claude CLI 支持使用简短别名(sonnet/opus/haiku)自动选择当前最新版本。
|
|
1018
|
+
*/
|
|
1019
|
+
async listModels() {
|
|
1020
|
+
return CLAUDE_MODELS;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* 读取 Claude 设置
|
|
1024
|
+
*/
|
|
1025
|
+
async getSettings() {
|
|
1026
|
+
try {
|
|
1027
|
+
const content = await readFile(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
1028
|
+
const parsed = JSON.parse(content);
|
|
1029
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
1030
|
+
return parsed;
|
|
1031
|
+
}
|
|
1032
|
+
return {};
|
|
1033
|
+
} catch {
|
|
1034
|
+
return {};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* 更新 Claude 设置(读取-合并-写入)
|
|
1039
|
+
* @param settings 要合并的设置项
|
|
1040
|
+
*/
|
|
1041
|
+
async updateSettings(settings) {
|
|
1042
|
+
const current = await this.getSettings();
|
|
1043
|
+
const merged = { ...current, ...settings };
|
|
1044
|
+
await writeFile(
|
|
1045
|
+
CLAUDE_SETTINGS_PATH,
|
|
1046
|
+
JSON.stringify(merged, null, 2),
|
|
1047
|
+
"utf-8"
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* 释放适配器资源
|
|
1052
|
+
*/
|
|
1053
|
+
async dispose() {
|
|
1054
|
+
for (const [sessionId] of this.abortControllers) {
|
|
1055
|
+
this.abort(sessionId);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* 将代理配置注入到环境变量中
|
|
1060
|
+
* @param env 环境变量对象
|
|
1061
|
+
* @param proxyUrl 代理地址
|
|
1062
|
+
* @param noProxy 不走代理的地址列表
|
|
1063
|
+
*/
|
|
1064
|
+
applyProxyEnv(env, proxyUrl, noProxy) {
|
|
1065
|
+
env["http_proxy"] = proxyUrl;
|
|
1066
|
+
env["https_proxy"] = proxyUrl;
|
|
1067
|
+
env["HTTP_PROXY"] = proxyUrl;
|
|
1068
|
+
env["HTTPS_PROXY"] = proxyUrl;
|
|
1069
|
+
env["all_proxy"] = proxyUrl;
|
|
1070
|
+
env["ALL_PROXY"] = proxyUrl;
|
|
1071
|
+
if (noProxy) {
|
|
1072
|
+
env["no_proxy"] = noProxy;
|
|
1073
|
+
env["NO_PROXY"] = noProxy;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
buildArgs(request) {
|
|
1077
|
+
const args = [
|
|
1078
|
+
"-p",
|
|
1079
|
+
request.prompt,
|
|
1080
|
+
"--output-format",
|
|
1081
|
+
"stream-json",
|
|
1082
|
+
"--verbose"
|
|
1083
|
+
];
|
|
1084
|
+
if (request.resume) {
|
|
1085
|
+
args.push("--resume", request.resume);
|
|
1086
|
+
}
|
|
1087
|
+
const model = request.model ?? this.config?.model;
|
|
1088
|
+
if (model) {
|
|
1089
|
+
args.push("--model", model);
|
|
1090
|
+
}
|
|
1091
|
+
if (request.systemPrompt) {
|
|
1092
|
+
args.push("--system-prompt", request.systemPrompt);
|
|
1093
|
+
}
|
|
1094
|
+
const appendPrompt = request.appendSystemPrompt ?? this.config?.appendSystemPrompt;
|
|
1095
|
+
if (appendPrompt) {
|
|
1096
|
+
args.push("--append-system-prompt", appendPrompt);
|
|
1097
|
+
}
|
|
1098
|
+
if (request.allowedTools && request.allowedTools.length > 0) {
|
|
1099
|
+
for (const tool of request.allowedTools) {
|
|
1100
|
+
args.push("--allowedTools", tool);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (request.disallowedTools && request.disallowedTools.length > 0) {
|
|
1104
|
+
for (const tool of request.disallowedTools) {
|
|
1105
|
+
args.push("--disallowedTools", tool);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (request.maxTurns !== void 0) {
|
|
1109
|
+
args.push("--max-turns", String(request.maxTurns));
|
|
1110
|
+
}
|
|
1111
|
+
if (request.mcpServers) {
|
|
1112
|
+
for (const [name, config] of Object.entries(request.mcpServers)) {
|
|
1113
|
+
const serverSpec = {
|
|
1114
|
+
command: config.command,
|
|
1115
|
+
...config.args ? { args: config.args } : {},
|
|
1116
|
+
...config.env ? { env: config.env } : {},
|
|
1117
|
+
...config.url ? { url: config.url } : {}
|
|
1118
|
+
};
|
|
1119
|
+
args.push("--mcp-server", `${name}=${JSON.stringify(serverSpec)}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return args;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
// src/adapters/opencode/index.ts
|
|
1127
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
1128
|
+
import { homedir as homedir2 } from "os";
|
|
1129
|
+
import { join as join2 } from "path";
|
|
1130
|
+
|
|
1131
|
+
// src/adapters/opencode/types.ts
|
|
1132
|
+
var OpenCodeApiError = class extends Error {
|
|
1133
|
+
statusCode;
|
|
1134
|
+
responseBody;
|
|
1135
|
+
constructor(statusCode, responseBody) {
|
|
1136
|
+
super(`OpenCode API error (${statusCode}): ${responseBody}`);
|
|
1137
|
+
this.name = "OpenCodeApiError";
|
|
1138
|
+
this.statusCode = statusCode;
|
|
1139
|
+
this.responseBody = responseBody;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// src/adapters/opencode/api-client.ts
|
|
1144
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1145
|
+
var OpenCodeApiClient = class {
|
|
1146
|
+
baseUrl;
|
|
1147
|
+
/**
|
|
1148
|
+
* @param baseUrl opencode serve 服务的基础 URL,例如 http://localhost:39393
|
|
1149
|
+
*/
|
|
1150
|
+
constructor(baseUrl) {
|
|
1151
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 创建新会话
|
|
1155
|
+
* @param cwd 可选的工作目录
|
|
1156
|
+
* @returns 包含会话 ID 的对象
|
|
1157
|
+
*/
|
|
1158
|
+
async createSession(cwd) {
|
|
1159
|
+
const body = {};
|
|
1160
|
+
if (cwd) {
|
|
1161
|
+
body.cwd = cwd;
|
|
1162
|
+
}
|
|
1163
|
+
const response = await this.request(
|
|
1164
|
+
"/api/session",
|
|
1165
|
+
{
|
|
1166
|
+
method: "POST",
|
|
1167
|
+
body: JSON.stringify(body)
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
return response;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* 获取所有会话列表
|
|
1174
|
+
* @returns 会话数组
|
|
1175
|
+
*/
|
|
1176
|
+
async listSessions() {
|
|
1177
|
+
const response = await this.request(
|
|
1178
|
+
"/api/sessions",
|
|
1179
|
+
{ method: "GET" }
|
|
1180
|
+
);
|
|
1181
|
+
return response.sessions;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* 向指定会话发送消息
|
|
1185
|
+
* @param sessionId 会话 ID
|
|
1186
|
+
* @param content 消息内容
|
|
1187
|
+
*/
|
|
1188
|
+
async sendMessage(sessionId, content) {
|
|
1189
|
+
await this.request(
|
|
1190
|
+
`/api/session/${encodeURIComponent(sessionId)}/message`,
|
|
1191
|
+
{
|
|
1192
|
+
method: "POST",
|
|
1193
|
+
body: JSON.stringify({ content })
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* 以 SSE 流式方式订阅会话事件
|
|
1199
|
+
* @param sessionId 会话 ID
|
|
1200
|
+
* @param abortSignal 可选的中止信号
|
|
1201
|
+
* @yields OpenCode SSE 事件
|
|
1202
|
+
*/
|
|
1203
|
+
async *streamEvents(sessionId, abortSignal) {
|
|
1204
|
+
const url = `${this.baseUrl}/api/session/${encodeURIComponent(sessionId)}/events`;
|
|
1205
|
+
const response = await fetch(url, {
|
|
1206
|
+
method: "GET",
|
|
1207
|
+
headers: {
|
|
1208
|
+
Accept: "text/event-stream",
|
|
1209
|
+
"Cache-Control": "no-cache"
|
|
1210
|
+
},
|
|
1211
|
+
signal: abortSignal
|
|
1212
|
+
});
|
|
1213
|
+
if (!response.ok) {
|
|
1214
|
+
const body = await response.text().catch(() => "");
|
|
1215
|
+
throw new OpenCodeApiError(response.status, body);
|
|
1216
|
+
}
|
|
1217
|
+
if (!response.body) {
|
|
1218
|
+
throw new Error("Response body is null, SSE streaming unavailable");
|
|
1219
|
+
}
|
|
1220
|
+
const reader = response.body.getReader();
|
|
1221
|
+
const decoder = new TextDecoder();
|
|
1222
|
+
let buffer = "";
|
|
1223
|
+
try {
|
|
1224
|
+
while (true) {
|
|
1225
|
+
const { done, value } = await reader.read();
|
|
1226
|
+
if (done) break;
|
|
1227
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1228
|
+
const parts = buffer.split("\n\n");
|
|
1229
|
+
buffer = parts.pop() ?? "";
|
|
1230
|
+
for (const part of parts) {
|
|
1231
|
+
const event = this.parseSseEvent(part);
|
|
1232
|
+
if (event) {
|
|
1233
|
+
yield event;
|
|
1234
|
+
if (event.type === "done") {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (buffer.trim()) {
|
|
1241
|
+
const event = this.parseSseEvent(buffer);
|
|
1242
|
+
if (event) {
|
|
1243
|
+
yield event;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} finally {
|
|
1247
|
+
reader.releaseLock();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* 健康检查:验证 opencode serve 服务是否可达
|
|
1252
|
+
* @returns 服务是否健康
|
|
1253
|
+
*/
|
|
1254
|
+
async healthCheck() {
|
|
1255
|
+
try {
|
|
1256
|
+
const controller = new AbortController();
|
|
1257
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1258
|
+
const response = await fetch(`${this.baseUrl}/api/sessions`, {
|
|
1259
|
+
method: "GET",
|
|
1260
|
+
signal: controller.signal
|
|
1261
|
+
});
|
|
1262
|
+
clearTimeout(timer);
|
|
1263
|
+
return response.ok;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* 解析单个 SSE 事件文本块
|
|
1270
|
+
* @param raw 原始 SSE 文本块
|
|
1271
|
+
* @returns 解析后的事件对象,解析失败返回 null
|
|
1272
|
+
*/
|
|
1273
|
+
parseSseEvent(raw) {
|
|
1274
|
+
const lines = raw.split("\n");
|
|
1275
|
+
let data = "";
|
|
1276
|
+
for (const line of lines) {
|
|
1277
|
+
if (line.startsWith(":")) continue;
|
|
1278
|
+
if (line.startsWith("data: ")) {
|
|
1279
|
+
data += line.slice(6);
|
|
1280
|
+
} else if (line.startsWith("data:")) {
|
|
1281
|
+
data += line.slice(5);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (!data) return null;
|
|
1285
|
+
try {
|
|
1286
|
+
return JSON.parse(data);
|
|
1287
|
+
} catch {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 发送 HTTP 请求的通用方法
|
|
1293
|
+
* @param path API 路径
|
|
1294
|
+
* @param init fetch 请求选项
|
|
1295
|
+
* @returns 解析后的 JSON 响应
|
|
1296
|
+
*/
|
|
1297
|
+
async request(path, init) {
|
|
1298
|
+
const controller = new AbortController();
|
|
1299
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
1300
|
+
try {
|
|
1301
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1302
|
+
...init,
|
|
1303
|
+
headers: {
|
|
1304
|
+
"Content-Type": "application/json",
|
|
1305
|
+
...init.headers
|
|
1306
|
+
},
|
|
1307
|
+
signal: controller.signal
|
|
1308
|
+
});
|
|
1309
|
+
if (!response.ok) {
|
|
1310
|
+
const body = await response.text().catch(() => "");
|
|
1311
|
+
throw new OpenCodeApiError(response.status, body);
|
|
1312
|
+
}
|
|
1313
|
+
const text = await response.text();
|
|
1314
|
+
if (!text) return {};
|
|
1315
|
+
return JSON.parse(text);
|
|
1316
|
+
} finally {
|
|
1317
|
+
clearTimeout(timer);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/adapters/opencode/message-mapper.ts
|
|
1323
|
+
function mapOpenCodeEventToMessage(event) {
|
|
1324
|
+
const base = {
|
|
1325
|
+
timestamp: event.timestamp ?? Date.now(),
|
|
1326
|
+
sessionId: event.sessionId,
|
|
1327
|
+
raw: event
|
|
1328
|
+
};
|
|
1329
|
+
switch (event.type) {
|
|
1330
|
+
case "session.created":
|
|
1331
|
+
return {
|
|
1332
|
+
...base,
|
|
1333
|
+
type: "system",
|
|
1334
|
+
model: event.model,
|
|
1335
|
+
tools: event.tools,
|
|
1336
|
+
cwd: event.cwd
|
|
1337
|
+
};
|
|
1338
|
+
case "message.delta":
|
|
1339
|
+
return {
|
|
1340
|
+
...base,
|
|
1341
|
+
type: "stream_chunk",
|
|
1342
|
+
text: event.text
|
|
1343
|
+
};
|
|
1344
|
+
case "message.complete":
|
|
1345
|
+
return {
|
|
1346
|
+
...base,
|
|
1347
|
+
type: "result",
|
|
1348
|
+
text: event.text,
|
|
1349
|
+
durationMs: event.durationMs,
|
|
1350
|
+
costUsd: event.costUsd,
|
|
1351
|
+
inputTokens: event.inputTokens,
|
|
1352
|
+
outputTokens: event.outputTokens
|
|
1353
|
+
};
|
|
1354
|
+
case "tool_use.start":
|
|
1355
|
+
return {
|
|
1356
|
+
...base,
|
|
1357
|
+
type: "tool_use",
|
|
1358
|
+
toolName: event.toolName,
|
|
1359
|
+
input: event.input
|
|
1360
|
+
};
|
|
1361
|
+
case "tool_use.complete":
|
|
1362
|
+
return {
|
|
1363
|
+
...base,
|
|
1364
|
+
type: "tool_use",
|
|
1365
|
+
toolName: event.toolName,
|
|
1366
|
+
input: event.input,
|
|
1367
|
+
output: event.output
|
|
1368
|
+
};
|
|
1369
|
+
case "error":
|
|
1370
|
+
return {
|
|
1371
|
+
...base,
|
|
1372
|
+
type: "error",
|
|
1373
|
+
message: event.message,
|
|
1374
|
+
code: event.code
|
|
1375
|
+
};
|
|
1376
|
+
// message.start / tool_use.delta / done 事件不需要映射为独立消息
|
|
1377
|
+
case "message.start":
|
|
1378
|
+
case "tool_use.delta":
|
|
1379
|
+
case "done":
|
|
1380
|
+
return null;
|
|
1381
|
+
default:
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/adapters/opencode/index.ts
|
|
1387
|
+
var DEFAULT_SERVE_PORT = 39393;
|
|
1388
|
+
var SERVE_READY_TIMEOUT_MS = 3e4;
|
|
1389
|
+
var SERVE_POLL_INTERVAL_MS = 500;
|
|
1390
|
+
var TESTED_VERSION_RANGE = ">=0.1.0";
|
|
1391
|
+
var MAX_RESTARTS = 3;
|
|
1392
|
+
var RESTART_INTERVAL_MS = 2e3;
|
|
1393
|
+
var HEALTH_CHECK_INTERVAL_MS = 3e4;
|
|
1394
|
+
var OpenCodeAdapter = class extends AbstractAgentAdapter {
|
|
1395
|
+
backend = "opencode";
|
|
1396
|
+
apiClient = null;
|
|
1397
|
+
servePort;
|
|
1398
|
+
/**
|
|
1399
|
+
* @param processManager 进程管理器实例
|
|
1400
|
+
* @param servePort 可选的 serve 服务端口
|
|
1401
|
+
*/
|
|
1402
|
+
constructor(processManager, servePort) {
|
|
1403
|
+
super(processManager);
|
|
1404
|
+
this.servePort = servePort ?? DEFAULT_SERVE_PORT;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* 检查 opencode 命令是否可用
|
|
1408
|
+
*/
|
|
1409
|
+
async isAvailable() {
|
|
1410
|
+
try {
|
|
1411
|
+
const proc = new EphemeralProcess();
|
|
1412
|
+
const result = await proc.execute({
|
|
1413
|
+
command: "opencode",
|
|
1414
|
+
args: ["--version"],
|
|
1415
|
+
timeoutMs: 1e4
|
|
1416
|
+
});
|
|
1417
|
+
return result.exitCode === 0;
|
|
1418
|
+
} catch {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* 获取 opencode 版本号
|
|
1424
|
+
*/
|
|
1425
|
+
async getVersion() {
|
|
1426
|
+
const proc = new EphemeralProcess();
|
|
1427
|
+
const result = await proc.execute({
|
|
1428
|
+
command: "opencode",
|
|
1429
|
+
args: ["version"],
|
|
1430
|
+
timeoutMs: 1e4
|
|
1431
|
+
});
|
|
1432
|
+
if (result.exitCode !== 0) {
|
|
1433
|
+
throw new Error(`Failed to get opencode version: ${result.stderr}`);
|
|
1434
|
+
}
|
|
1435
|
+
return result.stdout.trim();
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* 获取适配器能力声明
|
|
1439
|
+
*/
|
|
1440
|
+
getCapabilities() {
|
|
1441
|
+
return {
|
|
1442
|
+
features: /* @__PURE__ */ new Set([
|
|
1443
|
+
"streaming",
|
|
1444
|
+
"session-resume",
|
|
1445
|
+
"system-prompt",
|
|
1446
|
+
"serve-mode"
|
|
1447
|
+
]),
|
|
1448
|
+
outputFormats: ["text"],
|
|
1449
|
+
testedVersionRange: TESTED_VERSION_RANGE
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* 执行查询:启动 serve 进程(如未启动),创建/恢复会话,发送消息并流式返回结果
|
|
1454
|
+
* @param request 请求参数
|
|
1455
|
+
* @yields AgentMessage 统一消息流
|
|
1456
|
+
*/
|
|
1457
|
+
async *query(request) {
|
|
1458
|
+
const client = await this.ensureServeRunning();
|
|
1459
|
+
let sessionId;
|
|
1460
|
+
if (request.resume) {
|
|
1461
|
+
sessionId = request.resume;
|
|
1462
|
+
} else {
|
|
1463
|
+
const session = await client.createSession(request.cwd);
|
|
1464
|
+
sessionId = session.id;
|
|
1465
|
+
}
|
|
1466
|
+
const controller = this.createAbortController(sessionId);
|
|
1467
|
+
try {
|
|
1468
|
+
await client.sendMessage(sessionId, request.prompt);
|
|
1469
|
+
for await (const event of client.streamEvents(
|
|
1470
|
+
sessionId,
|
|
1471
|
+
controller.signal
|
|
1472
|
+
)) {
|
|
1473
|
+
const message = mapOpenCodeEventToMessage(event);
|
|
1474
|
+
if (message) {
|
|
1475
|
+
yield message;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
} finally {
|
|
1479
|
+
this.removeAbortController(sessionId);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* 列出历史会话
|
|
1484
|
+
* @param options 查询选项
|
|
1485
|
+
*/
|
|
1486
|
+
async listSessions(options) {
|
|
1487
|
+
const client = await this.ensureServeRunning();
|
|
1488
|
+
const sessions = await client.listSessions();
|
|
1489
|
+
let filtered = sessions;
|
|
1490
|
+
if (options?.cwd) {
|
|
1491
|
+
filtered = filtered.filter((s) => s.cwd === options.cwd);
|
|
1492
|
+
}
|
|
1493
|
+
if (options?.limit !== void 0 && options.limit > 0) {
|
|
1494
|
+
filtered = filtered.slice(0, options.limit);
|
|
1495
|
+
}
|
|
1496
|
+
return filtered.map((s) => ({
|
|
1497
|
+
sessionId: s.id,
|
|
1498
|
+
backend: this.backend,
|
|
1499
|
+
summary: s.title,
|
|
1500
|
+
lastModified: s.updatedAt,
|
|
1501
|
+
cwd: s.cwd
|
|
1502
|
+
}));
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* 获取 OpenCode 可用模型列表(通过 `opencode models` 命令)
|
|
1506
|
+
*/
|
|
1507
|
+
async listModels() {
|
|
1508
|
+
try {
|
|
1509
|
+
const proc = new EphemeralProcess();
|
|
1510
|
+
const result = await proc.execute({
|
|
1511
|
+
command: "opencode",
|
|
1512
|
+
args: ["models"],
|
|
1513
|
+
timeoutMs: 15e3
|
|
1514
|
+
});
|
|
1515
|
+
if (result.exitCode !== 0) return [];
|
|
1516
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
1517
|
+
return lines.map((line) => {
|
|
1518
|
+
const trimmed = line.trim();
|
|
1519
|
+
const slashIndex = trimmed.indexOf("/");
|
|
1520
|
+
if (slashIndex >= 0) {
|
|
1521
|
+
return {
|
|
1522
|
+
id: trimmed,
|
|
1523
|
+
name: trimmed.slice(slashIndex + 1),
|
|
1524
|
+
provider: trimmed.slice(0, slashIndex)
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
return { id: trimmed, name: trimmed };
|
|
1528
|
+
});
|
|
1529
|
+
} catch {
|
|
1530
|
+
return [];
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* 读取 opencode 配置文件
|
|
1535
|
+
*/
|
|
1536
|
+
async getSettings() {
|
|
1537
|
+
const configPath = this.getConfigPath();
|
|
1538
|
+
try {
|
|
1539
|
+
const content = await readFile2(configPath, "utf-8");
|
|
1540
|
+
return JSON.parse(content);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return {};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* 更新 opencode 配置文件(读取-合并-写入)
|
|
1547
|
+
* @param settings 要合并的配置项
|
|
1548
|
+
*/
|
|
1549
|
+
async updateSettings(settings) {
|
|
1550
|
+
const configPath = this.getConfigPath();
|
|
1551
|
+
const configDir = join2(homedir2(), ".config", "opencode");
|
|
1552
|
+
let existing;
|
|
1553
|
+
try {
|
|
1554
|
+
const content = await readFile2(configPath, "utf-8");
|
|
1555
|
+
existing = JSON.parse(content);
|
|
1556
|
+
} catch {
|
|
1557
|
+
existing = {};
|
|
1558
|
+
}
|
|
1559
|
+
const merged = { ...existing, ...settings };
|
|
1560
|
+
await mkdir(configDir, { recursive: true });
|
|
1561
|
+
await writeFile2(configPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* 释放资源:停止 serve 进程,中止所有活跃流
|
|
1565
|
+
*/
|
|
1566
|
+
async dispose() {
|
|
1567
|
+
for (const [sessionId] of this.abortControllers) {
|
|
1568
|
+
this.abort(sessionId);
|
|
1569
|
+
}
|
|
1570
|
+
const persistent = this.processManager.getPersistent(this.backend);
|
|
1571
|
+
if (persistent) {
|
|
1572
|
+
await persistent.stop();
|
|
1573
|
+
}
|
|
1574
|
+
this.apiClient = null;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* 确保 opencode serve 进程已启动并可用
|
|
1578
|
+
* @returns 可用的 API 客户端
|
|
1579
|
+
*/
|
|
1580
|
+
async ensureServeRunning() {
|
|
1581
|
+
if (this.apiClient) {
|
|
1582
|
+
const healthy = await this.apiClient.healthCheck();
|
|
1583
|
+
if (healthy) return this.apiClient;
|
|
1584
|
+
}
|
|
1585
|
+
const client = new OpenCodeApiClient(
|
|
1586
|
+
`http://localhost:${this.servePort}`
|
|
1587
|
+
);
|
|
1588
|
+
const alreadyRunning = await client.healthCheck();
|
|
1589
|
+
if (alreadyRunning) {
|
|
1590
|
+
this.apiClient = client;
|
|
1591
|
+
return client;
|
|
1592
|
+
}
|
|
1593
|
+
await this.processManager.startPersistent(this.backend, {
|
|
1594
|
+
command: "opencode",
|
|
1595
|
+
args: ["serve", "--port", String(this.servePort)],
|
|
1596
|
+
maxRestarts: MAX_RESTARTS,
|
|
1597
|
+
restartIntervalMs: RESTART_INTERVAL_MS,
|
|
1598
|
+
healthCheckIntervalMs: HEALTH_CHECK_INTERVAL_MS,
|
|
1599
|
+
healthCheckFn: () => client.healthCheck().then((ok) => {
|
|
1600
|
+
if (!ok) throw new Error("Health check failed");
|
|
1601
|
+
return ok;
|
|
1602
|
+
})
|
|
1603
|
+
});
|
|
1604
|
+
await this.waitForServeReady(client);
|
|
1605
|
+
this.apiClient = client;
|
|
1606
|
+
return client;
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* 轮询等待 serve 服务可达
|
|
1610
|
+
* @param client API 客户端
|
|
1611
|
+
*/
|
|
1612
|
+
async waitForServeReady(client) {
|
|
1613
|
+
const deadline = Date.now() + SERVE_READY_TIMEOUT_MS;
|
|
1614
|
+
while (Date.now() < deadline) {
|
|
1615
|
+
const healthy = await client.healthCheck();
|
|
1616
|
+
if (healthy) return;
|
|
1617
|
+
await this.delay(SERVE_POLL_INTERVAL_MS);
|
|
1618
|
+
}
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`opencode serve did not become ready within ${SERVE_READY_TIMEOUT_MS}ms`
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* 延迟指定毫秒
|
|
1625
|
+
* @param ms 毫秒数
|
|
1626
|
+
*/
|
|
1627
|
+
delay(ms) {
|
|
1628
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* 获取 opencode 配置文件路径
|
|
1632
|
+
*/
|
|
1633
|
+
getConfigPath() {
|
|
1634
|
+
return join2(homedir2(), ".config", "opencode", "config.json");
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// src/porygon.ts
|
|
1639
|
+
var Porygon = class extends EventEmitter2 {
|
|
1640
|
+
config;
|
|
1641
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1642
|
+
interceptors;
|
|
1643
|
+
processManager;
|
|
1644
|
+
sessionManager;
|
|
1645
|
+
constructor(config) {
|
|
1646
|
+
super();
|
|
1647
|
+
this.config = ConfigLoader.load(config);
|
|
1648
|
+
this.interceptors = new InterceptorManager();
|
|
1649
|
+
this.processManager = new ProcessManager();
|
|
1650
|
+
this.sessionManager = new SessionManager();
|
|
1651
|
+
this.registerBuiltinAdapters();
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* 注册内置适配器
|
|
1655
|
+
*/
|
|
1656
|
+
registerBuiltinAdapters() {
|
|
1657
|
+
const claudeConfig = this.config.backends?.["claude"];
|
|
1658
|
+
const mergedClaudeConfig = {
|
|
1659
|
+
...claudeConfig,
|
|
1660
|
+
proxy: claudeConfig?.proxy ?? this.config.proxy
|
|
1661
|
+
};
|
|
1662
|
+
const claudeAdapter = new ClaudeAdapter(this.processManager, mergedClaudeConfig);
|
|
1663
|
+
this.registerAdapter(claudeAdapter);
|
|
1664
|
+
const opencodeAdapter = new OpenCodeAdapter(this.processManager);
|
|
1665
|
+
this.registerAdapter(opencodeAdapter);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* 注册自定义适配器
|
|
1669
|
+
* @param adapter 适配器实例
|
|
1670
|
+
*/
|
|
1671
|
+
registerAdapter(adapter) {
|
|
1672
|
+
this.adapters.set(adapter.backend, adapter);
|
|
1673
|
+
this.sessionManager.registerAdapter(adapter.backend, adapter);
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* 获取指定后端的适配器
|
|
1677
|
+
* @param backend 后端名称,默认使用配置中的 defaultBackend
|
|
1678
|
+
*/
|
|
1679
|
+
getAdapter(backend) {
|
|
1680
|
+
const name = backend ?? this.config.defaultBackend ?? "claude";
|
|
1681
|
+
const adapter = this.adapters.get(name);
|
|
1682
|
+
if (!adapter) throw new AdapterNotFoundError(name);
|
|
1683
|
+
return adapter;
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* 流式查询,返回 AgentMessage 异步生成器。
|
|
1687
|
+
* 作为与 LLM 后端交互的主要入口。
|
|
1688
|
+
* @param request 提示请求参数
|
|
1689
|
+
* @yields AgentMessage 消息流
|
|
1690
|
+
*/
|
|
1691
|
+
async *query(request) {
|
|
1692
|
+
const adapter = this.getAdapter(request.backend);
|
|
1693
|
+
const backendName = adapter.backend;
|
|
1694
|
+
const mergedRequest = this.mergeRequest(request, backendName);
|
|
1695
|
+
const processedPrompt = await this.interceptors.processInput(
|
|
1696
|
+
mergedRequest.prompt,
|
|
1697
|
+
{ backend: backendName, sessionId: mergedRequest.resume }
|
|
1698
|
+
);
|
|
1699
|
+
mergedRequest.prompt = processedPrompt;
|
|
1700
|
+
for await (const message of adapter.query(mergedRequest)) {
|
|
1701
|
+
if (message.type === "assistant" || message.type === "result") {
|
|
1702
|
+
const processedText = await this.interceptors.processOutput(
|
|
1703
|
+
message.text,
|
|
1704
|
+
{
|
|
1705
|
+
backend: backendName,
|
|
1706
|
+
sessionId: message.sessionId,
|
|
1707
|
+
messageType: message.type
|
|
1708
|
+
}
|
|
1709
|
+
);
|
|
1710
|
+
yield { ...message, text: processedText };
|
|
1711
|
+
} else {
|
|
1712
|
+
yield message;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* 简单运行模式,收集所有消息并返回最终结果文本
|
|
1718
|
+
* @param request 提示请求参数
|
|
1719
|
+
* @returns 最终结果文本
|
|
1720
|
+
*/
|
|
1721
|
+
async run(request) {
|
|
1722
|
+
let resultText = "";
|
|
1723
|
+
for await (const msg of this.query(request)) {
|
|
1724
|
+
if (msg.type === "result") {
|
|
1725
|
+
resultText = msg.text;
|
|
1726
|
+
} else if (msg.type === "assistant") {
|
|
1727
|
+
resultText = msg.text;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return resultText;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* 注册拦截器
|
|
1734
|
+
* @param direction 拦截方向
|
|
1735
|
+
* @param fn 拦截器函数
|
|
1736
|
+
* @returns 取消注册的函数
|
|
1737
|
+
*/
|
|
1738
|
+
use(direction, fn) {
|
|
1739
|
+
return this.interceptors.use(direction, fn);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* 获取后端能力声明
|
|
1743
|
+
* @param backend 后端名称
|
|
1744
|
+
*/
|
|
1745
|
+
getCapabilities(backend) {
|
|
1746
|
+
return this.getAdapter(backend).getCapabilities();
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* 获取指定后端的可用模型列表
|
|
1750
|
+
* @param backend 后端名称
|
|
1751
|
+
*/
|
|
1752
|
+
async listModels(backend) {
|
|
1753
|
+
return this.getAdapter(backend).listModels();
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* 对所有已注册后端进行健康检查
|
|
1757
|
+
*/
|
|
1758
|
+
async healthCheck() {
|
|
1759
|
+
const entries = Array.from(this.adapters.entries());
|
|
1760
|
+
const checks = entries.map(async ([name, adapter]) => {
|
|
1761
|
+
try {
|
|
1762
|
+
const available = await adapter.isAvailable();
|
|
1763
|
+
let compatibility = null;
|
|
1764
|
+
if (available) {
|
|
1765
|
+
compatibility = await adapter.checkCompatibility();
|
|
1766
|
+
if (!compatibility.supported) {
|
|
1767
|
+
this.emit(
|
|
1768
|
+
"health:degraded",
|
|
1769
|
+
name,
|
|
1770
|
+
compatibility.warnings.join("; ")
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return [name, { available, compatibility }];
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1777
|
+
return [name, { available: false, compatibility: null, error: errorMsg }];
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
const settled = await Promise.allSettled(checks);
|
|
1781
|
+
const results = {};
|
|
1782
|
+
for (const item of settled) {
|
|
1783
|
+
if (item.status === "fulfilled") {
|
|
1784
|
+
const [name, result] = item.value;
|
|
1785
|
+
results[name] = result;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return results;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* 读取或更新后端设置
|
|
1792
|
+
* @param backend 后端名称
|
|
1793
|
+
* @param newSettings 要更新的设置项(可选)
|
|
1794
|
+
* @returns 当前设置
|
|
1795
|
+
*/
|
|
1796
|
+
async settings(backend, newSettings) {
|
|
1797
|
+
const adapter = this.getAdapter(backend);
|
|
1798
|
+
if (newSettings) {
|
|
1799
|
+
await adapter.updateSettings(newSettings);
|
|
1800
|
+
}
|
|
1801
|
+
return adapter.getSettings();
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* 列出指定后端的会话
|
|
1805
|
+
* @param backend 后端名称
|
|
1806
|
+
* @param options 查询选项
|
|
1807
|
+
*/
|
|
1808
|
+
async listSessions(backend, options) {
|
|
1809
|
+
const name = backend ?? this.config.defaultBackend ?? "claude";
|
|
1810
|
+
return this.sessionManager.list(name, options);
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* 中止正在运行的查询
|
|
1814
|
+
* @param backend 后端名称
|
|
1815
|
+
* @param sessionId 会话 ID
|
|
1816
|
+
*/
|
|
1817
|
+
abort(backend, sessionId) {
|
|
1818
|
+
this.getAdapter(backend).abort(sessionId);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* 释放所有资源
|
|
1822
|
+
*/
|
|
1823
|
+
async dispose() {
|
|
1824
|
+
const promises = [];
|
|
1825
|
+
for (const [, adapter] of this.adapters) {
|
|
1826
|
+
promises.push(adapter.dispose());
|
|
1827
|
+
}
|
|
1828
|
+
await Promise.allSettled(promises);
|
|
1829
|
+
await this.processManager.terminateAll();
|
|
1830
|
+
this.adapters.clear();
|
|
1831
|
+
this.sessionManager.clearCache();
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* 合并请求参数与配置默认值
|
|
1835
|
+
* @param request 原始请求
|
|
1836
|
+
* @param backend 后端名称
|
|
1837
|
+
* @returns 合并后的请求
|
|
1838
|
+
*/
|
|
1839
|
+
mergeRequest(request, backend) {
|
|
1840
|
+
const backendConfig = this.config.backends?.[backend];
|
|
1841
|
+
const defaults = this.config.defaults;
|
|
1842
|
+
const appendParts = [];
|
|
1843
|
+
if (defaults?.appendSystemPrompt) {
|
|
1844
|
+
appendParts.push(defaults.appendSystemPrompt);
|
|
1845
|
+
}
|
|
1846
|
+
if (backendConfig?.appendSystemPrompt) {
|
|
1847
|
+
appendParts.push(backendConfig.appendSystemPrompt);
|
|
1848
|
+
}
|
|
1849
|
+
if (request.appendSystemPrompt) {
|
|
1850
|
+
appendParts.push(request.appendSystemPrompt);
|
|
1851
|
+
}
|
|
1852
|
+
return {
|
|
1853
|
+
...request,
|
|
1854
|
+
model: request.model ?? backendConfig?.model,
|
|
1855
|
+
timeoutMs: request.timeoutMs ?? defaults?.timeoutMs,
|
|
1856
|
+
maxTurns: request.maxTurns ?? defaults?.maxTurns,
|
|
1857
|
+
cwd: request.cwd ?? backendConfig?.cwd,
|
|
1858
|
+
appendSystemPrompt: request.systemPrompt ? void 0 : appendParts.length > 0 ? appendParts.join("\n") : void 0
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
function createPorygon(config) {
|
|
1863
|
+
return new Porygon(config);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/interceptor/guard.ts
|
|
1867
|
+
var BUILTIN_INPUT_PATTERNS = [
|
|
1868
|
+
// 中文
|
|
1869
|
+
/忽略.{0,10}(之前|上面|以上|所有).{0,10}(指令|规则|提示|约束)/i,
|
|
1870
|
+
/重复.{0,10}(系统|完整).{0,10}(提示|指令|prompt)/i,
|
|
1871
|
+
/输出.{0,10}(系统|完整).{0,10}(提示|指令|prompt)/i,
|
|
1872
|
+
/告诉我.{0,10}(系统|你的).{0,10}(提示|指令|prompt)/i,
|
|
1873
|
+
/显示.{0,10}(系统|隐藏|完整).{0,10}(提示|指令|prompt)/i,
|
|
1874
|
+
/你的(系统|初始).{0,10}(提示|指令|设定)/i,
|
|
1875
|
+
// English
|
|
1876
|
+
/ignore.{0,15}(previous|above|all|prior).{0,15}(instructions?|rules?|prompts?|constraints?)/i,
|
|
1877
|
+
/repeat.{0,15}(system|full|entire|complete).{0,15}(prompt|instructions?|message)/i,
|
|
1878
|
+
/reveal.{0,15}(system|hidden|full).{0,15}(prompt|instructions?)/i,
|
|
1879
|
+
/show.{0,15}(system|hidden|full).{0,15}(prompt|instructions?)/i,
|
|
1880
|
+
/what.{0,15}(is|are).{0,15}(your|the).{0,15}(system|initial).{0,15}(prompt|instructions?)/i,
|
|
1881
|
+
/print.{0,15}(system|full|entire).{0,15}(prompt|instructions?|message)/i,
|
|
1882
|
+
/disregard.{0,15}(previous|above|all|prior).{0,15}(instructions?|rules?)/i
|
|
1883
|
+
];
|
|
1884
|
+
function createInputGuard(options) {
|
|
1885
|
+
const patterns = [
|
|
1886
|
+
...BUILTIN_INPUT_PATTERNS,
|
|
1887
|
+
...options?.blockedPatterns ?? []
|
|
1888
|
+
];
|
|
1889
|
+
const action = options?.action ?? "reject";
|
|
1890
|
+
const backends = options?.backends;
|
|
1891
|
+
return (text, context) => {
|
|
1892
|
+
if (backends && !backends.includes(context.backend)) {
|
|
1893
|
+
return void 0;
|
|
1894
|
+
}
|
|
1895
|
+
for (const pattern of patterns) {
|
|
1896
|
+
if (pattern.test(text)) {
|
|
1897
|
+
if (action === "reject") {
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
return text.replace(pattern, "[REDACTED]");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (options?.customCheck?.(text, context)) {
|
|
1904
|
+
if (action === "reject") {
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
return "[REDACTED]";
|
|
1908
|
+
}
|
|
1909
|
+
return void 0;
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function createOutputGuard(options) {
|
|
1913
|
+
const keywords = options?.sensitiveKeywords ?? [];
|
|
1914
|
+
const action = options?.action ?? "redact";
|
|
1915
|
+
const backends = options?.backends;
|
|
1916
|
+
return (text, context) => {
|
|
1917
|
+
if (backends && !backends.includes(context.backend)) {
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
let matched = false;
|
|
1921
|
+
let result = text;
|
|
1922
|
+
for (const keyword of keywords) {
|
|
1923
|
+
if (text.includes(keyword)) {
|
|
1924
|
+
matched = true;
|
|
1925
|
+
if (action === "redact") {
|
|
1926
|
+
result = result.split(keyword).join("[REDACTED]");
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (!matched && options?.customCheck?.(text, context)) {
|
|
1931
|
+
matched = true;
|
|
1932
|
+
if (action === "redact") {
|
|
1933
|
+
result = "[REDACTED]";
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (!matched) {
|
|
1937
|
+
return void 0;
|
|
1938
|
+
}
|
|
1939
|
+
if (action === "reject") {
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
return result;
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
export {
|
|
1946
|
+
AbstractAgentAdapter,
|
|
1947
|
+
AdapterIncompatibleError,
|
|
1948
|
+
AdapterNotAvailableError,
|
|
1949
|
+
AdapterNotFoundError,
|
|
1950
|
+
AgentExecutionError,
|
|
1951
|
+
AgentTimeoutError,
|
|
1952
|
+
ClaudeAdapter,
|
|
1953
|
+
ConfigValidationError,
|
|
1954
|
+
InterceptorRejectedError,
|
|
1955
|
+
OpenCodeAdapter,
|
|
1956
|
+
Porygon,
|
|
1957
|
+
PorygonError,
|
|
1958
|
+
SessionNotFoundError,
|
|
1959
|
+
createInputGuard,
|
|
1960
|
+
createOutputGuard,
|
|
1961
|
+
createPorygon
|
|
1962
|
+
};
|
|
1963
|
+
//# sourceMappingURL=index.js.map
|