119 lines
3.9 KiB
TypeScript
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 || "更新技能失败"));
|
|
}
|
|
},
|
|
);
|