Add SMS verification auth flow
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 5s

This commit is contained in:
zyc 2026-05-28 16:08:30 +08:00
parent c3f616dc22
commit e8f381f73a
35 changed files with 3157 additions and 1309 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
SMS_ACCESS_KEY_ID=
SMS_ACCESS_KEY_SECRET=
SMS_SIGN_NAME=广州气元科技
SMS_TEMPLATE_CODE=SMS_506210397
SMS_REGION=cn-hangzhou

View File

@ -32,6 +32,10 @@ jobs:
echo "DOMAIN_APP is still toonflow.example.com. Replace it with the real domain before deploying."
exit 1
fi
if [ -z "${{ secrets.SMS_ACCESS_KEY_ID }}" ] || [ -z "${{ secrets.SMS_ACCESS_KEY_SECRET }}" ]; then
echo "SMS_ACCESS_KEY_ID and SMS_ACCESS_KEY_SECRET must be configured as repository secrets."
exit 1
fi
- name: Login to Volcano Engine CR
run: |
@ -112,6 +116,11 @@ jobs:
--docker-password="${{ env.CR_PASSWORD_ACTIVE }}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic toonflow-sms-secret \
--from-literal=SMS_ACCESS_KEY_ID='${{ secrets.SMS_ACCESS_KEY_ID }}' \
--from-literal=SMS_ACCESS_KEY_SECRET='${{ secrets.SMS_ACCESS_KEY_SECRET }}' \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f k8s/cert-manager-issuer.yaml
kubectl apply -f k8s/redirect-https-middleware.yaml
kubectl apply -f k8s/toonflow-pvc.yaml

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,22 @@ spec:
value: "10588"
- name: ossURL
value: "https://videoflow.airlabs.art"
- name: SMS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: toonflow-sms-secret
key: SMS_ACCESS_KEY_ID
- name: SMS_ACCESS_KEY_SECRET
valueFrom:
secretKeyRef:
name: toonflow-sms-secret
key: SMS_ACCESS_KEY_SECRET
- name: SMS_SIGN_NAME
value: "广州气元科技"
- name: SMS_TEMPLATE_CODE
value: "SMS_506210397"
- name: SMS_REGION
value: "cn-hangzhou"
volumeMounts:
- name: toonflow-data
mountPath: /app/data/db2.sqlite

View File

@ -11,9 +11,10 @@ import buildRoute from "@/core";
import path from "path";
import fs from "fs";
import u from "@/utils";
import jwt from "jsonwebtoken";
import socketInit from "@/socket/index";
import { isEletron } from "@/utils/getPath";
import { normalizeBearerToken, verifyAuthToken } from "@/lib/auth";
import { requestHasProjectAccess } from "@/lib/workspaceAccess";
const app = express();
const server = http.createServer(app);
@ -98,23 +99,34 @@ export default async function startServe(randomPort: Boolean = false) {
}
app.use(async (req, res, next) => {
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
if (!setting) return res.status(444).send({ message: "服务器秘钥未配置,请联系管理员" });
const { value: tokenKey } = setting;
// 白名单路径
if (
req.path === "/api/login/login" ||
req.path === "/api/login/register" ||
req.path === "/api/login/sendSmsCode" ||
req.path === "/api/login/resetPassword"
) {
return next();
}
// 从 header 或 query 参数获取 token
const rawToken = req.headers.authorization || (req.query.token as string) || "";
const token = rawToken.replace("Bearer ", "");
// 白名单路径
if (req.path === "/api/login/login") return next();
const token = normalizeBearerToken(rawToken);
if (!token) return res.status(401).send({ message: "未提供token" });
try {
const decoded = jwt.verify(token, tokenKey as string);
(req as any).user = decoded;
next();
} catch (err) {
const authUser = await verifyAuthToken(rawToken);
if (!authUser) {
return res.status(401).send({ message: "无效的token" });
}
(req as any).user = authUser;
const canAccessProject = await requestHasProjectAccess(req, authUser.id);
if (!canAccessProject) {
return res.status(403).send({ message: "无权访问该项目" });
}
next();
});
const router = await import("@/router");

View File

@ -1,3 +1,8 @@
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
dotenv.config();
// 判断是否为打包后的 Electron 环境
const isElectron = typeof process.versions?.electron !== "undefined";
let isPackaged = false;

66
src/lib/auth.ts Normal file
View File

@ -0,0 +1,66 @@
import { Request } from "express";
import jwt from "jsonwebtoken";
import u from "@/utils";
export interface AuthUser {
id: number;
name: string;
}
export function getCurrentUser(req: Request): AuthUser {
const user = (req as any).user as Partial<AuthUser> | undefined;
const id = Number(user?.id);
if (!Number.isFinite(id)) throw new Error("未登录");
return {
id,
name: String(user?.name ?? ""),
};
}
export async function getTokenKey(): Promise<string | null> {
const tokenData = await u.db("o_setting").where("key", "tokenKey").first();
return (tokenData?.value as string | undefined) ?? null;
}
export function createAuthToken(user: AuthUser, secret: string): string {
return jwt.sign({ id: user.id, name: user.name }, secret, { expiresIn: "180Days" });
}
export function normalizeBearerToken(rawToken?: string | string[] | null): string {
const token = Array.isArray(rawToken) ? rawToken[0] : rawToken;
return (token || "").replace("Bearer ", "");
}
export async function verifyAuthToken(rawToken?: string | string[] | null): Promise<AuthUser | null> {
const secret = await getTokenKey();
if (!secret) return null;
const token = normalizeBearerToken(rawToken);
if (!token) return null;
try {
const decoded = jwt.verify(token, secret) as Partial<AuthUser>;
const id = Number(decoded.id);
if (!Number.isFinite(id)) return null;
return {
id,
name: String(decoded.name ?? ""),
};
} catch {
return null;
}
}
export function publicUser(user: { id?: number | null; name?: string | null; phone?: string | null }) {
return {
id: Number(user.id),
name: user.name ?? "",
phone: user.phone ?? "",
};
}
export async function assertProjectAccess(projectId: number, userId: number): Promise<boolean> {
if (!Number.isFinite(projectId) || !Number.isFinite(userId)) return false;
const project = await u.db("o_project").where({ id: projectId, userId }).select("id").first();
return Boolean(project);
}

View File

@ -5,6 +5,7 @@ import { Knex } from "knex";
import db from "@/utils/db";
import { transform } from "sucrase";
import rawVendorData from "./vendor.json";
import { hashPassword, isHashedPassword } from "@/lib/password";
const vendorData = rawVendorData as Record<string, string>;
@ -57,6 +58,52 @@ export default async (knex: Knex): Promise<void> => {
errorReason: "软件退出导致失败",
});
if (await knex.schema.hasTable("o_user")) {
await addColumn("o_user", "phone", "text");
const adminUser = await db("o_user").where("id", 1).first();
if (!adminUser) {
await db("o_user").insert({ id: 1, name: "admin", password: await hashPassword("admin123") });
}
const users = await db("o_user").select("id", "password");
for (const user of users) {
if (user.password && !isHashedPassword(user.password)) {
await db("o_user").where("id", user.id).update({ password: await hashPassword(user.password) });
}
}
try {
await knex.raw("CREATE UNIQUE INDEX IF NOT EXISTS idx_o_user_name_unique ON o_user(name)");
await knex.raw("CREATE UNIQUE INDEX IF NOT EXISTS idx_o_user_phone_unique ON o_user(phone) WHERE phone IS NOT NULL AND phone != ''");
} catch (err) {
console.warn("[DB] 创建用户唯一索引失败:", u.error(err).message);
}
}
if (!(await knex.schema.hasTable("o_smsCode"))) {
await knex.schema.createTable("o_smsCode", (table) => {
table.integer("id").notNullable();
table.text("phone").notNullable();
table.text("purpose").notNullable();
table.text("codeHash").notNullable();
table.integer("expiresAt").notNullable();
table.integer("used").defaultTo(0);
table.integer("attempts").defaultTo(0);
table.integer("createTime").notNullable();
table.integer("sentAt").notNullable();
table.primary(["id"]);
table.unique(["id"]);
});
}
await addColumn("o_project", "userId", "integer");
if ((await knex.schema.hasTable("o_project")) && (await knex.schema.hasColumn("o_project", "userId"))) {
await db("o_project")
.whereNull("userId")
.orWhere("userId", 0)
.update({ userId: 1 });
}
// 添加新字段
await addColumn("o_prompt", "useData", "text");
// 添加新字段

View File

