新增统一案例存储仓储

This commit is contained in:
czj
2026-07-08 13:19:35 +08:00
parent 01c02049d4
commit ee98c3304b
11 changed files with 1497 additions and 51 deletions
+546 -15
View File
@@ -7,6 +7,16 @@ import type {
RubricVersion,
ScoringRun,
} from "../calibration/types";
import type {
CaseInput,
CaseListFilters,
CaseMetadataPatch,
OptimizationCase,
OptimizationCaseStatus,
OptimizationCaseType,
OptimizationResultVersion,
ResultVersionStatus,
} from "../cases/types";
import type {
OptimizationFactCard,
ImageInput,
@@ -43,6 +53,7 @@ export type NewBrandTemplate = Omit<
export interface ArticleJob {
id: string;
brand_template_id: string | null;
case_id: string | null;
source_title: string;
source_body: string;
image_inputs: ImageInput[];
@@ -56,6 +67,7 @@ export interface ArticleJob {
export interface NewArticleJob {
brand_template_id?: string | null;
case_id?: string | null;
source_title: string;
source_body: string;
image_inputs: ImageInput[];
@@ -81,6 +93,7 @@ interface BrandTemplateRow {
interface ArticleJobRow {
id: string;
brand_template_id: string | null;
case_id: string | null;
source_title: string;
source_body: string;
image_inputs: string;
@@ -111,8 +124,10 @@ interface QaReportRow {
interface ScoringRunRow {
id: string;
job_id: string;
revision: number;
result_version_id: string | null;
case_type: OptimizationCaseType;
job_id: string | null;
revision: number | null;
rubric_version_id: string;
dimension_scores: string;
composite_score: number;
@@ -122,9 +137,10 @@ interface ScoringRunRow {
interface PublicationRecordRow {
id: string;
job_id: string;
revision: number;
platform: PublishPlatform;
result_version_id: string | null;
job_id: string | null;
revision: number | null;
publish_target: string;
url: string;
published_at: string;
status: "draft" | "published" | "archived";
@@ -144,6 +160,54 @@ interface PerformanceSnapshotRow {
snapshot_at: string;
}
interface OptimizationCaseRow {
id: string;
case_type: OptimizationCaseType;
title: string;
summary: string;
status: OptimizationCaseStatus;
customer_name: string;
brand_name: string;
project_tags: string;
notes: string;
publish_target: string;
source_excerpt: string;
result_excerpt: string;
latest_result_version_id: string | null;
latest_version_number: number | null;
last_error_stage: string | null;
last_error_summary: string | null;
archived_at: string | null;
created_at: string;
updated_at: string;
}
interface CaseInputRow {
case_id: string;
case_type: OptimizationCaseType;
article_job_id: string | null;
payload: string;
created_at: string;
updated_at: string;
}
interface OptimizationResultVersionRow {
id: string;
case_id: string;
case_type: OptimizationCaseType;
version: number;
status: ResultVersionStatus;
article_job_id: string | null;
article_revision: number | null;
result_summary: string;
payload: string;
process_summary: string;
llm_audit_summary: string;
error_stage: string | null;
error_summary: string | null;
created_at: string;
}
function nowIso() {
return new Date().toISOString();
}
@@ -194,6 +258,15 @@ function toScoringRun(row: ScoringRunRow): ScoringRun {
};
}
function toPublicationRecord(row: PublicationRecordRow): PublicationRecord {
return {
...row,
platform: isPublishPlatform(row.publish_target)
? row.publish_target
: undefined,
};
}
function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
return {
...row,
@@ -202,6 +275,46 @@ function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot
};
}
function toOptimizationCase(row: OptimizationCaseRow): OptimizationCase {
return {
...row,
project_tags: parseJson<string[]>(row.project_tags),
};
}
function toCaseInput(row: CaseInputRow): CaseInput {
return {
...row,
payload: parseJson<CaseInput["payload"]>(row.payload),
};
}
function toOptimizationResultVersion(
row: OptimizationResultVersionRow,
): OptimizationResultVersion {
return {
...row,
payload: row.payload
? parseJson<OptimizationResultVersion["payload"]>(row.payload)
: null,
process_summary: parseJson<OptimizationResultVersion["process_summary"]>(
row.process_summary,
),
llm_audit_summary: parseJson<OptimizationResultVersion["llm_audit_summary"]>(
row.llm_audit_summary,
),
};
}
function isPublishPlatform(value: string): value is PublishPlatform {
return [
"official_site",
"media_article",
"comparison_review",
"recommendation_list",
].includes(value);
}
export function createBrandTemplate(
dbPath: string | undefined,
input: NewBrandTemplate,
@@ -264,6 +377,7 @@ export function createArticleJob(dbPath: string | undefined, input: NewArticleJo
const job: ArticleJob = {
id: `job_${nanoid(10)}`,
brand_template_id: input.brand_template_id ?? null,
case_id: input.case_id ?? null,
source_title: input.source_title,
source_body: input.source_body,
image_inputs: input.image_inputs,
@@ -277,10 +391,10 @@ export function createArticleJob(dbPath: string | undefined, input: NewArticleJo
db.prepare(
`insert into article_jobs (
id, brand_template_id, source_title, source_body, image_inputs,
id, brand_template_id, case_id, source_title, source_body, image_inputs,
publish_platform, user_instructions, status, export_paths, created_at, updated_at
) values (
@id, @brand_template_id, @source_title, @source_body, @image_inputs,
@id, @brand_template_id, @case_id, @source_title, @source_body, @image_inputs,
@publish_platform, @user_instructions, @status, @export_paths, @created_at, @updated_at
)`,
).run({
@@ -440,6 +554,379 @@ export function getLatestQaReport(dbPath: string | undefined, jobId: string) {
});
}
export function createOptimizationCase(
dbPath: string | undefined,
input: {
case_type: OptimizationCaseType;
title: string;
summary: string;
publish_target: string;
source_excerpt: string;
},
) {
return withDb(dbPath, (db) => {
const timestamp = nowIso();
const optimizationCase: OptimizationCase = {
id: `case_${nanoid(10)}`,
case_type: input.case_type,
title: input.title,
summary: input.summary,
status: "running",
customer_name: "",
brand_name: "",
project_tags: [],
notes: "",
publish_target: input.publish_target,
source_excerpt: input.source_excerpt,
result_excerpt: "",
latest_result_version_id: null,
latest_version_number: null,
last_error_stage: null,
last_error_summary: null,
archived_at: null,
created_at: timestamp,
updated_at: timestamp,
};
db.prepare(
`insert into optimization_cases (
id, case_type, title, summary, status, customer_name, brand_name,
project_tags, notes, publish_target, source_excerpt, result_excerpt,
latest_result_version_id, latest_version_number, last_error_stage,
last_error_summary, archived_at, created_at, updated_at
) values (
@id, @case_type, @title, @summary, @status, @customer_name, @brand_name,
@project_tags, @notes, @publish_target, @source_excerpt, @result_excerpt,
@latest_result_version_id, @latest_version_number, @last_error_stage,
@last_error_summary, @archived_at, @created_at, @updated_at
)`,
).run({
...optimizationCase,
project_tags: serialize(optimizationCase.project_tags),
});
return optimizationCase;
});
}
export function saveCaseInput(
dbPath: string | undefined,
input: Omit<CaseInput, "created_at" | "updated_at">,
) {
return withDb(dbPath, (db) => {
const timestamp = nowIso();
const saved: CaseInput = {
...input,
created_at: timestamp,
updated_at: timestamp,
};
db.prepare(
`insert into case_inputs (
case_id, case_type, article_job_id, payload, created_at, updated_at
) values (
@case_id, @case_type, @article_job_id, @payload, @created_at, @updated_at
)
on conflict(case_id) do update set
case_type = excluded.case_type,
article_job_id = excluded.article_job_id,
payload = excluded.payload,
updated_at = excluded.updated_at`,
).run({
...saved,
payload: serialize(saved.payload),
});
return saved;
});
}
export function listOptimizationCases(
dbPath: string | undefined,
filters: CaseListFilters,
) {
return withDb(dbPath, (db) => {
const where: string[] = [];
const values: unknown[] = [];
if (!filters.include_archived) {
where.push("archived_at is null");
}
if (filters.case_type) {
where.push("case_type = ?");
values.push(filters.case_type);
}
if (filters.status) {
where.push("status = ?");
values.push(filters.status);
}
if (filters.publish_target) {
where.push("publish_target = ?");
values.push(filters.publish_target);
}
if (filters.project_tag) {
where.push("project_tags like ?");
values.push(`%"${filters.project_tag}"%`);
}
if (filters.created_from) {
where.push("created_at >= ?");
values.push(filters.created_from);
}
if (filters.created_to) {
where.push("created_at <= ?");
values.push(filters.created_to);
}
if (filters.q) {
where.push(
"(title like ? or summary like ? or source_excerpt like ? or result_excerpt like ? or customer_name like ? or brand_name like ? or notes like ?)",
);
const keyword = `%${filters.q}%`;
values.push(keyword, keyword, keyword, keyword, keyword, keyword, keyword);
}
const clause = where.length > 0 ? `where ${where.join(" and ")}` : "";
return db
.prepare(`select * from optimization_cases ${clause} order by updated_at desc`)
.all(...values)
.map((row) => toOptimizationCase(row as OptimizationCaseRow));
});
}
export function getOptimizationCaseDetail(
dbPath: string | undefined,
caseId: string,
) {
return withDb(dbPath, (db) => {
const caseRow = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow | undefined;
if (!caseRow) return null;
const inputRow = db
.prepare("select * from case_inputs where case_id = ?")
.get(caseId) as CaseInputRow | undefined;
const versionRows = db
.prepare(
`select * from optimization_result_versions
where case_id = ?
order by version desc`,
)
.all(caseId) as OptimizationResultVersionRow[];
return {
case: toOptimizationCase(caseRow),
input: inputRow ? toCaseInput(inputRow) : null,
versions: versionRows.map(toOptimizationResultVersion),
};
});
}
export function updateOptimizationCaseMetadata(
dbPath: string | undefined,
caseId: string,
changes: CaseMetadataPatch,
) {
return withDb(dbPath, (db) => {
const existing = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow | undefined;
if (!existing) return null;
const updated = {
title: changes.title ?? existing.title,
customer_name: changes.customer_name ?? existing.customer_name,
brand_name: changes.brand_name ?? existing.brand_name,
project_tags:
changes.project_tags === undefined
? existing.project_tags
: serialize(changes.project_tags),
notes: changes.notes ?? existing.notes,
updated_at: nowIso(),
};
db.prepare(
`update optimization_cases set
title = @title,
customer_name = @customer_name,
brand_name = @brand_name,
project_tags = @project_tags,
notes = @notes,
updated_at = @updated_at
where id = @id`,
).run({ id: caseId, ...updated });
const row = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow;
return toOptimizationCase(row);
});
}
export function archiveOptimizationCase(
dbPath: string | undefined,
caseId: string,
) {
return updateCaseArchiveState(dbPath, caseId, true);
}
export function restoreOptimizationCase(
dbPath: string | undefined,
caseId: string,
) {
return updateCaseArchiveState(dbPath, caseId, false);
}
function updateCaseArchiveState(
dbPath: string | undefined,
caseId: string,
archived: boolean,
) {
return withDb(dbPath, (db) => {
const existing = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow | undefined;
if (!existing) return null;
const timestamp = nowIso();
const restoredStatus = existing.latest_result_version_id
? "optimized"
: existing.last_error_summary
? "failed"
: "running";
db.prepare(
`update optimization_cases set
status = ?,
archived_at = ?,
updated_at = ?
where id = ?`,
).run(
archived ? "archived" : restoredStatus,
archived ? timestamp : null,
timestamp,
caseId,
);
const row = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow;
return toOptimizationCase(row);
});
}
export function markOptimizationCaseFailed(
dbPath: string | undefined,
caseId: string,
input: { error_stage: string; error_summary: string },
) {
return withDb(dbPath, (db) => {
const timestamp = nowIso();
db.prepare(
`update optimization_cases set
status = 'failed',
last_error_stage = ?,
last_error_summary = ?,
updated_at = ?
where id = ?`,
).run(input.error_stage, input.error_summary, timestamp, caseId);
const row = db
.prepare("select * from optimization_cases where id = ?")
.get(caseId) as OptimizationCaseRow | undefined;
return row ? toOptimizationCase(row) : null;
});
}
export function createOptimizationResultVersion(
dbPath: string | undefined,
input: Omit<OptimizationResultVersion, "id" | "version" | "created_at">,
) {
return withDb(dbPath, (db) => {
const nextVersion =
((db
.prepare(
"select max(version) as version from optimization_result_versions where case_id = ?",
)
.get(input.case_id) as { version: number | null }).version ?? 0) + 1;
const createdAt = nowIso();
const resultVersion: OptimizationResultVersion = {
id: `ver_${nanoid(10)}`,
...input,
version: nextVersion,
created_at: createdAt,
};
db.prepare(
`insert into optimization_result_versions (
id, case_id, case_type, version, status, article_job_id, article_revision,
result_summary, payload, process_summary, llm_audit_summary,
error_stage, error_summary, created_at
) values (
@id, @case_id, @case_type, @version, @status, @article_job_id, @article_revision,
@result_summary, @payload, @process_summary, @llm_audit_summary,
@error_stage, @error_summary, @created_at
)`,
).run({
...resultVersion,
payload: resultVersion.payload ? serialize(resultVersion.payload) : "",
process_summary: serialize(resultVersion.process_summary),
llm_audit_summary: serialize(resultVersion.llm_audit_summary),
});
db.prepare(
`update optimization_cases set
status = @status,
result_excerpt = @result_excerpt,
latest_result_version_id = @latest_result_version_id,
latest_version_number = @latest_version_number,
last_error_stage = @last_error_stage,
last_error_summary = @last_error_summary,
updated_at = @updated_at
where id = @case_id`,
).run({
case_id: input.case_id,
status: input.status === "optimized" ? "optimized" : "failed",
result_excerpt: input.result_summary,
latest_result_version_id: resultVersion.id,
latest_version_number: resultVersion.version,
last_error_stage: input.error_stage,
last_error_summary: input.error_summary,
updated_at: createdAt,
});
return resultVersion;
});
}
export function getOptimizationResultVersion(
dbPath: string | undefined,
versionId: string,
) {
return withDb(dbPath, (db) => {
const row = db
.prepare("select * from optimization_result_versions where id = ?")
.get(versionId) as OptimizationResultVersionRow | undefined;
return row ? toOptimizationResultVersion(row) : null;
});
}
export function findResultVersionForArticleRevision(
dbPath: string | undefined,
jobId: string,
revision: number,
) {
return withDb(dbPath, (db) => {
const row = db
.prepare(
`select * from optimization_result_versions
where article_job_id = ? and article_revision = ?
order by version desc
limit 1`,
)
.get(jobId, revision) as OptimizationResultVersionRow | undefined;
return row ? toOptimizationResultVersion(row) : null;
});
}
export function saveRubricVersion(
dbPath: string | undefined,
rubric: RubricVersion,
@@ -472,11 +959,13 @@ export function saveScoringRun(dbPath: string | undefined, run: ScoringRun) {
return withDb(dbPath, (db) => {
db.prepare(
`insert into scoring_runs (
id, job_id, revision, rubric_version_id, dimension_scores,
id, result_version_id, case_type, job_id, revision, rubric_version_id, dimension_scores,
composite_score, rationale, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
run.id,
run.result_version_id ?? null,
run.case_type ?? "article",
run.job_id,
run.revision,
run.rubric_version_id,
@@ -507,28 +996,56 @@ export function getLatestScoringRun(
});
}
export function getLatestScoringRunForResultVersion(
dbPath: string | undefined,
resultVersionId: string,
) {
return withDb(dbPath, (db) => {
const row = db
.prepare(
`select * from scoring_runs
where result_version_id = ?
order by created_at desc
limit 1`,
)
.get(resultVersionId) as ScoringRunRow | undefined;
return row ? toScoringRun(row) : null;
});
}
export function createPublicationRecord(
dbPath: string | undefined,
input: Omit<PublicationRecord, "id" | "created_at" | "updated_at">,
input: Omit<
PublicationRecord,
"id" | "created_at" | "updated_at" | "result_version_id" | "publish_target"
> & {
result_version_id?: string | null;
publish_target?: string;
},
) {
return withDb(dbPath, (db) => {
const timestamp = nowIso();
const publishTarget = input.publish_target ?? input.platform ?? "未指定";
const record: PublicationRecord = {
id: `pub_${nanoid(10)}`,
...input,
result_version_id: input.result_version_id ?? null,
publish_target: publishTarget,
platform: isPublishPlatform(publishTarget) ? publishTarget : input.platform,
created_at: timestamp,
updated_at: timestamp,
};
db.prepare(
`insert into publication_records (
id, job_id, revision, platform, url, published_at, status,
id, result_version_id, job_id, revision, publish_target, url, published_at, status,
notes, created_at, updated_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
record.id,
record.result_version_id,
record.job_id,
record.revision,
record.platform,
record.publish_target,
record.url,
record.published_at,
record.status,
@@ -545,7 +1062,21 @@ export function listPublicationRecords(dbPath: string | undefined, jobId: string
db
.prepare("select * from publication_records where job_id = ? order by published_at desc")
.all(jobId)
.map((row) => row as PublicationRecordRow),
.map((row) => toPublicationRecord(row as PublicationRecordRow)),
);
}
export function listPublicationRecordsForResultVersion(
dbPath: string | undefined,
resultVersionId: string,
) {
return withDb(dbPath, (db) =>
db
.prepare(
"select * from publication_records where result_version_id = ? order by published_at desc",
)
.all(resultVersionId)
.map((row) => toPublicationRecord(row as PublicationRecordRow)),
);
}
@@ -554,7 +1085,7 @@ export function getPublicationRecord(dbPath: string | undefined, id: string) {
const row = db
.prepare("select * from publication_records where id = ?")
.get(id) as PublicationRecordRow | undefined;
return row ?? null;
return row ? toPublicationRecord(row) : null;
});
}