refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat
2026-09-09 22:02:10 +08:00
parent bacd502f5c
commit b5f3413ded
298 changed files with 12681 additions and 26428 deletions

View File

@@ -1,7 +1,6 @@
// AI 编辑命令契约(ADR-0007 / Runbook M6.1):宿主校验 + LLM 提示词约束的双契约。
// 本文件不复制 handler 约束——命令合法性硬守门在 M2.1 命令注册表(fail-closed),
// 这里只做:(1) LLM 输出 JSON 的结构解析与批级预检(dry-run 走同一注册表,零重复),
// (2) 生成喂给 LLM 的 op 契约(op 集合与注册表黄金同步,见测试)。
// AI 编辑命令的边界层:同一份注册表同时约束模型输出和宿主执行,避免提示词与真实能力列表分叉。
// 本文件只负责 JSON 结构解析、批量预检和提示词契约生成;命令的参数合法性仍由编辑命令注册表统一判定。
// 预检使用与正式执行相同的纯函数注册表,保证“模型看见的能力”和“宿主实际允许的能力”一致。
import { getEditorCommandRegistry, type EditCommand } from "@/lib/timeline/editor-commands";
import type { TimelineProject } from "@/types/timeline";
@@ -79,8 +78,8 @@ function assertPlanShape(raw: unknown): asserts raw is AiCommandPlan {
}
/**
* 解析 LLM 文本输出为 AiCommandPlan;结构非法时抛 AiCommandPlanError(供 UI 展示并回填模型)。
* commands 为空数组表示「只读问答 / 无法用现有命令完成」,面板直接展示 reasoning 作为回答。
* 解析模型文本为 AiCommandPlan;结构非法时抛 AiCommandPlanError,调用方可将明确原因展示给用户或回传模型修正。
* commands 为空数组表示只读问答或当前能力无法完成,调用方不应把它当成“执行成功的空修改”。
*/
export function parseAiCommandPlan(text: string): AiCommandPlan {
let raw: unknown;
@@ -96,8 +95,8 @@ export function parseAiCommandPlan(text: string): AiCommandPlan {
/**
* 批级预检:把整批命令按序 dry-run 到当前时间线(immutable 纯函数,无副作用)。
* 任何一条触发注册表 fail-closed(未知 op / 非法 payload / 引用不存在)即整批拒绝,
* 返回首个出错命令的下标与消息——ADR-0007「任一非法→整批拒绝,错误回填 LLM 修正」。
* 任何一条触发注册表的拒绝规则(未知 op、非法 payload 或引用不存在)即整批拒绝,
* 返回首个失败命令的位置和原因;正式执行不得绕过这一步或只提交部分命令。
*/
export function validateAiCommandBatch(project: TimelineProject, plan: AiCommandPlan): AiCommandBatchVerdict {
const registry = getEditorCommandRegistry();
@@ -114,13 +113,13 @@ export function validateAiCommandBatch(project: TimelineProject, plan: AiCommand
}
// ---------------------------------------------------------------------------
// LLM 提示词契约(ADR-0007:schema 同时用于宿主校验与模型输出约束)
// 模型提示词契约:schema 同时用于宿主校验与模型输出约束,具体交互决策见 ADR-0007。
// ---------------------------------------------------------------------------
/** AI 命令 schema 版本(Runbook M6.1):提示词契约/解析规则变更时递增,供 golden 与缓存失效对齐。 */
/** AI 命令 schema 版本;提示词契约或解析规则变化时递增,用于 golden 测试和缓存失效对齐。 */
export const AI_COMMAND_SCHEMA_VERSION = 1;
/** 单次 AI 编辑最多允许的命令条数(ADR-0007:防止模型一次输出失控编辑)。 */
/** 单次 AI 编辑最多允许的命令条数,防止一次模型输出造成不可控的大范围变更。 */
export const AI_EDITING_MAX_COMMANDS = 8;
/** 12 个黄金 op 的 LLM 可读 payload 契约;op 集合与注册表黄金同步(见测试)。 */
@@ -203,7 +202,7 @@ export const AI_EDITING_OUTPUT_FORMAT = `输出要求:
const AI_EDITING_OUTPUT_EXAMPLE = `示例:
{ "reasoning": "把第一个视频片段移到第 5 秒", "commands": [ { "op": "moveClip", "payload": { "id": "clip-a", "startMs": 5000 } } ] }`;
/** 组装 AI 编辑系统提示词:时间线摘要 + op 契约 + 输出约束(M6.3 面板喂给模型)。 */
/** 组装 AI 编辑系统提示词:时间线摘要、可用 op 契约和输出约束必须来自同一份注册表。 */
export function buildAiEditingSystemPrompt(timelineSummary: string): string {
const catalogLines = AI_EDITING_OP_CATALOG.map(
(entry) => `- ${entry.op}:${entry.desc}。payload:${entry.payload}`,

View File

@@ -1,4 +1,4 @@
// 资产 → 时间线片段 的纯转换(M3.4 asset-ingest)。
// 资产到时间线片段的纯转换:只做类型映射和结构组装,不读取网络或修改编辑器状态。
// 资产是"仅时间线作用域"的直连媒体:nodeId 用 `asset:<id>` 前缀(不指向画布节点),
// 预览/导出从 directMedia 解析媒体,不回画布查节点。

View File

@@ -17,7 +17,7 @@ export type EditorCommandRegistry = {
};
// ---------------------------------------------------------------------------
// 黄金命令集合 payload 类型(首版一期能力,对应 Runbook M2.1)
// 内置命令的 payload 类型;注册表、AI 命令契约和黄金测试必须共享同一组 op。
// ---------------------------------------------------------------------------
export type AddClipPayload = { clip: TimelineClip };

View File

@@ -1,36 +0,0 @@
// Mock 转写(M3.6):M4 后端转写任务未就绪前,用确定性伪转写模拟 ASR 结果。
// 同一 title + durationMs 永远生成相同的 SrtEntry[],便于测试与回归;
// M4 接真实任务客户端后本模块移除,UI 契约(SrtEntry[] 落字幕轨道)不变。
import type { SrtEntry } from "@/types/timeline";
const MOCK_CHUNK_MS = 1800;
const MOCK_GAP_MS = 200;
/** 从标题确定性生成一组模拟字幕条目(基于字符串 hash 稳定切分)。 */
export function mockTranscriptionEntries(title: string, durationMs: number): SrtEntry[] {
const text = title.trim() || "未命名素材";
const chunks = splitStable(text, 6);
const usable = Math.max(1, Math.floor(durationMs / (MOCK_CHUNK_MS + MOCK_GAP_MS)));
const count = Math.min(chunks.length, usable);
const step = durationMs / count;
const entries: SrtEntry[] = [];
for (let i = 0; i < count; i += 1) {
const startMs = Math.round(i * step);
const endMs = Math.min(durationMs, Math.round(startMs + MOCK_CHUNK_MS));
if (endMs <= startMs) break;
entries.push({ index: i, startMs, endMs, text: chunks[i % chunks.length] });
}
return entries;
}
/** 稳定切分:按可打印字符逐段取满 maxLen,确定性无随机。 */
function splitStable(text: string, maxLen: number): string[] {
const chars = Array.from(text);
const chunks: string[] = [];
for (let i = 0; i < chars.length; i += maxLen) {
chunks.push(chars.slice(i, i + maxLen).join(""));
}
return chunks;
}

View File

@@ -1,53 +0,0 @@
import type { SrtEntry } from "@/types/timeline";
export interface SubtitleHighlightLLMResult {
entryIndex: number;
shouldHighlight: boolean;
highlightText: string;
start: number;
end: number;
}
export function buildSubtitleHighlightSystemPrompt(): string {
return `你是一个视频字幕关键词高亮助手。请输出严格 JSON。
任务目标:
- 为每条字幕判断是否需要高亮
- 每条字幕最多高亮 1 段
- 只返回最值得高亮的一个关键词或短语
- start 和 end 使用 JavaScript 字符串下标
- end 为 exclusive
- 没有明确重点时返回 shouldHighlight=false
高亮标准:
- 优先高亮结论词、数字、身份词、首次/突破词、强反差词
- 不要高亮虚词、口头语、整句长短语
边界约束:
- 不负责颜色、字号、位置、动效
- 只返回 highlights 数组
- highlightText 必须严格等于原字幕文本切片结果`;
}
export function buildSubtitleHighlightUserMessage(entries: SrtEntry[]): string {
const lines = entries.map((entry) => {
return `entryIndex=${entry.index} | startMs=${entry.startMs} | endMs=${entry.endMs} | text=${entry.text}`;
});
return `请根据以下字幕和上下文,为每条字幕判断是否需要高亮,并返回 JSON:
{
"highlights": [
{
"entryIndex": 1,
"shouldHighlight": true,
"highlightText": "示例",
"start": 0,
"end": 2
}
]
}
字幕列表:
${lines.join("\n")}`;
}

View File

@@ -1,7 +1,7 @@
import { runBackendCanvasGenerationTask } from "@/lib/canvas/canvas-project-generation";
import type { AiConfig } from "@/stores/use-config-store";
import type { SrtEntry, SubtitleHighlight } from "@/types/timeline";
import { buildSubtitleHighlightSystemPrompt, buildSubtitleHighlightUserMessage } from "./subtitle-highlight-ai";
import { buildSubtitleHighlightSystemPrompt, buildSubtitleHighlightUserMessage } from "@/lib/prompts";
import { parseSubtitleHighlightResponse } from "./subtitle-highlight-service";
export interface SubtitleHighlightProgress {

View File

@@ -1,6 +1,12 @@
import type { SrtEntry, SubtitleHighlight } from "@/types/timeline";
import { filterValidSubtitleHighlights } from "./subtitle-highlights";
import type { SubtitleHighlightLLMResult } from "./subtitle-highlight-ai";
interface SubtitleHighlightLLMResult {
entryIndex: number;
shouldHighlight: boolean;
highlightText: string;
start: number;
end: number;
}
function isSubtitleHighlightLLMResult(value: unknown): value is SubtitleHighlightLLMResult {
if (!value || typeof value !== "object") {

View File

@@ -1,4 +1,4 @@
// 时间线确定性摘要(Runbook M6.2):把 TimelineProject v2 压缩成喂给 LLM 的中文上下文。
// 时间线确定性摘要:把 TimelineProject 压缩成稳定、可控长度的中文上下文,供 AI 编辑和诊断使用。
// 同一输入永远产出同一输出(无时间戳/无随机),供命令引用 id 与 AI 面板上下文注入。
import type { TimelineClip, TimelineProject, TimelineTrack } from "@/types/timeline";