2026-03-26 00:42:45 +08:00

119 lines
3.9 KiB
TypeScript

import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import fs from "fs/promises";
import path from "path";
import crypto from "crypto";
const router = express.Router();
const buildSkillFileName = (name: string) => {
const trimmed = name.trim();
const fileName = trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`;
const normalized = fileName.replace(/\\/g, "/");
if (!normalized || normalized.includes("/")) {
throw new Error("技能名称不能包含路径分隔符");
}
return normalized;
};
const buildRelativePath = (type: string, fileName: string) => {
return type === "references" ? path.posix.join("references", fileName) : fileName;
};
const resolveSkillFilePath = (relativePath: string) => {
return path.join(u.getPath("skills"), relativePath.replace(/\\/g, "/"));
};
const resolveState = (description: string, attributions: string[]) => {
if (!description.trim()) return -1;
if (attributions.length === 0) return -2;
return 1;
};
export default router.post(
"/",
validateFields({
id: z.string().min(1),
name: z.string().min(1).max(100),
description: z.string().optional(),
content: z.string().optional(),
attributions: z.array(z.string()).optional(),
}),
async (req, res) => {
try {
const { id, name, description, content, attributions } = req.body;
const current = await u.db("o_skillList").where("id", id).first();
if (!current) {
return res.status(404).send(error("技能不存在"));
}
const finalDescription = description ?? "";
const finalContent = content ?? "";
const rawAttributions = Array.isArray(attributions) ? attributions : [];
const finalAttributions = Array.from(
new Set(rawAttributions.filter((item: unknown): item is string => typeof item === "string" && item.trim().length > 0)),
);
const fileName = buildSkillFileName(name);
const relativePath = buildRelativePath(current.type, fileName);
const nextId = crypto.createHash("md5").update(relativePath).digest("hex");
const md5 = crypto.createHash("md5").update(finalContent).digest("hex");
const oldFilePath = resolveSkillFilePath(current.path);
const newFilePath = resolveSkillFilePath(relativePath);
const now = Date.now();
if (nextId !== id) {
const conflict = await u.db("o_skillList").where("id", nextId).first();
if (conflict) {
return res.status(400).send(error("技能名称冲突,请使用其他名称"));
}
}
await fs.mkdir(path.dirname(newFilePath), { recursive: true });
if (oldFilePath !== newFilePath) {
try {
await fs.rename(oldFilePath, newFilePath);
} catch {
// 文件不存在时直接按新路径写入即可
}
}
await fs.writeFile(newFilePath, finalContent, "utf-8");
if (nextId !== id) {
await u.db("o_skillAttribution").where("skillId", id).update({ skillId: nextId });
}
await u
.db("o_skillList")
.where("id", id)
.update({
id: nextId,
path: relativePath,
name: path.basename(fileName, ".md"),
description: finalDescription,
md5,
updateTime: now,
state: resolveState(finalDescription, finalAttributions),
});
await u.db("o_skillAttribution").where("skillId", nextId).delete();
if (finalAttributions.length > 0) {
await u.db("o_skillAttribution").insert(
finalAttributions.map((attribution: string) => ({
skillId: nextId,
attribution,
})),
);
}
res.status(200).send(success("更新技能成功"));
} catch (err: any) {
console.log(err);
res.status(400).send(error(err?.message || "更新技能失败"));
}
},
);