@actiondock/core 2.0.8 → 2.0.10
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/package.json +9 -3
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +1 -1
- package/src/catalog/types.ts +1 -9
- package/src/doctor/doctor.ts +99 -38
- package/src/execution/service.ts +47 -7
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +221 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +97 -9
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +126 -32
- package/src/runtime/context.ts +2 -1
- package/src/runtime/index.ts +1 -0
- package/src/runtime/module-loader.ts +67 -0
- package/src/runtime/process.ts +12 -11
- package/src/runtime/runner.ts +23 -36
- package/src/server/index.ts +1 -0
- package/src/server/routes/actions.ts +262 -0
- package/src/server/routes/common.ts +126 -0
- package/src/server/routes/config.ts +147 -0
- package/src/server/routes/doctor.ts +29 -0
- package/src/server/routes/health.ts +40 -0
- package/src/server/routes/index.ts +9 -0
- package/src/server/routes/info.ts +221 -0
- package/src/server/routes/playbooks.ts +109 -0
- package/src/server/routes/runs.ts +301 -0
- package/src/server/routes/state.ts +133 -0
- package/src/server/server.ts +82 -1323
- package/src/storage/index.ts +20 -4
- package/src/storage/sqlite.ts +60 -31
- package/src/storage/types.ts +8 -0
- package/src/utils/index.ts +2 -0
- package/src/build/builder.ts +0 -218
- package/src/export/skill.ts +0 -350
package/src/runtime/runner.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ActionDefinition,
|
|
5
5
|
ExecutionResult,
|
|
6
6
|
JsonValue,
|
|
7
|
+
Logger,
|
|
7
8
|
ProcessAPI,
|
|
8
9
|
ProgressReporter,
|
|
9
10
|
RuntimeError,
|
|
@@ -12,8 +13,7 @@ import type {
|
|
|
12
13
|
import type { ProjectConfig } from "../project/types";
|
|
13
14
|
import { validateSchema } from "../schema/validator";
|
|
14
15
|
import type { RuntimeStorage, TerminalRunStatus } from "../storage/types";
|
|
15
|
-
import {
|
|
16
|
-
import { getProcessExecutor } from "./process";
|
|
16
|
+
import { createActionContext, StderrLogger } from "./context";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* ActionRunner 初始化配置选项。
|
|
@@ -37,6 +37,8 @@ export interface RunnerOptions {
|
|
|
37
37
|
* 启动 Action 执行时的可选控制参数。
|
|
38
38
|
*/
|
|
39
39
|
export interface ExecutionStartOptions {
|
|
40
|
+
/** 显式指定的运行 ID */
|
|
41
|
+
runId?: string;
|
|
40
42
|
/** 根运行 ID */
|
|
41
43
|
rootRunId?: string;
|
|
42
44
|
/** 父级运行 ID(嵌套调用场景下建立调用链树) */
|
|
@@ -57,6 +59,8 @@ export interface ExecutionStartOptions {
|
|
|
57
59
|
process?: ProcessAPI;
|
|
58
60
|
/** 外部注入的进度报告器 */
|
|
59
61
|
progress?: ProgressReporter;
|
|
62
|
+
/** 外部注入的日志记录器 */
|
|
63
|
+
logger?: Logger;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -134,7 +138,7 @@ export class ActionRunner {
|
|
|
134
138
|
input: unknown = {},
|
|
135
139
|
options: ExecutionStartOptions = {}
|
|
136
140
|
): ExecutionHandle {
|
|
137
|
-
const runId = randomUUID();
|
|
141
|
+
const runId = options.runId || randomUUID();
|
|
138
142
|
const startedAt = new Date().toISOString();
|
|
139
143
|
const callStack = [...(options.callStack || [])];
|
|
140
144
|
|
|
@@ -243,26 +247,26 @@ export class ActionRunner {
|
|
|
243
247
|
};
|
|
244
248
|
|
|
245
249
|
// 5. 构建 ActionContext 运行时上下文
|
|
246
|
-
const
|
|
247
|
-
this.storage,
|
|
248
|
-
this.configOverrides,
|
|
249
|
-
this.projectConfig
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
): Promise<O> => {
|
|
250
|
+
const ctx = createActionContext({
|
|
251
|
+
storage: this.storage,
|
|
252
|
+
overrides: this.configOverrides,
|
|
253
|
+
projectConfig: this.projectConfig,
|
|
254
|
+
runId,
|
|
255
|
+
rootRunId: initialRun.rootRunId,
|
|
256
|
+
parentRunId: options.parentRunId,
|
|
257
|
+
signal: controller.signal,
|
|
258
|
+
process: options.process,
|
|
259
|
+
progress: options.progress,
|
|
260
|
+
logger: options.logger || new StderrLogger(action.id),
|
|
261
|
+
onActionInvoke: async (childAction, childInput, parentRunId) => {
|
|
259
262
|
const childResult = await this.execute(childAction, childInput, {
|
|
260
263
|
rootRunId: initialRun.rootRunId,
|
|
261
|
-
parentRunId
|
|
264
|
+
parentRunId,
|
|
262
265
|
callStack,
|
|
263
266
|
signal: controller.signal,
|
|
264
267
|
process: options.process,
|
|
265
268
|
progress: options.progress,
|
|
269
|
+
logger: options.logger,
|
|
266
270
|
});
|
|
267
271
|
if (!childResult.ok) {
|
|
268
272
|
const err = new Error(childResult.error.message);
|
|
@@ -270,26 +274,9 @@ export class ActionRunner {
|
|
|
270
274
|
(err as any).details = childResult.error.details;
|
|
271
275
|
throw err;
|
|
272
276
|
}
|
|
273
|
-
return childResult.data
|
|
277
|
+
return childResult.data;
|
|
274
278
|
},
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const ctx: ActionContext = {
|
|
278
|
-
config,
|
|
279
|
-
state,
|
|
280
|
-
actions: invoker,
|
|
281
|
-
process: options.process || getProcessExecutor(),
|
|
282
|
-
log,
|
|
283
|
-
progress: options.progress || {
|
|
284
|
-
report() {},
|
|
285
|
-
},
|
|
286
|
-
signal: controller.signal,
|
|
287
|
-
run: {
|
|
288
|
-
id: runId,
|
|
289
|
-
rootId: initialRun.rootRunId,
|
|
290
|
-
parentId: options.parentRunId,
|
|
291
|
-
},
|
|
292
|
-
};
|
|
279
|
+
});
|
|
293
280
|
|
|
294
281
|
// 6. 执行 Action 业务逻辑并与取消/超时信号进行竞态
|
|
295
282
|
const abortPromise = new Promise<never>((_, reject) => {
|
package/src/server/index.ts
CHANGED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { filterByIntent } from "../../filter";
|
|
4
|
+
import { loadActions, loadProjectConfig } from "../../project/loader";
|
|
5
|
+
import { listLinkedPackages, resolveActionProject } from "../../registry/registry";
|
|
6
|
+
import { ActionRunner } from "../../runtime/runner";
|
|
7
|
+
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body";
|
|
8
|
+
import { type RouteContext, jsonResponse } from "./common";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 处理 Action 列表、详情查询及同步/异步运行接口。
|
|
12
|
+
*/
|
|
13
|
+
export async function handleActionsRoutes(ctx: RouteContext): Promise<Response | null> {
|
|
14
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome, runtimeRegistry, options } = ctx;
|
|
15
|
+
|
|
16
|
+
// 1. Actions List: GET /api/v1/actions
|
|
17
|
+
if (pathname === "/api/v1/actions" && req.method === "GET") {
|
|
18
|
+
try {
|
|
19
|
+
const actionList: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
description: string;
|
|
22
|
+
packageId?: string;
|
|
23
|
+
}> = [];
|
|
24
|
+
|
|
25
|
+
if (projectRoot) {
|
|
26
|
+
const config = loadProjectConfig(projectRoot);
|
|
27
|
+
const actions = await loadActions(projectRoot, config.actionsDir);
|
|
28
|
+
for (const [id, a] of actions.entries()) {
|
|
29
|
+
actionList.push({
|
|
30
|
+
id,
|
|
31
|
+
description: a.description || "",
|
|
32
|
+
packageId: config.id,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const linked = listLinkedPackages(customHome);
|
|
38
|
+
for (const pkg of linked) {
|
|
39
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
40
|
+
if (!existsSync(pkg.path)) continue;
|
|
41
|
+
try {
|
|
42
|
+
const config = loadProjectConfig(pkg.path);
|
|
43
|
+
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
44
|
+
for (const [id, a] of actions.entries()) {
|
|
45
|
+
actionList.push({
|
|
46
|
+
id,
|
|
47
|
+
description: a.description || "",
|
|
48
|
+
packageId: pkg.id,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// 忽略故障包
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const intent = url.searchParams.get("intent");
|
|
57
|
+
const targetPkg = url.searchParams.get("package");
|
|
58
|
+
|
|
59
|
+
let filtered = targetPkg
|
|
60
|
+
? actionList.filter((a) => a.packageId === targetPkg)
|
|
61
|
+
: actionList;
|
|
62
|
+
|
|
63
|
+
if (intent) {
|
|
64
|
+
filtered = filterByIntent(
|
|
65
|
+
filtered,
|
|
66
|
+
intent,
|
|
67
|
+
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return jsonResponse(filtered, 200, corsHeaders);
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
return jsonResponse(
|
|
75
|
+
{
|
|
76
|
+
ok: false,
|
|
77
|
+
error: { code: "ACTIONS_LIST_ERROR", message: err.message },
|
|
78
|
+
},
|
|
79
|
+
500,
|
|
80
|
+
corsHeaders
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Action Show: GET /api/v1/actions/:id
|
|
86
|
+
const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
|
|
87
|
+
if (actionShowMatch && req.method === "GET") {
|
|
88
|
+
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
89
|
+
try {
|
|
90
|
+
const resolved = await resolveActionProject(
|
|
91
|
+
actionId,
|
|
92
|
+
projectRoot || process.cwd(),
|
|
93
|
+
customHome
|
|
94
|
+
);
|
|
95
|
+
const config = loadProjectConfig(resolved.projectRoot);
|
|
96
|
+
const actions = await loadActions(resolved.projectRoot, config.actionsDir);
|
|
97
|
+
const action = actions.get(resolved.actionId);
|
|
98
|
+
if (!action) {
|
|
99
|
+
return jsonResponse(
|
|
100
|
+
{
|
|
101
|
+
ok: false,
|
|
102
|
+
error: {
|
|
103
|
+
code: "ACTION_NOT_FOUND",
|
|
104
|
+
message: `Action '${resolved.actionId}' not found in package '${resolved.packageId}'`,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
404,
|
|
108
|
+
corsHeaders
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return jsonResponse(
|
|
113
|
+
{
|
|
114
|
+
id: action.id,
|
|
115
|
+
packageId: resolved.packageId,
|
|
116
|
+
description: action.description || "",
|
|
117
|
+
inputSchema: action.inputSchema || null,
|
|
118
|
+
outputSchema: action.outputSchema || null,
|
|
119
|
+
},
|
|
120
|
+
200,
|
|
121
|
+
corsHeaders
|
|
122
|
+
);
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
return jsonResponse(
|
|
125
|
+
{
|
|
126
|
+
ok: false,
|
|
127
|
+
error: { code: "ACTION_NOT_FOUND", message: err.message },
|
|
128
|
+
},
|
|
129
|
+
404,
|
|
130
|
+
corsHeaders
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Action Run: POST /api/v1/actions/:id/run
|
|
136
|
+
const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
|
|
137
|
+
if (actionRunMatch && req.method === "POST") {
|
|
138
|
+
const actionId = decodeURIComponent(actionRunMatch[1]);
|
|
139
|
+
let body: any = {};
|
|
140
|
+
try {
|
|
141
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
if (err instanceof RequestTooLargeError) {
|
|
144
|
+
return jsonResponse(
|
|
145
|
+
{
|
|
146
|
+
ok: false,
|
|
147
|
+
runId: randomUUID(),
|
|
148
|
+
error: { code: "REQUEST_TOO_LARGE", message: err.message },
|
|
149
|
+
},
|
|
150
|
+
413,
|
|
151
|
+
corsHeaders
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (err instanceof InvalidJsonError) {
|
|
155
|
+
return jsonResponse(
|
|
156
|
+
{
|
|
157
|
+
ok: false,
|
|
158
|
+
runId: randomUUID(),
|
|
159
|
+
error: { code: "INVALID_JSON", message: err.message },
|
|
160
|
+
},
|
|
161
|
+
400,
|
|
162
|
+
corsHeaders
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return jsonResponse(
|
|
166
|
+
{
|
|
167
|
+
ok: false,
|
|
168
|
+
runId: randomUUID(),
|
|
169
|
+
error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
|
|
170
|
+
},
|
|
171
|
+
400,
|
|
172
|
+
corsHeaders
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const resolved = await resolveActionProject(
|
|
178
|
+
actionId,
|
|
179
|
+
projectRoot || process.cwd(),
|
|
180
|
+
customHome
|
|
181
|
+
);
|
|
182
|
+
const config = loadProjectConfig(resolved.projectRoot);
|
|
183
|
+
const actions = await loadActions(resolved.projectRoot, config.actionsDir);
|
|
184
|
+
const storage = runtimeRegistry.getStorage(config.id, resolved.projectRoot);
|
|
185
|
+
|
|
186
|
+
const runner = new ActionRunner({
|
|
187
|
+
packageId: config.id,
|
|
188
|
+
storage,
|
|
189
|
+
projectConfig: config,
|
|
190
|
+
configOverrides: body?.config || {},
|
|
191
|
+
actions,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const isAsync = body?.execution?.mode === "async" || body?.async === true;
|
|
195
|
+
const timeoutMs =
|
|
196
|
+
typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
|
|
197
|
+
? body.execution.timeoutMs
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
if (isAsync) {
|
|
201
|
+
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
202
|
+
timeoutMs,
|
|
203
|
+
});
|
|
204
|
+
runtimeRegistry.executionManager.register(handle);
|
|
205
|
+
|
|
206
|
+
// 监听结算以广播 SSE 完成事件
|
|
207
|
+
handle.result
|
|
208
|
+
.then((res) => {
|
|
209
|
+
runtimeRegistry.emit(handle.runId, { type: "finish", data: res });
|
|
210
|
+
})
|
|
211
|
+
.catch((err) => {
|
|
212
|
+
runtimeRegistry.emit(handle.runId, {
|
|
213
|
+
type: "finish",
|
|
214
|
+
data: {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: {
|
|
217
|
+
code: "ACTION_EXECUTION_ERROR",
|
|
218
|
+
message: err?.message || String(err),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return jsonResponse(
|
|
225
|
+
{
|
|
226
|
+
ok: true,
|
|
227
|
+
runId: handle.runId,
|
|
228
|
+
status: "running",
|
|
229
|
+
streamUrl: `/api/v1/runs/${handle.runId}/stream`,
|
|
230
|
+
},
|
|
231
|
+
202,
|
|
232
|
+
corsHeaders
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 同步执行模式
|
|
237
|
+
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
238
|
+
signal: req.signal,
|
|
239
|
+
timeoutMs,
|
|
240
|
+
});
|
|
241
|
+
runtimeRegistry.executionManager.register(handle);
|
|
242
|
+
const result = await handle.result;
|
|
243
|
+
|
|
244
|
+
return jsonResponse(result, 200, corsHeaders);
|
|
245
|
+
} catch (err: any) {
|
|
246
|
+
return jsonResponse(
|
|
247
|
+
{
|
|
248
|
+
ok: false,
|
|
249
|
+
runId: randomUUID(),
|
|
250
|
+
error: {
|
|
251
|
+
code: "ACTION_EXECUTION_ERROR",
|
|
252
|
+
message: err.message || String(err),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
500,
|
|
256
|
+
corsHeaders
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { loadProjectConfig } from "../../project/loader";
|
|
3
|
+
import { listLinkedPackages, resolvePackageRoot } from "../../registry/registry";
|
|
4
|
+
import type { RuntimeStorage } from "../../storage/types";
|
|
5
|
+
import type { RunRecord } from "@actiondock/sdk";
|
|
6
|
+
import type { ServerRuntimeRegistry } from "../runtime-registry";
|
|
7
|
+
import type { ServerOptions } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 路由处理统一上下文对象。
|
|
11
|
+
*/
|
|
12
|
+
export interface RouteContext {
|
|
13
|
+
req: Request;
|
|
14
|
+
url: URL;
|
|
15
|
+
pathname: string;
|
|
16
|
+
corsHeaders: Record<string, string>;
|
|
17
|
+
projectRoot: string | null;
|
|
18
|
+
customHome?: string;
|
|
19
|
+
runtimeRegistry: ServerRuntimeRegistry;
|
|
20
|
+
options: ServerOptions;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 辅助函数:构造带 CORS 头的标准 JSON HTTP 响应。
|
|
25
|
+
*/
|
|
26
|
+
export function jsonResponse(
|
|
27
|
+
data: unknown,
|
|
28
|
+
status = 200,
|
|
29
|
+
corsHeaders: Record<string, string> = {}
|
|
30
|
+
): Response {
|
|
31
|
+
return new Response(JSON.stringify(data, null, 2), {
|
|
32
|
+
status,
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
...corsHeaders,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 依据 package 参数或项目上下文解析目标 Storage 实例与根目录。
|
|
42
|
+
*/
|
|
43
|
+
export function resolveStorageForPackage(
|
|
44
|
+
packageIdOrPath: string | undefined,
|
|
45
|
+
runtimeRegistry: ServerRuntimeRegistry,
|
|
46
|
+
projectRoot?: string | null,
|
|
47
|
+
customHome?: string
|
|
48
|
+
): { packageId: string; storage: RuntimeStorage; projectRoot?: string } {
|
|
49
|
+
if (packageIdOrPath) {
|
|
50
|
+
const root = resolvePackageRoot(packageIdOrPath, customHome);
|
|
51
|
+
if (root) {
|
|
52
|
+
const config = loadProjectConfig(root);
|
|
53
|
+
return {
|
|
54
|
+
packageId: config.id,
|
|
55
|
+
storage: runtimeRegistry.getStorage(config.id, root),
|
|
56
|
+
projectRoot: root,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
packageId: packageIdOrPath,
|
|
61
|
+
storage: runtimeRegistry.getStorage(packageIdOrPath),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (projectRoot) {
|
|
66
|
+
const config = loadProjectConfig(projectRoot);
|
|
67
|
+
return {
|
|
68
|
+
packageId: config.id,
|
|
69
|
+
storage: runtimeRegistry.getStorage(config.id, projectRoot),
|
|
70
|
+
projectRoot,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const linked = listLinkedPackages(customHome);
|
|
75
|
+
if (linked.length > 0) {
|
|
76
|
+
const first = linked[0];
|
|
77
|
+
return {
|
|
78
|
+
packageId: first.id,
|
|
79
|
+
storage: runtimeRegistry.getStorage(first.id, first.path),
|
|
80
|
+
projectRoot: first.path,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
packageId: "default",
|
|
86
|
+
storage: runtimeRegistry.getStorage("default"),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 跨活跃连接与所有已知持久化存储全局检索指定 runId 的运行记录。
|
|
92
|
+
*/
|
|
93
|
+
export function findRunAcrossStorages(
|
|
94
|
+
runId: string,
|
|
95
|
+
runtimeRegistry: ServerRuntimeRegistry,
|
|
96
|
+
projectRoot?: string | null,
|
|
97
|
+
customHome?: string
|
|
98
|
+
): { storage: RuntimeStorage; run: RunRecord } | null {
|
|
99
|
+
const inMemory = runtimeRegistry.findRun(runId);
|
|
100
|
+
if (inMemory) return inMemory as { storage: RuntimeStorage; run: RunRecord };
|
|
101
|
+
|
|
102
|
+
if (projectRoot) {
|
|
103
|
+
try {
|
|
104
|
+
const config = loadProjectConfig(projectRoot);
|
|
105
|
+
const storage = runtimeRegistry.getStorage(config.id, projectRoot);
|
|
106
|
+
const run = storage.getRun(runId);
|
|
107
|
+
if (run) return { storage, run };
|
|
108
|
+
} catch {
|
|
109
|
+
// 忽略读取错误
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const linked = listLinkedPackages(customHome);
|
|
115
|
+
for (const pkg of linked) {
|
|
116
|
+
if (!existsSync(pkg.path)) continue;
|
|
117
|
+
const storage = runtimeRegistry.getStorage(pkg.id, pkg.path);
|
|
118
|
+
const run = storage.getRun(runId);
|
|
119
|
+
if (run) return { storage, run };
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// 忽略读取错误
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { loadProjectConfig } from "../../project/loader";
|
|
2
|
+
import { readJsonBody } from "../body";
|
|
3
|
+
import { type RouteContext, jsonResponse, resolveStorageForPackage } from "./common";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 处理配置元数据与当前值读取、更新及删除接口。
|
|
7
|
+
*/
|
|
8
|
+
export async function handleConfigRoutes(ctx: RouteContext): Promise<Response | null> {
|
|
9
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome, runtimeRegistry, options } = ctx;
|
|
10
|
+
|
|
11
|
+
// 1. Config Env Check: GET /api/v1/config/env
|
|
12
|
+
if (pathname === "/api/v1/config/env" && req.method === "GET") {
|
|
13
|
+
try {
|
|
14
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
15
|
+
const { packageId, projectRoot: root } = resolveStorageForPackage(
|
|
16
|
+
pkgParam,
|
|
17
|
+
runtimeRegistry,
|
|
18
|
+
projectRoot,
|
|
19
|
+
customHome
|
|
20
|
+
);
|
|
21
|
+
if (!root) {
|
|
22
|
+
return jsonResponse({ ok: true, packageId, envChecks: [] }, 200, corsHeaders);
|
|
23
|
+
}
|
|
24
|
+
const cfg = loadProjectConfig(root);
|
|
25
|
+
const declared = cfg.config || {};
|
|
26
|
+
const envChecks: any[] = [];
|
|
27
|
+
for (const [k, def] of Object.entries(declared)) {
|
|
28
|
+
const envKeys = [
|
|
29
|
+
k,
|
|
30
|
+
`ACTIONDOCK_${k}`,
|
|
31
|
+
`${packageId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${k}`,
|
|
32
|
+
];
|
|
33
|
+
const foundEnv = envKeys.find((ek) => process.env[ek] !== undefined);
|
|
34
|
+
envChecks.push({
|
|
35
|
+
key: k,
|
|
36
|
+
required: def.default === undefined,
|
|
37
|
+
satisfied: Boolean(foundEnv || def.default !== undefined),
|
|
38
|
+
matchedEnv: foundEnv || null,
|
|
39
|
+
hasDefault: def.default !== undefined,
|
|
40
|
+
secret: Boolean(def.secret),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return jsonResponse({ ok: true, packageId, envChecks }, 200, corsHeaders);
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
return jsonResponse(
|
|
46
|
+
{ ok: false, error: { code: "CONFIG_ENV_ERROR", message: err.message } },
|
|
47
|
+
500,
|
|
48
|
+
corsHeaders
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Config Query: GET /api/v1/config
|
|
54
|
+
if (pathname === "/api/v1/config" && req.method === "GET") {
|
|
55
|
+
try {
|
|
56
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
57
|
+
const { packageId, storage, projectRoot: root } = resolveStorageForPackage(
|
|
58
|
+
pkgParam,
|
|
59
|
+
runtimeRegistry,
|
|
60
|
+
projectRoot,
|
|
61
|
+
customHome
|
|
62
|
+
);
|
|
63
|
+
const stored = storage.listConfig();
|
|
64
|
+
let declared: Record<string, any> = {};
|
|
65
|
+
if (root) {
|
|
66
|
+
try {
|
|
67
|
+
const cfg = loadProjectConfig(root);
|
|
68
|
+
declared = cfg.config || {};
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
const maskedValues: Record<string, any> = {};
|
|
72
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
73
|
+
if (declared[k]?.secret) {
|
|
74
|
+
maskedValues[k] = "******";
|
|
75
|
+
} else {
|
|
76
|
+
maskedValues[k] = v;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return jsonResponse(
|
|
80
|
+
{ ok: true, packageId, declared, values: maskedValues },
|
|
81
|
+
200,
|
|
82
|
+
corsHeaders
|
|
83
|
+
);
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
return jsonResponse(
|
|
86
|
+
{ ok: false, error: { code: "CONFIG_LIST_ERROR", message: err.message } },
|
|
87
|
+
500,
|
|
88
|
+
corsHeaders
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Config Update: PUT / POST /api/v1/config
|
|
94
|
+
if (pathname === "/api/v1/config" && (req.method === "PUT" || req.method === "POST")) {
|
|
95
|
+
try {
|
|
96
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
97
|
+
const pkgParam = url.searchParams.get("package") || body.package || undefined;
|
|
98
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
99
|
+
pkgParam,
|
|
100
|
+
runtimeRegistry,
|
|
101
|
+
projectRoot,
|
|
102
|
+
customHome
|
|
103
|
+
);
|
|
104
|
+
const key = body.key;
|
|
105
|
+
if (!key) {
|
|
106
|
+
return jsonResponse(
|
|
107
|
+
{ ok: false, error: { code: "INVALID_ARGUMENT", message: "Config 'key' is required" } },
|
|
108
|
+
400,
|
|
109
|
+
corsHeaders
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
storage.setConfig(key, body.value);
|
|
113
|
+
return jsonResponse({ ok: true, packageId, key, message: "updated" }, 200, corsHeaders);
|
|
114
|
+
} catch (err: any) {
|
|
115
|
+
return jsonResponse(
|
|
116
|
+
{ ok: false, error: { code: "CONFIG_SET_ERROR", message: err.message } },
|
|
117
|
+
500,
|
|
118
|
+
corsHeaders
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Config Delete: DELETE /api/v1/config/:key
|
|
124
|
+
const configKeyMatch = pathname.match(/^\/api\/v1\/config\/([^/]+)$/);
|
|
125
|
+
if (configKeyMatch && req.method === "DELETE") {
|
|
126
|
+
try {
|
|
127
|
+
const key = decodeURIComponent(configKeyMatch[1]);
|
|
128
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
129
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
130
|
+
pkgParam,
|
|
131
|
+
runtimeRegistry,
|
|
132
|
+
projectRoot,
|
|
133
|
+
customHome
|
|
134
|
+
);
|
|
135
|
+
const deleted = storage.deleteConfig(key);
|
|
136
|
+
return jsonResponse({ ok: true, packageId, key, deleted }, 200, corsHeaders);
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
return jsonResponse(
|
|
139
|
+
{ ok: false, error: { code: "CONFIG_DELETE_ERROR", message: err.message } },
|
|
140
|
+
500,
|
|
141
|
+
corsHeaders
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { runDoctorChecks } from "../../doctor/doctor";
|
|
2
|
+
import { type RouteContext, jsonResponse } from "./common";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 处理环境与依赖诊断接口(GET /api/v1/doctor)。
|
|
6
|
+
*/
|
|
7
|
+
export async function handleDoctorRoute(ctx: RouteContext): Promise<Response | null> {
|
|
8
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome } = ctx;
|
|
9
|
+
|
|
10
|
+
if (pathname !== "/api/v1/doctor" || req.method !== "GET") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
16
|
+
const report = await runDoctorChecks({
|
|
17
|
+
cwd: projectRoot || process.cwd(),
|
|
18
|
+
packageIdOrPath: targetPkg,
|
|
19
|
+
customHome,
|
|
20
|
+
});
|
|
21
|
+
return jsonResponse({ ok: true, report }, 200, corsHeaders);
|
|
22
|
+
} catch (err: any) {
|
|
23
|
+
return jsonResponse(
|
|
24
|
+
{ ok: false, error: { code: "DOCTOR_ERROR", message: err.message } },
|
|
25
|
+
500,
|
|
26
|
+
corsHeaders
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|