@actiondock/core 2.0.9 → 2.0.11-beta.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/package.json +2 -2
- 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 +348 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +46 -7
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +271 -34
- package/src/runtime/context.ts +8 -3
- 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 +193 -68
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { verifyBearerToken } from "../security";
|
|
2
|
+
import { type RouteContext, jsonResponse } from "./common";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 处理健康检查与就绪状态接口(支持 /api/v1/health 与 /health)。
|
|
6
|
+
*/
|
|
7
|
+
export async function handleHealthRoute(ctx: RouteContext): Promise<Response | null> {
|
|
8
|
+
const { req, pathname, corsHeaders, options, projectRoot } = ctx;
|
|
9
|
+
|
|
10
|
+
if (pathname !== "/api/v1/health" && pathname !== "/health") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!verifyBearerToken(req, options.token)) {
|
|
15
|
+
return jsonResponse(
|
|
16
|
+
{
|
|
17
|
+
ok: false,
|
|
18
|
+
error: {
|
|
19
|
+
code: "UNAUTHORIZED",
|
|
20
|
+
message: "Invalid or missing Bearer token",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
401,
|
|
24
|
+
corsHeaders
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const healthData: Record<string, unknown> = {
|
|
29
|
+
status: "ok",
|
|
30
|
+
version: "2.0.0",
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
uptime: process.uptime(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (options.exposeDebugInfo && projectRoot) {
|
|
36
|
+
healthData.projectRoot = projectRoot;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return jsonResponse(healthData, 200, corsHeaders);
|
|
40
|
+
}
|