@ -16,14 +16,34 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
builder: (table) => {
table.integer("id").notNullable();
table.text("name");
table.text("phone");
table.text("password");
table.primary(["id"]);
table.unique(["id"]);
table.unique(["name"]);
table.unique(["phone"]);
},
initData: async (knex) => {
await knex("o_user").insert([{ id: 1, name: "admin", password: "admin123" }]);
},
},
// 短信验证码表
{
name: "o_smsCode",
builder: (table) => {
table.integer("id").notNullable();
table.text("phone").notNullable();
table.text("purpose").notNullable();
table.text("codeHash").notNullable();
table.integer("expiresAt").notNullable();
table.integer("used").defaultTo(0);
table.integer("attempts").defaultTo(0);
table.integer("createTime").notNullable();
table.integer("sentAt").notNullable();
table.primary(["id"]);
table.unique(["id"]);
},
},
//项目表
{
name: "o_project",

30
src/lib/password.ts Normal file
View File

@ -0,0 +1,30 @@
import crypto from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(crypto.scrypt);
const PASSWORD_PREFIX = "scrypt";
const KEY_LENGTH = 64;
export function isHashedPassword(password?: string | null): boolean {
return Boolean(password?.startsWith(`${PASSWORD_PREFIX}$`));
}
export async function hashPassword(password: string): Promise<string> {
const salt = crypto.randomBytes(16).toString("hex");
const key = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer;
return `${PASSWORD_PREFIX}$${salt}$${key.toString("hex")}`;
}
export async function verifyPassword(input: string, stored?: string | null): Promise<boolean> {
if (!stored) return false;
if (!isHashedPassword(stored)) return input === stored;
const [, salt, storedKey] = stored.split("$");
if (!salt || !storedKey) return false;
const inputKey = (await scryptAsync(input, salt, KEY_LENGTH)) as Buffer;
const storedBuffer = Buffer.from(storedKey, "hex");
if (inputKey.length !== storedBuffer.length) return false;
return crypto.timingSafeEqual(inputKey, storedBuffer);
}

73
src/lib/sms.ts Normal file
View File

@ -0,0 +1,73 @@
import axios from "axios";
import crypto from "crypto";
const endpoint = "https://dysmsapi.aliyuncs.com/";
function percentEncode(value: string) {
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
}
function getSmsConfig() {
return {
accessKeyId: process.env.SMS_ACCESS_KEY_ID || process.env.ALIBABA_CLOUD_ACCESS_KEY_ID || "",
accessKeySecret: process.env.SMS_ACCESS_KEY_SECRET || process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET || "",
signName: process.env.SMS_SIGN_NAME || "",
templateCode: process.env.SMS_TEMPLATE_CODE || "",
region: process.env.SMS_REGION || "cn-hangzhou",
};
}
function signQuery(params: Record<string, string>, accessKeySecret: string) {
const canonicalizedQuery = Object.keys(params)
.sort()
.map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`)
.join("&");
const stringToSign = `GET&${percentEncode("/")}&${percentEncode(canonicalizedQuery)}`;
return crypto
.createHmac("sha1", `${accessKeySecret}&`)
.update(stringToSign)
.digest("base64");
}
export function maskPhone(phone: string) {
return phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
}
export function isValidMainlandPhone(phone: string) {
return /^1[3-9]\d{9}$/.test(phone);
}
export async function sendSmsCode(phone: string, code: string) {
const config = getSmsConfig();
if (!config.accessKeyId || !config.accessKeySecret || !config.signName || !config.templateCode) {
throw new Error("短信服务未配置");
}
const params: Record<string, string> = {
AccessKeyId: config.accessKeyId,
Action: "SendSms",
Format: "JSON",
PhoneNumbers: phone,
RegionId: config.region,
SignatureMethod: "HMAC-SHA1",
SignatureNonce: crypto.randomUUID(),
SignatureVersion: "1.0",
SignName: config.signName,
TemplateCode: config.templateCode,
TemplateParam: JSON.stringify({ code }),
Timestamp: new Date().toISOString(),
Version: "2017-05-25",
};
const signature = signQuery(params, config.accessKeySecret);
const query = Object.entries({ ...params, Signature: signature })
.map(([key, value]) => `${percentEncode(key)}=${percentEncode(value)}`)
.join("&");
const { data } = await axios.get(`${endpoint}?${query}`, { timeout: 10000 });
if (data?.Code !== "OK") {
throw new Error(data?.Message || data?.Code || "短信发送失败");
}
return data;
}

47
src/lib/smsCode.ts Normal file
View File

@ -0,0 +1,47 @@
import u from "@/utils";
import { hashPassword, verifyPassword } from "@/lib/password";
export type SmsPurpose = "register" | "resetPassword";
const EXPIRES_IN_MS = 5 * 60 * 1000;
const RESEND_INTERVAL_MS = 60 * 1000;
const MAX_ATTEMPTS = 5;
export function createNumericCode() {
return String(Math.floor(100000 + Math.random() * 900000));
}
export async function assertCanSendSmsCode(phone: string, purpose: SmsPurpose) {
const latest = await u.db("o_smsCode").where({ phone, purpose, used: 0 }).orderBy("createTime", "desc").first();
if (latest?.sentAt && Date.now() - latest.sentAt < RESEND_INTERVAL_MS) {
const waitSeconds = Math.ceil((RESEND_INTERVAL_MS - (Date.now() - latest.sentAt)) / 1000);
throw new Error(`验证码发送过于频繁,请 ${waitSeconds} 秒后再试`);
}
}
export async function saveSmsCode(phone: string, purpose: SmsPurpose, code: string) {
await u.db("o_smsCode").insert({
id: Date.now(),
phone,
purpose,
codeHash: await hashPassword(code),
expiresAt: Date.now() + EXPIRES_IN_MS,
used: 0,
attempts: 0,
createTime: Date.now(),
sentAt: Date.now(),
});
}
export async function verifySmsCode(phone: string, purpose: SmsPurpose, code: string) {
const record = await u.db("o_smsCode").where({ phone, purpose, used: 0 }).orderBy("createTime", "desc").first();
if (!record) throw new Error("验证码不存在或已失效");
if ((record.attempts ?? 0) >= MAX_ATTEMPTS) throw new Error("验证码尝试次数过多,请重新获取");
if ((record.expiresAt ?? 0) < Date.now()) throw new Error("验证码已过期");
await u.db("o_smsCode").where("id", record.id).update({ attempts: (record.attempts ?? 0) + 1 });
const matched = await verifyPassword(code, record.codeHash);
if (!matched) throw new Error("验证码错误");
await u.db("o_smsCode").where("id", record.id).update({ used: 1 });
}

139
src/lib/workspaceAccess.ts Normal file
View File

@ -0,0 +1,139 @@
import { Request } from "express";
import u from "@/utils";
import { assertProjectAccess } from "@/lib/auth";
type ProjectIdLookup = (req: Request) => Promise<Array<number | null | undefined>>;
function toNumber(value: unknown): number | null {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : null;
}
function toNumberList(value: unknown): number[] {
if (!Array.isArray(value)) return [];
return value.map(toNumber).filter((item): item is number => item != null);
}
async function projectIdsFromTable(table: string, ids: number[]) {
ids = ids.filter(Number.isFinite);
if (!ids.length) return [];
return u
.db(table as any)
.whereIn("id", ids)
.select("projectId")
.then((rows: any[]) => rows.map((row) => row.projectId));
}
async function projectIdsFromImage(ids: number[]) {
ids = ids.filter(Number.isFinite);
if (!ids.length) return [];
return u
.db("o_image")
.leftJoin("o_assets", "o_assets.id", "o_image.assetsId")
.whereIn("o_image.id", ids)
.select("o_assets.projectId")
.then((rows) => rows.map((row: any) => row.projectId));
}
async function projectIdsFromEvents(ids: number[]) {
ids = ids.filter(Number.isFinite);
if (!ids.length) return [];
return u
.db("o_eventChapter")
.leftJoin("o_novel", "o_novel.id", "o_eventChapter.novelId")
.whereIn("o_eventChapter.eventId", ids)
.select("o_novel.projectId")
.then((rows) => rows.map((row: any) => row.projectId));
}
async function projectIdsFromFlow(ids: number[]) {
ids = ids.filter(Number.isFinite);
if (!ids.length) return [];
const [assetRows, storyboardRows] = await Promise.all([
u.db("o_assets").whereIn("flowId", ids).select("projectId"),
u.db("o_storyboard").whereIn("flowId", ids).select("projectId"),
]);
return [...assetRows, ...storyboardRows].map((row: any) => row.projectId);
}
async function projectIdsFromWorkbenchItems(req: Request) {
const items = Array.isArray(req.body?.items) ? req.body.items : [];
const assetIds = items.filter((item: any) => item.sources === "assets").map((item: any) => Number(item.id));
const storyboardIds = items.filter((item: any) => item.sources === "storyboard").map((item: any) => Number(item.id));
const [assets, storyboards] = await Promise.all([projectIdsFromTable("o_assets", assetIds), projectIdsFromTable("o_storyboard", storyboardIds)]);
return [...assets, ...storyboards];
}
const projectIdAliases: Record<string, string[]> = {
"/api/project/delProject": ["id"],
"/api/project/editProject": ["id"],
"/api/project/getSingleProject": ["id"],
"/api/general/getSingleProject": ["id"],
"/api/general/updateProject": ["id"],
};
const resourceLookups: Record<string, ProjectIdLookup> = {
"/api/assets/delAssets": (req) => projectIdsFromTable("o_assets", [Number(req.body?.id)]),
"/api/assets/updateAssets": (req) => projectIdsFromTable("o_assets", [Number(req.body?.id)]),
"/api/assets/getImage": (req) => projectIdsFromTable("o_assets", [Number(req.body?.assetsId)]),
"/api/assets/delImage": (req) => projectIdsFromImage([Number(req.body?.id)]),
"/api/assetsGenerate/cancelGenerate": (req) => projectIdsFromImage([Number(req.body?.id)]),
"/api/script/delScript": (req) => projectIdsFromTable("o_script", toNumberList(req.body?.ids)),
"/api/script/exportScript": (req) => projectIdsFromTable("o_script", toNumberList(req.body?.id)),
"/api/script/pollScriptAssets": (req) => projectIdsFromTable("o_script", toNumberList(req.body?.ids)),
"/api/script/updateScript": (req) => projectIdsFromTable("o_script", [Number(req.body?.id)]),
"/api/novel/batchDeleteNovel": (req) => projectIdsFromTable("o_novel", toNumberList(req.body?.ids)),
"/api/novel/delNovel": (req) => projectIdsFromTable("o_novel", [Number(req.body?.id)]),
"/api/novel/getNovelEventState": (req) => projectIdsFromTable("o_novel", toNumberList(req.body?.ids)),
"/api/novel/updateNovel": (req) => projectIdsFromTable("o_novel", [Number(req.body?.id)]),
"/api/novel/event/batchDeleteEvent": (req) => projectIdsFromEvents(toNumberList(req.body?.ids)),
"/api/novel/event/deletEvent": (req) => projectIdsFromEvents([Number(req.body?.id)]),
"/api/production/assets/pollingImage": (req) => projectIdsFromTable("o_assets", toNumberList(req.body?.ids)),
"/api/production/assets/updateAssetsUrl": (req) => projectIdsFromTable("o_assets", [Number(req.body?.id)]),
"/api/production/storyboard/editStoryboardInfo": (req) => projectIdsFromTable("o_storyboard", [Number(req.body?.id)]),
"/api/production/storyboard/pollingImage": (req) => projectIdsFromTable("o_storyboard", toNumberList(req.body?.ids)),
"/api/production/storyboard/previewImage": (req) => projectIdsFromTable("o_storyboard", toNumberList(req.body?.storyboardIds)),
"/api/production/storyboard/removeFrame": (req) => projectIdsFromTable("o_storyboard", [Number(req.body?.id)]),
"/api/production/storyboard/updateStoryboardUrl": (req) => projectIdsFromTable("o_storyboard", [Number(req.body?.id)]),
"/api/production/workbench/deleteTrack": (req) => projectIdsFromTable("o_videoTrack", [Number(req.body?.id)]),
"/api/production/workbench/delVideo": (req) => projectIdsFromTable("o_video", [Number(req.body?.id)]),
"/api/production/workbench/getAudioBindAssetsList": (req) => projectIdsFromTable("o_assets", toNumberList(req.body?.assetsIds)),
"/api/production/workbench/getFileUrl": projectIdsFromWorkbenchItems,
"/api/production/workbench/selectVideo": (req) => projectIdsFromTable("o_videoTrack", [Number(req.body?.trackId)]),
"/api/production/workbench/updateVideoDuration": (req) => projectIdsFromTable("o_videoTrack", [Number(req.body?.id)]),
"/api/production/workbench/updateVideoPrompt": (req) => projectIdsFromTable("o_videoTrack", [Number(req.body?.id)]),
"/api/production/editImage/getImageFlow": (req) => projectIdsFromFlow([Number(req.body?.id)]),
"/api/production/editImage/updateImageFlow": (req) => projectIdsFromFlow([Number(req.body?.flowId)]),
"/api/scriptAgent/updateData": (req) => projectIdsFromTable("o_agentWorkData", [Number(req.body?.id)]),
"/api/task/taskDetails": (req) => projectIdsFromTable("o_tasks", [Number(req.body?.taskId)]),
};
async function getRequestProjectIds(req: Request): Promise<number[]> {
const projectIds = new Set<number>();
const directProjectId = toNumber(req.body?.projectId ?? req.query?.projectId);
if (directProjectId != null) projectIds.add(directProjectId);
for (const key of projectIdAliases[req.path] ?? []) {
const value = toNumber(req.body?.[key] ?? req.query?.[key]);
if (value != null) projectIds.add(value);
}
const lookup = resourceLookups[req.path];
if (lookup) {
const resolvedProjectIds = await lookup(req);
resolvedProjectIds.forEach((projectId) => {
const value = toNumber(projectId);
if (value != null) projectIds.add(value);
});
}
return Array.from(projectIds);
}
export async function requestHasProjectAccess(req: Request, userId: number): Promise<boolean> {
const projectIds = await getRequestProjectIds(req);
for (const projectId of projectIds) {
if (!(await assertProjectAccess(projectId, userId))) return false;
}
return true;
}

View File

@ -1,4 +1,4 @@
// @routes-hash d3a4d7955a8f63d5cc1c769612bd48c2
// @routes-hash 3e9f14716fc04f376339fe13c7f95b40
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -36,138 +36,143 @@ import route32 from "./routes/general/generalStatistics";
import route33 from "./routes/general/getSingleProject";
import route34 from "./routes/general/updateProject";
import route35 from "./routes/login/login";
import route36 from "./routes/modelSelect/getModelDetail";
import route37 from "./routes/modelSelect/getModelList";
import route38 from "./routes/novel/addNovel";
import route39 from "./routes/novel/batchDeleteNovel";
import route40 from "./routes/novel/delNovel";
import route41 from "./routes/novel/event/batchDeleteEvent";
import route42 from "./routes/novel/event/deletEvent";
import route43 from "./routes/novel/event/generateEvents";
import route44 from "./routes/novel/event/getEvent";
import route45 from "./routes/novel/getNovel";
import route46 from "./routes/novel/getNovelData";
import route47 from "./routes/novel/getNovelEventState";
import route48 from "./routes/novel/getNovelIndex";
import route49 from "./routes/novel/updateNovel";
import route50 from "./routes/other/deleteAllData";
import route51 from "./routes/other/getVersion";
import route52 from "./routes/production/assets/batchGenerateAssetsImage";
import route53 from "./routes/production/assets/deleteAssetsDireve";
import route54 from "./routes/production/assets/pollingImage";
import route55 from "./routes/production/assets/updateAssetsUrl";
import route56 from "./routes/production/editImage/generateFlowImage";
import route57 from "./routes/production/editImage/getImageDefaultModle";
import route58 from "./routes/production/editImage/getImageFlow";
import route59 from "./routes/production/editImage/saveImageFlow";
import route60 from "./routes/production/editImage/updateImageFlow";
import route61 from "./routes/production/editImage/uploadImage";
import route62 from "./routes/production/getFlowData";
import route63 from "./routes/production/getStoryboardData";
import route64 from "./routes/production/saveFlowData";
import route65 from "./routes/production/storyboard/addStoryboard";
import route66 from "./routes/production/storyboard/batchAddStoryboardInfo";
import route67 from "./routes/production/storyboard/batchDelete";
import route68 from "./routes/production/storyboard/batchGenerateImage";
import route69 from "./routes/production/storyboard/downPreviewImage";
import route70 from "./routes/production/storyboard/editStoryboardInfo";
import route71 from "./routes/production/storyboard/getStoryboardData";
import route72 from "./routes/production/storyboard/pollingImage";
import route73 from "./routes/production/storyboard/previewImage";
import route74 from "./routes/production/storyboard/removeFrame";
import route75 from "./routes/production/storyboard/updateStoryboardUrl";
import route76 from "./routes/production/workbench/addTrack";
import route77 from "./routes/production/workbench/batchGeneratePrompt";
import route78 from "./routes/production/workbench/batchGenerateVideo";
import route79 from "./routes/production/workbench/checkVideoStateList";
import route80 from "./routes/production/workbench/deleteTrack";
import route81 from "./routes/production/workbench/delVideo";
import route82 from "./routes/production/workbench/generateVideo";
import route83 from "./routes/production/workbench/generateVideoPrompt";
import route84 from "./routes/production/workbench/getAudioBindAssetsList";
import route85 from "./routes/production/workbench/getFileUrl";
import route86 from "./routes/production/workbench/getGenerateData";
import route87 from "./routes/production/workbench/getVideoList";
import route88 from "./routes/production/workbench/selectVideo";
import route89 from "./routes/production/workbench/updateVideoDuration";
import route90 from "./routes/production/workbench/updateVideoPrompt";
import route91 from "./routes/project/addDirectorManual";
import route92 from "./routes/project/addProject";
import route93 from "./routes/project/addVisualManual";
import route94 from "./routes/project/deleteDirectorManual";
import route95 from "./routes/project/deleteVisualManual";
import route96 from "./routes/project/delProject";
import route97 from "./routes/project/editDirectorlManual";
import route98 from "./routes/project/editProject";
import route99 from "./routes/project/editVisualManual";
import route100 from "./routes/project/getModelDetails";
import route101 from "./routes/project/getProject";
import route102 from "./routes/project/getVisualManual";
import route103 from "./routes/project/queryDirectorManual";
import route104 from "./routes/project/visualManual";
import route105 from "./routes/script/addScript";
import route106 from "./routes/script/batchAddScript";
import route107 from "./routes/script/delScript";
import route108 from "./routes/script/exportScript";
import route109 from "./routes/script/extractAssets";
import route110 from "./routes/script/getAiRegex";
import route111 from "./routes/script/getScrptApi";
import route112 from "./routes/script/pollScriptAssets";
import route113 from "./routes/script/updateScript";
import route114 from "./routes/scriptAgent/getPlanData";
import route115 from "./routes/scriptAgent/setPlanData";
import route116 from "./routes/scriptAgent/updateData";
import route117 from "./routes/setting/about/checkUpdate";
import route118 from "./routes/setting/about/downloadApp";
import route119 from "./routes/setting/agentDeploy/agentSetKey";
import route120 from "./routes/setting/agentDeploy/deployAgentModel";
import route121 from "./routes/setting/agentDeploy/getAgentDeploy";
import route122 from "./routes/setting/agentDeploy/getAgentUseMode";
import route123 from "./routes/setting/agentDeploy/updateUseMode";
import route124 from "./routes/setting/dbConfig/clearData";
import route125 from "./routes/setting/dbConfig/clearTable";
import route126 from "./routes/setting/dbConfig/dbInfo";
import route127 from "./routes/setting/dbConfig/exportData";
import route128 from "./routes/setting/dbConfig/importData";
import route129 from "./routes/setting/dev/getSwitchAiDevTool";
import route130 from "./routes/setting/dev/updateSwitchAiDevTool";
import route131 from "./routes/setting/fileManagement/openFolder";
import route132 from "./routes/setting/getTextModel";
import route133 from "./routes/setting/loginConfig/getUser";
import route134 from "./routes/setting/loginConfig/updateUserPwd";
import route135 from "./routes/setting/memoryConfig/delAllMemory";
import route136 from "./routes/setting/memoryConfig/getMemory";
import route137 from "./routes/setting/memoryConfig/sureMemory";
import route138 from "./routes/setting/modelMap/bindingPrompt";
import route139 from "./routes/setting/modelMap/deletePrompt";
import route140 from "./routes/setting/modelMap/getImageAndVideoModel";
import route141 from "./routes/setting/modelMap/getPromptList";
import route142 from "./routes/setting/modelMap/savePrompt";
import route143 from "./routes/setting/modelMap/updatePrompt";
import route144 from "./routes/setting/promptManage/getPrompt";
import route145 from "./routes/setting/promptManage/updatePrompt";
import route146 from "./routes/setting/skillManagement/getSkillContent";
import route147 from "./routes/setting/skillManagement/getSkillList";
import route148 from "./routes/setting/skillManagement/saveSkillContent";
import route149 from "./routes/setting/vendorConfig/addVendor";
import route150 from "./routes/setting/vendorConfig/addVendorModel";
import route151 from "./routes/setting/vendorConfig/deleteVendor";
import route152 from "./routes/setting/vendorConfig/delVendorModel";
import route153 from "./routes/setting/vendorConfig/enableVendor";
import route154 from "./routes/setting/vendorConfig/getCodeByLink";
import route155 from "./routes/setting/vendorConfig/getVendorList";
import route156 from "./routes/setting/vendorConfig/modelTest";
import route157 from "./routes/setting/vendorConfig/modelTest/imageTest";
import route158 from "./routes/setting/vendorConfig/modelTest/textTest";
import route159 from "./routes/setting/vendorConfig/modelTest/videoTest";
import route160 from "./routes/setting/vendorConfig/updateCode";
import route161 from "./routes/setting/vendorConfig/updateVendorInputs";
import route162 from "./routes/setting/vendorConfig/upVendorModel";
import route163 from "./routes/task/getProject";
import route164 from "./routes/task/getTaskApi";
import route165 from "./routes/task/getTaskCategories";
import route166 from "./routes/task/taskDetails";
import route167 from "./routes/test/test";
import route36 from "./routes/login/me";
import route37 from "./routes/login/register";
import route38 from "./routes/login/resetPassword";
import route39 from "./routes/login/sendSmsCode";
import route40 from "./routes/modelSelect/getModelDetail";
import route41 from "./routes/modelSelect/getModelList";
import route42 from "./routes/novel/addNovel";
import route43 from "./routes/novel/batchDeleteNovel";
import route44 from "./routes/novel/delNovel";
import route45 from "./routes/novel/event/batchDeleteEvent";
import route46 from "./routes/novel/event/deletEvent";
import route47 from "./routes/novel/event/generateEvents";
import route48 from "./routes/novel/event/getEvent";
import route49 from "./routes/novel/getNovel";
import route50 from "./routes/novel/getNovelData";
import route51 from "./routes/novel/getNovelEventState";
import route52 from "./routes/novel/getNovelIndex";
import route53 from "./routes/novel/updateNovel";
import route54 from "./routes/other/deleteAllData";
import route55 from "./routes/other/getVersion";
import route56 from "./routes/production/assets/batchGenerateAssetsImage";
import route57 from "./routes/production/assets/deleteAssetsDireve";
import route58 from "./routes/production/assets/pollingImage";
import route59 from "./routes/production/assets/updateAssetsUrl";
import route60 from "./routes/production/editImage/generateFlowImage";
import route61 from "./routes/production/editImage/getImageDefaultModle";
import route62 from "./routes/production/editImage/getImageFlow";
import route63 from "./routes/production/editImage/saveImageFlow";
import route64 from "./routes/production/editImage/updateImageFlow";
import route65 from "./routes/production/editImage/uploadImage";
import route66 from "./routes/production/getFlowData";
import route67 from "./routes/production/getStoryboardData";
import route68 from "./routes/production/saveFlowData";
import route69 from "./routes/production/storyboard/addStoryboard";
import route70 from "./routes/production/storyboard/batchAddStoryboardInfo";
import route71 from "./routes/production/storyboard/batchDelete";
import route72 from "./routes/production/storyboard/batchGenerateImage";
import route73 from "./routes/production/storyboard/downPreviewImage";
import route74 from "./routes/production/storyboard/editStoryboardInfo";
import route75 from "./routes/production/storyboard/getStoryboardData";
import route76 from "./routes/production/storyboard/pollingImage";
import route77 from "./routes/production/storyboard/previewImage";
import route78 from "./routes/production/storyboard/removeFrame";
import route79 from "./routes/production/storyboard/updateStoryboardUrl";
import route80 from "./routes/production/workbench/addTrack";
import route81 from "./routes/production/workbench/batchGeneratePrompt";
import route82 from "./routes/production/workbench/batchGenerateVideo";
import route83 from "./routes/production/workbench/checkVideoStateList";
import route84 from "./routes/production/workbench/deleteTrack";
import route85 from "./routes/production/workbench/delVideo";
import route86 from "./routes/production/workbench/generateVideo";
import route87 from "./routes/production/workbench/generateVideoPrompt";
import route88 from "./routes/production/workbench/getAudioBindAssetsList";
import route89 from "./routes/production/workbench/getFileUrl";
import route90 from "./routes/production/workbench/getGenerateData";
import route91 from "./routes/production/workbench/getVideoList";
import route92 from "./routes/production/workbench/selectVideo";
import route93 from "./routes/production/workbench/updateVideoDuration";
import route94 from "./routes/production/workbench/updateVideoPrompt";
import route95 from "./routes/project/addDirectorManual";
import route96 from "./routes/project/addProject";
import route97 from "./routes/project/addVisualManual";
import route98 from "./routes/project/deleteDirectorManual";
import route99 from "./routes/project/deleteVisualManual";
import route100 from "./routes/project/delProject";
import route101 from "./routes/project/editDirectorlManual";
import route102 from "./routes/project/editProject";
import route103 from "./routes/project/editVisualManual";
import route104 from "./routes/project/getModelDetails";
import route105 from "./routes/project/getProject";
import route106 from "./routes/project/getSingleProject";
import route107 from "./routes/project/getVisualManual";
import route108 from "./routes/project/queryDirectorManual";
import route109 from "./routes/project/visualManual";
import route110 from "./routes/script/addScript";
import route111 from "./routes/script/batchAddScript";
import route112 from "./routes/script/delScript";
import route113 from "./routes/script/exportScript";
import route114 from "./routes/script/extractAssets";
import route115 from "./routes/script/getAiRegex";
import route116 from "./routes/script/getScrptApi";
import route117 from "./routes/script/pollScriptAssets";
import route118 from "./routes/script/updateScript";
import route119 from "./routes/scriptAgent/getPlanData";
import route120 from "./routes/scriptAgent/setPlanData";
import route121 from "./routes/scriptAgent/updateData";
import route122 from "./routes/setting/about/checkUpdate";
import route123 from "./routes/setting/about/downloadApp";
import route124 from "./routes/setting/agentDeploy/agentSetKey";
import route125 from "./routes/setting/agentDeploy/deployAgentModel";
import route126 from "./routes/setting/agentDeploy/getAgentDeploy";
import route127 from "./routes/setting/agentDeploy/getAgentUseMode";
import route128 from "./routes/setting/agentDeploy/updateUseMode";
import route129 from "./routes/setting/dbConfig/clearData";
import route130 from "./routes/setting/dbConfig/clearTable";
import route131 from "./routes/setting/dbConfig/dbInfo";
import route132 from "./routes/setting/dbConfig/exportData";
import route133 from "./routes/setting/dbConfig/importData";
import route134 from "./routes/setting/dev/getSwitchAiDevTool";
import route135 from "./routes/setting/dev/updateSwitchAiDevTool";
import route136 from "./routes/setting/fileManagement/openFolder";
import route137 from "./routes/setting/getTextModel";
import route138 from "./routes/setting/loginConfig/getUser";
import route139 from "./routes/setting/loginConfig/updateUserPwd";
import route140 from "./routes/setting/memoryConfig/delAllMemory";
import route141 from "./routes/setting/memoryConfig/getMemory";
import route142 from "./routes/setting/memoryConfig/sureMemory";
import route143 from "./routes/setting/modelMap/bindingPrompt";
import route144 from "./routes/setting/modelMap/deletePrompt";
import route145 from "./routes/setting/modelMap/getImageAndVideoModel";
import route146 from "./routes/setting/modelMap/getPromptList";
import route147 from "./routes/setting/modelMap/savePrompt";
import route148 from "./routes/setting/modelMap/updatePrompt";
import route149 from "./routes/setting/promptManage/getPrompt";
import route150 from "./routes/setting/promptManage/updatePrompt";
import route151 from "./routes/setting/skillManagement/getSkillContent";
import route152 from "./routes/setting/skillManagement/getSkillList";
import route153 from "./routes/setting/skillManagement/saveSkillContent";
import route154 from "./routes/setting/vendorConfig/addVendor";
import route155 from "./routes/setting/vendorConfig/addVendorModel";
import route156 from "./routes/setting/vendorConfig/deleteVendor";
import route157 from "./routes/setting/vendorConfig/delVendorModel";
import route158 from "./routes/setting/vendorConfig/enableVendor";
import route159 from "./routes/setting/vendorConfig/getCodeByLink";
import route160 from "./routes/setting/vendorConfig/getVendorList";
import route161 from "./routes/setting/vendorConfig/modelTest";
import route162 from "./routes/setting/vendorConfig/modelTest/imageTest";
import route163 from "./routes/setting/vendorConfig/modelTest/textTest";
import route164 from "./routes/setting/vendorConfig/modelTest/videoTest";
import route165 from "./routes/setting/vendorConfig/updateCode";
import route166 from "./routes/setting/vendorConfig/updateVendorInputs";
import route167 from "./routes/setting/vendorConfig/upVendorModel";
import route168 from "./routes/task/getProject";
import route169 from "./routes/task/getTaskApi";
import route170 from "./routes/task/getTaskCategories";
import route171 from "./routes/task/taskDetails";
import route172 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -205,136 +210,141 @@ export default async (app: Express) => {
app.use("/api/general/getSingleProject", route33);
app.use("/api/general/updateProject", route34);
app.use("/api/login/login", route35);
app.use("/api/modelSelect/getModelDetail", route36);
app.use("/api/modelSelect/getModelList", route37);
app.use("/api/novel/addNovel", route38);
app.use("/api/novel/batchDeleteNovel", route39);
app.use("/api/novel/delNovel", route40);
app.use("/api/novel/event/batchDeleteEvent", route41);
app.use("/api/novel/event/deletEvent", route42);
app.use("/api/novel/event/generateEvents", route43);
app.use("/api/novel/event/getEvent", route44);
app.use("/api/novel/getNovel", route45);
app.use("/api/novel/getNovelData", route46);
app.use("/api/novel/getNovelEventState", route47);
app.use("/api/novel/getNovelIndex", route48);
app.use("/api/novel/updateNovel", route49);
app.use("/api/other/deleteAllData", route50);
app.use("/api/other/getVersion", route51);
app.use("/api/production/assets/batchGenerateAssetsImage", route52);
app.use("/api/production/assets/deleteAssetsDireve", route53);
app.use("/api/production/assets/pollingImage", route54);
app.use("/api/production/assets/updateAssetsUrl", route55);
app.use("/api/production/editImage/generateFlowImage", route56);
app.use("/api/production/editImage/getImageDefaultModle", route57);
app.use("/api/production/editImage/getImageFlow", route58);
app.use("/api/production/editImage/saveImageFlow", route59);
app.use("/api/production/editImage/updateImageFlow", route60);
app.use("/api/production/editImage/uploadImage", route61);
app.use("/api/production/getFlowData", route62);
app.use("/api/production/getStoryboardData", route63);
app.use("/api/production/saveFlowData", route64);
app.use("/api/production/storyboard/addStoryboard", route65);
app.use("/api/production/storyboard/batchAddStoryboardInfo", route66);
app.use("/api/production/storyboard/batchDelete", route67);
app.use("/api/production/storyboard/batchGenerateImage", route68);
app.use("/api/production/storyboard/downPreviewImage", route69);
app.use("/api/production/storyboard/editStoryboardInfo", route70);
app.use("/api/production/storyboard/getStoryboardData", route71);
app.use("/api/production/storyboard/pollingImage", route72);
app.use("/api/production/storyboard/previewImage", route73);
app.use("/api/production/storyboard/removeFrame", route74);
app.use("/api/production/storyboard/updateStoryboardUrl", route75);
app.use("/api/production/workbench/addTrack", route76);
app.use("/api/production/workbench/batchGeneratePrompt", route77);
app.use("/api/production/workbench/batchGenerateVideo", route78);
app.use("/api/production/workbench/checkVideoStateList", route79);
app.use("/api/production/workbench/deleteTrack", route80);
app.use("/api/production/workbench/delVideo", route81);
app.use("/api/production/workbench/generateVideo", route82);
app.use("/api/production/workbench/generateVideoPrompt", route83);
app.use("/api/production/workbench/getAudioBindAssetsList", route84);
app.use("/api/production/workbench/getFileUrl", route85);
app.use("/api/production/workbench/getGenerateData", route86);
app.use("/api/production/workbench/getVideoList", route87);
app.use("/api/production/workbench/selectVideo", route88);
app.use("/api/production/workbench/updateVideoDuration", route89);
app.use("/api/production/workbench/updateVideoPrompt", route90);
app.use("/api/project/addDirectorManual", route91);
app.use("/api/project/addProject", route92);
app.use("/api/project/addVisualManual", route93);
app.use("/api/project/deleteDirectorManual", route94);
app.use("/api/project/deleteVisualManual", route95);
app.use("/api/project/delProject", route96);
app.use("/api/project/editDirectorlManual", route97);
app.use("/api/project/editProject", route98);
app.use("/api/project/editVisualManual", route99);
app.use("/api/project/getModelDetails", route100);
app.use("/api/project/getProject", route101);
app.use("/api/project/getVisualManual", route102);
app.use("/api/project/queryDirectorManual", route103);
app.use("/api/project/visualManual", route104);
app.use("/api/script/addScript", route105);
app.use("/api/script/batchAddScript", route106);
app.use("/api/script/delScript", route107);
app.use("/api/script/exportScript", route108);
app.use("/api/script/extractAssets", route109);
app.use("/api/script/getAiRegex", route110);
app.use("/api/script/getScrptApi", route111);
app.use("/api/script/pollScriptAssets", route112);
app.use("/api/script/updateScript", route113);
app.use("/api/scriptAgent/getPlanData", route114);
app.use("/api/scriptAgent/setPlanData", route115);
app.use("/api/scriptAgent/updateData", route116);
app.use("/api/setting/about/checkUpdate", route117);
app.use("/api/setting/about/downloadApp", route118);
app.use("/api/setting/agentDeploy/agentSetKey", route119);
app.use("/api/setting/agentDeploy/deployAgentModel", route120);
app.use("/api/setting/agentDeploy/getAgentDeploy", route121);
app.use("/api/setting/agentDeploy/getAgentUseMode", route122);
app.use("/api/setting/agentDeploy/updateUseMode", route123);
app.use("/api/setting/dbConfig/clearData", route124);
app.use("/api/setting/dbConfig/clearTable", route125);
app.use("/api/setting/dbConfig/dbInfo", route126);
app.use("/api/setting/dbConfig/exportData", route127);
app.use("/api/setting/dbConfig/importData", route128);
app.use("/api/setting/dev/getSwitchAiDevTool", route129);
app.use("/api/setting/dev/updateSwitchAiDevTool", route130);
app.use("/api/setting/fileManagement/openFolder", route131);
app.use("/api/setting/getTextModel", route132);
app.use("/api/setting/loginConfig/getUser", route133);
app.use("/api/setting/loginConfig/updateUserPwd", route134);
app.use("/api/setting/memoryConfig/delAllMemory", route135);
app.use("/api/setting/memoryConfig/getMemory", route136);
app.use("/api/setting/memoryConfig/sureMemory", route137);
app.use("/api/setting/modelMap/bindingPrompt", route138);
app.use("/api/setting/modelMap/deletePrompt", route139);
app.use("/api/setting/modelMap/getImageAndVideoModel", route140);
app.use("/api/setting/modelMap/getPromptList", route141);
app.use("/api/setting/modelMap/savePrompt", route142);
app.use("/api/setting/modelMap/updatePrompt", route143);
app.use("/api/setting/promptManage/getPrompt", route144);
app.use("/api/setting/promptManage/updatePrompt", route145);
app.use("/api/setting/skillManagement/getSkillContent", route146);
app.use("/api/setting/skillManagement/getSkillList", route147);
app.use("/api/setting/skillManagement/saveSkillContent", route148);
app.use("/api/setting/vendorConfig/addVendor", route149);
app.use("/api/setting/vendorConfig/addVendorModel", route150);
app.use("/api/setting/vendorConfig/deleteVendor", route151);
app.use("/api/setting/vendorConfig/delVendorModel", route152);
app.use("/api/setting/vendorConfig/enableVendor", route153);
app.use("/api/setting/vendorConfig/getCodeByLink", route154);
app.use("/api/setting/vendorConfig/getVendorList", route155);
app.use("/api/setting/vendorConfig/modelTest", route156);
app.use("/api/setting/vendorConfig/modelTest/imageTest", route157);
app.use("/api/setting/vendorConfig/modelTest/textTest", route158);
app.use("/api/setting/vendorConfig/modelTest/videoTest", route159);
app.use("/api/setting/vendorConfig/updateCode", route160);
app.use("/api/setting/vendorConfig/updateVendorInputs", route161);
app.use("/api/setting/vendorConfig/upVendorModel", route162);
app.use("/api/task/getProject", route163);
app.use("/api/task/getTaskApi", route164);
app.use("/api/task/getTaskCategories", route165);
app.use("/api/task/taskDetails", route166);
app.use("/api/test/test", route167);
app.use("/api/login/me", route36);
app.use("/api/login/register", route37);
app.use("/api/login/resetPassword", route38);
app.use("/api/login/sendSmsCode", route39);
app.use("/api/modelSelect/getModelDetail", route40);
app.use("/api/modelSelect/getModelList", route41);
app.use("/api/novel/addNovel", route42);
app.use("/api/novel/batchDeleteNovel", route43);
app.use("/api/novel/delNovel", route44);
app.use("/api/novel/event/batchDeleteEvent", route45);
app.use("/api/novel/event/deletEvent", route46);
app.use("/api/novel/event/generateEvents", route47);
app.use("/api/novel/event/getEvent", route48);
app.use("/api/novel/getNovel", route49);
app.use("/api/novel/getNovelData", route50);
app.use("/api/novel/getNovelEventState", route51);
app.use("/api/novel/getNovelIndex", route52);
app.use("/api/novel/updateNovel", route53);
app.use("/api/other/deleteAllData", route54);
app.use("/api/other/getVersion", route55);
app.use("/api/production/assets/batchGenerateAssetsImage", route56);
app.use("/api/production/assets/deleteAssetsDireve", route57);
app.use("/api/production/assets/pollingImage", route58);
app.use("/api/production/assets/updateAssetsUrl", route59);
app.use("/api/production/editImage/generateFlowImage", route60);
app.use("/api/production/editImage/getImageDefaultModle", route61);
app.use("/api/production/editImage/getImageFlow", route62);
app.use("/api/production/editImage/saveImageFlow", route63);
app.use("/api/production/editImage/updateImageFlow", route64);
app.use("/api/production/editImage/uploadImage", route65);
app.use("/api/production/getFlowData", route66);
app.use("/api/production/getStoryboardData", route67);
app.use("/api/production/saveFlowData", route68);
app.use("/api/production/storyboard/addStoryboard", route69);
app.use("/api/production/storyboard/batchAddStoryboardInfo", route70);
app.use("/api/production/storyboard/batchDelete", route71);
app.use("/api/production/storyboard/batchGenerateImage", route72);
app.use("/api/production/storyboard/downPreviewImage", route73);
app.use("/api/production/storyboard/editStoryboardInfo", route74);
app.use("/api/production/storyboard/getStoryboardData", route75);
app.use("/api/production/storyboard/pollingImage", route76);
app.use("/api/production/storyboard/previewImage", route77);
app.use("/api/production/storyboard/removeFrame", route78);
app.use("/api/production/storyboard/updateStoryboardUrl", route79);
app.use("/api/production/workbench/addTrack", route80);
app.use("/api/production/workbench/batchGeneratePrompt", route81);
app.use("/api/production/workbench/batchGenerateVideo", route82);
app.use("/api/production/workbench/checkVideoStateList", route83);
app.use("/api/production/workbench/deleteTrack", route84);
app.use("/api/production/workbench/delVideo", route85);
app.use("/api/production/workbench/generateVideo", route86);
app.use("/api/production/workbench/generateVideoPrompt", route87);
app.use("/api/production/workbench/getAudioBindAssetsList", route88);
app.use("/api/production/workbench/getFileUrl", route89);
app.use("/api/production/workbench/getGenerateData", route90);
app.use("/api/production/workbench/getVideoList", route91);
app.use("/api/production/workbench/selectVideo", route92);
app.use("/api/production/workbench/updateVideoDuration", route93);
app.use("/api/production/workbench/updateVideoPrompt", route94);
app.use("/api/project/addDirectorManual", route95);
app.use("/api/project/addProject", route96);
app.use("/api/project/addVisualManual", route97);
app.use("/api/project/deleteDirectorManual", route98);
app.use("/api/project/deleteVisualManual", route99);
app.use("/api/project/delProject", route100);
app.use("/api/project/editDirectorlManual", route101);
app.use("/api/project/editProject", route102);
app.use("/api/project/editVisualManual", route103);
app.use("/api/project/getModelDetails", route104);
app.use("/api/project/getProject", route105);
app.use("/api/project/getSingleProject", route106);
app.use("/api/project/getVisualManual", route107);
app.use("/api/project/queryDirectorManual", route108);
app.use("/api/project/visualManual", route109);
app.use("/api/script/addScript", route110);
app.use("/api/script/batchAddScript", route111);
app.use("/api/script/delScript", route112);
app.use("/api/script/exportScript", route113);
app.use("/api/script/extractAssets", route114);
app.use("/api/script/getAiRegex", route115);
app.use("/api/script/getScrptApi", route116);
app.use("/api/script/pollScriptAssets", route117);
app.use("/api/script/updateScript", route118);
app.use("/api/scriptAgent/getPlanData", route119);
app.use("/api/scriptAgent/setPlanData", route120);
app.use("/api/scriptAgent/updateData", route121);
app.use("/api/setting/about/checkUpdate", route122);
app.use("/api/setting/about/downloadApp", route123);
app.use("/api/setting/agentDeploy/agentSetKey", route124);
app.use("/api/setting/agentDeploy/deployAgentModel", route125);
app.use("/api/setting/agentDeploy/getAgentDeploy", route126);
app.use("/api/setting/agentDeploy/getAgentUseMode", route127);
app.use("/api/setting/agentDeploy/updateUseMode", route128);
app.use("/api/setting/dbConfig/clearData", route129);
app.use("/api/setting/dbConfig/clearTable", route130);
app.use("/api/setting/dbConfig/dbInfo", route131);
app.use("/api/setting/dbConfig/exportData", route132);
app.use("/api/setting/dbConfig/importData", route133);
app.use("/api/setting/dev/getSwitchAiDevTool", route134);
app.use("/api/setting/dev/updateSwitchAiDevTool", route135);
app.use("/api/setting/fileManagement/openFolder", route136);
app.use("/api/setting/getTextModel", route137);
app.use("/api/setting/loginConfig/getUser", route138);
app.use("/api/setting/loginConfig/updateUserPwd", route139);
app.use("/api/setting/memoryConfig/delAllMemory", route140);
app.use("/api/setting/memoryConfig/getMemory", route141);
app.use("/api/setting/memoryConfig/sureMemory", route142);
app.use("/api/setting/modelMap/bindingPrompt", route143);
app.use("/api/setting/modelMap/deletePrompt", route144);
app.use("/api/setting/modelMap/getImageAndVideoModel", route145);
app.use("/api/setting/modelMap/getPromptList", route146);
app.use("/api/setting/modelMap/savePrompt", route147);
app.use("/api/setting/modelMap/updatePrompt", route148);
app.use("/api/setting/promptManage/getPrompt", route149);
app.use("/api/setting/promptManage/updatePrompt", route150);
app.use("/api/setting/skillManagement/getSkillContent", route151);
app.use("/api/setting/skillManagement/getSkillList", route152);
app.use("/api/setting/skillManagement/saveSkillContent", route153);
app.use("/api/setting/vendorConfig/addVendor", route154);
app.use("/api/setting/vendorConfig/addVendorModel", route155);
app.use("/api/setting/vendorConfig/deleteVendor", route156);
app.use("/api/setting/vendorConfig/delVendorModel", route157);
app.use("/api/setting/vendorConfig/enableVendor", route158);
app.use("/api/setting/vendorConfig/getCodeByLink", route159);
app.use("/api/setting/vendorConfig/getVendorList", route160);
app.use("/api/setting/vendorConfig/modelTest", route161);
app.use("/api/setting/vendorConfig/modelTest/imageTest", route162);
app.use("/api/setting/vendorConfig/modelTest/textTest", route163);
app.use("/api/setting/vendorConfig/modelTest/videoTest", route164);
app.use("/api/setting/vendorConfig/updateCode", route165);
app.use("/api/setting/vendorConfig/updateVendorInputs", route166);
app.use("/api/setting/vendorConfig/upVendorModel", route167);
app.use("/api/task/getProject", route168);
app.use("/api/task/getTaskApi", route169);
app.use("/api/task/getTaskCategories", route170);
app.use("/api/task/taskDetails", route171);
app.use("/api/test/test", route172);
}

View File

@ -1,18 +1,12 @@
import express from "express";
import u from "@/utils";
import jwt from "jsonwebtoken";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
import { createAuthToken, getTokenKey, publicUser } from "@/lib/auth";
import { hashPassword, isHashedPassword, verifyPassword } from "@/lib/password";
const router = express.Router();
export function setToken(payload: string | object, expiresIn: string | number, secret: string): string {
if (!payload || typeof secret !== "string" || !secret) {
throw new Error("参数不合法");
}
return (jwt.sign as any)(payload, secret, { expiresIn });
}
// 登录
export default router.post(
"/",
@ -21,24 +15,24 @@ export default router.post(
password: z.string(),
}),
async (req, res) => {
const { username, password } = req.body;
const username = String(req.body.username || "").trim();
const password = String(req.body.password || "");
const data = await u.db("o_user").where("name", "=", username).first();
const data = await u.db("o_user").where("name", "=", username).orWhere("phone", username).first();
if (!data) return res.status(400).send(error("登录失败"));
if (data!.password == password && data!.name == username) {
const tokenData = await u.db("o_setting").where("key", "tokenKey").first();
if (!tokenData) return res.status(400).send(error("未找到tokenKey"));
const token = setToken(
{
id: data!.id,
name: data!.name,
},
"180Days",
tokenData?.value as string,
);
const validPassword = await verifyPassword(password, data.password);
if (validPassword) {
const tokenKey = await getTokenKey();
if (!tokenKey) return res.status(400).send(error("未找到tokenKey"));
return res.status(200).send(success({ token: "Bearer " + token, name: data!.name, id: data!.id }, "登录成功"));
if (!isHashedPassword(data.password)) {
await u.db("o_user").where("id", data.id).update({ password: await hashPassword(password) });
}
const user = publicUser(data);
const token = createAuthToken(user, tokenKey);
return res.status(200).send(success({ token: "Bearer " + token, ...user }, "登录成功"));
} else {
return res.status(400).send(error("用户名或密码错误"));
}

13
src/routes/login/me.ts Normal file
View File

@ -0,0 +1,13 @@
import express from "express";
import u from "@/utils";
import { getCurrentUser, publicUser } from "@/lib/auth";
import { success, error } from "@/lib/responseFormat";
const router = express.Router();
export default router.get("/", async (req, res) => {
const authUser = getCurrentUser(req);
const user = await u.db("o_user").where("id", authUser.id).select("id", "name").first();
if (!user) return res.status(401).send(error("用户不存在"));
return res.status(200).send(success(publicUser(user)));
});

View File

@ -0,0 +1,57 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { createAuthToken, getTokenKey, publicUser } from "@/lib/auth";
import { hashPassword } from "@/lib/password";
import { verifySmsCode } from "@/lib/smsCode";
import { isValidMainlandPhone } from "@/lib/sms";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
username: z.string().min(2).max(32),
phone: z.string(),
code: z.string(),
password: z.string().min(6).max(64),
}),
async (req, res) => {
const username = String(req.body.username || "").trim();
const phone = String(req.body.phone || "").trim();
const code = String(req.body.code || "").trim();
const password = String(req.body.password || "");
if (username.length < 2 || username.length > 32) {
return res.status(400).send(error("用户名长度需为 2-32 位"));
}
if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(username)) {
return res.status(400).send(error("用户名只能包含中文、英文、数字、下划线和横线"));
}
if (!isValidMainlandPhone(phone)) return res.status(400).send(error("手机号格式不正确"));
const exists = await u.db("o_user").where("name", username).first();
if (exists) return res.status(400).send(error("用户名已存在"));
const phoneExists = await u.db("o_user").where("phone", phone).first();
if (phoneExists) return res.status(400).send(error("手机号已注册"));
const tokenKey = await getTokenKey();
if (!tokenKey) return res.status(400).send(error("未找到tokenKey"));
await verifySmsCode(phone, "register", code);
const id = Date.now();
await u.db("o_user").insert({
id,
name: username,
phone,
password: await hashPassword(password),
});
const user = publicUser({ id, name: username, phone });
const token = createAuthToken(user, tokenKey);
return res.status(200).send(success({ token: "Bearer " + token, ...user }, "注册成功"));
},
);

View File

@ -0,0 +1,36 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { hashPassword } from "@/lib/password";
import { verifySmsCode } from "@/lib/smsCode";
import { isValidMainlandPhone } from "@/lib/sms";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
phone: z.string(),
code: z.string(),
password: z.string().min(6).max(64),
}),
async (req, res) => {
const phone = String(req.body.phone || "").trim();
const code = String(req.body.code || "").trim();
const password = String(req.body.password || "");
if (!isValidMainlandPhone(phone)) return res.status(400).send(error("手机号格式不正确"));
const user = await u.db("o_user").where("phone", phone).first();
if (!user) return res.status(400).send(error("该手机号尚未注册"));
await verifySmsCode(phone, "resetPassword", code);
await u.db("o_user").where("id", user.id).update({
password: await hashPassword(password),
});
return res.status(200).send(success(null, "密码已重置"));
},
);

View File

@ -0,0 +1,34 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { createNumericCode, assertCanSendSmsCode, saveSmsCode } from "@/lib/smsCode";
import { isValidMainlandPhone, maskPhone, sendSmsCode } from "@/lib/sms";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
phone: z.string(),
purpose: z.enum(["register", "resetPassword"]),
}),
async (req, res) => {
const phone = String(req.body.phone || "").trim();
const purpose = req.body.purpose as "register" | "resetPassword";
if (!isValidMainlandPhone(phone)) return res.status(400).send(error("手机号格式不正确"));
const user = await u.db("o_user").where("phone", phone).first();
if (purpose === "register" && user) return res.status(400).send(error("手机号已注册"));
if (purpose === "resetPassword" && !user) return res.status(400).send(error("该手机号尚未注册"));
await assertCanSendSmsCode(phone, purpose);
const code = createNumericCode();
await sendSmsCode(phone, code);
await saveSmsCode(phone, purpose, code);
return res.status(200).send(success({ phone: maskPhone(phone), expiresIn: 300 }, "验证码已发送"));
},
);

View File

@ -3,6 +3,7 @@ import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { getCurrentUser } from "@/lib/auth";
const router = express.Router();
// 新增项目
@ -23,6 +24,7 @@ export default router.post(
}),
async (req, res) => {
const { projectType, name, intro, type, directorManual, artStyle, videoRatio, imageModel, videoModel, imageQuality, mode } = req.body;
const user = getCurrentUser(req);
await u.db("o_project").insert({
id: Date.now(),
@ -33,7 +35,7 @@ export default router.post(
artStyle,
videoRatio,
directorManual,
userId: 1,
userId: user.id,
imageModel,
videoModel,
createTime: Date.now(),

View File

@ -1,10 +1,12 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { getCurrentUser } from "@/lib/auth";
const router = express.Router();
// 获取项目
export default router.post("/", async (req, res) => {
const data = await u.db("o_project").select("*");
const user = getCurrentUser(req);
const data = await u.db("o_project").where("userId", user.id).select("*").orderBy("createTime", "desc");
res.status(200).send(success(data));
});

View File

@ -0,0 +1,19 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.number(),
}),
async (req, res) => {
const { id } = req.body;
const data = await u.db("o_project").where("id", id).select("*");
res.status(200).send(success(data));
},
);

View File

@ -1,9 +1,11 @@
import express from "express";
import u from "@/utils";
import { getCurrentUser, publicUser } from "@/lib/auth";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.get("/", async (req, res) => {
const data = await u.db("o_user").select("*").first();
res.status(200).send(success(data));
const user = getCurrentUser(req);
const data = await u.db("o_user").where("id", user.id).select("id", "name", "phone").first();
res.status(200).send(success(data ? publicUser(data) : null));
});

View File

@ -1,23 +1,35 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { getCurrentUser } from "@/lib/auth";
import { hashPassword } from "@/lib/password";
const router = express.Router();
export default router.post(
"/",
validateFields({
name: z.string(),
password: z.string(),
id: z.number(),
password: z.string().optional().nullable(),
id: z.number().optional().nullable(),
}),
async (req, res) => {
const { name, password, id } = req.body;
await u.db("o_user").where("id", id).update({
name,
password,
});
const user = getCurrentUser(req);
const name = String(req.body.name || "").trim();
const password = String(req.body.password || "");
if (password && (password.length < 6 || password.length > 64)) {
return res.status(400).send(error("密码长度需为 6-64 位"));
}
const exists = await u.db("o_user").where("name", name).whereNot("id", user.id).first();
if (exists) return res.status(400).send(error("用户名已存在"));
const updateData: { name: string; password?: string } = { name };
if (password) updateData.password = await hashPassword(password);
await u.db("o_user").where("id", user.id).update(updateData);
res.status(200).send(success("保存设置成功"));
},
);

View File

@ -1,10 +1,12 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { getCurrentUser } from "@/lib/auth";
const router = express.Router();
export default router.post("/", async (req, res) => {
const list = await u.db("o_project").select("id", "name").groupBy("name");
const user = getCurrentUser(req);
const list = await u.db("o_project").where("userId", user.id).select("id", "name").groupBy("id", "name");
const data = list.filter((item) => item.name);
res.status(200).send(success(data));
});

View File

@ -2,7 +2,8 @@ import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { number, z } from "zod";
import { z } from "zod";
import { getCurrentUser } from "@/lib/auth";
const router = express.Router();
export default router.post(
"/",
@ -15,10 +16,12 @@ export default router.post(
}),
async (req, res) => {
const { taskClass, state, projectId, page = 1, limit = 10 }: any = req.body;
const user = getCurrentUser(req);
const offset = (page - 1) * limit;
const data = await u
.db("o_tasks")
.leftJoin("o_project", "o_project.id", "o_tasks.projectId")
.where("o_project.userId", user.id)
.andWhere((qb) => {
if (taskClass) {
qb.andWhere("o_tasks.taskClass", taskClass);
@ -36,6 +39,8 @@ export default router.post(
.orderBy("o_tasks.id", "desc");
const totalQuery = (await u
.db("o_tasks")
.leftJoin("o_project", "o_project.id", "o_tasks.projectId")
.where("o_project.userId", user.id)
.andWhere((qb) => {
if (taskClass) {
qb.andWhere("o_tasks.taskClass", taskClass);

View File

@ -1,27 +1,15 @@
import jwt from "jsonwebtoken";
import u from "@/utils";
import { Namespace, Socket } from "socket.io";
import * as agent from "@/agents/productionAgent/index";
import ResTool from "@/socket/resTool";
async function verifyToken(rawToken: string): Promise<Boolean> {
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
if (!setting) return false;
const { value: tokenKey } = setting;
if (!rawToken) return false;
const token = rawToken.replace("Bearer ", "");
try {
jwt.verify(token, tokenKey as string);
return true;
} catch (err) {
return false;
}
}
import { assertProjectAccess, verifyAuthToken } from "@/lib/auth";
export default (nsp: Namespace) => {
nsp.on("connection", async (socket: Socket) => {
const token = socket.handshake.auth.token;
if (!token || !(await verifyToken(token))) {
const authUser = await verifyAuthToken(token);
const projectId = Number(socket.handshake.auth.projectId);
if (!authUser || !projectId || !(await assertProjectAccess(projectId, authUser.id))) {
console.log("[productionAgent] 连接失败token无效");
socket.disconnect();
return;
@ -36,7 +24,7 @@ export default (nsp: Namespace) => {
console.log("[productionAgent] 已连接:", socket.id);
let resTool = new ResTool(socket, {
projectId: socket.handshake.auth.projectId,
projectId,
scriptId: socket.handshake.auth.scriptId,
});
let abortController: AbortController | null = null;
@ -46,7 +34,11 @@ export default (nsp: Namespace) => {
thinlLevel: 0,
};
socket.on("updateContext", (data: { isolationKey: string; projectId: number; scriptId: number }, callback) => {
socket.on("updateContext", async (data: { isolationKey: string; projectId: number; scriptId: number }, callback) => {
if (!(await assertProjectAccess(Number(data.projectId), authUser.id))) {
callback?.({ success: false, message: "无权访问该项目" });
return;
}
isolationKey = data.isolationKey;
resTool = new ResTool(socket, {
projectId: data.projectId,

View File

@ -1,27 +1,15 @@
import jwt from "jsonwebtoken";
import u from "@/utils";
import { Namespace, Socket } from "socket.io";
import * as agent from "@/agents/scriptAgent/index";
import ResTool from "@/socket/resTool";
async function verifyToken(rawToken: string): Promise<Boolean> {
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
if (!setting) return false;
const { value: tokenKey } = setting;
if (!rawToken) return false;
const token = rawToken.replace("Bearer ", "");
try {
jwt.verify(token, tokenKey as string);
return true;
} catch (err) {
return false;
}
}
import { assertProjectAccess, verifyAuthToken } from "@/lib/auth";
export default (nsp: Namespace) => {
nsp.on("connection", async (socket: Socket) => {
const token = socket.handshake.auth.token;
if (!token || !(await verifyToken(token))) {
const authUser = await verifyAuthToken(token);
const projectId = Number(socket.handshake.auth.projectId);
if (!authUser || !projectId || !(await assertProjectAccess(projectId, authUser.id))) {
console.log("[scriptAgent] 连接失败token无效");
socket.disconnect();
return;
@ -36,7 +24,7 @@ export default (nsp: Namespace) => {
console.log("[scriptAgent] 已连接:", socket.id);
const resTool = new ResTool(socket, {
projectId: socket.handshake.auth.projectId,
projectId,
});
let abortController: AbortController | null = null;

View File

@ -140,6 +140,17 @@ export interface o_project {
'videoModel'?: string | null;
'videoRatio'?: string | null;
}
export interface o_smsCode {
'attempts'?: number | null;
'codeHash': string;
'createTime': number;
'expiresAt': number;
'id'?: number;
'phone': string;
'purpose': string;
'sentAt': number;
'used'?: number | null;
}
export interface o_prompt {
'data'?: string | null;
'id'?: number;
@ -211,6 +222,7 @@ export interface o_tasks {
export interface o_user {
'id'?: number;
'name'?: string | null;
'phone'?: string | null;
'password'?: string | null;
}
export interface o_vendorConfig {
@ -262,6 +274,7 @@ export interface DB {
"o_script": o_script;
"o_scriptAssets": o_scriptAssets;
"o_setting": o_setting;
"o_smsCode": o_smsCode;
"o_skillAttribution": o_skillAttribution;
"o_skillList": o_skillList;
"o_storyboard": o_storyboard;

View File

@ -26,6 +26,7 @@
},
"exclude": [
"node_modules",
"web-core",
"data/**/*.ts",
"dist",
"build"

View File

@ -4,8 +4,11 @@
<t-form-item :label="$t('settings.login.username')" name="name">
<t-input v-model="formData.name" :placeholder="$t('settings.login.usernamePlaceholder')" clearable width="100%" />
</t-form-item>
<t-form-item label="手机号" name="phone">
<t-input v-model="formData.phone" placeholder="未绑定手机号" disabled width="100%" />
</t-form-item>
<t-form-item :label="$t('settings.login.password')" name="password">
<t-input v-model="formData.password" type="password" :placeholder="$t('settings.login.passwordPlaceholder')" />
<t-input v-model="formData.password" type="password" placeholder="留空则不修改密码" />
</t-form-item>
<t-form-item :status-icon="false">
<t-space size="small">
@ -23,6 +26,7 @@ import axios from "@/utils/axios";
interface UserForm {
id: number | null;
name: string;
phone: string;
password: string;
}
@ -32,6 +36,7 @@ const loading = ref(false);
const formData = ref<UserForm>({
id: null,
name: "",
phone: "",
password: "",
});
@ -41,8 +46,7 @@ const formRules: FormRules<UserForm> = {
{ min: 2, max: 20, message: $t("settings.login.msg.usernameLength"), trigger: "blur" },
],
password: [
{ required: true, message: $t("settings.login.msg.enterPassword"), trigger: "blur" },
{ min: 6, max: 20, message: $t("settings.login.msg.passwordLength"), trigger: "blur" },
{ min: 6, max: 64, message: $t("settings.login.msg.passwordLength"), trigger: "blur" },
],
};
@ -52,7 +56,8 @@ async function fetchUserInfo() {
formData.value = {
id: res.data.id ?? null,
name: res.data.name ?? "",
password: res.data.password ?? "",
phone: res.data.phone ?? "",
password: "",
};
} catch (error) {
window.$message.error($t("settings.login.msg.fetchFailed"));

View File

@ -44,6 +44,7 @@ async function handleLogout() {
localStorage.removeItem("token");
//
localStorage.removeItem("user");
localStorage.removeItem("userId");
window.$message.success($t("settings.logout.msg.logoutSuccess"));

View File

@ -68,39 +68,86 @@
<section class="formBox" aria-labelledby="login-title">
<div class="formHeader">
<span class="formEyebrow"><i></i> SECURE ACCESS</span>
<h2 id="login-title">登录</h2>
<p>进入团队短剧生产工作台</p>
<h2 id="login-title">{{ formTitle }}</h2>
<p>{{ formSubtitle }}</p>
</div>
<form class="login-form" @submit.prevent="handleLogin">
<label class="fieldLabel" for="login-username">用户名</label>
<t-input id="login-username" v-model="state.user.username" placeholder="请输入用户名" autocomplete="username" size="large">
<div v-if="!isResetMode" class="authSwitch" role="tablist" aria-label="账号入口">
<button type="button" :class="{ active: authMode === 'login' }" @click="setAuthMode('login')">登录</button>
<button type="button" :class="{ active: authMode === 'register' }" @click="setAuthMode('register')">注册</button>
</div>
<form class="login-form" @submit.prevent="handleSubmit">
<template v-if="!isResetMode">
<label class="fieldLabel" for="login-username">用户名/手机号</label>
<t-input id="login-username" v-model="state.user.username" placeholder="请输入用户名或手机号" autocomplete="username" size="large">
<template #prefix-icon>
<t-icon name="user" />
</template>
</t-input>
<label class="fieldLabel" for="login-password">密码</label>
</template>
<template v-if="needsSmsCode">
<label class="fieldLabel" for="login-phone">手机号</label>
<t-input id="login-phone" v-model="state.user.phone" placeholder="请输入手机号" autocomplete="tel" size="large">
<template #prefix-icon>
<t-icon name="mobile" />
</template>
</t-input>
<label class="fieldLabel" for="login-code">验证码</label>
<div class="codeRow">
<t-input id="login-code" v-model="state.user.code" placeholder="请输入短信验证码" autocomplete="one-time-code" size="large">
<template #prefix-icon>
<t-icon name="lock-on" />
</template>
</t-input>
<t-button theme="default" size="large" :loading="state.smsLoading" :disabled="smsCountdown > 0" @click="handleSendSmsCode">
{{ smsCountdown > 0 ? `${smsCountdown}s` : "获取验证码" }}
</t-button>
</div>
</template>
<label class="fieldLabel" for="login-password">{{ isResetMode ? "新密码" : "密码" }}</label>
<t-input
id="login-password"
v-model="state.user.password"
type="password"
placeholder="请输入密码"
autocomplete="current-password"
:placeholder="isResetMode ? '请输入新密码' : '请输入密码'"
:autocomplete="isRegisterMode || isResetMode ? 'new-password' : 'current-password'"
size="large">
<template #prefix-icon>
<t-icon name="lock-on" />
</template>
</t-input>
<div class="loginOptions">
<t-checkbox v-model="rememberMe">记住我</t-checkbox>
<button class="forgotBtn" type="button" @click="handleForgotPassword">忘记密码?</button>
<template v-if="isRegisterMode || isResetMode">
<label class="fieldLabel" for="login-confirm-password">确认密码</label>
<t-input
id="login-confirm-password"
v-model="state.user.confirmPassword"
type="password"
placeholder="请再次输入密码"
autocomplete="new-password"
size="large">
<template #prefix-icon>
<t-icon name="lock-on" />
</template>
</t-input>
</template>
<div v-if="!isRegisterMode" class="loginOptions">
<t-checkbox v-if="!isResetMode" v-model="rememberMe">记住我</t-checkbox>
<span v-else></span>
<button class="forgotBtn" type="button" @click="handleForgotPassword">{{ isResetMode ? "返回登录" : "忘记密码?" }}</button>
</div>
<t-button class="loginBtn" theme="primary" size="large" type="submit" :loading="state.loginLoading" block>进入工作台</t-button>
<t-button class="loginBtn" theme="primary" size="large" type="submit" :loading="state.loginLoading" block>
{{ primaryButtonText }}
</t-button>
</form>
<div class="tips">
<div v-if="!needsSmsCode" class="tips">
<span>默认账号</span>
<strong>admin</strong>
<span>/</span>
@ -141,6 +188,27 @@ const defaultBaseUrl = getDefaultApiBaseUrl();
const tempBaseUrl = ref(baseUrl.value);
const rememberedUsername = typeof window !== "undefined" ? localStorage.getItem("toonflowRememberedUsername") || "" : "";
const rememberMe = ref(Boolean(rememberedUsername));
const authMode = ref("login");
const isRegisterMode = computed(() => authMode.value === "register");
const isResetMode = computed(() => authMode.value === "reset");
const needsSmsCode = computed(() => isRegisterMode.value || isResetMode.value);
const smsCountdown = ref(0);
let smsTimer = null;
const formTitle = computed(() => {
if (isRegisterMode.value) return "创建账号";
if (isResetMode.value) return "重置密码";
return "登录";
});
const formSubtitle = computed(() => {
if (isRegisterMode.value) return "手机号验证后创建独立工作区";
if (isResetMode.value) return "通过手机号验证码找回访问权限";
return "进入你的短剧生产工作台";
});
const primaryButtonText = computed(() => {
if (isRegisterMode.value) return "创建并进入";
if (isResetMode.value) return "重置密码";
return "进入工作台";
});
//
const handleSaveSetting = () => {
@ -154,8 +222,12 @@ const state = ref({
loginLoading: false,
user: {
username: rememberedUsername,
phone: "",
code: "",
password: "",
confirmPassword: "",
},
smsLoading: false,
rules: {
username: [{ required: true, message: "请输入用户名" }],
password: [{ required: true, message: "请输入密码" }],
@ -163,7 +235,67 @@ const state = ref({
});
const handleForgotPassword = () => {
window.$message.info("请联系管理员重置密码");
setAuthMode(isResetMode.value ? "login" : "reset");
};
const setAuthMode = (mode) => {
authMode.value = mode;
state.value.user.code = "";
state.value.user.password = "";
state.value.user.confirmPassword = "";
};
const validatePhone = () => {
const phone = state.value.user.phone.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
window.$message.warning("请输入正确的手机号");
return "";
}
return phone;
};
const startSmsCountdown = () => {
smsCountdown.value = 60;
if (smsTimer) window.clearInterval(smsTimer);
smsTimer = window.setInterval(() => {
smsCountdown.value -= 1;
if (smsCountdown.value <= 0 && smsTimer) {
window.clearInterval(smsTimer);
smsTimer = null;
}
}, 1000);
};
const handleSendSmsCode = () => {
const phone = validatePhone();
if (!phone) return;
state.value.smsLoading = true;
axios
.post("/login/sendSmsCode", {
phone,
purpose: isResetMode.value ? "resetPassword" : "register",
})
.then(() => {
startSmsCountdown();
window.$message.success("验证码已发送");
})
.catch((e) => {
window.$message.error(e.message);
})
.finally(() => {
state.value.smsLoading = false;
});
};
const persistSession = (data, username) => {
localStorage.setItem("token", data.token);
localStorage.setItem("userId", data.id);
if (rememberMe.value && authMode.value === "login") {
localStorage.setItem("toonflowRememberedUsername", username);
} else if (authMode.value === "login") {
localStorage.removeItem("toonflowRememberedUsername");
}
Router.push("/project");
};
const handleLogin = () => {
@ -176,14 +308,7 @@ const handleLogin = () => {
axios
.post("/login/login", obj)
.then(({ data }) => {
localStorage.setItem("token", data.token);
localStorage.setItem("userId", data.id);
if (rememberMe.value) {
localStorage.setItem("toonflowRememberedUsername", obj.username);
} else {
localStorage.removeItem("toonflowRememberedUsername");
}
Router.push("/project");
persistSession(data, obj.username);
window.$message.success("登录成功");
state.value.loginLoading = false;
})
@ -192,6 +317,68 @@ const handleLogin = () => {
window.$message.error(e.message);
});
};
const handleRegister = () => {
const { username, phone, code, password, confirmPassword } = state.value.user;
if (!username || !phone || !code || !password || !confirmPassword) {
window.$message.warning("请完整填写注册信息");
return;
}
if (!validatePhone()) return;
if (password !== confirmPassword) {
window.$message.warning("两次输入的密码不一致");
return;
}
state.value.loginLoading = true;
axios
.post("/login/register", { username, phone, code, password })
.then(({ data }) => {
persistSession(data, username);
window.$message.success("注册成功");
state.value.loginLoading = false;
})
.catch((e) => {
state.value.loginLoading = false;
window.$message.error(e.message);
});
};
const handleResetPassword = () => {
const { phone, code, password, confirmPassword } = state.value.user;
if (!phone || !code || !password || !confirmPassword) {
window.$message.warning("请完整填写重置信息");
return;
}
if (!validatePhone()) return;
if (password !== confirmPassword) {
window.$message.warning("两次输入的密码不一致");
return;
}
state.value.loginLoading = true;
axios
.post("/login/resetPassword", { phone, code, password })
.then(() => {
state.value.user.username = phone;
setAuthMode("login");
window.$message.success("密码已重置,请重新登录");
})
.catch((e) => {
window.$message.error(e.message);
})
.finally(() => {
state.value.loginLoading = false;
});
};
const handleSubmit = () => {
if (isResetMode.value) handleResetPassword();
else if (isRegisterMode.value) handleRegister();
else handleLogin();
};
onBeforeUnmount(() => {
if (smsTimer) window.clearInterval(smsTimer);
});
</script>
<style lang="scss" scoped>
@ -538,6 +725,39 @@ const handleLogin = () => {
line-height: 1.5;
}
.authSwitch {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
height: 42px;
margin: -6px 0 20px;
padding: 4px;
border: 1px solid rgba(185, 188, 210, 0.16);
border-radius: var(--air-field-radius);
background: rgba(255, 255, 255, 0.04);
button {
border: 0;
border-radius: calc(var(--air-field-radius) - 4px);
background: transparent;
color: rgba(226, 230, 246, 0.68);
font-family: inherit;
font-size: 13px;
font-weight: 780;
cursor: pointer;
transition:
background-color 160ms var(--air-ease-out),
color 160ms var(--air-ease-out),
box-shadow 160ms var(--air-ease-out);
}
button.active {
color: #07121a;
background: linear-gradient(100deg, #65d9cb 0%, #8ea3ff 100%);
box-shadow: 0 10px 22px rgba(101, 217, 203, 0.18);
}
}
.login-form {
position: relative;
display: flex;
@ -553,6 +773,24 @@ const handleLogin = () => {
line-height: 1;
}
.codeRow {
display: grid;
grid-template-columns: minmax(0, 1fr) 116px;
gap: 10px;
align-items: center;
:deep(.t-button) {
height: 48px !important;
min-height: 48px !important;
border-color: rgba(185, 188, 210, 0.2) !important;
border-radius: var(--air-field-radius) !important;
background: rgba(255, 255, 255, 0.07) !important;
color: var(--air-text-primary) !important;
font-size: 13px;
font-weight: 750;
}
}
.login-form {
:deep(.t-input) {
height: 48px !important;
@ -856,6 +1094,10 @@ const handleLogin = () => {
font-size: 32px;
}
.codeRow {
grid-template-columns: 1fr;
}
.tips {
flex-wrap: wrap;
padding-top: 24px;

View File

@ -27,8 +27,9 @@ instance.interceptors.response.use(
return response.data;
},
function (error) {
if (error.status === 401) {
if (error.response?.status === 401) {
localStorage.removeItem("token");
localStorage.removeItem("userId");
router.push("/login");
MessagePlugin.error(window.$t("common.sessionExpired"));
}