@yinuo-ngm/server 1.0.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/lib/app.d.ts +2 -0
- package/lib/app.js +65 -0
- package/lib/common/api.d.ts +2 -0
- package/lib/common/api.js +13 -0
- package/lib/common/editor.d.ts +5 -0
- package/lib/common/editor.js +69 -0
- package/lib/env.d.ts +7 -0
- package/lib/env.js +16 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +20 -0
- package/lib/plugins/core.plugin.d.ts +9 -0
- package/lib/plugins/core.plugin.js +16 -0
- package/lib/plugins/error-handler.plugin.d.ts +6 -0
- package/lib/plugins/error-handler.plugin.js +113 -0
- package/lib/plugins/request-id.plugin.d.ts +3 -0
- package/lib/plugins/request-id.plugin.js +14 -0
- package/lib/plugins/routes.plugin.d.ts +3 -0
- package/lib/plugins/routes.plugin.js +17 -0
- package/lib/plugins/static.plugin.d.ts +2 -0
- package/lib/plugins/static.plugin.js +24 -0
- package/lib/plugins/success-handle.plugin.d.ts +3 -0
- package/lib/plugins/success-handle.plugin.js +30 -0
- package/lib/plugins/ws/topics/index.d.ts +1 -0
- package/lib/plugins/ws/topics/index.js +17 -0
- package/lib/plugins/ws/topics/syslog.ws.d.ts +9 -0
- package/lib/plugins/ws/topics/syslog.ws.js +28 -0
- package/lib/plugins/ws/topics/task.ws.d.ts +12 -0
- package/lib/plugins/ws/topics/task.ws.js +75 -0
- package/lib/plugins/ws/ws.context.d.ts +12 -0
- package/lib/plugins/ws/ws.context.js +35 -0
- package/lib/plugins/ws/ws.plugin.d.ts +3 -0
- package/lib/plugins/ws/ws.plugin.js +76 -0
- package/lib/plugins/ws/ws.router.d.ts +20 -0
- package/lib/plugins/ws/ws.router.js +67 -0
- package/lib/routes/config.routes.d.ts +2 -0
- package/lib/routes/config.routes.js +65 -0
- package/lib/routes/dashboard.routes.d.ts +2 -0
- package/lib/routes/dashboard.routes.js +33 -0
- package/lib/routes/deps.route.d.ts +2 -0
- package/lib/routes/deps.route.js +25 -0
- package/lib/routes/fs.routes.d.ts +2 -0
- package/lib/routes/fs.routes.js +29 -0
- package/lib/routes/index.d.ts +9 -0
- package/lib/routes/index.js +22 -0
- package/lib/routes/project.routes.d.ts +2 -0
- package/lib/routes/project.routes.js +196 -0
- package/lib/routes/rss.routes.d.ts +2 -0
- package/lib/routes/rss.routes.js +53 -0
- package/lib/routes/system.routes.d.ts +2 -0
- package/lib/routes/system.routes.js +28 -0
- package/lib/routes/task.routes.d.ts +2 -0
- package/lib/routes/task.routes.js +48 -0
- package/package.json +29 -0
- package/src/app.ts +76 -0
- package/src/common/api.ts +12 -0
- package/src/common/editor.ts +49 -0
- package/src/env.ts +12 -0
- package/src/index.ts +21 -0
- package/src/plugins/core.plugin.ts +34 -0
- package/src/plugins/error-handler.plugin.ts +168 -0
- package/src/plugins/request-id.plugin.ts +14 -0
- package/src/plugins/routes.plugin.ts +24 -0
- package/src/plugins/static.plugin.ts +30 -0
- package/src/plugins/success-handle.plugin.ts +33 -0
- package/src/plugins/ws/topics/index.ts +1 -0
- package/src/plugins/ws/topics/syslog.ws.ts +36 -0
- package/src/plugins/ws/topics/task.ws.ts +96 -0
- package/src/plugins/ws/ws.context.ts +32 -0
- package/src/plugins/ws/ws.plugin.ts +103 -0
- package/src/plugins/ws/ws.router.ts +79 -0
- package/src/routes/config.routes.ts +86 -0
- package/src/routes/dashboard.routes.ts +43 -0
- package/src/routes/deps.route.ts +51 -0
- package/src/routes/fs.routes.ts +31 -0
- package/src/routes/index.ts +19 -0
- package/src/routes/project.routes.ts +265 -0
- package/src/routes/rss.routes.ts +58 -0
- package/src/routes/system.routes.ts +32 -0
- package/src/routes/task.routes.ts +85 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { AppError } from "@yinuo-ngm/core";
|
|
2
|
+
import { type FastifyInstance } from "fastify";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { openFolder } from "../common/editor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Project routes
|
|
8
|
+
*/
|
|
9
|
+
export default async function projectRoutes(fastify: FastifyInstance) {
|
|
10
|
+
/**
|
|
11
|
+
* 列出所有项目
|
|
12
|
+
* POST /list
|
|
13
|
+
*/
|
|
14
|
+
fastify.get("/list", async () => {
|
|
15
|
+
const projects = await fastify.core.project.list();
|
|
16
|
+
return projects;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 更新项目(编辑)
|
|
21
|
+
*/
|
|
22
|
+
fastify.post("/update/:id", async (req, reply) => {
|
|
23
|
+
const { id } = req.params as { id: string };
|
|
24
|
+
const body = req.body as Partial<{
|
|
25
|
+
name: string;
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
scripts: Record<string, string>;
|
|
28
|
+
}>;
|
|
29
|
+
try {
|
|
30
|
+
// 明确禁止修改的字段
|
|
31
|
+
if ((body as any).root) {
|
|
32
|
+
reply.code(400);
|
|
33
|
+
return { message: "root is immutable" };
|
|
34
|
+
}
|
|
35
|
+
const updated = await fastify.core.project.update(id, {
|
|
36
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
37
|
+
...(body.env !== undefined ? { env: body.env } : {}),
|
|
38
|
+
...(body.scripts !== undefined ? { scripts: body.scripts } : {}),
|
|
39
|
+
});
|
|
40
|
+
return updated;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err instanceof Error) {
|
|
43
|
+
reply.code(400);
|
|
44
|
+
return { message: err.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 获取项目详情
|
|
51
|
+
*/
|
|
52
|
+
fastify.get("/getInfo/:id", async (req,) => {
|
|
53
|
+
const { id } = req.params as { id: string };
|
|
54
|
+
const project = await fastify.core.project.get(id);
|
|
55
|
+
return project;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 删除项目
|
|
60
|
+
*/
|
|
61
|
+
fastify.get("/delete/:id", async (req) => {
|
|
62
|
+
const { id } = req.params as { id: string };
|
|
63
|
+
await fastify.core.project.remove(id);
|
|
64
|
+
return { id };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 检查路径是否合法 / 是否已存在项目
|
|
69
|
+
*/
|
|
70
|
+
fastify.post("/check", async (req) => {
|
|
71
|
+
const body = req.body as { rootPath: string };
|
|
72
|
+
return fastify.core.project.checkRoot(body.rootPath);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 扫描项目(只读)
|
|
77
|
+
* POST /detect
|
|
78
|
+
*/
|
|
79
|
+
fastify.post("/detect", async (req) => {
|
|
80
|
+
const body = req.body as { rootPath: string };
|
|
81
|
+
const root = path.resolve(body.rootPath);
|
|
82
|
+
|
|
83
|
+
// 直接复用 core.project.scan
|
|
84
|
+
const meta = await fastify.core.project.scan(root);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
framework: meta.framework ?? "unknown",
|
|
88
|
+
hasPackageJson: !!meta.scripts && Object.keys(meta.scripts).length > 0,
|
|
89
|
+
scripts: meta.scripts ?? {},
|
|
90
|
+
recommendedScript:
|
|
91
|
+
meta.scripts?.dev
|
|
92
|
+
? "dev"
|
|
93
|
+
: meta.scripts?.start
|
|
94
|
+
? "start"
|
|
95
|
+
: Object.keys(meta.scripts ?? {})[0],
|
|
96
|
+
hasGit: meta.hasGit ?? false,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 导入已有项目
|
|
102
|
+
*/
|
|
103
|
+
fastify.post("/import", async (req) => {
|
|
104
|
+
const body = req.body as {
|
|
105
|
+
root: string;
|
|
106
|
+
name?: string;
|
|
107
|
+
syncTasks?: boolean;
|
|
108
|
+
};
|
|
109
|
+
const project = await fastify.core.project.importProject({
|
|
110
|
+
root: body.root,
|
|
111
|
+
name: body.name,
|
|
112
|
+
});
|
|
113
|
+
// 要自动 sync task,这里加
|
|
114
|
+
if (body.syncTasks === true) {
|
|
115
|
+
await fastify.core.task.refreshByProject(project.id);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: project.id,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 检查 root 是否是一个可导入的项目
|
|
124
|
+
*/
|
|
125
|
+
fastify.post("/checkImport", async (req) => {
|
|
126
|
+
const body = req.body as { root: string };
|
|
127
|
+
return fastify.core.project.checkImport(body.root);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 创建新项目(暂不接 scaffold)
|
|
132
|
+
*/
|
|
133
|
+
fastify.post("/create", async (req) => {
|
|
134
|
+
// const body = req.body as {
|
|
135
|
+
// name: string;
|
|
136
|
+
// root: string;
|
|
137
|
+
// scripts?: Record<string, string>;
|
|
138
|
+
// syncTasks?: boolean;
|
|
139
|
+
// };
|
|
140
|
+
|
|
141
|
+
// const chk = await fastify.core.project.checkRoot(body.root);
|
|
142
|
+
// if (!chk.ok) return chk;
|
|
143
|
+
|
|
144
|
+
// const project = await fastify.core.project.create({
|
|
145
|
+
// name: body.name,
|
|
146
|
+
// root: chk.root,
|
|
147
|
+
// scripts: body.scripts,
|
|
148
|
+
// });
|
|
149
|
+
|
|
150
|
+
// let syncedSpecs: any[] | undefined;
|
|
151
|
+
// if (body.syncTasks === true) {
|
|
152
|
+
// syncedSpecs = await fastify.core.task.syncSpecsFromProjectScripts(
|
|
153
|
+
// project.id,
|
|
154
|
+
// project.root,
|
|
155
|
+
// project.scripts ?? {}
|
|
156
|
+
// );
|
|
157
|
+
// }
|
|
158
|
+
// return { id: project.id, syncedSpecs };
|
|
159
|
+
const body = req.body as {
|
|
160
|
+
name: string;
|
|
161
|
+
root: string;
|
|
162
|
+
};
|
|
163
|
+
const project = await fastify.core.project.create({
|
|
164
|
+
name: body.name,
|
|
165
|
+
root: body.root,
|
|
166
|
+
});
|
|
167
|
+
return { id: project.id };
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 设置收藏
|
|
173
|
+
* POST /projects/favorite/:id
|
|
174
|
+
* body: { isFavorite: boolean }
|
|
175
|
+
*/
|
|
176
|
+
fastify.post("/favorite/:id", async (req, reply) => {
|
|
177
|
+
const { id } = req.params as { id: string };
|
|
178
|
+
const body = req.body as { isFavorite?: boolean };
|
|
179
|
+
if (typeof body?.isFavorite !== "boolean") {
|
|
180
|
+
reply.code(400);
|
|
181
|
+
return { message: "isFavorite must be boolean" };
|
|
182
|
+
}
|
|
183
|
+
const updated = await fastify.core.project.setFavorite(id, body.isFavorite);
|
|
184
|
+
return updated;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 切换收藏
|
|
189
|
+
* POST /projects/favorite/:id/toggle
|
|
190
|
+
*/
|
|
191
|
+
fastify.post("/favorite/:id/toggle", async (req) => {
|
|
192
|
+
const { id } = req.params as { id: string };
|
|
193
|
+
const updated = await fastify.core.project.toggleFavorite(id);
|
|
194
|
+
return updated;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
fastify.post("/lastOpened/:id", async (req) => {
|
|
198
|
+
const { id } = req.params as { id: string };
|
|
199
|
+
const body = req.body as { timestamp: number };
|
|
200
|
+
if (typeof body?.timestamp !== "number") {
|
|
201
|
+
throw new AppError("INVALID_TIMESTAMP", "timestamp must be a number");
|
|
202
|
+
}
|
|
203
|
+
const updated = await fastify.core.project.setLastOpened(id, body.timestamp);
|
|
204
|
+
return updated;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
fastify.post("/rename/:id", async (req) => {
|
|
208
|
+
const { id } = req.params as { id: string };
|
|
209
|
+
const body = req.body as { name: string };
|
|
210
|
+
if (typeof body?.name !== "string" || body.name.trim() === "") {
|
|
211
|
+
throw new AppError("INVALID_NAME", "name must be a non-empty string");
|
|
212
|
+
}
|
|
213
|
+
const updated = await fastify.core.project.rename(id, body.name.trim());
|
|
214
|
+
return updated;
|
|
215
|
+
})
|
|
216
|
+
fastify.post("/edit/:id", async (req) => {
|
|
217
|
+
const { id } = req.params as { id: string };
|
|
218
|
+
const body = req.body as { name: string; description?: string; repoPageUrl?: string; };
|
|
219
|
+
if (typeof body?.name !== "string" || body.name.trim() === "") {
|
|
220
|
+
throw new AppError("INVALID_NAME", "name must be a non-empty string");
|
|
221
|
+
}
|
|
222
|
+
const updated = await fastify.core.project.edit(id, { name: body.name.trim(), description: body.description?.trim(), repoPageUrl: body.repoPageUrl?.trim() });
|
|
223
|
+
return updated;
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 在编辑器打开项目
|
|
229
|
+
* POST /projects/openInEditor/:id
|
|
230
|
+
* body: { editor?: "code" | "system" }
|
|
231
|
+
*/
|
|
232
|
+
fastify.post("/openInEditor/:id", async (req) => {
|
|
233
|
+
try {
|
|
234
|
+
const { id } = req.params as { id: string };
|
|
235
|
+
const body = req.body as { editor?: "code" | "system" };
|
|
236
|
+
const p = await fastify.core.project.get(id);
|
|
237
|
+
const editor = body?.editor || "code";
|
|
238
|
+
await openFolder(p.root, { editor });
|
|
239
|
+
return { ok: true };
|
|
240
|
+
} catch (e: any) {
|
|
241
|
+
throw new AppError("EDITOR_LAUNCH_FAILED", e?.message || "openInEditor failed");
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
fastify.post("/bootstrap/cli", async (req) => {
|
|
246
|
+
const body = req.body as any;
|
|
247
|
+
return await fastify.core.bootstrap.bootstrapByCli(body);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
fastify.post("/bootstrap/git", async (req) => {
|
|
251
|
+
const body = req.body as any;
|
|
252
|
+
return await fastify.core.bootstrap.bootstrapByGit(body);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
fastify.post("/bootstrap/pickRoot", async (req) => {
|
|
256
|
+
const body = req.body as any;
|
|
257
|
+
const taskId = String(body?.taskId ?? "").trim();
|
|
258
|
+
const pickedRoot = String(body?.pickedRoot ?? "").trim();
|
|
259
|
+
if (!taskId) throw new AppError("BAD_REQUEST", "taskId is required");
|
|
260
|
+
if (!pickedRoot) throw new AppError("BAD_REQUEST", "pickedRoot is required");
|
|
261
|
+
const r = await fastify.core.bootstrap.pickWorkspaceRoot({ taskId, pickedRoot });
|
|
262
|
+
return r;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AppError } from "@yinuo-ngm/core";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import Parser from "rss-parser";
|
|
4
|
+
|
|
5
|
+
type CacheEntry = { expireAt: number; payload: any };
|
|
6
|
+
const cache = new Map<string, CacheEntry>();
|
|
7
|
+
|
|
8
|
+
const parser = new Parser({
|
|
9
|
+
timeout: 15000, // 15s
|
|
10
|
+
});
|
|
11
|
+
export default async function rssRoutes(app: FastifyInstance) {
|
|
12
|
+
app.get("/preview", async (req) => {
|
|
13
|
+
const q = req.query as { url?: string; limit?: string; force?: string; cacheSec?: string };
|
|
14
|
+
const url = (q.url ?? "").trim();
|
|
15
|
+
if (!url) {
|
|
16
|
+
// return reply.code(400).send({ ok: false, message: "url required" });
|
|
17
|
+
throw new AppError("INVALID_RSS_URL", "url required", { query: q });
|
|
18
|
+
}
|
|
19
|
+
// limit between 1 and 100, default 20
|
|
20
|
+
const limit = Math.min(Math.max(parseInt(q.limit ?? "20", 10) || 20, 1), 100);
|
|
21
|
+
// force fetch, default false
|
|
22
|
+
const force = (q.force ?? "0") === "1";
|
|
23
|
+
// cache duration between 30 and 3600 seconds, default 2400
|
|
24
|
+
const cacheSec = Math.min(Math.max(parseInt(q.cacheSec ?? "2400", 10) || 2400, 30), 3600);
|
|
25
|
+
const key = `${url}|${limit}`;
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
if (!force) {
|
|
28
|
+
const hit = cache.get(key);
|
|
29
|
+
if (hit && hit.expireAt > now) return hit.payload;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const feed = await parser.parseURL(url);
|
|
33
|
+
const items = (feed.items ?? []).slice(0, limit).map((it) => ({
|
|
34
|
+
title: it.title ?? "",
|
|
35
|
+
link: (it.link ?? it.guid ?? "").toString(),
|
|
36
|
+
pubDate: it.isoDate ?? (it.pubDate ? new Date(it.pubDate).toISOString() : undefined),
|
|
37
|
+
author: (it.creator ?? it.author ?? "")?.toString() || undefined,
|
|
38
|
+
summary: (it.contentSnippet ?? it.content ?? "")?.toString() || undefined,
|
|
39
|
+
}));
|
|
40
|
+
const payload = {
|
|
41
|
+
title: feed.title,
|
|
42
|
+
description: feed.description,
|
|
43
|
+
link: feed.link,
|
|
44
|
+
items,
|
|
45
|
+
fetchedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
cache.set(key, { expireAt: now + cacheSec * 1000, payload });
|
|
48
|
+
return payload;
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
// return reply.code(500).send({
|
|
51
|
+
// ok: false,
|
|
52
|
+
// message: "RSS_FETCH_FAILED",
|
|
53
|
+
// detail: e?.message || String(e),
|
|
54
|
+
// });
|
|
55
|
+
throw new AppError(`RSS_FETCH_FAILED`, e?.message || String(e), { url });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { env } from "../env";
|
|
3
|
+
|
|
4
|
+
export default async function systemRoutes(fastify: FastifyInstance) {
|
|
5
|
+
fastify.get("/health", async () => (
|
|
6
|
+
{
|
|
7
|
+
ts: Date.now(),
|
|
8
|
+
name: "ngm-server",
|
|
9
|
+
pid: process.pid,
|
|
10
|
+
uptime: process.uptime(),
|
|
11
|
+
version: process.env.npm_package_version,
|
|
12
|
+
dataDir: env.dataDir
|
|
13
|
+
}
|
|
14
|
+
));
|
|
15
|
+
|
|
16
|
+
fastify.post("/shutdown", async () => {
|
|
17
|
+
fastify.log.info("Shutdown requested via /shutdown");
|
|
18
|
+
|
|
19
|
+
// 异步关闭,避免阻塞响应
|
|
20
|
+
setTimeout(async () => {
|
|
21
|
+
try {
|
|
22
|
+
await fastify.close(); // ← 会触发 onClose
|
|
23
|
+
process.exit(0);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error("Graceful shutdown failed", e);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}, 50);
|
|
29
|
+
|
|
30
|
+
return { ok: true };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AppError } from "@yinuo-ngm/core";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
/** 判断该项目是否已有 specs(用于懒加载) */
|
|
4
|
+
async function ensureSpecs(fastify: FastifyInstance, projectId: string) {
|
|
5
|
+
const specs = await fastify.core.task.listSpecsByProject(projectId);
|
|
6
|
+
if (specs.length === 0) {
|
|
7
|
+
await fastify.core.task.refreshByProject(projectId);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function taskRoutes(fastify: FastifyInstance) {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取 task views(spec + runtime 聚合)
|
|
15
|
+
* 懒加载,如果该项目未 refresh 过则自动 refresh
|
|
16
|
+
* GET /views/:projectId
|
|
17
|
+
*/
|
|
18
|
+
fastify.get("/list/:projectId", async (req) => {
|
|
19
|
+
const { projectId } = req.params as { projectId: string };
|
|
20
|
+
await ensureSpecs(fastify, projectId);
|
|
21
|
+
return fastify.core.task.listViewsByProject(projectId);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 刷新(从 ProjectService.get(projectId) 的 scripts 重新生成 specs)
|
|
26
|
+
* 返回 views,前端可直接渲染
|
|
27
|
+
* POST /refresh/:projectId
|
|
28
|
+
*/
|
|
29
|
+
fastify.post("/refresh/:projectId", async (req) => {
|
|
30
|
+
const { projectId } = req.params as { projectId: string };
|
|
31
|
+
// refreshByProject 内部会从 ProjectService.get() 拉 root/scripts/pm 并更新 specs
|
|
32
|
+
return await fastify.core.task.refreshByProject(projectId);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 启动任务(唯一方式)
|
|
37
|
+
* POST /start
|
|
38
|
+
* body: { taskId: string }
|
|
39
|
+
*/
|
|
40
|
+
fastify.post("/start", async (req) => {
|
|
41
|
+
const body = req.body as { taskId?: string };
|
|
42
|
+
const taskId = body?.taskId?.trim();
|
|
43
|
+
if (!taskId) throw new AppError("TASK_ID_REQUIRED", "taskId is required", { body });
|
|
44
|
+
return await fastify.core.task.start(taskId);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 停止任务
|
|
50
|
+
* POST /stop
|
|
51
|
+
* body: { taskId: string }
|
|
52
|
+
*/
|
|
53
|
+
fastify.post("/stop", async (req) => {
|
|
54
|
+
const body = req.body as { taskId?: string };
|
|
55
|
+
const taskId = body?.taskId?.trim();
|
|
56
|
+
if (!taskId) throw new AppError("TASK_ID_REQUIRED", "taskId is required", { body });
|
|
57
|
+
return await fastify.core.task.stop(taskId);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 查询任务状态
|
|
62
|
+
* GET /status/:taskId
|
|
63
|
+
*/
|
|
64
|
+
fastify.get("/status/:taskId", async (req) => {
|
|
65
|
+
const { taskId } = req.params as { taskId: string };
|
|
66
|
+
return await fastify.core.task.status(taskId);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** 列出所有活跃的任务(running / stopping) */
|
|
70
|
+
fastify.get("/active", async () => {
|
|
71
|
+
return await fastify.core.task.listActive();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 拉取某次运行的任务日志
|
|
76
|
+
* GET /log/run/:runId?tail=200
|
|
77
|
+
*/
|
|
78
|
+
fastify.get("/log/run/:runId", async (req) => {
|
|
79
|
+
const { runId } = req.params as { runId: string };
|
|
80
|
+
const { tail } = req.query as { tail?: string };
|
|
81
|
+
const limit = Math.min(Math.max(Number(tail) || 200, 1), 5000);
|
|
82
|
+
return await fastify.core.task.getTailLogsByRun(runId, limit);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
package/tsconfig.json
ADDED