@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,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = systemRoutes;
|
|
4
|
+
const env_1 = require("../env");
|
|
5
|
+
async function systemRoutes(fastify) {
|
|
6
|
+
fastify.get("/health", async () => ({
|
|
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_1.env.dataDir
|
|
13
|
+
}));
|
|
14
|
+
fastify.post("/shutdown", async () => {
|
|
15
|
+
fastify.log.info("Shutdown requested via /shutdown");
|
|
16
|
+
setTimeout(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await fastify.close();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.error("Graceful shutdown failed", e);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}, 50);
|
|
26
|
+
return { ok: true };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = taskRoutes;
|
|
4
|
+
const core_1 = require("@yinuo-ngm/core");
|
|
5
|
+
async function ensureSpecs(fastify, projectId) {
|
|
6
|
+
const specs = await fastify.core.task.listSpecsByProject(projectId);
|
|
7
|
+
if (specs.length === 0) {
|
|
8
|
+
await fastify.core.task.refreshByProject(projectId);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function taskRoutes(fastify) {
|
|
12
|
+
fastify.get("/list/:projectId", async (req) => {
|
|
13
|
+
const { projectId } = req.params;
|
|
14
|
+
await ensureSpecs(fastify, projectId);
|
|
15
|
+
return fastify.core.task.listViewsByProject(projectId);
|
|
16
|
+
});
|
|
17
|
+
fastify.post("/refresh/:projectId", async (req) => {
|
|
18
|
+
const { projectId } = req.params;
|
|
19
|
+
return await fastify.core.task.refreshByProject(projectId);
|
|
20
|
+
});
|
|
21
|
+
fastify.post("/start", async (req) => {
|
|
22
|
+
const body = req.body;
|
|
23
|
+
const taskId = body?.taskId?.trim();
|
|
24
|
+
if (!taskId)
|
|
25
|
+
throw new core_1.AppError("TASK_ID_REQUIRED", "taskId is required", { body });
|
|
26
|
+
return await fastify.core.task.start(taskId);
|
|
27
|
+
});
|
|
28
|
+
fastify.post("/stop", async (req) => {
|
|
29
|
+
const body = req.body;
|
|
30
|
+
const taskId = body?.taskId?.trim();
|
|
31
|
+
if (!taskId)
|
|
32
|
+
throw new core_1.AppError("TASK_ID_REQUIRED", "taskId is required", { body });
|
|
33
|
+
return await fastify.core.task.stop(taskId);
|
|
34
|
+
});
|
|
35
|
+
fastify.get("/status/:taskId", async (req) => {
|
|
36
|
+
const { taskId } = req.params;
|
|
37
|
+
return await fastify.core.task.status(taskId);
|
|
38
|
+
});
|
|
39
|
+
fastify.get("/active", async () => {
|
|
40
|
+
return await fastify.core.task.listActive();
|
|
41
|
+
});
|
|
42
|
+
fastify.get("/log/run/:runId", async (req) => {
|
|
43
|
+
const { runId } = req.params;
|
|
44
|
+
const { tail } = req.query;
|
|
45
|
+
const limit = Math.min(Math.max(Number(tail) || 200, 1), 5000);
|
|
46
|
+
return await fastify.core.task.getTailLogsByRun(runId, limit);
|
|
47
|
+
});
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yinuo-ngm/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"author": "ZhangJing",
|
|
8
|
+
"license": "ISC",
|
|
9
|
+
"types": "lib/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx watch src/index.ts",
|
|
12
|
+
"build": "tsc -p tsconfig.build.json",
|
|
13
|
+
"start": "node lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@fastify/static": "^9.0.0",
|
|
17
|
+
"@fastify/websocket": "^11.2.0",
|
|
18
|
+
"@yinuo-ngm/core": "^0.1.0",
|
|
19
|
+
"@types/node": "^25.0.3",
|
|
20
|
+
"fastify": "^5.6.2",
|
|
21
|
+
"fastify-plugin": "^5.1.0",
|
|
22
|
+
"launch-editor": "^2.12.0",
|
|
23
|
+
"rss-parser": "^3.13.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"pino-pretty": "^13.1.3",
|
|
27
|
+
"tsx": "^4.21.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Fastify, { FastifyBaseLogger } from "fastify";
|
|
2
|
+
import corePlugin from "./plugins/core.plugin";
|
|
3
|
+
import { errorHandlerPlugin } from "./plugins/error-handler.plugin";
|
|
4
|
+
import requestIdPlugin from "./plugins/request-id.plugin";
|
|
5
|
+
import routesPlugin from "./plugins/routes.plugin";
|
|
6
|
+
import staticPlugin from "./plugins/static.plugin";
|
|
7
|
+
import successHandlerPlugin from "./plugins/success-handle.plugin";
|
|
8
|
+
import wsPlugin from "./plugins/ws/ws.plugin";
|
|
9
|
+
function normalizeLogLevel(v?: string) {
|
|
10
|
+
// pino levels: fatal error warn info debug trace silent
|
|
11
|
+
const lv = (v ?? "").toLowerCase().trim();
|
|
12
|
+
if (!lv) return undefined;
|
|
13
|
+
if (["fatal", "error", "warn", "info", "debug", "trace", "silent"].includes(lv)) return lv;
|
|
14
|
+
return "info";
|
|
15
|
+
}
|
|
16
|
+
function createFastifyLogger(): false | FastifyBaseLogger | undefined {
|
|
17
|
+
const level = normalizeLogLevel(process.env.NGM_LOG_LEVEL);
|
|
18
|
+
|
|
19
|
+
// 1) 未指定级别 => 默认关闭(安静)
|
|
20
|
+
if (!level) return false;
|
|
21
|
+
|
|
22
|
+
// 2) dev 下可读输出(需要安装 pino-pretty)
|
|
23
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
24
|
+
if (isDev) {
|
|
25
|
+
return {
|
|
26
|
+
level,
|
|
27
|
+
transport: {
|
|
28
|
+
target: "pino-pretty",
|
|
29
|
+
options: {
|
|
30
|
+
colorize: true,
|
|
31
|
+
translateTime: "HH:MM:ss.l",
|
|
32
|
+
ignore: "pid,hostname",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} as any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3) prod 下输出 JSON(结构化)
|
|
39
|
+
return { level } as any;
|
|
40
|
+
}
|
|
41
|
+
export async function createServer() {
|
|
42
|
+
const fastify = Fastify({
|
|
43
|
+
logger: createFastifyLogger(),
|
|
44
|
+
genReqId: (req) => {
|
|
45
|
+
// 优先使用前端传入的 X-Request-Id
|
|
46
|
+
const hdr = req.headers["x-request-id"];
|
|
47
|
+
if (typeof hdr === "string" && hdr.trim()) return hdr.trim();
|
|
48
|
+
return crypto.randomUUID();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 基础插件:request-id + error handler + success handler
|
|
53
|
+
await fastify.register(requestIdPlugin);
|
|
54
|
+
await fastify.register(errorHandlerPlugin);
|
|
55
|
+
await fastify.register(successHandlerPlugin);
|
|
56
|
+
|
|
57
|
+
// core
|
|
58
|
+
await fastify.register(corePlugin);
|
|
59
|
+
// 先 websocket,再 routes(尤其 ws 路由)
|
|
60
|
+
await fastify.register(wsPlugin);
|
|
61
|
+
// routes
|
|
62
|
+
await fastify.register(routesPlugin);
|
|
63
|
+
|
|
64
|
+
// www
|
|
65
|
+
await fastify.register(staticPlugin)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
fastify.addHook('onClose', async () => {
|
|
69
|
+
// 在这里执行任何需要在服务器关闭时完成的异步操作
|
|
70
|
+
// 例如,关闭数据库连接、清理资源等
|
|
71
|
+
await fastify.core.dispose?.();
|
|
72
|
+
fastify.log.info('Server is closing, performing cleanup...');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return fastify;
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
|
|
3
|
+
export function ok<T>(req: FastifyRequest, reply: FastifyReply, data: T, status = 200) {
|
|
4
|
+
return reply.status(status).send({
|
|
5
|
+
ok: true as const,
|
|
6
|
+
data,
|
|
7
|
+
meta: {
|
|
8
|
+
requestId: req.id,
|
|
9
|
+
ts: Date.now(),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AppError } from "@yinuo-ngm/core";
|
|
2
|
+
import launchEditor from "launch-editor";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
export interface OpenFolderOptions {
|
|
6
|
+
editor?: "code" | "system";
|
|
7
|
+
file?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 在指定文件夹打开编辑器
|
|
12
|
+
* @param folder 文件夹路径
|
|
13
|
+
* @param opts OpenFolderOptions 选项
|
|
14
|
+
*/
|
|
15
|
+
export async function openFolder(folder: string, opts: OpenFolderOptions = {}): Promise<void> {
|
|
16
|
+
const editor = opts.editor ?? "code";
|
|
17
|
+
const file = opts.file;
|
|
18
|
+
|
|
19
|
+
const target = file
|
|
20
|
+
? path.resolve(folder, file)
|
|
21
|
+
: path.resolve(folder);
|
|
22
|
+
|
|
23
|
+
return new Promise<void>((resolve, reject) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
|
|
26
|
+
// 兜底:避免 callback 不触发导致请求永远挂起
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
if (settled) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
resolve();
|
|
31
|
+
}, 200);
|
|
32
|
+
|
|
33
|
+
launchEditor(
|
|
34
|
+
target,
|
|
35
|
+
editor === "system" ? undefined : editor,
|
|
36
|
+
(fileName, errorMsg) => {
|
|
37
|
+
// fastify.log.info(`launchEditor callback invoked: file=${fileName}, error=${errorMsg}`);
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (errorMsg) {
|
|
42
|
+
reject(new AppError("EDITOR_NOT_FOUND", errorMsg, { fileName, editor, folder, file, target }));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import os from "os";
|
|
3
|
+
|
|
4
|
+
export const env = {
|
|
5
|
+
port: Number(process.env.NGM_SERVER_PORT || 3210),
|
|
6
|
+
host: process.env.NGM_SERVER_HOST || "127.0.0.1",
|
|
7
|
+
dataDir:
|
|
8
|
+
process.env.NGM_DATA_DIR ||
|
|
9
|
+
path.join(os.homedir(), ".ng-manager"),
|
|
10
|
+
logLevel: process.env.NGM_LOG_LEVEL || "info",
|
|
11
|
+
sysLogCapacity: 3000,
|
|
12
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import { createServer } from "./app";
|
|
3
|
+
import { env } from "./env";
|
|
4
|
+
|
|
5
|
+
async function start() {
|
|
6
|
+
const { port, host } = env;
|
|
7
|
+
const app = await createServer();
|
|
8
|
+
await app.listen({ port, host });
|
|
9
|
+
app.log.info(`local server listening on http://${host}:${port}`);
|
|
10
|
+
app.core.sysLog.append({
|
|
11
|
+
level: "info",
|
|
12
|
+
text: `Server started at http://${host}:${port}`,
|
|
13
|
+
source: "server",
|
|
14
|
+
scope: "system",
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start().catch((err) => {
|
|
19
|
+
console.error("Failed to start server:", err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/server/plugins/core.plugin.ts
|
|
2
|
+
|
|
3
|
+
import type { FastifyInstance } from "fastify";
|
|
4
|
+
import fp from "fastify-plugin";
|
|
5
|
+
|
|
6
|
+
import { createCoreApp, type CoreApp } from "@yinuo-ngm/core";
|
|
7
|
+
import { env } from "../env";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fastify 装饰器类型扩展
|
|
11
|
+
*/
|
|
12
|
+
declare module "fastify" {
|
|
13
|
+
interface FastifyInstance {
|
|
14
|
+
core: CoreApp;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Core Plugin
|
|
20
|
+
* - 创建 CoreApp
|
|
21
|
+
* - 注入到 fastify.core
|
|
22
|
+
*/
|
|
23
|
+
export default fp(async function corePlugin(
|
|
24
|
+
fastify: FastifyInstance
|
|
25
|
+
) {
|
|
26
|
+
const coreApp = await createCoreApp({
|
|
27
|
+
sysLogCapacity: env.sysLogCapacity,
|
|
28
|
+
dataDir: env.dataDir, // 可选: 自定义数据目录
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
fastify.decorate("core", coreApp);
|
|
32
|
+
|
|
33
|
+
fastify.log.info("[core] core app initialized");
|
|
34
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
3
|
+
import { AppError, type ErrorCode } from "@yinuo-ngm/core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ErrorCode → HTTP Status 映射
|
|
7
|
+
*
|
|
8
|
+
* 约定:
|
|
9
|
+
* - 4xx:客户端/业务条件不满足
|
|
10
|
+
* - 5xx:服务端/系统错误
|
|
11
|
+
*/
|
|
12
|
+
export const ERROR_STATUS: Record<ErrorCode, number> = {
|
|
13
|
+
/* ---------------- Project ---------------- */
|
|
14
|
+
PROJECT_NOT_FOUND: 404,
|
|
15
|
+
PROJECT_ROOT_INVALID: 400,
|
|
16
|
+
PROJECT_ALREADY_EXISTS: 409,
|
|
17
|
+
|
|
18
|
+
/* ---------------- Project Import ---------------- */
|
|
19
|
+
PROJECT_IMPORT_NOT_EXISTS: 404, // 路径不存在
|
|
20
|
+
PROJECT_IMPORT_NOT_DIR: 400, // 不是目录
|
|
21
|
+
PROJECT_IMPORT_ALREADY_REGISTERED: 409, // 已导入
|
|
22
|
+
PROJECT_IMPORT_NOT_RECOGNIZED: 422, // 不像项目(语义错误)
|
|
23
|
+
PROJECT_IMPORT_SCAN_FAILED: 500, // 扫描失败(IO/解析)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/* ---------------- Project Creation ---------------- */
|
|
28
|
+
INVALID_NAME: 400, // 无效的项目名称
|
|
29
|
+
TARGET_EXISTS: 409, // 目标路径已存在
|
|
30
|
+
INVALID_REPO_URL: 400, // 无效的仓库地址
|
|
31
|
+
INVALID_PARENT_DIR: 400, // 无效的父目录
|
|
32
|
+
GIT_CHECKOUT_FAILED: 500, // Git 检出失败
|
|
33
|
+
BOOTSTRAP_NOT_IN_PICK_STATE: 400, // 当前不处于选择根目录状态
|
|
34
|
+
BOOTSTRAP_CTX_NOT_FOUND: 404, // 引导上下文未找到
|
|
35
|
+
BOOTSTRAP_INVALID_PICKED_ROOT: 400, // 选择的根目录无效
|
|
36
|
+
BOOTSTRAP_NOT_WAITING_PICK: 400, // 引导未处于等待选择状态
|
|
37
|
+
|
|
38
|
+
/* ---------------- Project Analysis ---------------- */
|
|
39
|
+
PROJECT_ANGULAR_JSON_INVALID: 400, // angular.json 无效
|
|
40
|
+
PROJECT_ANGULAR_JSON_NOT_FOUND: 404, // angular.json 未找到
|
|
41
|
+
PROJECT_VITE_CONFIG_INVALID: 400, // vite 配置无效
|
|
42
|
+
PROJECT_VUE_CONFIG_NOT_FOUND: 404, // vite 配置未找到
|
|
43
|
+
|
|
44
|
+
/* ---------------- Config ---------------- */
|
|
45
|
+
CONFIG_BACKUP_NOT_FOUND: 404, // 配置备份不存在
|
|
46
|
+
CONFIG_READ_FAILED: 500, // 配置读取失败
|
|
47
|
+
CONFIG_WRITE_FAILED: 500, // 配置写入失败
|
|
48
|
+
CONFIG_CONFLICT: 409, // 配置冲突
|
|
49
|
+
CONFIG_OPEN_FAILED: 500, // 配置文件打开失败
|
|
50
|
+
CONFIG_SCHEMA_NOT_FOUND: 404, // 配置 schema 未找到
|
|
51
|
+
CONFIG_DOMAIN_NOT_FOUND: 404, // 配置 domain 未找到
|
|
52
|
+
CONFIG_DOC_NOT_FOUND: 404, // 配置 doc 未找到
|
|
53
|
+
|
|
54
|
+
/* ---------------- Task / Process ---------------- */
|
|
55
|
+
TASK_NOT_FOUND: 404,
|
|
56
|
+
TASK_ALREADY_RUNNING: 409,
|
|
57
|
+
PROCESS_SPAWN_FAILED: 500,
|
|
58
|
+
TASK_SPEC_NOT_FOUND: 404,
|
|
59
|
+
TASK_NOT_RUNNABLE: 400,
|
|
60
|
+
RUN_NOT_FOUND: 404,
|
|
61
|
+
TASK_ID_REQUIRED: 400,
|
|
62
|
+
COMMAND_NOT_FOUND: 404,
|
|
63
|
+
|
|
64
|
+
// WS
|
|
65
|
+
BAD_JSON: 400,
|
|
66
|
+
BAD_MSG: 400,
|
|
67
|
+
OP_NOT_SUPPORTED: 400,
|
|
68
|
+
HANDLER_FAILED: 500,
|
|
69
|
+
TOPIC_NOT_FOUND: 404,
|
|
70
|
+
OP_NOT_FOUND: 400,
|
|
71
|
+
/* ---------------- File System ---------------- */
|
|
72
|
+
FS_PATH_NOT_FOUND: 404,
|
|
73
|
+
FS_ALREADY_EXISTS: 409,
|
|
74
|
+
FS_PERMISSION_DENIED: 403,
|
|
75
|
+
FS_EXISTS_FAILED: 500,
|
|
76
|
+
FS_INVALID_NAME: 400,
|
|
77
|
+
FS_MKDIR_FAILED: 500,
|
|
78
|
+
|
|
79
|
+
/* ---------------- Editor ---------------- */
|
|
80
|
+
EDITOR_NOT_FOUND: 404,
|
|
81
|
+
EDITOR_LAUNCH_FAILED: 500,
|
|
82
|
+
|
|
83
|
+
/* ---------------- Infra / Auth ---------------- */
|
|
84
|
+
STORAGE_IO_ERROR: 500,
|
|
85
|
+
UNAUTHORIZED: 401,
|
|
86
|
+
INVALID_TIMESTAMP: 400, // 时间戳无效
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/* ---------------- Deps ---------------- */
|
|
90
|
+
DEP_INSTALL_FAILED: 500,
|
|
91
|
+
DEP_UNINSTALL_FAILED: 500,
|
|
92
|
+
DEP_NOT_FOUND: 404,
|
|
93
|
+
|
|
94
|
+
/* ---------------- Dashboard ---------------- */
|
|
95
|
+
DASHBOARD_CONFLICT: 409,
|
|
96
|
+
WIDGET_NOT_FOUND: 404,
|
|
97
|
+
WIDGET_LOCKED: 423,
|
|
98
|
+
|
|
99
|
+
/* ---------------- Dashboard RSS ---------------- */
|
|
100
|
+
RSS_FETCH_FAILED: 500,
|
|
101
|
+
INVALID_RSS_URL: 400,
|
|
102
|
+
|
|
103
|
+
/* ---------------- Fallback ---------------- */
|
|
104
|
+
UNKNOWN_ERROR: 500,
|
|
105
|
+
|
|
106
|
+
/* ---------------- Generic ---------------- */
|
|
107
|
+
BAD_REQUEST: 400,
|
|
108
|
+
NOT_IMPLEMENTED: 501,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export function mapStatus(code: ErrorCode): number {
|
|
112
|
+
return ERROR_STATUS[code] ?? 400;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 构建错误响应体
|
|
117
|
+
*/
|
|
118
|
+
function buildErrorBody(reqId: string | undefined, code: string, message: string, details?: any) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false as const,
|
|
121
|
+
error: {
|
|
122
|
+
code,
|
|
123
|
+
message,
|
|
124
|
+
...(details !== undefined ? { details } : {}),
|
|
125
|
+
},
|
|
126
|
+
meta: {
|
|
127
|
+
requestId: reqId,
|
|
128
|
+
ts: Date.now(),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 全局错误处理插件: 将错误转换为标准响应体
|
|
135
|
+
* - 业务错误(AppError)按其 code 映射状态码
|
|
136
|
+
* - schema 校验错误返回 400
|
|
137
|
+
* - 其他未知错误返回 500,并记录日志
|
|
138
|
+
*/
|
|
139
|
+
export const errorHandlerPlugin: FastifyPluginAsync = fp(async (app) => {
|
|
140
|
+
app.setErrorHandler((err, req, reply) => {
|
|
141
|
+
const requestId = req.id;
|
|
142
|
+
// 1) core 业务错误
|
|
143
|
+
if (err instanceof AppError || isCoreAppError(err)) {
|
|
144
|
+
const status = mapStatus(err.code as ErrorCode);
|
|
145
|
+
return reply.status(status).send(buildErrorBody(requestId, err.code, err.message, err.meta));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2) Fastify schema 校验错误
|
|
149
|
+
// @ts-expect-error fastify error typing
|
|
150
|
+
if (err?.validation) {
|
|
151
|
+
return reply
|
|
152
|
+
.status(400)
|
|
153
|
+
// @ts-expect-error
|
|
154
|
+
.send(buildErrorBody(requestId, "VALIDATION_ERROR", "参数不合法", err.validation));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3) 未知错误:只记录日志
|
|
158
|
+
req.log.error({ err, requestId }, "Unhandled error");
|
|
159
|
+
return reply.status(500).send(buildErrorBody(requestId, "INTERNAL_ERROR", "服务异常,请稍后重试"));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
function isCoreAppError(err: any): err is { code: string; message: string; meta?: any } {
|
|
165
|
+
return err && typeof err === "object" && typeof err.code === "string" && typeof err.message === "string";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default errorHandlerPlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
3
|
+
|
|
4
|
+
export const requestIdPlugin: FastifyPluginAsync = fp(async (app) => {
|
|
5
|
+
// 让 req.id 优先使用前端传入的 X-Request-Id
|
|
6
|
+
// 注意:Fastify 的 genReqId 只能在 Fastify() 初始化时配置
|
|
7
|
+
// 只负责“回传 header”
|
|
8
|
+
app.addHook("onSend", async (req, reply, payload) => {
|
|
9
|
+
reply.header("X-Request-Id", req.id);
|
|
10
|
+
return payload;
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default requestIdPlugin;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import { configRoutes, dashboardRoutes, depsRoutes, fsRoutes, projectRoutes, rssRoutes, systemRoutes, taskRoutes } from "../routes";
|
|
4
|
+
|
|
5
|
+
export default fp(async function routesPlugin(fastify: FastifyInstance) {
|
|
6
|
+
// system
|
|
7
|
+
await fastify.register(systemRoutes);
|
|
8
|
+
// routes
|
|
9
|
+
await fastify.register(taskRoutes, { prefix: '/api/tasks' });
|
|
10
|
+
//projects
|
|
11
|
+
await fastify.register(projectRoutes, { prefix: '/api/projects' });
|
|
12
|
+
// deps
|
|
13
|
+
await fastify.register(depsRoutes, { prefix: '/api/deps' });
|
|
14
|
+
// fs
|
|
15
|
+
await fastify.register(fsRoutes, { prefix: '/api/fs' });
|
|
16
|
+
// config
|
|
17
|
+
await fastify.register(configRoutes, { prefix: '/api/config' });
|
|
18
|
+
|
|
19
|
+
// dashboard
|
|
20
|
+
await fastify.register(dashboardRoutes, { prefix: '/api/dashboard' });
|
|
21
|
+
|
|
22
|
+
// rss
|
|
23
|
+
await fastify.register(rssRoutes, { prefix: '/api/rss' });
|
|
24
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fastifyStatic from "@fastify/static";
|
|
4
|
+
|
|
5
|
+
export default fp(async function staticPlugin(fastify) {
|
|
6
|
+
/**
|
|
7
|
+
* __dirname === packages/server/lib/plugins
|
|
8
|
+
* 回到 monorepo 根,再进 www/browser
|
|
9
|
+
*/
|
|
10
|
+
const webRoot = path.resolve(__dirname, "../../www/browser");
|
|
11
|
+
|
|
12
|
+
fastify.log.info(`[static] serving webapp from ${webRoot}`);
|
|
13
|
+
|
|
14
|
+
await fastify.register(fastifyStatic, {
|
|
15
|
+
root: webRoot,
|
|
16
|
+
index: "index.html",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// SPA fallback(非 API 请求 → index.html)
|
|
20
|
+
fastify.setNotFoundHandler((req, reply) => {
|
|
21
|
+
if (
|
|
22
|
+
req.method === "GET" &&
|
|
23
|
+
!req.url.startsWith("/api") &&
|
|
24
|
+
!req.url.startsWith("/ws")
|
|
25
|
+
) {
|
|
26
|
+
return reply.sendFile("index.html");
|
|
27
|
+
}
|
|
28
|
+
reply.code(404).send({ ok: false, message: "Not Found" });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
3
|
+
|
|
4
|
+
export const successHandlerPlugin: FastifyPluginAsync = fp(async (app) => {
|
|
5
|
+
app.addHook("preSerialization", async (req, reply, payload) => {
|
|
6
|
+
// 非 2xx:不处理(错误已由 errorHandler 接管)
|
|
7
|
+
if (reply.statusCode < 200 || reply.statusCode >= 300) {
|
|
8
|
+
return payload;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 没有 payload(204 等)
|
|
12
|
+
if (payload === undefined || payload === null) {
|
|
13
|
+
return payload;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 非 JSON(stream / buffer / string)
|
|
17
|
+
const type = reply.getHeader("content-type");
|
|
18
|
+
if (typeof type === "string" && !type.includes("application/json")) {
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
// 统一包装成功返回
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
data: payload,
|
|
25
|
+
meta: {
|
|
26
|
+
requestId: req.id,
|
|
27
|
+
ts: Date.now(),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default successHandlerPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./task.ws";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WsClientMsg, WsServerMsg, LogLine } from "@yinuo-ngm/core";
|
|
2
|
+
import { WsContext } from "../ws.context";
|
|
3
|
+
import type { TopicHandler } from "../ws.router";
|
|
4
|
+
const KEY_ALL = "syslog:all";
|
|
5
|
+
export type SyslogWsDeps = {
|
|
6
|
+
getSyslogTail: (tail: number) => LogLine[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createSyslogTopicHandler(
|
|
10
|
+
deps: SyslogWsDeps,
|
|
11
|
+
getAllClients: () => Iterable<WsContext>
|
|
12
|
+
): TopicHandler & { push(entry: LogLine): void } {
|
|
13
|
+
return {
|
|
14
|
+
topic: "syslog",
|
|
15
|
+
async sub(ctx, msg: Extract<WsClientMsg, { op: "sub"; topic: "syslog" }>) {
|
|
16
|
+
const tail = Math.max(0, Number(msg?.tail ?? 0));
|
|
17
|
+
ctx.addSub("syslog", KEY_ALL);
|
|
18
|
+
|
|
19
|
+
if (tail > 0) {
|
|
20
|
+
const entries = await deps.getSyslogTail(tail);
|
|
21
|
+
// 用一个 batch,前端更好处理
|
|
22
|
+
const m: WsServerMsg = { op: "syslog.tail", entries: entries ?? [] };
|
|
23
|
+
ctx.send(m);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
unsub(ctx) {
|
|
27
|
+
ctx.delSub("syslog", KEY_ALL);
|
|
28
|
+
},
|
|
29
|
+
push(entry: LogLine) {
|
|
30
|
+
const m: WsServerMsg = { op: "syslog.append", entry };
|
|
31
|
+
for (const c of getAllClients()) {
|
|
32
|
+
if (c.hasSub("syslog", KEY_ALL)) c.send(m);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|