Files
omniai-server/src/routes/assets.js
T
2026-06-02 13:14:10 +08:00

217 lines
6.8 KiB
JavaScript

"use strict";
const { requireAuth, pool } = require("./context");
const ASSET_TYPES = new Set(["character", "scene", "prop", "video", "image", "asset", "other"]);
const ASSET_STATUSES = new Set(["ready", "draft", "reviewing", "pending", "failed"]);
function cleanText(value, maxLength) {
return String(value || "").trim().slice(0, maxLength);
}
function safeJsonString(value, fallback) {
if (value === undefined) return JSON.stringify(fallback);
try {
return JSON.stringify(value ?? fallback);
} catch {
return JSON.stringify(fallback);
}
}
function parseJson(value, fallback) {
if (!value || typeof value !== "string") return fallback;
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
function normalizeTags(value) {
return Array.isArray(value)
? value.map((item) => cleanText(item, 40)).filter(Boolean).slice(0, 20)
: [];
}
function normalizeAssetPayload(body = {}, partial = false) {
const name = cleanText(body.name ?? body.title, 200);
if (!partial && !name) return { error: "Missing asset name" };
const type = cleanText(body.type ?? body.assetType, 32);
const status = cleanText(body.status, 32);
const sourceTaskId = Number(body.sourceTaskId ?? body.source_task_id);
return {
value: {
name: name || undefined,
type: ASSET_TYPES.has(type) ? type : partial ? undefined : "asset",
description: cleanText(body.description, 5000) || null,
url: cleanText(body.url ?? body.imageUrl, 1000) || null,
ossKey: cleanText(body.ossKey ?? body.oss_key, 500) || null,
tagsJson: safeJsonString(normalizeTags(body.tags), []),
status: ASSET_STATUSES.has(status) ? status : partial ? undefined : "ready",
sourceTaskId: Number.isFinite(sourceTaskId) ? sourceTaskId : null,
sourceProjectId: cleanText(body.sourceProjectId ?? body.projectId ?? body.source_project_id, 64) || null,
metadataJson: safeJsonString(body.metadata, {}),
},
};
}
function formatAsset(row) {
return {
id: Number(row.id),
type: row.type,
name: row.name,
description: row.description || "",
url: row.url || null,
ossKey: row.oss_key || null,
imageUrl: row.url || "",
tags: parseJson(row.tags_json, []),
status: row.status,
sourceTaskId: row.source_task_id == null ? null : String(row.source_task_id),
sourceProjectId: row.source_project_id || null,
metadata: parseJson(row.metadata_json, {}),
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function registerAssetRoutes(router) {
router.get("/assets", requireAuth, async (req, res) => {
try {
const type = cleanText(req.query.type, 32);
const status = cleanText(req.query.status, 32);
const q = cleanText(req.query.q, 120).toLowerCase();
const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 200);
const params = [req.user.id];
const where = ["user_id = $1"];
if (ASSET_TYPES.has(type)) {
params.push(type);
where.push(`type = $${params.length}`);
}
if (ASSET_STATUSES.has(status)) {
params.push(status);
where.push(`status = $${params.length}`);
}
if (q) {
params.push(`%${q}%`);
where.push(`(LOWER(name) LIKE $${params.length} OR LOWER(COALESCE(description, '')) LIKE $${params.length} OR LOWER(tags_json) LIKE $${params.length})`);
}
params.push(limit);
const { rows } = await pool.query(
`
SELECT *
FROM web_assets
WHERE ${where.join(" AND ")}
ORDER BY updated_at DESC
LIMIT $${params.length}
`,
params,
);
res.json({ assets: rows.map(formatAsset) });
} catch (err) {
console.error("[assets] list failed:", err.message);
res.status(500).json({ error: "Failed to load assets" });
}
});
router.post("/assets", requireAuth, async (req, res) => {
const payload = normalizeAssetPayload(req.body || {});
if (payload.error) return res.status(400).json({ error: payload.error });
try {
const asset = payload.value;
const { rows } = await pool.query(
`
INSERT INTO web_assets (
user_id, type, name, description, url, oss_key, tags_json,
status, source_task_id, source_project_id, metadata_json
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`,
[
req.user.id,
asset.type,
asset.name,
asset.description,
asset.url,
asset.ossKey,
asset.tagsJson,
asset.status,
asset.sourceTaskId,
asset.sourceProjectId,
asset.metadataJson,
],
);
res.status(201).json({ asset: formatAsset(rows[0]) });
} catch (err) {
console.error("[assets] create failed:", err.message);
res.status(500).json({ error: "Failed to create asset" });
}
});
router.patch("/assets/:id", requireAuth, async (req, res) => {
const payload = normalizeAssetPayload(req.body || {}, true);
if (payload.error) return res.status(400).json({ error: payload.error });
const fieldMap = {
name: "name",
type: "type",
description: "description",
url: "url",
ossKey: "oss_key",
tagsJson: "tags_json",
status: "status",
sourceTaskId: "source_task_id",
sourceProjectId: "source_project_id",
metadataJson: "metadata_json",
};
const values = [];
const updates = [];
Object.entries(payload.value).forEach(([key, value]) => {
if (value === undefined) return;
values.push(value);
updates.push(`${fieldMap[key]} = $${values.length}`);
});
if (!updates.length) return res.status(400).json({ error: "No asset fields to update" });
values.push(req.params.id, req.user.id);
try {
const { rows } = await pool.query(
`
UPDATE web_assets
SET ${updates.join(", ")}, updated_at = NOW()
WHERE id = $${values.length - 1} AND user_id = $${values.length}
RETURNING *
`,
values,
);
if (!rows[0]) return res.status(404).json({ error: "Asset not found" });
res.json({ asset: formatAsset(rows[0]) });
} catch (err) {
console.error("[assets] update failed:", err.message);
res.status(500).json({ error: "Failed to update asset" });
}
});
router.delete("/assets/:id", requireAuth, async (req, res) => {
try {
const { rowCount } = await pool.query("DELETE FROM web_assets WHERE id = $1 AND user_id = $2", [
req.params.id,
req.user.id,
]);
if (!rowCount) return res.status(404).json({ error: "Asset not found" });
res.json({ success: true });
} catch (err) {
console.error("[assets] delete failed:", err.message);
res.status(500).json({ error: "Failed to delete asset" });
}
});
}
module.exports = { registerAssetRoutes };