231 lines
11 KiB
JavaScript
231 lines
11 KiB
JavaScript
import { dbAll } from "./db.mjs";
|
|
|
|
const SEARCH_LIMIT = 60;
|
|
|
|
function placeholders(values) {
|
|
return values.map(() => "?").join(",");
|
|
}
|
|
|
|
function likePattern(value) {
|
|
return `%${String(value || "").trim().replace(/[\\%_]/g, "\\$&").slice(0, 80)}%`;
|
|
}
|
|
|
|
function scopeProjects(context, scope) {
|
|
const accessible = Array.isArray(context?.projects) ? context.projects : [];
|
|
if (scope === "project" && context?.project?.id) return [context.project.id];
|
|
return accessible.map((project) => project.id).filter(Boolean);
|
|
}
|
|
|
|
function baseResult(type, typeLabel, row, targetTab, title, subtitle, status = "") {
|
|
return {
|
|
type,
|
|
typeLabel,
|
|
id: row.id,
|
|
title: String(title || row.name || row.title || row.id),
|
|
subtitle: String(subtitle || ""),
|
|
status: String(status || row.status || ""),
|
|
updatedAt: row.updated_at || row.updatedAt || row.created_at || "",
|
|
targetTab,
|
|
organizationId: row.organization_id || "",
|
|
workspaceId: row.workspace_id || "",
|
|
projectId: row.project_id || row.projectId || (type === "project" ? row.id : "")
|
|
};
|
|
}
|
|
|
|
function sortResults(results) {
|
|
return results.sort((left, right) => {
|
|
const leftTitle = left.title.toLocaleLowerCase();
|
|
const rightTitle = right.title.toLocaleLowerCase();
|
|
const leftStarts = leftTitle.startsWith(left.query) ? 1 : 0;
|
|
const rightStarts = rightTitle.startsWith(right.query) ? 1 : 0;
|
|
if (leftStarts !== rightStarts) return rightStarts - leftStarts;
|
|
return String(right.updatedAt).localeCompare(String(left.updatedAt));
|
|
});
|
|
}
|
|
|
|
export function searchPlatform(context, { query = "", scope = "workspace", limit = 40 } = {}) {
|
|
const normalizedQuery = String(query || "").trim().slice(0, 80);
|
|
if (!normalizedQuery) return { query: "", scope, total: 0, results: [] };
|
|
|
|
const projectIds = scopeProjects(context, scope === "project" ? "project" : "workspace");
|
|
if (!projectIds.length) return { query: normalizedQuery, scope, total: 0, results: [] };
|
|
|
|
const safeLimit = Math.min(SEARCH_LIMIT, Math.max(1, Number(limit || 40)));
|
|
const perType = Math.min(12, Math.max(4, Math.ceil(safeLimit / 8) + 2));
|
|
const ids = placeholders(projectIds);
|
|
const pattern = likePattern(normalizedQuery);
|
|
const results = [];
|
|
|
|
const projects = dbAll(
|
|
`SELECT p.*, w.name AS workspace_name, w.organization_id AS organization_id
|
|
FROM projects p
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (p.name LIKE ? ESCAPE '\\' OR p.type LIKE ? ESCAPE '\\')
|
|
ORDER BY p.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, perType]
|
|
);
|
|
for (const row of projects) {
|
|
results.push(baseResult("project", "项目", row, "factory", row.name, `${row.workspace_name} · ${row.type}`, row.status));
|
|
}
|
|
|
|
const episodes = dbAll(
|
|
`SELECT e.*, se.season_number, se.title AS season_title, se.series_id,
|
|
p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id, w.name AS workspace_name
|
|
FROM episodes e
|
|
JOIN seasons se ON se.id = e.season_id
|
|
JOIN series s ON s.id = se.series_id
|
|
JOIN projects p ON p.id = s.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (e.title LIKE ? ESCAPE '\\' OR e.hook LIKE ? ESCAPE '\\' OR e.cliffhanger LIKE ? ESCAPE '\\')
|
|
ORDER BY e.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, pattern, perType]
|
|
);
|
|
for (const row of episodes) {
|
|
results.push(baseResult("episode", "分集", row, "bible", row.title, `${row.project_name} · 第 ${row.episode_number} 集 · ${row.season_title}`, row.status));
|
|
}
|
|
|
|
const documents = dbAll(
|
|
`SELECT d.*, e.episode_number, e.title AS episode_title,
|
|
p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
|
FROM script_documents d
|
|
LEFT JOIN episodes e ON e.id = d.episode_id
|
|
JOIN projects p ON p.id = d.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (d.title LIKE ? ESCAPE '\\' OR d.content LIKE ? ESCAPE '\\')
|
|
ORDER BY d.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, perType]
|
|
);
|
|
for (const row of documents) {
|
|
results.push(baseResult("script", "剧本", row, "script", row.title, `${row.project_name}${row.episode_title ? ` · ${row.episode_title}` : ""} · v${row.version_number}`, row.status));
|
|
}
|
|
|
|
const knowledgeScopeClause = scope === "project" && context?.project?.id
|
|
? " AND (kd.project_id IS NULL OR kd.project_id = ?)"
|
|
: projectIds.length
|
|
? ` AND (kd.project_id IS NULL OR kd.project_id IN (${ids}))`
|
|
: " AND kd.project_id IS NULL";
|
|
const knowledgeParams = scope === "project" && context?.project?.id
|
|
? [context.organization.id, context.workspace.id, pattern, pattern, pattern, context.project.id, perType]
|
|
: projectIds.length
|
|
? [context.organization.id, context.workspace.id, pattern, pattern, pattern, ...projectIds, perType]
|
|
: [context.organization.id, context.workspace.id, pattern, pattern, pattern, perType];
|
|
const knowledgeDocuments = dbAll(
|
|
`SELECT kd.*
|
|
FROM knowledge_documents kd
|
|
WHERE kd.organization_id = ? AND kd.workspace_id = ?
|
|
AND (kd.title LIKE ? ESCAPE '\\' OR kd.content LIKE ? ESCAPE '\\' OR kd.summary LIKE ? ESCAPE '\\')
|
|
${knowledgeScopeClause}
|
|
ORDER BY kd.updated_at DESC LIMIT ?`,
|
|
knowledgeParams
|
|
);
|
|
for (const row of knowledgeDocuments) {
|
|
results.push(baseResult("knowledge_document", "知识素材", row, "knowledge", row.title, `${row.source_type} · ${row.scope_mode === "project" ? "项目级" : "工作区级"}`, row.status));
|
|
}
|
|
|
|
const knowledgeChunkParams = scope === "project" && context?.project?.id
|
|
? [context.organization.id, context.workspace.id, pattern, pattern, context.project.id, perType]
|
|
: projectIds.length
|
|
? [context.organization.id, context.workspace.id, pattern, pattern, ...projectIds, perType]
|
|
: [context.organization.id, context.workspace.id, pattern, pattern, perType];
|
|
const knowledgeChunks = dbAll(
|
|
`SELECT kc.*, kd.title AS document_title, kd.project_id, kd.scope_mode, kd.organization_id, kd.workspace_id
|
|
FROM knowledge_chunks kc
|
|
JOIN knowledge_documents kd ON kd.id = kc.document_id
|
|
WHERE kd.organization_id = ? AND kd.workspace_id = ?
|
|
AND (kc.heading LIKE ? ESCAPE '\\' OR kc.content LIKE ? ESCAPE '\\')
|
|
${knowledgeScopeClause}
|
|
ORDER BY kd.updated_at DESC, kc.chunk_index ASC LIMIT ?`,
|
|
knowledgeChunkParams
|
|
);
|
|
for (const row of knowledgeChunks) {
|
|
results.push(baseResult("knowledge_chunk", "知识片段", row, "knowledge", row.heading || row.document_title, `${row.document_title} · chunk ${row.chunk_index}`, row.chunk_type));
|
|
}
|
|
|
|
const shots = dbAll(
|
|
`SELECT sh.*, e.episode_number, e.title AS episode_title,
|
|
p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
|
FROM shots sh
|
|
JOIN episodes e ON e.id = sh.episode_id
|
|
JOIN seasons se ON se.id = e.season_id
|
|
JOIN series s ON s.id = se.series_id
|
|
JOIN projects p ON p.id = s.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (sh.title LIKE ? ESCAPE '\\' OR sh.id LIKE ? ESCAPE '\\' OR sh.continuity_json LIKE ? ESCAPE '\\' OR p.name LIKE ? ESCAPE '\\' OR s.title LIKE ? ESCAPE '\\' OR e.title LIKE ? ESCAPE '\\')
|
|
ORDER BY sh.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, pattern, pattern, pattern, pattern, perType]
|
|
);
|
|
for (const row of shots) {
|
|
results.push(baseResult("shot", "镜头", row, "director", row.title, `${row.project_name} · E${String(row.episode_number).padStart(2, "0")} · S${String(row.shot_number).padStart(2, "0")}`, row.status));
|
|
}
|
|
|
|
const assets = dbAll(
|
|
`SELECT a.*, p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
|
FROM assets a
|
|
JOIN projects p ON p.id = a.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (a.name LIKE ? ESCAPE '\\' OR a.kind LIKE ? ESCAPE '\\')
|
|
ORDER BY a.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, perType]
|
|
);
|
|
for (const row of assets) {
|
|
results.push(baseResult("asset", "资产", row, "casting", row.name, `${row.project_name} · ${row.kind}`, row.lock_status));
|
|
}
|
|
|
|
const tasks = dbAll(
|
|
`SELECT t.*, p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id,
|
|
u.display_name AS assignee_name
|
|
FROM project_tasks t
|
|
JOIN projects p ON p.id = t.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
LEFT JOIN users u ON u.id = t.assignee_user_id
|
|
WHERE p.id IN (${ids})
|
|
AND (t.title LIKE ? ESCAPE '\\' OR t.description LIKE ? ESCAPE '\\')
|
|
ORDER BY t.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, perType]
|
|
);
|
|
for (const row of tasks) {
|
|
results.push(baseResult("task", "任务", row, "tasks", row.title, `${row.project_name} · ${row.assignee_name || "未分派"}`, row.status));
|
|
}
|
|
|
|
const jobs = dbAll(
|
|
`SELECT j.*, p.name AS project_name, w.organization_id AS organization_id
|
|
FROM generation_jobs j
|
|
JOIN projects p ON p.id = j.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (j.kind LIKE ? ESCAPE '\\' OR j.status LIKE ? ESCAPE '\\' OR j.id LIKE ? ESCAPE '\\' OR j.error_message LIKE ? ESCAPE '\\')
|
|
ORDER BY j.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, pattern, pattern, perType]
|
|
);
|
|
for (const row of jobs) {
|
|
results.push(baseResult("job", "生成任务", row, "jobs", row.kind || row.id, `${row.project_name} · ${row.id}`, row.status));
|
|
}
|
|
|
|
const deliveries = dbAll(
|
|
`SELECT d.*, p.name AS project_name, w.organization_id AS organization_id
|
|
FROM deliveries d
|
|
JOIN projects p ON p.id = d.project_id
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.id IN (${ids})
|
|
AND (d.version LIKE ? ESCAPE '\\' OR d.channel LIKE ? ESCAPE '\\' OR d.status LIKE ? ESCAPE '\\' OR d.id LIKE ? ESCAPE '\\')
|
|
ORDER BY d.updated_at DESC LIMIT ?`,
|
|
[...projectIds, pattern, pattern, pattern, pattern, perType]
|
|
);
|
|
for (const row of deliveries) {
|
|
results.push(baseResult("delivery", "交付", row, "export", `${row.project_name} · ${row.version}`, `${row.channel} · ${row.id}`, row.status));
|
|
}
|
|
|
|
const ranked = sortResults(results.map((result) => ({ ...result, query: normalizedQuery.toLocaleLowerCase() })));
|
|
return {
|
|
query: normalizedQuery,
|
|
scope: scope === "project" && context?.project?.id ? "project" : "workspace",
|
|
total: ranked.length,
|
|
results: ranked.slice(0, safeLimit).map(({ query: _query, ...result }) => result)
|
|
};
|
|
}
|