agent-anywhere-gateway 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 +46 -0
- package/bin/agent-anywhere-gateway.js +13 -0
- package/config/cloudflare.gateway.env.example +9 -0
- package/package.json +29 -0
- package/src/adapters/local-agent-adapter.js +131 -0
- package/src/gateway/client.js +422 -0
- package/src/gateway/main.js +224 -0
- package/src/gateway/providers.js +28 -0
- package/src/gateway/runner.js +337 -0
- package/src/gateway.js +7 -0
- package/src/lib/capabilities.js +1 -0
- package/src/lib/local-discovery.js +322 -0
- package/src/lib/path-policy.js +1 -0
- package/src/runtimes/claude-code-headless-runtime.js +547 -0
- package/src/runtimes/claude-code-runtime.js +984 -0
- package/src/runtimes/codex-app-server-client.js +157 -0
- package/src/runtimes/codex-app-server-runtime.js +790 -0
- package/src/runtimes/codex-runtime.js +418 -0
- package/src/runtimes/mock-runtime.js +140 -0
- package/src/shared/capabilities.js +175 -0
- package/src/shared/gateway-protocol.js +26 -0
- package/src/shared/http-utils.js +78 -0
- package/src/shared/image-attachments.js +269 -0
- package/src/shared/path-policy.js +110 -0
- package/src/shared/project-files.js +119 -0
- package/src/shared/providers.js +27 -0
- package/src/shared/runtime-environment.js +32 -0
- package/src/shared/websocket.js +258 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
const { execFile } = require("node:child_process");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { buildCapabilities, codexPolicyForMode } = require("../shared/capabilities");
|
|
5
|
+
|
|
6
|
+
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
7
|
+
const modelCatalogCache = new Map();
|
|
8
|
+
|
|
9
|
+
async function loadCodexSdk() {
|
|
10
|
+
try {
|
|
11
|
+
return await import("@openai/codex-sdk");
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const wrapped = new Error(`无法加载 @openai/codex-sdk:${error.message}`);
|
|
14
|
+
wrapped.statusCode = 500;
|
|
15
|
+
wrapped.cause = error;
|
|
16
|
+
throw wrapped;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveCodexExecutable(codexPathOverride = process.env.CODEX_CLI_PATH) {
|
|
21
|
+
if (codexPathOverride) {
|
|
22
|
+
const resolved = path.resolve(codexPathOverride);
|
|
23
|
+
if (fs.existsSync(resolved)) {
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
const error = new Error(`CODEX_CLI_PATH 不可用:${codexPathOverride}`);
|
|
27
|
+
error.statusCode = 500;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const localBinary = path.join(__dirname, "..", "..", "node_modules", ".bin", process.platform === "win32" ? "codex.cmd" : "codex");
|
|
32
|
+
if (fs.existsSync(localBinary)) {
|
|
33
|
+
return localBinary;
|
|
34
|
+
}
|
|
35
|
+
return "codex";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildCodexThreadOptions({ projectPath, settings }) {
|
|
39
|
+
const policy = codexPolicyForMode(settings.mode);
|
|
40
|
+
const approvalPolicy = settings.approval_policy || "on-request";
|
|
41
|
+
return {
|
|
42
|
+
model: settings.model,
|
|
43
|
+
sandboxMode: policy.sandbox,
|
|
44
|
+
workingDirectory: projectPath,
|
|
45
|
+
skipGitRepoCheck: true,
|
|
46
|
+
modelReasoningEffort: settings.reasoning_effort,
|
|
47
|
+
networkAccessEnabled: policy.networkAccess,
|
|
48
|
+
approvalPolicy
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseCodexModelCatalog(catalog) {
|
|
53
|
+
const rows = Array.isArray(catalog?.models)
|
|
54
|
+
? catalog.models
|
|
55
|
+
: Array.isArray(catalog?.data)
|
|
56
|
+
? catalog.data
|
|
57
|
+
: [];
|
|
58
|
+
const visibleRows = rows
|
|
59
|
+
.filter((row) => row && typeof row === "object")
|
|
60
|
+
.filter((row) => row.hidden !== true && row.visibility !== "hidden");
|
|
61
|
+
const sortedRows = visibleRows.slice().sort((a, b) => {
|
|
62
|
+
const left = Number.isFinite(a.priority) ? a.priority : Number.MAX_SAFE_INTEGER;
|
|
63
|
+
const right = Number.isFinite(b.priority) ? b.priority : Number.MAX_SAFE_INTEGER;
|
|
64
|
+
return left - right;
|
|
65
|
+
});
|
|
66
|
+
const models = [];
|
|
67
|
+
const reasoningEfforts = [];
|
|
68
|
+
for (const row of sortedRows) {
|
|
69
|
+
const model = String(row.slug || row.model || row.id || "").trim();
|
|
70
|
+
if (model && !models.includes(model)) {
|
|
71
|
+
models.push(model);
|
|
72
|
+
}
|
|
73
|
+
const efforts = row.supported_reasoning_levels || row.supportedReasoningEfforts || [];
|
|
74
|
+
for (const effortRow of efforts) {
|
|
75
|
+
const effort = String(effortRow.effort || effortRow.reasoningEffort || "").trim();
|
|
76
|
+
if (effort && !reasoningEfforts.includes(effort)) {
|
|
77
|
+
reasoningEfforts.push(effort);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
models,
|
|
83
|
+
reasoning_efforts: reasoningEfforts
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseJsonObject(stdout) {
|
|
88
|
+
const text = String(stdout || "").trim();
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(text);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const start = text.indexOf("{");
|
|
93
|
+
const end = text.lastIndexOf("}");
|
|
94
|
+
if (start >= 0 && end > start) {
|
|
95
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runCodexDebugModels({ codexPathOverride, timeoutMs = 5000, execFileImpl = execFile } = {}) {
|
|
102
|
+
const codexPath = resolveCodexExecutable(codexPathOverride);
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
execFileImpl(codexPath, ["debug", "models"], {
|
|
105
|
+
env: process.env,
|
|
106
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
107
|
+
timeout: timeoutMs
|
|
108
|
+
}, (error, stdout, stderr) => {
|
|
109
|
+
if (error) {
|
|
110
|
+
reject(new Error((stderr || error.message).slice(-4000)));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
resolve(parseJsonObject(stdout));
|
|
115
|
+
} catch (parseError) {
|
|
116
|
+
reject(new Error(`Codex 模型目录不是合法 JSON:${parseError.message}`));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function discoverCodexCapabilities({
|
|
123
|
+
codexPathOverride,
|
|
124
|
+
timeoutMs = 5000,
|
|
125
|
+
force = false,
|
|
126
|
+
runDebugModels = runCodexDebugModels,
|
|
127
|
+
provider = "codex"
|
|
128
|
+
} = {}) {
|
|
129
|
+
const cacheKey = codexPathOverride || process.env.CODEX_CLI_PATH || "default";
|
|
130
|
+
const nowMs = Date.now();
|
|
131
|
+
const cached = modelCatalogCache.get(cacheKey);
|
|
132
|
+
if (!force && cached && nowMs - cached.updatedAt < MODEL_CACHE_TTL_MS) {
|
|
133
|
+
return cached.capabilities;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const catalog = await runDebugModels({ codexPathOverride, timeoutMs });
|
|
137
|
+
const parsed = parseCodexModelCatalog(catalog);
|
|
138
|
+
const capabilities = buildCapabilities(provider, parsed);
|
|
139
|
+
modelCatalogCache.set(cacheKey, {
|
|
140
|
+
capabilities,
|
|
141
|
+
updatedAt: nowMs
|
|
142
|
+
});
|
|
143
|
+
return capabilities;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderValue(value) {
|
|
147
|
+
if (value === undefined || value === null) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === "string") {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
return value.map((item) => renderValue(item)).filter(Boolean).join("\n");
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "object") {
|
|
157
|
+
if (typeof value.text === "string") {
|
|
158
|
+
return value.text;
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(value.content)) {
|
|
161
|
+
return renderValue(value.content);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return JSON.stringify(value, null, 2);
|
|
165
|
+
} catch {
|
|
166
|
+
return String(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return String(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function fileChangeMessage(item) {
|
|
173
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
174
|
+
if (!changes.length) {
|
|
175
|
+
return item.status === "failed" ? "文件修改失败" : "文件修改完成";
|
|
176
|
+
}
|
|
177
|
+
const summary = changes
|
|
178
|
+
.map((change) => `${change.kind || "update"} ${change.path || ""}`.trim())
|
|
179
|
+
.join(", ");
|
|
180
|
+
return item.status === "failed" ? `文件修改失败:${summary}` : `文件修改完成:${summary}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function convertThreadItemEvent(event) {
|
|
184
|
+
const item = event.item || {};
|
|
185
|
+
const itemStatus = item.status || (event.type === "item.completed" ? "completed" : "in_progress");
|
|
186
|
+
|
|
187
|
+
if (item.type === "agent_message") {
|
|
188
|
+
if (event.type !== "item.completed") {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const text = String(item.text || "").trim();
|
|
192
|
+
return text ? [{ type: "delta", payload: { text } }] : [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (item.type === "reasoning") {
|
|
196
|
+
const text = String(item.text || "").trim();
|
|
197
|
+
return text ? [{ type: "activity", payload: { message: text, kind: "agent" } }] : [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (item.type === "command_execution") {
|
|
201
|
+
if (event.type === "item.started") {
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
type: "tool_use",
|
|
205
|
+
payload: {
|
|
206
|
+
tool_name: "exec_command",
|
|
207
|
+
tool_input: { cmd: item.command || "" },
|
|
208
|
+
tool_use_id: item.id || null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
if (event.type === "item.completed") {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
type: "tool_result",
|
|
217
|
+
payload: {
|
|
218
|
+
tool_use_id: item.id || null,
|
|
219
|
+
content: String(item.aggregated_output || ""),
|
|
220
|
+
is_error: itemStatus === "failed" || (Number.isInteger(item.exit_code) && item.exit_code !== 0)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
return [{ type: "activity", payload: { message: "命令执行中", kind: "tool_progress" } }];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (item.type === "mcp_tool_call") {
|
|
229
|
+
if (event.type === "item.started") {
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
type: "tool_use",
|
|
233
|
+
payload: {
|
|
234
|
+
tool_name: item.tool || "mcp_tool",
|
|
235
|
+
tool_input: item.arguments || {},
|
|
236
|
+
tool_use_id: item.id || null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "item.completed") {
|
|
242
|
+
const content = item.error?.message || renderValue(item.result?.content || item.result);
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
type: "tool_result",
|
|
246
|
+
payload: {
|
|
247
|
+
tool_use_id: item.id || null,
|
|
248
|
+
content,
|
|
249
|
+
is_error: itemStatus === "failed" || Boolean(item.error)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
return [{ type: "activity", payload: { message: `调用工具:${item.tool || "mcp_tool"}`, kind: "tool_progress" } }];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (item.type === "web_search") {
|
|
258
|
+
if (event.type === "item.started") {
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
type: "tool_use",
|
|
262
|
+
payload: {
|
|
263
|
+
tool_name: "web_search",
|
|
264
|
+
tool_input: { query: item.query || "" },
|
|
265
|
+
tool_use_id: item.id || null
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
if (event.type === "item.completed") {
|
|
271
|
+
return [
|
|
272
|
+
{
|
|
273
|
+
type: "tool_result",
|
|
274
|
+
payload: {
|
|
275
|
+
tool_use_id: item.id || null,
|
|
276
|
+
content: `搜索完成:${item.query || ""}`,
|
|
277
|
+
is_error: false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (item.type === "file_change") {
|
|
285
|
+
return [{ type: "activity", payload: { message: fileChangeMessage(item), kind: "tool_progress" } }];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (item.type === "todo_list") {
|
|
289
|
+
const todos = Array.isArray(item.items) ? item.items : [];
|
|
290
|
+
const completed = todos.filter((todo) => todo.completed).length;
|
|
291
|
+
return [{ type: "activity", payload: { message: `计划进度:${completed}/${todos.length}`, kind: "status" } }];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (item.type === "error") {
|
|
295
|
+
return [{ type: "error", payload: { message: item.message || "Codex 事件错误" } }];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function convertCodexEvent(event) {
|
|
302
|
+
if (!event || typeof event !== "object") {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (event.type === "thread.started") {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
type: "runtime_session",
|
|
310
|
+
payload: {
|
|
311
|
+
runtime_session_id: event.thread_id || null,
|
|
312
|
+
working_directory: ""
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (event.type === "turn.started") {
|
|
319
|
+
return [{ type: "activity", payload: { message: "Codex 开始处理任务", kind: "status" } }];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (event.type === "turn.completed") {
|
|
323
|
+
return [{ type: "usage", payload: { usage: event.usage || null } }];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (event.type === "turn.failed") {
|
|
327
|
+
return [{ type: "error", payload: { message: event.error?.message || "Codex 执行失败" } }];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (event.type === "error") {
|
|
331
|
+
return [{ type: "error", payload: { message: event.message || "Codex 事件错误" } }];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
|
|
335
|
+
return convertThreadItemEvent(event);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
class CodexRuntime {
|
|
342
|
+
constructor({ codexPathOverride, cliPath, sdk, provider = "codex" } = {}) {
|
|
343
|
+
this.provider = provider;
|
|
344
|
+
this.codexPathOverride = codexPathOverride || cliPath || process.env.CODEX_CLI_PATH || undefined;
|
|
345
|
+
this.sdk = sdk || null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async createClient() {
|
|
349
|
+
const sdk = this.sdk || await loadCodexSdk();
|
|
350
|
+
return new sdk.Codex({
|
|
351
|
+
codexPathOverride: this.codexPathOverride,
|
|
352
|
+
config: {
|
|
353
|
+
show_raw_agent_reasoning: false
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async *run({ session, project, message, attachments = [], settings }) {
|
|
359
|
+
const threadOptions = buildCodexThreadOptions({
|
|
360
|
+
projectPath: project.path,
|
|
361
|
+
settings
|
|
362
|
+
});
|
|
363
|
+
const client = await this.createClient();
|
|
364
|
+
const thread = session.runtime_session_id
|
|
365
|
+
? client.resumeThread(session.runtime_session_id, threadOptions)
|
|
366
|
+
: client.startThread(threadOptions);
|
|
367
|
+
|
|
368
|
+
yield {
|
|
369
|
+
type: "activity",
|
|
370
|
+
payload: {
|
|
371
|
+
message: `启动 Codex SDK:${project.path}`,
|
|
372
|
+
kind: "status"
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const images = attachments.filter((image) => image?.path);
|
|
377
|
+
const input = images.length
|
|
378
|
+
? [{ type: "text", text: message }, ...images.map((image) => ({ type: "local_image", path: image.path }))]
|
|
379
|
+
: message;
|
|
380
|
+
const { events } = await thread.runStreamed(input);
|
|
381
|
+
for await (const event of events) {
|
|
382
|
+
if (event.type === "turn.failed") {
|
|
383
|
+
const error = new Error(event.error?.message || "Codex 执行失败");
|
|
384
|
+
error.statusCode = 500;
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
if (event.type === "error") {
|
|
388
|
+
const error = new Error(event.message || "Codex 事件错误");
|
|
389
|
+
error.statusCode = 500;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
for (const normalized of convertCodexEvent(event)) {
|
|
393
|
+
yield normalized;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
yield {
|
|
398
|
+
type: "complete",
|
|
399
|
+
payload: {
|
|
400
|
+
message: "Codex 执行完成。"
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async discoverCapabilities() {
|
|
406
|
+
return discoverCodexCapabilities({ codexPathOverride: this.codexPathOverride, provider: this.provider });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = {
|
|
411
|
+
CodexRuntime,
|
|
412
|
+
buildCodexThreadOptions,
|
|
413
|
+
convertCodexEvent,
|
|
414
|
+
discoverCodexCapabilities,
|
|
415
|
+
parseCodexModelCatalog,
|
|
416
|
+
resolveCodexExecutable,
|
|
417
|
+
runCodexDebugModels
|
|
418
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
function wait(ms) {
|
|
2
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
class MockRuntime {
|
|
6
|
+
constructor({ delayMs = 10 } = {}) {
|
|
7
|
+
this.provider = "mock";
|
|
8
|
+
this.delayMs = delayMs;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async *run({ session, project, message, attachments = [], settings, requestApproval }) {
|
|
12
|
+
yield {
|
|
13
|
+
type: "activity",
|
|
14
|
+
payload: {
|
|
15
|
+
message: `Mock agent 已连接 ${project.path}`,
|
|
16
|
+
kind: "status"
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
await wait(this.delayMs);
|
|
20
|
+
|
|
21
|
+
yield {
|
|
22
|
+
type: "tool_use",
|
|
23
|
+
payload: {
|
|
24
|
+
tool_name: "inspect_project",
|
|
25
|
+
tool_input: {
|
|
26
|
+
path: project.path,
|
|
27
|
+
mode: settings.mode
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
await wait(this.delayMs);
|
|
32
|
+
|
|
33
|
+
yield {
|
|
34
|
+
type: "tool_result",
|
|
35
|
+
payload: {
|
|
36
|
+
tool_name: "inspect_project",
|
|
37
|
+
content: "项目结构已读取,准备执行用户任务。",
|
|
38
|
+
is_error: false
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
await wait(this.delayMs);
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
/approval|权限确认|需要权限/i.test(message) &&
|
|
45
|
+
requestApproval &&
|
|
46
|
+
settings.mode !== "auto-review" &&
|
|
47
|
+
settings.approval_policy !== "never"
|
|
48
|
+
) {
|
|
49
|
+
const decision = await requestApproval({
|
|
50
|
+
runtime_request_id: `mock-${Date.now()}`,
|
|
51
|
+
kind: "command_execution",
|
|
52
|
+
command: "mock dangerous command",
|
|
53
|
+
cwd: project.path,
|
|
54
|
+
reason: "Mock runtime 请求权限确认",
|
|
55
|
+
available_decisions: ["approved", "rejected"]
|
|
56
|
+
});
|
|
57
|
+
if (decision.status === "cancelled" || decision.decision === "cancel") {
|
|
58
|
+
yield {
|
|
59
|
+
type: "cancelled",
|
|
60
|
+
payload: {
|
|
61
|
+
message: "Mock runtime 权限请求已取消。"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (decision.status !== "approved" && decision.decision !== "approved") {
|
|
67
|
+
const error = new Error("Mock runtime 权限请求被拒绝。");
|
|
68
|
+
error.statusCode = 403;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (/browser|浏览器|页面/i.test(message)) {
|
|
74
|
+
yield {
|
|
75
|
+
type: "browser_output",
|
|
76
|
+
payload: {
|
|
77
|
+
title: "Agent Anywhere",
|
|
78
|
+
url: "http://localhost:8787",
|
|
79
|
+
summary: "浏览器输出区域可展示 URL、标题、截图摘要或检查结果。"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
await wait(this.delayMs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const summary = [
|
|
86
|
+
`已在 ${session.provider} 会话中接收任务:${message}`,
|
|
87
|
+
attachments.length ? `已接收 ${attachments.length} 张图片。` : "",
|
|
88
|
+
`模型 ${settings.model},思考强度 ${settings.reasoning_effort},模式 ${settings.mode}。`
|
|
89
|
+
].filter(Boolean).join("\n");
|
|
90
|
+
|
|
91
|
+
for (const chunk of summary.match(/.{1,18}/g) || [summary]) {
|
|
92
|
+
yield {
|
|
93
|
+
type: "delta",
|
|
94
|
+
payload: {
|
|
95
|
+
text: chunk
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await wait(this.delayMs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
yield {
|
|
102
|
+
type: "complete",
|
|
103
|
+
payload: {
|
|
104
|
+
message: summary
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async cancelTurn() {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async steerTurn({ message, attachments = [] }) {
|
|
114
|
+
return { accepted: true, message, image_count: attachments.length };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listRuntimeSessions({ project, limit = 50 } = {}) {
|
|
118
|
+
return [{
|
|
119
|
+
id: "mock-thread",
|
|
120
|
+
runtime_session_id: "mock-thread",
|
|
121
|
+
title: "Mock runtime session",
|
|
122
|
+
cwd: project?.path || "",
|
|
123
|
+
provider: this.provider
|
|
124
|
+
}].slice(0, limit);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async readRuntimeSession({ runtimeSessionId, includeTurns = true } = {}) {
|
|
128
|
+
return {
|
|
129
|
+
thread: {
|
|
130
|
+
id: runtimeSessionId || "mock-thread",
|
|
131
|
+
title: "Mock runtime session"
|
|
132
|
+
},
|
|
133
|
+
turns: includeTurns ? [] : undefined
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
MockRuntime
|
|
140
|
+
};
|