feat: expand commercial ai drama platform

This commit is contained in:
xz
2026-08-31 21:09:48 +08:00
parent f06e583c9b
commit 8caf947b80
35 changed files with 10181 additions and 188 deletions
+454 -1
View File
@@ -35,6 +35,7 @@ ensureColumn("generation_jobs", "request_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("generation_jobs", "result_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("generation_jobs", "error_message", "TEXT NOT NULL DEFAULT ''");
ensureColumn("generation_jobs", "max_attempts", "INTEGER NOT NULL DEFAULT 3");
ensureColumn("generation_jobs", "model_route_approval_id", "TEXT");
ensureColumn("generation_jobs", "next_run_at", "TEXT");
ensureColumn("generation_jobs", "leased_by", "TEXT");
ensureColumn("generation_jobs", "leased_at", "TEXT");
@@ -83,11 +84,17 @@ ensureColumn("asset_versions", "file_name", "TEXT NOT NULL DEFAULT ''");
ensureColumn("asset_versions", "mime_type", "TEXT NOT NULL DEFAULT 'application/octet-stream'");
ensureColumn("asset_versions", "file_size", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("asset_versions", "content_sha256", "TEXT NOT NULL DEFAULT ''");
ensureColumn("asset_versions", "provenance_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("asset_versions", "risk_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("asset_versions", "tags_json", "TEXT NOT NULL DEFAULT '[]'");
ensureColumn("asset_versions", "license_scope", "TEXT NOT NULL DEFAULT ''");
ensureColumn("asset_versions", "expires_at", "TEXT");
ensureColumn("shots", "current_version_id", "TEXT");
ensureColumn("deliveries", "active_batch_id", "TEXT");
ensureColumn("delivery_releases", "idempotency_key", "TEXT NOT NULL DEFAULT ''");
ensureColumn("delivery_releases", "preflight_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("delivery_releases", "result_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("script_documents", "metadata_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("projects", "archived_at", "TEXT");
ensureColumn("projects", "archived_by", "TEXT");
ensureColumn("projects", "archived_from_status", "TEXT");
@@ -96,6 +103,11 @@ ensureColumn("workspace_members", "access_mode", "TEXT NOT NULL DEFAULT 'all'");
ensureColumn("auth_sessions", "device_id", "TEXT");
ensureColumn("auth_sessions", "risk_level", "TEXT NOT NULL DEFAULT 'medium'");
ensureColumn("auth_sessions", "risk_score", "INTEGER NOT NULL DEFAULT 50");
ensureColumn("knowledge_documents", "rights_status", "TEXT NOT NULL DEFAULT 'needs-evidence'");
ensureColumn("knowledge_documents", "provenance_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("knowledge_documents", "tags_json", "TEXT NOT NULL DEFAULT '[]'");
ensureColumn("knowledge_documents", "risk_json", "TEXT NOT NULL DEFAULT '{}'");
ensureColumn("knowledge_documents", "current_version_number", "INTEGER NOT NULL DEFAULT 1");
db.exec("CREATE INDEX IF NOT EXISTS idx_projects_lifecycle ON projects(workspace_id, status, updated_at)");
db.exec("CREATE INDEX IF NOT EXISTS idx_invites_token ON invitations(token_hash, status)");
db.exec("CREATE INDEX IF NOT EXISTS idx_identity_providers_org ON identity_providers(organization_id, enabled, status)");
@@ -113,12 +125,24 @@ db.exec("CREATE INDEX IF NOT EXISTS idx_auth_sessions_device ON auth_sessions(de
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_channels_scope ON delivery_channels(organization_id, workspace_id, project_id, enabled, created_at)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_releases_scope ON delivery_releases(organization_id, workspace_id, project_id, delivery_id, status, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_releases_idempotency ON delivery_releases(organization_id, project_id, idempotency_key)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_clearance_reports_scope ON delivery_clearance_reports(organization_id, workspace_id, project_id, delivery_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_clearance_reports_release ON delivery_clearance_reports(release_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_links_token ON delivery_access_links(token_hash, status, expires_at)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_links_scope ON delivery_access_links(organization_id, workspace_id, project_id, release_id, status, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_events_link ON delivery_access_events(link_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_events_release ON delivery_access_events(release_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_feedback_link ON delivery_access_feedback(link_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_delivery_access_feedback_release ON delivery_access_feedback(release_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_model_route_approvals_scope ON model_route_approval_requests(organization_id, workspace_id, status, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_model_route_approvals_requester ON model_route_approval_requests(requester_user_id, status, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_model_route_approvals_job ON model_route_approval_requests(job_id, status)");
db.exec("CREATE INDEX IF NOT EXISTS idx_generation_jobs_model_approval ON generation_jobs(model_route_approval_id)");
db.exec("CREATE INDEX IF NOT EXISTS idx_commercial_approvals_org_status ON commercial_approval_requests(organization_id, status, updated_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_commercial_approvals_requester ON commercial_approval_requests(requester_user_id, status, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_commercial_approvals_target ON commercial_approval_requests(organization_id, request_type, target_key, status)");
db.exec("CREATE INDEX IF NOT EXISTS idx_asset_versions_governance ON asset_versions(asset_id, rights_status, expires_at)");
db.exec("CREATE INDEX IF NOT EXISTS idx_asset_governance_reviews_asset ON asset_governance_reviews(asset_id, created_at DESC)");
db.exec("CREATE INDEX IF NOT EXISTS idx_asset_governance_reviews_scope ON asset_governance_reviews(organization_id, workspace_id, project_id, risk_status, created_at DESC)");
function migrateApiClientSecrets() {
const rows = dbAll("SELECT id, client_key, client_key_hash, client_key_prefix, key_version FROM api_clients");
@@ -232,6 +256,7 @@ function seedRoles() {
["job:create", "创建生成任务"],
["job:prioritize", "调整任务优先级"],
["model:manage", "注册和管理模型连接器"],
["model:approve", "审批模型路由、外部连接器和一次性生成调用"],
["usage:view", "查看用量和成本"],
["billing:manage", "管理套餐和账单"],
["quota:manage", "管理组织席位与工作区配额"],
@@ -258,7 +283,7 @@ function seedRoles() {
org_admin: [
"organization:manage", "organization:members:invite", "workspace:create", "workspace:manage",
"workspace:members:manage", "project:create", "project:manage", "project:members:manage",
"workflow:manage", "task:view", "task:manage", "task:complete", "model:manage", "usage:view", "billing:manage", "quota:manage", "qa:review", "delivery:approve", "delivery:view", "compliance:manage", "audit:view", "queue:manage", "voice:approve",
"workflow:manage", "task:view", "task:manage", "task:complete", "model:manage", "model:approve", "usage:view", "billing:manage", "quota:manage", "qa:review", "delivery:approve", "delivery:view", "compliance:manage", "audit:view", "queue:manage", "voice:approve",
"system:settings:view", "service:health:view", "organization:roles:manage"
],
producer: ["project:create", "project:manage", "project:members:manage", "workflow:manage", "task:view", "task:manage", "task:complete", "script:read", "job:create", "job:prioritize", "usage:view", "delivery:approve", "delivery:view", "voice:approve"],
@@ -328,6 +353,171 @@ function seedOrganization({ id, name, slug, ownerUserId, description, workspaceI
insertIgnore("INSERT OR IGNORE INTO quota_allocations(id, organization_id, workspace_id, metric, limit_value, used_value, unit, period_start, period_end, created_at, updated_at) VALUES (?, ?, ?, 'storage', 1024, 128, 'GB', ?, ?, ?, ?)", [`quota-${workspaceId}-storage`, id, workspaceId, monthStart(), monthEnd(), timestamp, timestamp]);
}
function commercialEntitlementDefaults(billing = {}) {
const clipLimit = Number(billing.monthly_clip_quota || 2400);
const storageGb = Number(billing.storage_gb || 1024);
const seatLimit = Number(billing.seat_limit || 12);
return [
["limit.seats", "组织席位", "tenant", seatLimit, "人", 1, "block", { commercialGate: "member-invite-and-sso" }],
["limit.workspaces", "工作区数量", "tenant", 8, "个", 1, "block", { commercialGate: "workspace:create" }],
["limit.projects", "项目数量", "production", 36, "个", 1, "block", { commercialGate: "project:create" }],
["limit.generation_jobs_monthly", "月度生成任务", "production", clipLimit, "job", 1, "block", { commercialGate: "generation_job:create" }],
["limit.storage_gb", "存储容量", "storage", storageGb, "GB", 1, "block", { commercialGate: "storage:write" }],
["limit.model_connectors", "模型连接器", "modelops", 16, "个", 1, "block", { commercialGate: "model_connector:create" }],
["limit.api_clients", "API 客户端", "system", 8, "个", 1, "block", { commercialGate: "api_client:create" }],
["limit.knowledge_documents", "知识库素材", "knowledge", 300, "篇", 1, "block", { commercialGate: "knowledge_document:ingest" }],
["limit.knowledge_context_packs", "知识上下文包", "knowledge", 180, "包", 1, "block", { commercialGate: "knowledge_context_pack:create" }],
["limit.delivery_channels", "交付渠道", "delivery", 12, "个", 1, "block", { commercialGate: "delivery_channel:create" }],
["feature.batch_generation", "批量生产", "feature", 1, "开关", 1, "block", { description: "允许创建批量生成与流水线任务" }],
["feature.private_delivery_portal", "客户交付门户", "feature", 1, "开关", 1, "block", { description: "允许创建带令牌的客户预览/下载门户" }],
["feature.comfyui_adapter", "ComfyUI 可选桥接", "feature", 1, "开关", 0, "block", { description: "默认关闭,明确启用后才可作为适配器" }],
["feature.external_cloud_connectors", "外部云连接器", "feature", 1, "开关", 0, "block", { description: "默认关闭,付费/公网模型必须显式审批" }]
];
}
function seedCommercialPlans() {
const timestamp = now();
const plans = [
["plan-starter-local", "starter-local", "Starter Local", "单工作室本地试制版,适合一条短剧流水线验证。", "monthly", "CNY", 0, 5, 256, 500, { workspaces: 2, projects: 8, modelConnectors: 6, apiClients: 3, knowledgeDocuments: 80, knowledgeContextPacks: 40, deliveryChannels: 3 }, { batchGeneration: false, privateDeliveryPortal: true, comfyuiAdapter: false, externalCloudConnectors: false }, { allowedCostModes: ["local"], externalApprovalRequired: true }, "社区支持"],
["plan-studio-local", "studio-local", "Studio Local", "商业工作室私有生产版,覆盖剧本、资产、生成、审片、交付和账单。", "monthly", "CNY", 0, 12, 1024, 2400, { workspaces: 8, projects: 36, modelConnectors: 16, apiClients: 8, knowledgeDocuments: 300, knowledgeContextPacks: 180, deliveryChannels: 12 }, { batchGeneration: true, privateDeliveryPortal: true, comfyuiAdapter: false, externalCloudConnectors: false }, { allowedCostModes: ["local", "mixed-with-approval"], externalApprovalRequired: true }, "工作日响应"],
["plan-enterprise-private", "enterprise-private", "Enterprise Private", "多组织私有化商业版,适合平台代理、内容厂牌和多项目制片团队。", "annual", "CNY", 0, 50, 8192, 20000, { workspaces: 50, projects: 300, modelConnectors: 80, apiClients: 50, knowledgeDocuments: 5000, knowledgeContextPacks: 2000, deliveryChannels: 60 }, { batchGeneration: true, privateDeliveryPortal: true, comfyuiAdapter: true, externalCloudConnectors: false }, { allowedCostModes: ["local", "mixed-with-approval"], externalApprovalRequired: true }, "专属运维窗口"]
];
for (const [id, tierKey, name, description, billingCycle, currency, baseFee, seatLimit, storageGb, monthlyClipQuota, limits, features, connectorPolicy, supportSla] of plans) {
insertIgnore(
"INSERT OR IGNORE INTO subscription_plan_templates(id, tier_key, name, description, billing_cycle, currency, base_fee, seat_limit, storage_gb, monthly_clip_quota, limits_json, features_json, connector_policy_json, support_sla, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
[id, tierKey, name, description, billingCycle, currency, baseFee, seatLimit, storageGb, monthlyClipQuota, JSON.stringify(limits), JSON.stringify(features), JSON.stringify(connectorPolicy), supportSla, timestamp, timestamp]
);
}
}
function seedOrganizationEntitlements() {
const timestamp = now();
const organizations = dbAll(
`SELECT o.id AS organization_id,
b.seat_limit, b.storage_gb, b.monthly_clip_quota
FROM organizations o
LEFT JOIN billing_accounts b ON b.organization_id = o.id
WHERE o.status = 'active'`
);
for (const organization of organizations) {
for (const [key, label, category, limitValue, unit, enabled, enforcement, metadata] of commercialEntitlementDefaults(organization)) {
insertIgnore(
"INSERT OR IGNORE INTO organization_entitlements(id, organization_id, entitlement_key, label, category, limit_value, unit, enabled, enforcement, source, metadata_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'plan', ?, ?, ?)",
[`ent-${organization.organization_id}-${key.replace(/[^a-z0-9]+/gi, "-")}`, organization.organization_id, key, label, category, limitValue, unit, enabled, enforcement, JSON.stringify(metadata), timestamp, timestamp]
);
}
}
}
function seedCommercialApprovals() {
const timestamp = now();
const requests = [
{
id: "commercial-approval-seed-entitlement",
organizationId: "org-studio-lab",
workspaceId: "ws-local-aidrama",
projectId: "thunder-mouth",
requestType: "entitlement_overage",
title: "《雨夜来电》本月追加 500 个生成任务",
status: "submitted",
priority: "high",
targetKey: "limit.generation_jobs_monthly",
currentValue: 2400,
requestedValue: 2900,
unit: "job",
businessReason: "连续三集试制需要补拍转场镜头和反应镜头,仍限定为本地 Runner 执行。",
risk: { localOnly: true, paidCloud: false, singleFrameGate: true, continuityLedger: true },
evidence: { project: "thunder-mouth", source: "seed://commercial/entitlement-overage" },
requester: "u-writer",
reviewer: null,
reviewedAt: null,
decisionNote: "",
effect: {}
},
{
id: "commercial-approval-seed-connector",
organizationId: "org-studio-lab",
workspaceId: "ws-local-aidrama",
projectId: "thunder-mouth",
requestType: "external_connector",
title: "评估 NewAPI 中转的外部官方模型通道",
status: "submitted",
priority: "urgent",
targetKey: "feature.external_cloud_connectors",
currentValue: 0,
requestedValue: 1,
unit: "开关",
businessReason: "仅申请连接能力评估,不自动调用付费云端节点;需要管理员确认成本和合规边界。",
risk: { localOnly: false, paidCloud: true, requiresExplicitApproval: true, secretStored: false },
evidence: { connectorPolicy: "env-secret-only", source: "seed://commercial/external-connector" },
requester: "u-owner",
reviewer: null,
reviewedAt: null,
decisionNote: "",
effect: {}
},
{
id: "commercial-approval-seed-compliance",
organizationId: "org-studio-lab",
workspaceId: "ws-local-aidrama",
projectId: "thunder-mouth",
requestType: "compliance_review",
title: "原创小说素材版权与分块入库复核",
status: "approved",
priority: "medium",
targetKey: "commercial-rights-review",
currentValue: 0,
requestedValue: 1,
unit: "次",
businessReason: "确认《雨夜来电》素材按原创来源入库,允许用于剧本拆解和知识上下文包。",
risk: { sourceType: "original", knowledgeChunking: true, derivativeUse: "internal-production" },
evidence: { knowledgeDocumentId: "knowledge-rain-night", source: "seed://commercial/compliance-review" },
requester: "u-owner",
reviewer: "u-owner",
reviewedAt: timestamp,
decisionNote: "示例放行:原创素材,后续交付仍需逐素材证据。",
effect: { complianceRecordId: "commercial-compliance-seed-rain-night" }
}
];
for (const request of requests) {
insertIgnore(
`INSERT OR IGNORE INTO commercial_approval_requests(
id, organization_id, workspace_id, project_id, request_type, title, status, priority, target_key,
current_value, requested_value, unit, business_reason, risk_assessment_json, evidence_json,
decision_note, effect_json, requester_user_id, reviewer_user_id, reviewed_at, expires_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`,
[
request.id,
request.organizationId,
request.workspaceId,
request.projectId,
request.requestType,
request.title,
request.status,
request.priority,
request.targetKey,
request.currentValue,
request.requestedValue,
request.unit,
request.businessReason,
JSON.stringify(request.risk),
JSON.stringify(request.evidence),
request.decisionNote,
JSON.stringify(request.effect),
request.requester,
request.reviewer,
request.reviewedAt,
timestamp,
timestamp
]
);
}
insertIgnore(
"INSERT OR IGNORE INTO compliance_records(id, organization_id, workspace_id, project_id, subject_type, subject_id, policy_key, status, evidence_json, reviewed_by, reviewed_at, created_at, updated_at) VALUES ('commercial-compliance-seed-rain-night', 'org-studio-lab', 'ws-local-aidrama', 'thunder-mouth', 'knowledge_document', 'knowledge-rain-night', 'commercial-rights-review', 'approved', ?, 'u-owner', ?, ?, ?)",
[JSON.stringify({ source: "seed://commercial/compliance-review", knowledgeDocumentId: "knowledge-rain-night" }), timestamp, timestamp, timestamp]
);
}
function seedMembersAndProjects() {
const timestamp = now();
const membershipRows = [
@@ -365,6 +555,8 @@ function seedModels() {
const models = [
["owned-i2v", "org-studio-lab", "ws-local-aidrama", "自有图生视频平台", "http-json", ["image-to-video", "first-last-frame", "vertical-video"], "http://127.0.0.1:7860/api/generate/i2v", "not-connected", "local", 0, "u-owner"],
["owned-image", "org-studio-lab", "ws-local-aidrama", "自有图片/改图平台", "http-json", ["text-to-image", "image-edit", "single-frame"], "http://127.0.0.1:7860/api/generate/image", "not-connected", "local", 0, "u-owner"],
["owned-story-parser", "org-studio-lab", "ws-local-aidrama", "自有文本解析网关", "openai-compatible", ["chat", "script-split", "knowledge-extract"], "http://127.0.0.1:7862/v1", "ready", "local", 0, "u-owner"],
["owned-embedding", "org-studio-lab", "ws-local-aidrama", "自有向量检索网关", "openai-compatible", ["embedding", "rerank", "knowledge-retrieval"], "http://127.0.0.1:7863/v1", "planned", "local", 0, "u-owner"],
["local-tts", "org-studio-lab", "ws-local-aidrama", "本地固定声线 TTS", "http-json", ["tts", "voice-lock", "subtitle-timing"], "http://127.0.0.1:7861/api/tts", "planned", "local", 0, "u-owner"],
["newapi-audio-production", "org-studio-lab", "ws-local-aidrama", "NewAPI 音频中转", "openai-compatible-audio", ["tts", "voice-lock", "emotion-control", "asr", "subtitle-timing"], "https://newapi.ysblack.com/v1", "ready", "mixed", 1, "u-owner"],
["comfyui-optional", "org-studio-lab", "ws-local-aidrama", "ComfyUI 工作流桥接", "comfyui", ["workflow", "qwen-image", "qwen-edit"], "http://127.0.0.1:8188", "optional", "mixed", 1, "u-owner"],
@@ -375,6 +567,200 @@ function seedModels() {
}
}
function seedModelCatalog() {
const timestamp = now();
const entries = [
["catalog-qwen-image-2d", "org-studio-lab", "ws-local-aidrama", "owned-image", "qwen-image-2d-lock", "Qwen Image 国漫单画面", "qwen-image", ["text-to-image", "single-frame", "continuity-lock"], 32768, 4096, { mode: "per-image", estimatedCny: 0.18, locality: "local" }, "active", "approved", { aspect: "9:16", guardrails: ["single-frame-only", "no-collage"] }],
["catalog-wan-i2v", "org-studio-lab", "ws-local-aidrama", "owned-i2v", "wan-i2v-lastframe", "Wan 图生视频连续版", "wan-video", ["image-to-video", "first-last-frame", "clip-bridge"], 65536, 4096, { mode: "per-clip", estimatedCny: 0.42, locality: "local" }, "active", "approved", { durationSec: [5, 10], notes: "优先使用上一段实际末帧" }],
["catalog-qwen-parser", "org-studio-lab", "ws-local-aidrama", "owned-story-parser", "qwen-script-parser", "Qwen 剧本/小说解析", "qwen-text", ["chat", "script-split", "knowledge-extract", "scene-parse"], 131072, 8192, { mode: "per-1k-tokens", estimatedCny: 0.03, locality: "local" }, "active", "approved", { languages: ["zh-CN"], preferredFor: ["script-import", "knowledge-ingest"] }],
["catalog-bge-m3", "org-studio-lab", "ws-local-aidrama", "owned-embedding", "bge-m3-local", "BGE-M3 向量检索", "embedding", ["embedding", "retrieval", "knowledge-search"], 8192, 0, { mode: "per-1k-tokens", estimatedCny: 0.01, locality: "local" }, "planned", "approved", { dimensions: 1024, preferredFor: ["knowledge-search"] }],
["catalog-indextts", "org-studio-lab", "ws-local-aidrama", "newapi-audio-production", "IndexTTS-2.5", "IndexTTS-2.5 固定声线", "tts", ["tts", "voice-lock", "emotion-control"], 16384, 2048, { mode: "per-10s-audio", estimatedCny: 0.12, locality: "mixed" }, "active", "review", { referenceRequired: true, approvalGate: "voice-rights" }],
["catalog-paraformer", "org-studio-lab", "ws-local-aidrama", "newapi-audio-production", "paraformer-zh-long", "Paraformer 长音频 ASR", "asr", ["asr", "subtitle-timing", "alignment"], 16384, 2048, { mode: "per-minute-audio", estimatedCny: 0.05, locality: "mixed" }, "active", "approved", { output: "verbose_json" }],
["catalog-northstar-image", "org-northstar", "ws-northstar-main", "northstar-image", "northstar-sd-xl", "北辰 SDXL 单画面", "sdxl", ["text-to-image", "single-frame"], 32768, 4096, { mode: "per-image", estimatedCny: 0.16, locality: "local" }, "active", "approved", { aspect: "9:16" }]
];
for (const [id, organizationId, workspaceId, connectorId, modelKey, displayName, family, capabilities, contextWindow, maxOutputTokens, cost, status, approvalStatus, metadata] of entries) {
insertIgnore(
"INSERT OR IGNORE INTO model_catalog_entries(id, organization_id, workspace_id, connector_id, model_key, display_name, family, capabilities_json, context_window, max_output_tokens, cost_json, status, approval_status, metadata_json, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'u-owner', ?, ?)",
[id, organizationId, workspaceId, connectorId, modelKey, displayName, family, JSON.stringify(capabilities), contextWindow, maxOutputTokens, JSON.stringify(cost), status, approvalStatus, JSON.stringify(metadata), timestamp, timestamp]
);
}
}
function seedModelRoutes() {
const timestamp = now();
const routes = [
["route-script-ingest", "org-studio-lab", "ws-local-aidrama", "小说/剧本导入解析", "knowledge-ingest", "chunk-and-extract", "catalog-qwen-parser", "catalog-bge-m3", "prefer-local", "follow-model", 15, "active", { chunkStrategy: "chapter-scene-dialogue", maxChunkChars: 520 }],
["route-script-materialize", "org-studio-lab", "ws-local-aidrama", "剧本拆解与场景草稿", "script-pipeline", "scene-draft", "catalog-qwen-parser", null, "prefer-local", "follow-model", 10, "active", { target: "script_documents" }],
["route-image-keyframe", "org-studio-lab", "ws-local-aidrama", "单画面关键帧", "ai-manhua-drama", "image-keyframe", "catalog-qwen-image-2d", null, "local-only", "follow-model", 60, "active", { qaGate: "single-frame" }],
["route-video-clip", "org-studio-lab", "ws-local-aidrama", "图生视频片段", "ai-manhua-drama", "video-clip", "catalog-wan-i2v", null, "local-only", "follow-model", 120, "active", { requireActualLastFrame: true }],
["route-voice-tts", "org-studio-lab", "ws-local-aidrama", "角色固定配音", "voice-pipeline", "tts", "catalog-indextts", null, "prefer-approved", "explicit-review", 40, "active", { referenceAssetKind: "voice", forbidRandomNativeVoice: true }],
["route-voice-asr", "org-studio-lab", "ws-local-aidrama", "ASR 对齐校验", "voice-pipeline", "asr", "catalog-paraformer", null, "prefer-approved", "follow-model", 20, "active", { output: "verbose_json" }]
];
for (const [id, organizationId, workspaceId, name, workflowKey, operationKey, primaryModelId, fallbackModelId, policyMode, approvalMode, budgetLimitCny, status, policy] of routes) {
insertIgnore(
"INSERT OR IGNORE INTO model_routing_policies(id, organization_id, workspace_id, name, workflow_key, operation_key, primary_model_id, fallback_model_id, policy_mode, approval_mode, budget_limit_cny, status, policy_json, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'u-owner', ?, ?)",
[id, organizationId, workspaceId, name, workflowKey, operationKey, primaryModelId, fallbackModelId, policyMode, approvalMode, budgetLimitCny, status, JSON.stringify(policy), timestamp, timestamp]
);
}
}
function seedKnowledgeLibrary() {
const timestamp = now();
const content = [
"第一章 雨夜来电",
"暴雨压在旧城区的玻璃连廊上,陈宇刚结束夜班巡检,就接到唐夏发来的定位。她只说了一句:不要让任何人碰那把蓝伞。",
"",
"唐夏:我到地铁口了,可这里的警戒线被人挪开了。",
"陈宇:别过去,积水边可能有落地电线。你先站在连廊灯下,右手不要离开蓝伞。",
"",
"第二章 失踪的雨披",
"两人会合后发现黄色雨披不见了,警戒锥却比下午多了一组。唐夏开始怀疑,有人提前布置过现场,像是在等他们出现。"
].join("\n");
const analysis = {
parser: "local-rule-v2",
chapterCount: 2,
chunkCount: 4,
summary: "暴雨夜的地铁口事故线索,包含角色对话、场景限制、关键道具和连续性信息。",
entities: {
characters: ["陈宇", "唐夏"],
locations: ["旧城区玻璃连廊", "地铁口"],
props: ["蓝伞", "黄色雨披", "警戒线", "警戒锥"]
}
};
const provenance = {
sourceLabel: "平台原创示例小说",
author: "AI Drama Platform Demo",
rightsOwner: "本地示例组织",
evidenceRef: "seed://original/rain-night",
licenseNote: "原创演示素材,仅用于本地样例和 smoke 测试",
sourceUrl: "",
importedFrom: "seed"
};
const tags = ["原创", "雨夜", "连续性样例"];
const risk = {
scanner: "local-governance-v1",
status: "pass",
score: 0,
rightsStatus: "approved",
sourceType: "novel",
tags,
issues: [],
checks: {
provenanceEvidence: true,
commercialRightsApproved: true,
knownIpReferences: 0,
personaLikenessRisks: 0,
singleFramePolicyRisks: 0,
safetySensitiveMatches: 0
},
scannedAt: timestamp
};
insertIgnore(
"INSERT OR IGNORE INTO knowledge_documents(id, organization_id, workspace_id, project_id, scope_mode, title, source_type, language, content, status, summary, analysis_json, metadata_json, created_by, created_at, updated_at) VALUES ('knowledge-rain-night', 'org-studio-lab', 'ws-local-aidrama', NULL, 'workspace', '《雨夜来电》原始小说素材', 'novel', 'zh-CN', ?, 'indexed', ?, ?, ?, 'u-owner', ?, ?)",
[content, analysis.summary, JSON.stringify(analysis), JSON.stringify({ sourceLabel: "原创小说", rights: "original", recommendedWorkflow: "ai-manhua-drama" }), timestamp, timestamp]
);
dbRun(
`UPDATE knowledge_documents
SET rights_status = 'approved',
provenance_json = ?,
tags_json = ?,
risk_json = ?,
current_version_number = CASE WHEN current_version_number < 1 THEN 1 ELSE current_version_number END
WHERE id = 'knowledge-rain-night'
AND (rights_status = 'needs-evidence' OR provenance_json = '{}' OR risk_json = '{}')`,
[JSON.stringify(provenance), JSON.stringify(tags), JSON.stringify(risk)]
);
insertIgnore(
`INSERT OR IGNORE INTO knowledge_document_versions(
id, document_id, version_number, title, source_type, language, content, summary,
analysis_json, provenance_json, tags_json, risk_json, metadata_json, created_by, created_at
) VALUES ('knowledge-rain-night-v1', 'knowledge-rain-night', 1, '《雨夜来电》原始小说素材', 'novel', 'zh-CN', ?, ?, ?, ?, ?, ?, ?, 'u-owner', ?)`,
[content, analysis.summary, JSON.stringify(analysis), JSON.stringify(provenance), JSON.stringify(tags), JSON.stringify(risk), JSON.stringify({ sourceLabel: "原创小说", rights: "original", recommendedWorkflow: "ai-manhua-drama", seed: true }), timestamp]
);
const chunks = [
["knowledge-rain-night-chunk-1", 1, "chapter", "第一章 雨夜来电", "暴雨压在旧城区的玻璃连廊上,陈宇刚结束夜班巡检,就接到唐夏发来的定位。她只说了一句:不要让任何人碰那把蓝伞。", 116, ["暴雨", "玻璃连廊", "蓝伞"], { characters: ["陈宇", "唐夏"], locations: ["旧城区玻璃连廊"], props: ["蓝伞"] }, { chapter: 1, sceneHint: "开场建立场景与冲突" }],
["knowledge-rain-night-chunk-2", 2, "dialogue", "地铁口对话", "唐夏:我到地铁口了,可这里的警戒线被人挪开了。\n陈宇:别过去,积水边可能有落地电线。你先站在连廊灯下,右手不要离开蓝伞。", 124, ["地铁口", "警戒线", "落地电线"], { characters: ["陈宇", "唐夏"], locations: ["地铁口"], props: ["蓝伞", "警戒线"] }, { chapter: 1, mouthAvoidance: true, sceneHint: "适合侧脸/反应镜头对白" }],
["knowledge-rain-night-chunk-3", 3, "chapter", "第二章 失踪的雨披", "两人会合后发现黄色雨披不见了,警戒锥却比下午多了一组。", 58, ["黄色雨披", "警戒锥"], { characters: ["陈宇", "唐夏"], props: ["黄色雨披", "警戒锥"] }, { chapter: 2, sceneHint: "道具连续性检查点" }],
["knowledge-rain-night-chunk-4", 4, "lore", "现场异常", "唐夏开始怀疑,有人提前布置过现场,像是在等他们出现。", 42, ["异常布置", "悬念"], { characters: ["唐夏"] }, { chapter: 2, sceneHint: "结尾悬念与下一集钩子" }]
];
for (const [id, chunkIndex, chunkType, heading, body, tokenEstimate, keywords, entities, metadata] of chunks) {
insertIgnore(
"INSERT OR IGNORE INTO knowledge_chunks(id, document_id, chunk_index, chunk_type, heading, content, token_estimate, keywords_json, entities_json, metadata_json, created_at) VALUES (?, 'knowledge-rain-night', ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[id, chunkIndex, chunkType, heading, body, tokenEstimate, JSON.stringify(keywords), JSON.stringify(entities), JSON.stringify(metadata), timestamp]
);
}
const openingChunks = chunks.slice(0, 2);
const citations = openingChunks.map(([id, chunkIndex, chunkType, heading], index) => ({
key: `K${index + 1}`,
documentId: "knowledge-rain-night",
documentTitle: "《雨夜来电》原始小说素材",
chunkId: id,
chunkIndex,
heading,
sourceType: "novel",
scopeMode: "workspace",
rightsStatus: "approved",
riskStatus: "pass",
projectId: null,
chunkType
}));
const packChunks = openingChunks.map(([id, chunkIndex, chunkType, heading, body, tokenEstimate, keywords, entities], index) => ({
citationKey: citations[index].key,
id,
documentId: "knowledge-rain-night",
documentTitle: "《雨夜来电》原始小说素材",
chunkIndex,
chunkType,
heading,
content: body,
tokenEstimate,
keywords,
entities,
rightsStatus: "approved",
riskStatus: "pass"
}));
const packGovernance = {
status: "pass",
rights: { approved: packChunks.length },
risk: { pass: packChunks.length },
blockingCount: 0,
reviewCount: 0
};
const promptContext = [
"# 知识库上下文包:雨夜开场冲突",
"使用要求:基于引用素材做原创改编;保留人物、道具、地点和时间线连续性;生成画面仍必须是一张完整单画面,不得输出多格、拼图或分屏。",
"治理摘要:pass;未批准/需复核片段 0;阻断片段 0。",
...packChunks.map((chunk) => [
`## [${chunk.citationKey}] ${chunk.heading}`,
`来源:《${chunk.documentTitle}》 / novel / chunk ${chunk.chunkIndex} / rights=approved / risk=pass`,
`内容:${chunk.content}`
].join("\n"))
].join("\n\n");
insertIgnore(
`INSERT OR IGNORE INTO knowledge_context_packs(
id, organization_id, workspace_id, project_id, scope_mode, name, query, source_type, max_tokens,
token_estimate, chunk_ids_json, citations_json, chunks_json, prompt_context, status,
metadata_json, created_by, created_at, updated_at
) VALUES ('knowledge-pack-rain-night-opening', 'org-studio-lab', 'ws-local-aidrama', NULL, 'workspace', '雨夜开场冲突', '蓝伞 地铁口', 'novel', 800, ?, ?, ?, ?, ?, 'active', ?, 'u-owner', ?, ?)`,
[
packChunks.reduce((sum, chunk) => sum + Number(chunk.tokenEstimate || 0), 0),
JSON.stringify(packChunks.map((chunk) => chunk.id)),
JSON.stringify(citations),
JSON.stringify(packChunks),
promptContext,
JSON.stringify({ seed: true, usage: "script-draft", rights: "original", governance: packGovernance }),
timestamp,
timestamp
]
);
dbRun(
`UPDATE knowledge_context_packs
SET citations_json = ?, chunks_json = ?, prompt_context = ?, metadata_json = ?
WHERE id = 'knowledge-pack-rain-night-opening'`,
[JSON.stringify(citations), JSON.stringify(packChunks), promptContext, JSON.stringify({ seed: true, usage: "script-draft", rights: "original", governance: packGovernance })]
);
}
function seedSystemGovernance() {
const timestamp = now();
insertIgnore("INSERT OR IGNORE INTO system_admins(user_id, role_key, status, created_at, updated_at) VALUES ('u-owner', 'system_admin', 'active', ?, ?)", [timestamp, timestamp]);
@@ -577,6 +963,67 @@ function seedProductionGraph() {
for (const audit of audits) {
insertIgnore("INSERT OR IGNORE INTO audit_logs(id, organization_id, workspace_id, project_id, actor_user_id, action, target_type, target_id, result, metadata_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [...audit, timestamp]);
}
insertIgnore("INSERT OR IGNORE INTO series(id, project_id, title, logline, format, visual_style, continuity_rule, show_engine, created_at, updated_at) VALUES (?, ?, ?, ?, 'vertical-9:16', ?, ?, ?, ?, ?)", [
"series-northstar-pilot",
"northstar-pilot",
"山海志异·试播集",
"北辰试制部用于验证多组织隔离的原创国漫试播项目。",
"原创国漫 2D 动画风格,竖屏 9:16,单一完整画面,干净线稿与克制动效。",
"角色、场景、道具、天气、镜头和声线全部随项目隔离,禁止串用其他组织素材。",
"以单镜头冲突推进试播片段,所有生成先经过本地 Runner 与审片门。",
timestamp,
timestamp
]);
insertIgnore("INSERT OR IGNORE INTO seasons(id, series_id, season_number, title, created_at, updated_at) VALUES (?, ?, 1, '第一季', ?, ?)", ["season-northstar-pilot-1", "series-northstar-pilot", timestamp, timestamp]);
insertIgnore("INSERT OR IGNORE INTO episodes(id, season_id, episode_number, title, status, target_duration_sec, hook, cliffhanger, created_at, updated_at) VALUES (?, ?, 1, '雾中石门', 'production', 45, '雾气里出现不该存在的石门。', '门后传来第二个自己的声音。', ?, ?)", ["episode-northstar-pilot-01", "season-northstar-pilot-1", timestamp, timestamp]);
const northstarShot = {
id: "shot-northstar-pilot-001",
title: "雾中石门出现",
durationSec: 6,
characterIds: ["northstar-yun"],
locationId: "northstar-fog-gate",
propIds: ["northstar-bronze-bell"],
camera: "稳定中景,角色在画面右侧停步,石门在远处雾中显现,单一连续画面。",
action: "少年抬手按住铜铃,雾气向石门方向收束,镜头不切分。",
firstFrame: "episode-start",
lastFrame: "pending-actual-last-frame",
transitionFromPrevious: "episode-start",
prompt: "ONE SINGLE COMPLETE 9:16 CHINESE ANIMATION FRAME, no panels, one scene only.",
negativePrompt: "no split screen, no comic panel, no collage, no contact sheet, no storyboard",
seed: 260831
};
insertIgnore("INSERT OR IGNORE INTO shots(id, episode_id, shot_number, title, status, first_frame_path, last_frame_path, continuity_json, created_at, updated_at) VALUES (?, ?, 1, ?, 'draft', ?, ?, ?, ?, ?)", [
northstarShot.id,
"episode-northstar-pilot-01",
northstarShot.title,
northstarShot.firstFrame,
northstarShot.lastFrame,
JSON.stringify({ camera: northstarShot.camera, transition: northstarShot.transitionFromPrevious }),
timestamp,
timestamp
]);
insertIgnore("INSERT OR IGNORE INTO shot_versions(id, shot_id, version_number, payload_json, status, created_by, created_at) VALUES (?, ?, 1, ?, 'draft', 'u-producer', ?)", [`${northstarShot.id}-v1`, northstarShot.id, JSON.stringify(northstarShot), timestamp]);
dbRun("UPDATE shots SET current_version_id = COALESCE(current_version_id, ?) WHERE id = ?", [`${northstarShot.id}-v1`, northstarShot.id]);
insertIgnore("INSERT OR IGNORE INTO generation_jobs(id, organization_id, workspace_id, project_id, episode_id, shot_id, kind, adapter_id, status, priority, cost_policy, output_path, qa_status, request_json, result_json, error_message, created_by, created_at, updated_at) VALUES (?, 'org-northstar', 'ws-northstar-main', 'northstar-pilot', 'episode-northstar-pilot-01', ?, '单画面关键帧', 'northstar-image', 'queued', 45, 'local', 'storage/jobs/northstar-pilot/shot-001/keyframe.png', 'wait', ?, '{}', '', 'u-producer', ?, ?)", [
"job-northstar-pilot-keyframe-001",
northstarShot.id,
JSON.stringify({ schema: "ai-drama.job.v1", seeded: true, job: { adapterId: "northstar-image" }, constraints: { singleFrameOnly: true, imageOutputCount: 1 } }),
timestamp,
timestamp
]);
insertIgnore("INSERT OR IGNORE INTO reviews(id, organization_id, workspace_id, project_id, shot_id, lane, status, score, evidence_json, created_at, updated_at) VALUES (?, 'org-northstar', 'ws-northstar-main', 'northstar-pilot', ?, 'single-frame', 'pending', NULL, ?, ?, ?)", [
"review-northstar-pilot-single-frame",
northstarShot.id,
JSON.stringify({ blockers: ["等待北辰审片员确认一图一画面证据。"], source: "seed" }),
timestamp,
timestamp
]);
insertIgnore("INSERT OR IGNORE INTO deliveries(id, organization_id, workspace_id, project_id, version, manifest_path, channel, status, created_at, updated_at) VALUES (?, 'org-northstar', 'ws-northstar-main', 'northstar-pilot', 'v0.1-internal', 'storage/deliveries/northstar-pilot/v0.1/delivery-manifest.json', 'internal', 'review', ?, ?)", [
"delivery-northstar-pilot-v01",
timestamp,
timestamp
]);
}
withTransaction(() => {
@@ -614,11 +1061,17 @@ withTransaction(() => {
risk: "medium"
});
insertIgnore("INSERT OR IGNORE INTO workspaces(id, organization_id, name, slug, description, status, created_at, updated_at) VALUES ('ws-pilot', 'org-studio-lab', '素材实验室', 'asset-lab', '角色、场景和模型试验空间', 'active', ?, ?)", [now(), now()]);
seedCommercialPlans();
seedOrganizationEntitlements();
seedCommercialApprovals();
seedMembersAndProjects();
seedModels();
seedModelCatalog();
seedModelRoutes();
seedSystemGovernance();
seedWorkflowTemplates();
seedProductionGraph();
seedKnowledgeLibrary();
seedUserNotifications();
});