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 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 '\\') ORDER BY sh.updated_at DESC LIMIT ?`, [...projectIds, 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) }; }