新增统一案例存储仓储
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS optimization_cases (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
case_type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
customer_name TEXT NOT NULL DEFAULT '',
|
||||||
|
brand_name TEXT NOT NULL DEFAULT '',
|
||||||
|
project_tags TEXT NOT NULL DEFAULT '[]',
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
publish_target TEXT NOT NULL DEFAULT '',
|
||||||
|
source_excerpt TEXT NOT NULL DEFAULT '',
|
||||||
|
result_excerpt TEXT NOT NULL DEFAULT '',
|
||||||
|
latest_result_version_id TEXT,
|
||||||
|
latest_version_number INTEGER,
|
||||||
|
last_error_stage TEXT,
|
||||||
|
last_error_summary TEXT,
|
||||||
|
archived_at TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS case_inputs (
|
||||||
|
case_id TEXT PRIMARY KEY,
|
||||||
|
case_type TEXT NOT NULL,
|
||||||
|
article_job_id TEXT,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (case_id) REFERENCES optimization_cases(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (article_job_id) REFERENCES article_jobs(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS optimization_result_versions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
case_id TEXT NOT NULL,
|
||||||
|
case_type TEXT NOT NULL,
|
||||||
|
version INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
article_job_id TEXT,
|
||||||
|
article_revision INTEGER,
|
||||||
|
result_summary TEXT NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
process_summary TEXT NOT NULL,
|
||||||
|
llm_audit_summary TEXT NOT NULL,
|
||||||
|
error_stage TEXT,
|
||||||
|
error_summary TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
UNIQUE (case_id, version),
|
||||||
|
FOREIGN KEY (case_id) REFERENCES optimization_cases(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (article_job_id) REFERENCES article_jobs(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_optimization_cases_updated_at
|
||||||
|
ON optimization_cases(updated_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_optimization_cases_case_type
|
||||||
|
ON optimization_cases(case_type);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_optimization_cases_status
|
||||||
|
ON optimization_cases(status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_optimization_result_versions_case
|
||||||
|
ON optimization_result_versions(case_id, version DESC);
|
||||||
|
|
||||||
|
ALTER TABLE article_jobs ADD COLUMN case_id TEXT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS publication_records_next (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
result_version_id TEXT,
|
||||||
|
job_id TEXT,
|
||||||
|
revision INTEGER,
|
||||||
|
publish_target TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
published_at TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
notes TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (result_version_id)
|
||||||
|
REFERENCES optimization_result_versions(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO publication_records_next (
|
||||||
|
id, result_version_id, job_id, revision, publish_target, url,
|
||||||
|
published_at, status, notes, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, NULL, job_id, revision, platform, url,
|
||||||
|
published_at, status, notes, created_at, updated_at
|
||||||
|
FROM publication_records;
|
||||||
|
|
||||||
|
DROP TABLE publication_records;
|
||||||
|
ALTER TABLE publication_records_next RENAME TO publication_records;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_publication_records_result_version
|
||||||
|
ON publication_records(result_version_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_publication_records_job_revision
|
||||||
|
ON publication_records(job_id, revision);
|
||||||
|
|
||||||
|
ALTER TABLE scoring_runs ADD COLUMN result_version_id TEXT;
|
||||||
|
ALTER TABLE scoring_runs ADD COLUMN case_type TEXT NOT NULL DEFAULT 'article';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_scoring_runs_result_version
|
||||||
|
ON scoring_runs(result_version_id);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PublishPlatform, QaReport } from "../domain/types";
|
import type { PublishPlatform, QaReport } from "../domain/types";
|
||||||
|
import type { OptimizationCaseType } from "../cases/types";
|
||||||
|
|
||||||
export type CalibrationDirection =
|
export type CalibrationDirection =
|
||||||
| "better_than_expected"
|
| "better_than_expected"
|
||||||
@@ -27,8 +28,10 @@ export interface RubricVersion {
|
|||||||
|
|
||||||
export interface ScoringRun {
|
export interface ScoringRun {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id?: string | null;
|
||||||
revision: number;
|
case_type?: OptimizationCaseType;
|
||||||
|
job_id: string | null;
|
||||||
|
revision: number | null;
|
||||||
rubric_version_id: string;
|
rubric_version_id: string;
|
||||||
dimension_scores: Record<string, number>;
|
dimension_scores: Record<string, number>;
|
||||||
composite_score: number;
|
composite_score: number;
|
||||||
@@ -38,9 +41,11 @@ export interface ScoringRun {
|
|||||||
|
|
||||||
export interface PublicationRecord {
|
export interface PublicationRecord {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id: string | null;
|
||||||
revision: number;
|
job_id: string | null;
|
||||||
platform: PublishPlatform;
|
revision: number | null;
|
||||||
|
publish_target: string;
|
||||||
|
platform?: PublishPlatform;
|
||||||
url: string;
|
url: string;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
status: "draft" | "published" | "archived";
|
status: "draft" | "published" | "archived";
|
||||||
|
|||||||
@@ -44,11 +44,18 @@ export const performanceMetricsSchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const publicationInputSchema = z.object({
|
export const publicationInputSchema = z.object({
|
||||||
platform: publishPlatformSchema,
|
platform: publishPlatformSchema.optional(),
|
||||||
|
publish_target: z.string().trim().min(1).optional(),
|
||||||
url: z.string().trim().url(),
|
url: z.string().trim().url(),
|
||||||
published_at: z.string().datetime(),
|
published_at: z.string().datetime(),
|
||||||
notes: optionalTextSchema.default(""),
|
notes: optionalTextSchema.default(""),
|
||||||
});
|
}).transform((input) => ({
|
||||||
|
platform: input.platform,
|
||||||
|
publish_target: input.publish_target ?? input.platform ?? "未指定",
|
||||||
|
url: input.url,
|
||||||
|
published_at: input.published_at,
|
||||||
|
notes: input.notes,
|
||||||
|
}));
|
||||||
|
|
||||||
export const manualPerformanceInputSchema = z
|
export const manualPerformanceInputSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -100,8 +107,10 @@ export const rubricVersionSchema = z.object({
|
|||||||
|
|
||||||
export const scoringRunSchema = z.object({
|
export const scoringRunSchema = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
job_id: z.string().trim().min(1),
|
result_version_id: z.string().trim().min(1).nullable().optional(),
|
||||||
revision: z.number().int().positive(),
|
case_type: z.enum(["article", "human_copy"]).optional(),
|
||||||
|
job_id: z.string().trim().min(1).nullable(),
|
||||||
|
revision: z.number().int().positive().nullable(),
|
||||||
rubric_version_id: z.string().trim().min(1),
|
rubric_version_id: z.string().trim().min(1),
|
||||||
dimension_scores: z.record(z.string(), z.number().min(0).max(5)),
|
dimension_scores: z.record(z.string(), z.number().min(0).max(5)),
|
||||||
composite_score: z.number().min(0).max(10),
|
composite_score: z.number().min(0).max(10),
|
||||||
@@ -111,9 +120,11 @@ export const scoringRunSchema = z.object({
|
|||||||
|
|
||||||
export const publicationRecordSchema = z.object({
|
export const publicationRecordSchema = z.object({
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
job_id: z.string().trim().min(1),
|
result_version_id: z.string().trim().min(1).nullable(),
|
||||||
revision: z.number().int().positive(),
|
job_id: z.string().trim().min(1).nullable(),
|
||||||
platform: publishPlatformSchema,
|
revision: z.number().int().positive().nullable(),
|
||||||
|
publish_target: z.string().trim().min(1),
|
||||||
|
platform: publishPlatformSchema.optional(),
|
||||||
url: z.string().trim().url(),
|
url: z.string().trim().url(),
|
||||||
published_at: z.string().datetime(),
|
published_at: z.string().datetime(),
|
||||||
status: z.enum(["draft", "published", "archived"]),
|
status: z.enum(["draft", "published", "archived"]),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("createD1Repository", () => {
|
|||||||
expect(bind).toHaveBeenCalledWith(
|
expect(bind).toHaveBeenCalledWith(
|
||||||
job.id,
|
job.id,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
"Title",
|
"Title",
|
||||||
"Body",
|
"Body",
|
||||||
"[]",
|
"[]",
|
||||||
@@ -40,6 +41,7 @@ describe("createD1Repository", () => {
|
|||||||
const first = vi.fn().mockResolvedValue({
|
const first = vi.fn().mockResolvedValue({
|
||||||
id: "job_123",
|
id: "job_123",
|
||||||
brand_template_id: null,
|
brand_template_id: null,
|
||||||
|
case_id: null,
|
||||||
source_title: "Title",
|
source_title: "Title",
|
||||||
source_body: "Body",
|
source_body: "Body",
|
||||||
image_inputs: "[]",
|
image_inputs: "[]",
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ describe("sqlite repositories", () => {
|
|||||||
"article_jobs",
|
"article_jobs",
|
||||||
"brand_templates",
|
"brand_templates",
|
||||||
"calibration_events",
|
"calibration_events",
|
||||||
|
"case_inputs",
|
||||||
"fact_cards",
|
"fact_cards",
|
||||||
|
"optimization_cases",
|
||||||
|
"optimization_result_versions",
|
||||||
"optimized_articles",
|
"optimized_articles",
|
||||||
"performance_snapshots",
|
"performance_snapshots",
|
||||||
"publication_records",
|
"publication_records",
|
||||||
@@ -60,6 +63,23 @@ describe("sqlite repositories", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds case references to article jobs and result-version publications", () => {
|
||||||
|
const db = createDatabase(dbPath);
|
||||||
|
const articleJobColumns = db
|
||||||
|
.prepare("pragma table_info(article_jobs)")
|
||||||
|
.all()
|
||||||
|
.map((row) => (row as { name: string }).name);
|
||||||
|
const publicationColumns = db
|
||||||
|
.prepare("pragma table_info(publication_records)")
|
||||||
|
.all()
|
||||||
|
.map((row) => (row as { name: string }).name);
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
expect(articleJobColumns).toContain("case_id");
|
||||||
|
expect(publicationColumns).toContain("result_version_id");
|
||||||
|
expect(publicationColumns).toContain("publish_target");
|
||||||
|
});
|
||||||
|
|
||||||
it("inserts and fetches a brand template", () => {
|
it("inserts and fetches a brand template", () => {
|
||||||
const created = createBrandTemplate(dbPath, {
|
const created = createBrandTemplate(dbPath, {
|
||||||
brand_name: "Example",
|
brand_name: "Example",
|
||||||
|
|||||||
@@ -111,4 +111,122 @@ describe("createSqliteRepository", () => {
|
|||||||
]);
|
]);
|
||||||
expect(event.observations).toEqual(["表现高于预期"]);
|
expect(event.observations).toEqual(["表现高于预期"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("creates, lists, updates, archives, and restores optimization cases", async () => {
|
||||||
|
const repository = createSqliteRepository(dbPath);
|
||||||
|
|
||||||
|
const created = await repository.createOptimizationCase({
|
||||||
|
case_type: "human_copy",
|
||||||
|
title: "人味文案优化:朋友圈",
|
||||||
|
summary: "原文摘要",
|
||||||
|
publish_target: "朋友圈",
|
||||||
|
source_excerpt: "原文摘要",
|
||||||
|
});
|
||||||
|
|
||||||
|
await repository.saveCaseInput({
|
||||||
|
case_id: created.id,
|
||||||
|
case_type: "human_copy",
|
||||||
|
article_job_id: null,
|
||||||
|
payload: {
|
||||||
|
source_text: "原文摘要",
|
||||||
|
goal: "自然一点",
|
||||||
|
intensity: "light",
|
||||||
|
user_instructions: "",
|
||||||
|
publish_target: "朋友圈",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
repository.listOptimizationCases({ include_archived: false }),
|
||||||
|
).resolves.toEqual([expect.objectContaining({ id: created.id })]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
repository.updateOptimizationCaseMetadata(created.id, {
|
||||||
|
customer_name: "客户A",
|
||||||
|
brand_name: "品牌B",
|
||||||
|
project_tags: ["朋友圈"],
|
||||||
|
notes: "保留口语。",
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
customer_name: "客户A",
|
||||||
|
project_tags: ["朋友圈"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await repository.archiveOptimizationCase(created.id);
|
||||||
|
await expect(
|
||||||
|
repository.listOptimizationCases({ include_archived: false }),
|
||||||
|
).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await repository.restoreOptimizationCase(created.id);
|
||||||
|
await expect(repository.getOptimizationCaseDetail(created.id)).resolves.toMatchObject({
|
||||||
|
case: { id: created.id, status: "running" },
|
||||||
|
input: expect.objectContaining({ case_id: created.id }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates multiple result versions and binds publication to a version", async () => {
|
||||||
|
const repository = createSqliteRepository(dbPath);
|
||||||
|
const optimizationCase = await repository.createOptimizationCase({
|
||||||
|
case_type: "human_copy",
|
||||||
|
title: "人味文案优化:私域",
|
||||||
|
summary: "原文",
|
||||||
|
publish_target: "私域",
|
||||||
|
source_excerpt: "原文",
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = await repository.createOptimizationResultVersion({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "human_copy",
|
||||||
|
status: "optimized",
|
||||||
|
article_job_id: null,
|
||||||
|
article_revision: null,
|
||||||
|
result_summary: "第一版",
|
||||||
|
payload: {
|
||||||
|
optimized_text: "第一版文案",
|
||||||
|
change_notes: [],
|
||||||
|
ai_taste_checks: [],
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
process_summary: [],
|
||||||
|
llm_audit_summary: [],
|
||||||
|
error_stage: null,
|
||||||
|
error_summary: null,
|
||||||
|
});
|
||||||
|
const second = await repository.createOptimizationResultVersion({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "human_copy",
|
||||||
|
status: "optimized",
|
||||||
|
article_job_id: null,
|
||||||
|
article_revision: null,
|
||||||
|
result_summary: "第二版",
|
||||||
|
payload: {
|
||||||
|
optimized_text: "第二版文案",
|
||||||
|
change_notes: [],
|
||||||
|
ai_taste_checks: [],
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
process_summary: [],
|
||||||
|
llm_audit_summary: [],
|
||||||
|
error_stage: null,
|
||||||
|
error_summary: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first.version).toBe(1);
|
||||||
|
expect(second.version).toBe(2);
|
||||||
|
|
||||||
|
const publication = await repository.createPublicationRecord({
|
||||||
|
result_version_id: second.id,
|
||||||
|
job_id: null,
|
||||||
|
revision: null,
|
||||||
|
publish_target: "私域",
|
||||||
|
url: "https://example.com/private",
|
||||||
|
published_at: "2026-07-08T12:00:00.000Z",
|
||||||
|
status: "published",
|
||||||
|
notes: "客户私域发布",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
repository.listPublicationRecordsForResultVersion(second.id),
|
||||||
|
).resolves.toEqual([expect.objectContaining({ id: publication.id })]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+492
-14
@@ -5,6 +5,16 @@ import type {
|
|||||||
PublicationRecord,
|
PublicationRecord,
|
||||||
ScoringRun,
|
ScoringRun,
|
||||||
} from "../calibration/types";
|
} from "../calibration/types";
|
||||||
|
import type {
|
||||||
|
CaseInput,
|
||||||
|
CaseListFilters,
|
||||||
|
CaseMetadataPatch,
|
||||||
|
OptimizationCase,
|
||||||
|
OptimizationCaseStatus,
|
||||||
|
OptimizationCaseType,
|
||||||
|
OptimizationResultVersion,
|
||||||
|
ResultVersionStatus,
|
||||||
|
} from "../cases/types";
|
||||||
import type {
|
import type {
|
||||||
OptimizationFactCard,
|
OptimizationFactCard,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
@@ -35,6 +45,7 @@ interface BrandTemplateRow {
|
|||||||
interface ArticleJobRow {
|
interface ArticleJobRow {
|
||||||
id: string;
|
id: string;
|
||||||
brand_template_id: string | null;
|
brand_template_id: string | null;
|
||||||
|
case_id: string | null;
|
||||||
source_title: string;
|
source_title: string;
|
||||||
source_body: string;
|
source_body: string;
|
||||||
image_inputs: string;
|
image_inputs: string;
|
||||||
@@ -65,8 +76,10 @@ interface QaReportRow {
|
|||||||
|
|
||||||
interface ScoringRunRow {
|
interface ScoringRunRow {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id: string | null;
|
||||||
revision: number;
|
case_type: OptimizationCaseType;
|
||||||
|
job_id: string | null;
|
||||||
|
revision: number | null;
|
||||||
rubric_version_id: string;
|
rubric_version_id: string;
|
||||||
dimension_scores: string;
|
dimension_scores: string;
|
||||||
composite_score: number;
|
composite_score: number;
|
||||||
@@ -76,9 +89,10 @@ interface ScoringRunRow {
|
|||||||
|
|
||||||
interface PublicationRecordRow {
|
interface PublicationRecordRow {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id: string | null;
|
||||||
revision: number;
|
job_id: string | null;
|
||||||
platform: PublishPlatform;
|
revision: number | null;
|
||||||
|
publish_target: string;
|
||||||
url: string;
|
url: string;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
status: "draft" | "published" | "archived";
|
status: "draft" | "published" | "archived";
|
||||||
@@ -98,6 +112,54 @@ interface PerformanceSnapshotRow {
|
|||||||
snapshot_at: string;
|
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() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -138,6 +200,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 {
|
function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
@@ -146,6 +217,86 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateD1CaseArchiveState(
|
||||||
|
db: D1Database,
|
||||||
|
caseId: string,
|
||||||
|
archived: boolean,
|
||||||
|
) {
|
||||||
|
const existing = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const timestamp = nowIso();
|
||||||
|
const restoredStatus = existing.latest_result_version_id
|
||||||
|
? "optimized"
|
||||||
|
: existing.last_error_summary
|
||||||
|
? "failed"
|
||||||
|
: "running";
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`update optimization_cases set
|
||||||
|
status = ?,
|
||||||
|
archived_at = ?,
|
||||||
|
updated_at = ?
|
||||||
|
where id = ?`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
archived ? "archived" : restoredStatus,
|
||||||
|
archived ? timestamp : null,
|
||||||
|
timestamp,
|
||||||
|
caseId,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
const row = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
return row ? toOptimizationCase(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function createD1Repository(db: D1Database): AppRepository {
|
export function createD1Repository(db: D1Database): AppRepository {
|
||||||
return {
|
return {
|
||||||
async createBrandTemplate(input) {
|
async createBrandTemplate(input) {
|
||||||
@@ -201,6 +352,7 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
const job: ArticleJob = {
|
const job: ArticleJob = {
|
||||||
id: `job_${nanoid(10)}`,
|
id: `job_${nanoid(10)}`,
|
||||||
brand_template_id: input.brand_template_id ?? null,
|
brand_template_id: input.brand_template_id ?? null,
|
||||||
|
case_id: input.case_id ?? null,
|
||||||
source_title: input.source_title,
|
source_title: input.source_title,
|
||||||
source_body: input.source_body,
|
source_body: input.source_body,
|
||||||
image_inputs: input.image_inputs,
|
image_inputs: input.image_inputs,
|
||||||
@@ -215,13 +367,14 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`insert into article_jobs (
|
`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
|
publish_platform, user_instructions, status, export_paths, created_at, updated_at
|
||||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
job.id,
|
job.id,
|
||||||
job.brand_template_id,
|
job.brand_template_id,
|
||||||
|
job.case_id,
|
||||||
job.source_title,
|
job.source_title,
|
||||||
job.source_body,
|
job.source_body,
|
||||||
serialize(job.image_inputs),
|
serialize(job.image_inputs),
|
||||||
@@ -356,6 +509,302 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
.first<QaReportRow>();
|
.first<QaReportRow>();
|
||||||
return row ? parseJson<QaReport>(row.report) : null;
|
return row ? parseJson<QaReport>(row.report) : null;
|
||||||
},
|
},
|
||||||
|
async createOptimizationCase(input) {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
await 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
optimizationCase.id,
|
||||||
|
optimizationCase.case_type,
|
||||||
|
optimizationCase.title,
|
||||||
|
optimizationCase.summary,
|
||||||
|
optimizationCase.status,
|
||||||
|
optimizationCase.customer_name,
|
||||||
|
optimizationCase.brand_name,
|
||||||
|
serialize(optimizationCase.project_tags),
|
||||||
|
optimizationCase.notes,
|
||||||
|
optimizationCase.publish_target,
|
||||||
|
optimizationCase.source_excerpt,
|
||||||
|
optimizationCase.result_excerpt,
|
||||||
|
optimizationCase.latest_result_version_id,
|
||||||
|
optimizationCase.latest_version_number,
|
||||||
|
optimizationCase.last_error_stage,
|
||||||
|
optimizationCase.last_error_summary,
|
||||||
|
optimizationCase.archived_at,
|
||||||
|
optimizationCase.created_at,
|
||||||
|
optimizationCase.updated_at,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
return optimizationCase;
|
||||||
|
},
|
||||||
|
async saveCaseInput(input) {
|
||||||
|
const timestamp = nowIso();
|
||||||
|
const saved: CaseInput = {
|
||||||
|
...input,
|
||||||
|
created_at: timestamp,
|
||||||
|
updated_at: timestamp,
|
||||||
|
};
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`insert into case_inputs (
|
||||||
|
case_id, case_type, article_job_id, payload, created_at, updated_at
|
||||||
|
) values (?, ?, ?, ?, ?, ?)
|
||||||
|
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`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
saved.case_id,
|
||||||
|
saved.case_type,
|
||||||
|
saved.article_job_id,
|
||||||
|
serialize(saved.payload),
|
||||||
|
saved.created_at,
|
||||||
|
saved.updated_at,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
return saved;
|
||||||
|
},
|
||||||
|
async listOptimizationCases(filters) {
|
||||||
|
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 ")}` : "";
|
||||||
|
const prepared = db.prepare(
|
||||||
|
`select * from optimization_cases ${clause} order by updated_at desc`,
|
||||||
|
);
|
||||||
|
const result =
|
||||||
|
values.length > 0
|
||||||
|
? await prepared.bind(...values).all<OptimizationCaseRow>()
|
||||||
|
: await prepared.all<OptimizationCaseRow>();
|
||||||
|
return result.results.map(toOptimizationCase);
|
||||||
|
},
|
||||||
|
async getOptimizationCaseDetail(caseId) {
|
||||||
|
const caseRow = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
if (!caseRow) return null;
|
||||||
|
const inputRow = await db
|
||||||
|
.prepare("select * from case_inputs where case_id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<CaseInputRow>();
|
||||||
|
const versionRows = await db
|
||||||
|
.prepare(
|
||||||
|
`select * from optimization_result_versions
|
||||||
|
where case_id = ?
|
||||||
|
order by version desc`,
|
||||||
|
)
|
||||||
|
.bind(caseId)
|
||||||
|
.all<OptimizationResultVersionRow>();
|
||||||
|
return {
|
||||||
|
case: toOptimizationCase(caseRow),
|
||||||
|
input: inputRow ? toCaseInput(inputRow) : null,
|
||||||
|
versions: versionRows.results.map(toOptimizationResultVersion),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async updateOptimizationCaseMetadata(caseId, changes) {
|
||||||
|
const existing = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
if (!existing) return null;
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`update optimization_cases set
|
||||||
|
title = ?,
|
||||||
|
customer_name = ?,
|
||||||
|
brand_name = ?,
|
||||||
|
project_tags = ?,
|
||||||
|
notes = ?,
|
||||||
|
updated_at = ?
|
||||||
|
where id = ?`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
changes.title ?? existing.title,
|
||||||
|
changes.customer_name ?? existing.customer_name,
|
||||||
|
changes.brand_name ?? existing.brand_name,
|
||||||
|
changes.project_tags === undefined
|
||||||
|
? existing.project_tags
|
||||||
|
: serialize(changes.project_tags),
|
||||||
|
changes.notes ?? existing.notes,
|
||||||
|
nowIso(),
|
||||||
|
caseId,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
const row = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
return row ? toOptimizationCase(row) : null;
|
||||||
|
},
|
||||||
|
async archiveOptimizationCase(caseId) {
|
||||||
|
return updateD1CaseArchiveState(db, caseId, true);
|
||||||
|
},
|
||||||
|
async restoreOptimizationCase(caseId) {
|
||||||
|
return updateD1CaseArchiveState(db, caseId, false);
|
||||||
|
},
|
||||||
|
async markOptimizationCaseFailed(caseId, input) {
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`update optimization_cases set
|
||||||
|
status = 'failed',
|
||||||
|
last_error_stage = ?,
|
||||||
|
last_error_summary = ?,
|
||||||
|
updated_at = ?
|
||||||
|
where id = ?`,
|
||||||
|
)
|
||||||
|
.bind(input.error_stage, input.error_summary, nowIso(), caseId)
|
||||||
|
.run();
|
||||||
|
const row = await db
|
||||||
|
.prepare("select * from optimization_cases where id = ?")
|
||||||
|
.bind(caseId)
|
||||||
|
.first<OptimizationCaseRow>();
|
||||||
|
return row ? toOptimizationCase(row) : null;
|
||||||
|
},
|
||||||
|
async createOptimizationResultVersion(input) {
|
||||||
|
const latest = await db
|
||||||
|
.prepare(
|
||||||
|
"select max(version) as version from optimization_result_versions where case_id = ?",
|
||||||
|
)
|
||||||
|
.bind(input.case_id)
|
||||||
|
.first<{ version: number | null }>();
|
||||||
|
const createdAt = nowIso();
|
||||||
|
const resultVersion: OptimizationResultVersion = {
|
||||||
|
id: `ver_${nanoid(10)}`,
|
||||||
|
...input,
|
||||||
|
version: (latest?.version ?? 0) + 1,
|
||||||
|
created_at: createdAt,
|
||||||
|
};
|
||||||
|
await 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
resultVersion.id,
|
||||||
|
resultVersion.case_id,
|
||||||
|
resultVersion.case_type,
|
||||||
|
resultVersion.version,
|
||||||
|
resultVersion.status,
|
||||||
|
resultVersion.article_job_id,
|
||||||
|
resultVersion.article_revision,
|
||||||
|
resultVersion.result_summary,
|
||||||
|
resultVersion.payload ? serialize(resultVersion.payload) : "",
|
||||||
|
serialize(resultVersion.process_summary),
|
||||||
|
serialize(resultVersion.llm_audit_summary),
|
||||||
|
resultVersion.error_stage,
|
||||||
|
resultVersion.error_summary,
|
||||||
|
resultVersion.created_at,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`update optimization_cases set
|
||||||
|
status = ?,
|
||||||
|
result_excerpt = ?,
|
||||||
|
latest_result_version_id = ?,
|
||||||
|
latest_version_number = ?,
|
||||||
|
last_error_stage = ?,
|
||||||
|
last_error_summary = ?,
|
||||||
|
updated_at = ?
|
||||||
|
where id = ?`,
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
input.status === "optimized" ? "optimized" : "failed",
|
||||||
|
input.result_summary,
|
||||||
|
resultVersion.id,
|
||||||
|
resultVersion.version,
|
||||||
|
input.error_stage,
|
||||||
|
input.error_summary,
|
||||||
|
createdAt,
|
||||||
|
input.case_id,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
return resultVersion;
|
||||||
|
},
|
||||||
|
async getOptimizationResultVersion(versionId) {
|
||||||
|
const row = await db
|
||||||
|
.prepare("select * from optimization_result_versions where id = ?")
|
||||||
|
.bind(versionId)
|
||||||
|
.first<OptimizationResultVersionRow>();
|
||||||
|
return row ? toOptimizationResultVersion(row) : null;
|
||||||
|
},
|
||||||
|
async findResultVersionForArticleRevision(jobId, revision) {
|
||||||
|
const row = await db
|
||||||
|
.prepare(
|
||||||
|
`select * from optimization_result_versions
|
||||||
|
where article_job_id = ? and article_revision = ?
|
||||||
|
order by version desc
|
||||||
|
limit 1`,
|
||||||
|
)
|
||||||
|
.bind(jobId, revision)
|
||||||
|
.first<OptimizationResultVersionRow>();
|
||||||
|
return row ? toOptimizationResultVersion(row) : null;
|
||||||
|
},
|
||||||
async saveRubricVersion(rubric) {
|
async saveRubricVersion(rubric) {
|
||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -385,12 +834,14 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`insert into scoring_runs (
|
`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
|
composite_score, rationale, created_at
|
||||||
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
run.id,
|
run.id,
|
||||||
|
run.result_version_id ?? null,
|
||||||
|
run.case_type ?? "article",
|
||||||
run.job_id,
|
run.job_id,
|
||||||
run.revision,
|
run.revision,
|
||||||
run.rubric_version_id,
|
run.rubric_version_id,
|
||||||
@@ -414,26 +865,43 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
.first<ScoringRunRow>();
|
.first<ScoringRunRow>();
|
||||||
return row ? toScoringRun(row) : null;
|
return row ? toScoringRun(row) : null;
|
||||||
},
|
},
|
||||||
|
async getLatestScoringRunForResultVersion(resultVersionId) {
|
||||||
|
const row = await db
|
||||||
|
.prepare(
|
||||||
|
`select * from scoring_runs
|
||||||
|
where result_version_id = ?
|
||||||
|
order by created_at desc
|
||||||
|
limit 1`,
|
||||||
|
)
|
||||||
|
.bind(resultVersionId)
|
||||||
|
.first<ScoringRunRow>();
|
||||||
|
return row ? toScoringRun(row) : null;
|
||||||
|
},
|
||||||
async createPublicationRecord(input) {
|
async createPublicationRecord(input) {
|
||||||
const timestamp = nowIso();
|
const timestamp = nowIso();
|
||||||
|
const publishTarget = input.publish_target ?? input.platform ?? "未指定";
|
||||||
const record: PublicationRecord = {
|
const record: PublicationRecord = {
|
||||||
id: `pub_${nanoid(10)}`,
|
id: `pub_${nanoid(10)}`,
|
||||||
...input,
|
...input,
|
||||||
|
result_version_id: input.result_version_id ?? null,
|
||||||
|
publish_target: publishTarget,
|
||||||
|
platform: isPublishPlatform(publishTarget) ? publishTarget : input.platform,
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
updated_at: timestamp,
|
updated_at: timestamp,
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`insert into publication_records (
|
`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
|
notes, created_at, updated_at
|
||||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
record.id,
|
record.id,
|
||||||
|
record.result_version_id,
|
||||||
record.job_id,
|
record.job_id,
|
||||||
record.revision,
|
record.revision,
|
||||||
record.platform,
|
record.publish_target,
|
||||||
record.url,
|
record.url,
|
||||||
record.published_at,
|
record.published_at,
|
||||||
record.status,
|
record.status,
|
||||||
@@ -451,13 +919,23 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
)
|
)
|
||||||
.bind(jobId)
|
.bind(jobId)
|
||||||
.all<PublicationRecordRow>();
|
.all<PublicationRecordRow>();
|
||||||
return result.results;
|
return result.results.map(toPublicationRecord);
|
||||||
|
},
|
||||||
|
async listPublicationRecordsForResultVersion(resultVersionId) {
|
||||||
|
const result = await db
|
||||||
|
.prepare(
|
||||||
|
"select * from publication_records where result_version_id = ? order by published_at desc",
|
||||||
|
)
|
||||||
|
.bind(resultVersionId)
|
||||||
|
.all<PublicationRecordRow>();
|
||||||
|
return result.results.map(toPublicationRecord);
|
||||||
},
|
},
|
||||||
async getPublicationRecord(id) {
|
async getPublicationRecord(id) {
|
||||||
return db
|
const row = await db
|
||||||
.prepare("select * from publication_records where id = ?")
|
.prepare("select * from publication_records where id = ?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.first<PublicationRecordRow>();
|
.first<PublicationRecordRow>();
|
||||||
|
return row ? toPublicationRecord(row) : null;
|
||||||
},
|
},
|
||||||
async savePerformanceSnapshot(snapshot) {
|
async savePerformanceSnapshot(snapshot) {
|
||||||
await db
|
await db
|
||||||
|
|||||||
+546
-15
@@ -7,6 +7,16 @@ import type {
|
|||||||
RubricVersion,
|
RubricVersion,
|
||||||
ScoringRun,
|
ScoringRun,
|
||||||
} from "../calibration/types";
|
} from "../calibration/types";
|
||||||
|
import type {
|
||||||
|
CaseInput,
|
||||||
|
CaseListFilters,
|
||||||
|
CaseMetadataPatch,
|
||||||
|
OptimizationCase,
|
||||||
|
OptimizationCaseStatus,
|
||||||
|
OptimizationCaseType,
|
||||||
|
OptimizationResultVersion,
|
||||||
|
ResultVersionStatus,
|
||||||
|
} from "../cases/types";
|
||||||
import type {
|
import type {
|
||||||
OptimizationFactCard,
|
OptimizationFactCard,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
@@ -43,6 +53,7 @@ export type NewBrandTemplate = Omit<
|
|||||||
export interface ArticleJob {
|
export interface ArticleJob {
|
||||||
id: string;
|
id: string;
|
||||||
brand_template_id: string | null;
|
brand_template_id: string | null;
|
||||||
|
case_id: string | null;
|
||||||
source_title: string;
|
source_title: string;
|
||||||
source_body: string;
|
source_body: string;
|
||||||
image_inputs: ImageInput[];
|
image_inputs: ImageInput[];
|
||||||
@@ -56,6 +67,7 @@ export interface ArticleJob {
|
|||||||
|
|
||||||
export interface NewArticleJob {
|
export interface NewArticleJob {
|
||||||
brand_template_id?: string | null;
|
brand_template_id?: string | null;
|
||||||
|
case_id?: string | null;
|
||||||
source_title: string;
|
source_title: string;
|
||||||
source_body: string;
|
source_body: string;
|
||||||
image_inputs: ImageInput[];
|
image_inputs: ImageInput[];
|
||||||
@@ -81,6 +93,7 @@ interface BrandTemplateRow {
|
|||||||
interface ArticleJobRow {
|
interface ArticleJobRow {
|
||||||
id: string;
|
id: string;
|
||||||
brand_template_id: string | null;
|
brand_template_id: string | null;
|
||||||
|
case_id: string | null;
|
||||||
source_title: string;
|
source_title: string;
|
||||||
source_body: string;
|
source_body: string;
|
||||||
image_inputs: string;
|
image_inputs: string;
|
||||||
@@ -111,8 +124,10 @@ interface QaReportRow {
|
|||||||
|
|
||||||
interface ScoringRunRow {
|
interface ScoringRunRow {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id: string | null;
|
||||||
revision: number;
|
case_type: OptimizationCaseType;
|
||||||
|
job_id: string | null;
|
||||||
|
revision: number | null;
|
||||||
rubric_version_id: string;
|
rubric_version_id: string;
|
||||||
dimension_scores: string;
|
dimension_scores: string;
|
||||||
composite_score: number;
|
composite_score: number;
|
||||||
@@ -122,9 +137,10 @@ interface ScoringRunRow {
|
|||||||
|
|
||||||
interface PublicationRecordRow {
|
interface PublicationRecordRow {
|
||||||
id: string;
|
id: string;
|
||||||
job_id: string;
|
result_version_id: string | null;
|
||||||
revision: number;
|
job_id: string | null;
|
||||||
platform: PublishPlatform;
|
revision: number | null;
|
||||||
|
publish_target: string;
|
||||||
url: string;
|
url: string;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
status: "draft" | "published" | "archived";
|
status: "draft" | "published" | "archived";
|
||||||
@@ -144,6 +160,54 @@ interface PerformanceSnapshotRow {
|
|||||||
snapshot_at: string;
|
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() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
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 {
|
function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
|
||||||
return {
|
return {
|
||||||
...row,
|
...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(
|
export function createBrandTemplate(
|
||||||
dbPath: string | undefined,
|
dbPath: string | undefined,
|
||||||
input: NewBrandTemplate,
|
input: NewBrandTemplate,
|
||||||
@@ -264,6 +377,7 @@ export function createArticleJob(dbPath: string | undefined, input: NewArticleJo
|
|||||||
const job: ArticleJob = {
|
const job: ArticleJob = {
|
||||||
id: `job_${nanoid(10)}`,
|
id: `job_${nanoid(10)}`,
|
||||||
brand_template_id: input.brand_template_id ?? null,
|
brand_template_id: input.brand_template_id ?? null,
|
||||||
|
case_id: input.case_id ?? null,
|
||||||
source_title: input.source_title,
|
source_title: input.source_title,
|
||||||
source_body: input.source_body,
|
source_body: input.source_body,
|
||||||
image_inputs: input.image_inputs,
|
image_inputs: input.image_inputs,
|
||||||
@@ -277,10 +391,10 @@ export function createArticleJob(dbPath: string | undefined, input: NewArticleJo
|
|||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`insert into article_jobs (
|
`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
|
publish_platform, user_instructions, status, export_paths, created_at, updated_at
|
||||||
) values (
|
) 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
|
@publish_platform, @user_instructions, @status, @export_paths, @created_at, @updated_at
|
||||||
)`,
|
)`,
|
||||||
).run({
|
).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(
|
export function saveRubricVersion(
|
||||||
dbPath: string | undefined,
|
dbPath: string | undefined,
|
||||||
rubric: RubricVersion,
|
rubric: RubricVersion,
|
||||||
@@ -472,11 +959,13 @@ export function saveScoringRun(dbPath: string | undefined, run: ScoringRun) {
|
|||||||
return withDb(dbPath, (db) => {
|
return withDb(dbPath, (db) => {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`insert into scoring_runs (
|
`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
|
composite_score, rationale, created_at
|
||||||
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
).run(
|
).run(
|
||||||
run.id,
|
run.id,
|
||||||
|
run.result_version_id ?? null,
|
||||||
|
run.case_type ?? "article",
|
||||||
run.job_id,
|
run.job_id,
|
||||||
run.revision,
|
run.revision,
|
||||||
run.rubric_version_id,
|
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(
|
export function createPublicationRecord(
|
||||||
dbPath: string | undefined,
|
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) => {
|
return withDb(dbPath, (db) => {
|
||||||
const timestamp = nowIso();
|
const timestamp = nowIso();
|
||||||
|
const publishTarget = input.publish_target ?? input.platform ?? "未指定";
|
||||||
const record: PublicationRecord = {
|
const record: PublicationRecord = {
|
||||||
id: `pub_${nanoid(10)}`,
|
id: `pub_${nanoid(10)}`,
|
||||||
...input,
|
...input,
|
||||||
|
result_version_id: input.result_version_id ?? null,
|
||||||
|
publish_target: publishTarget,
|
||||||
|
platform: isPublishPlatform(publishTarget) ? publishTarget : input.platform,
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
updated_at: timestamp,
|
updated_at: timestamp,
|
||||||
};
|
};
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`insert into publication_records (
|
`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
|
notes, created_at, updated_at
|
||||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
).run(
|
).run(
|
||||||
record.id,
|
record.id,
|
||||||
|
record.result_version_id,
|
||||||
record.job_id,
|
record.job_id,
|
||||||
record.revision,
|
record.revision,
|
||||||
record.platform,
|
record.publish_target,
|
||||||
record.url,
|
record.url,
|
||||||
record.published_at,
|
record.published_at,
|
||||||
record.status,
|
record.status,
|
||||||
@@ -545,7 +1062,21 @@ export function listPublicationRecords(dbPath: string | undefined, jobId: string
|
|||||||
db
|
db
|
||||||
.prepare("select * from publication_records where job_id = ? order by published_at desc")
|
.prepare("select * from publication_records where job_id = ? order by published_at desc")
|
||||||
.all(jobId)
|
.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
|
const row = db
|
||||||
.prepare("select * from publication_records where id = ?")
|
.prepare("select * from publication_records where id = ?")
|
||||||
.get(id) as PublicationRecordRow | undefined;
|
.get(id) as PublicationRecordRow | undefined;
|
||||||
return row ?? null;
|
return row ? toPublicationRecord(row) : null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import type {
|
|||||||
RubricVersion,
|
RubricVersion,
|
||||||
ScoringRun,
|
ScoringRun,
|
||||||
} from "../calibration/types";
|
} from "../calibration/types";
|
||||||
|
import type {
|
||||||
|
CaseInput,
|
||||||
|
CaseListFilters,
|
||||||
|
CaseMetadataPatch,
|
||||||
|
OptimizationCase,
|
||||||
|
OptimizationCaseDetail,
|
||||||
|
OptimizationCaseType,
|
||||||
|
OptimizationResultVersion,
|
||||||
|
} from "../cases/types";
|
||||||
import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
||||||
import type {
|
import type {
|
||||||
ArticleJob,
|
ArticleJob,
|
||||||
@@ -38,16 +47,55 @@ export interface AppRepository {
|
|||||||
saveRubricVersion(rubric: RubricVersion): Promise<RubricVersion>;
|
saveRubricVersion(rubric: RubricVersion): Promise<RubricVersion>;
|
||||||
saveScoringRun(run: ScoringRun): Promise<ScoringRun>;
|
saveScoringRun(run: ScoringRun): Promise<ScoringRun>;
|
||||||
getLatestScoringRun(jobId: string, revision: number): Promise<ScoringRun | null>;
|
getLatestScoringRun(jobId: string, revision: number): Promise<ScoringRun | null>;
|
||||||
|
getLatestScoringRunForResultVersion(resultVersionId: string): Promise<ScoringRun | null>;
|
||||||
createPublicationRecord(
|
createPublicationRecord(
|
||||||
input: Omit<PublicationRecord, "id" | "created_at" | "updated_at">,
|
input: NewPublicationRecord,
|
||||||
): Promise<PublicationRecord>;
|
): Promise<PublicationRecord>;
|
||||||
listPublicationRecords(jobId: string): Promise<PublicationRecord[]>;
|
listPublicationRecords(jobId: string): Promise<PublicationRecord[]>;
|
||||||
|
listPublicationRecordsForResultVersion(resultVersionId: string): Promise<PublicationRecord[]>;
|
||||||
getPublicationRecord(id: string): Promise<PublicationRecord | null>;
|
getPublicationRecord(id: string): Promise<PublicationRecord | null>;
|
||||||
savePerformanceSnapshot(snapshot: PerformanceSnapshot): Promise<PerformanceSnapshot>;
|
savePerformanceSnapshot(snapshot: PerformanceSnapshot): Promise<PerformanceSnapshot>;
|
||||||
listPerformanceSnapshots(publicationId: string): Promise<PerformanceSnapshot[]>;
|
listPerformanceSnapshots(publicationId: string): Promise<PerformanceSnapshot[]>;
|
||||||
saveCalibrationEvent(event: CalibrationEvent): Promise<CalibrationEvent>;
|
saveCalibrationEvent(event: CalibrationEvent): Promise<CalibrationEvent>;
|
||||||
|
createOptimizationCase(input: {
|
||||||
|
case_type: OptimizationCaseType;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
publish_target: string;
|
||||||
|
source_excerpt: string;
|
||||||
|
}): Promise<OptimizationCase>;
|
||||||
|
saveCaseInput(input: Omit<CaseInput, "created_at" | "updated_at">): Promise<CaseInput>;
|
||||||
|
listOptimizationCases(filters: CaseListFilters): Promise<OptimizationCase[]>;
|
||||||
|
getOptimizationCaseDetail(caseId: string): Promise<OptimizationCaseDetail | null>;
|
||||||
|
updateOptimizationCaseMetadata(
|
||||||
|
caseId: string,
|
||||||
|
changes: CaseMetadataPatch,
|
||||||
|
): Promise<OptimizationCase | null>;
|
||||||
|
archiveOptimizationCase(caseId: string): Promise<OptimizationCase | null>;
|
||||||
|
restoreOptimizationCase(caseId: string): Promise<OptimizationCase | null>;
|
||||||
|
markOptimizationCaseFailed(
|
||||||
|
caseId: string,
|
||||||
|
input: { error_stage: string; error_summary: string },
|
||||||
|
): Promise<OptimizationCase | null>;
|
||||||
|
createOptimizationResultVersion(
|
||||||
|
input: Omit<OptimizationResultVersion, "id" | "version" | "created_at">,
|
||||||
|
): Promise<OptimizationResultVersion>;
|
||||||
|
getOptimizationResultVersion(versionId: string): Promise<OptimizationResultVersion | null>;
|
||||||
|
findResultVersionForArticleRevision(
|
||||||
|
jobId: string,
|
||||||
|
revision: number,
|
||||||
|
): Promise<OptimizationResultVersion | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NewPublicationRecord = Omit<
|
||||||
|
PublicationRecord,
|
||||||
|
"id" | "created_at" | "updated_at" | "result_version_id" | "publish_target" | "platform"
|
||||||
|
> & {
|
||||||
|
result_version_id?: string | null;
|
||||||
|
publish_target?: string;
|
||||||
|
platform?: PublicationRecord["platform"];
|
||||||
|
};
|
||||||
|
|
||||||
interface RuntimeRepositoryOptions {
|
interface RuntimeRepositoryOptions {
|
||||||
appRuntime?: string;
|
appRuntime?: string;
|
||||||
dbPath?: string;
|
dbPath?: string;
|
||||||
|
|||||||
+84
-9
@@ -20,6 +20,7 @@ export function initializeSchema(db: Database.Database) {
|
|||||||
create table if not exists article_jobs (
|
create table if not exists article_jobs (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
brand_template_id text,
|
brand_template_id text,
|
||||||
|
case_id text,
|
||||||
source_title text not null,
|
source_title text not null,
|
||||||
source_body text not null,
|
source_body text not null,
|
||||||
image_inputs text not null,
|
image_inputs text not null,
|
||||||
@@ -32,6 +33,39 @@ export function initializeSchema(db: Database.Database) {
|
|||||||
foreign key (brand_template_id) references brand_templates(id)
|
foreign key (brand_template_id) references brand_templates(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table if not exists optimization_cases (
|
||||||
|
id text primary key,
|
||||||
|
case_type text not null,
|
||||||
|
title text not null,
|
||||||
|
summary text not null,
|
||||||
|
status text not null,
|
||||||
|
customer_name text not null default '',
|
||||||
|
brand_name text not null default '',
|
||||||
|
project_tags text not null default '[]',
|
||||||
|
notes text not null default '',
|
||||||
|
publish_target text not null default '',
|
||||||
|
source_excerpt text not null default '',
|
||||||
|
result_excerpt text not null default '',
|
||||||
|
latest_result_version_id text,
|
||||||
|
latest_version_number integer,
|
||||||
|
last_error_stage text,
|
||||||
|
last_error_summary text,
|
||||||
|
archived_at text,
|
||||||
|
created_at text not null,
|
||||||
|
updated_at text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists case_inputs (
|
||||||
|
case_id text primary key,
|
||||||
|
case_type text not null,
|
||||||
|
article_job_id text,
|
||||||
|
payload text not null,
|
||||||
|
created_at text not null,
|
||||||
|
updated_at text not null,
|
||||||
|
foreign key (case_id) references optimization_cases(id) on delete cascade,
|
||||||
|
foreign key (article_job_id) references article_jobs(id) on delete set null
|
||||||
|
);
|
||||||
|
|
||||||
create table if not exists fact_cards (
|
create table if not exists fact_cards (
|
||||||
job_id text primary key,
|
job_id text primary key,
|
||||||
source text not null,
|
source text not null,
|
||||||
@@ -61,6 +95,26 @@ export function initializeSchema(db: Database.Database) {
|
|||||||
references optimized_articles(job_id, revision) on delete cascade
|
references optimized_articles(job_id, revision) on delete cascade
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table if not exists optimization_result_versions (
|
||||||
|
id text primary key,
|
||||||
|
case_id text not null,
|
||||||
|
case_type text not null,
|
||||||
|
version integer not null,
|
||||||
|
status text not null,
|
||||||
|
article_job_id text,
|
||||||
|
article_revision integer,
|
||||||
|
result_summary text not null,
|
||||||
|
payload text not null,
|
||||||
|
process_summary text not null,
|
||||||
|
llm_audit_summary text not null,
|
||||||
|
error_stage text,
|
||||||
|
error_summary text,
|
||||||
|
created_at text not null,
|
||||||
|
unique (case_id, version),
|
||||||
|
foreign key (case_id) references optimization_cases(id) on delete cascade,
|
||||||
|
foreign key (article_job_id) references article_jobs(id) on delete set null
|
||||||
|
);
|
||||||
|
|
||||||
create table if not exists rubric_versions (
|
create table if not exists rubric_versions (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
version text not null,
|
version text not null,
|
||||||
@@ -73,31 +127,34 @@ export function initializeSchema(db: Database.Database) {
|
|||||||
|
|
||||||
create table if not exists scoring_runs (
|
create table if not exists scoring_runs (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
job_id text not null,
|
result_version_id text,
|
||||||
revision integer not null,
|
case_type text not null default 'article',
|
||||||
|
job_id text,
|
||||||
|
revision integer,
|
||||||
rubric_version_id text not null,
|
rubric_version_id text not null,
|
||||||
dimension_scores text not null,
|
dimension_scores text not null,
|
||||||
composite_score real not null,
|
composite_score real not null,
|
||||||
rationale text not null,
|
rationale text not null,
|
||||||
created_at text not null,
|
created_at text not null,
|
||||||
foreign key (job_id, revision)
|
foreign key (result_version_id)
|
||||||
references optimized_articles(job_id, revision) on delete cascade,
|
references optimization_result_versions(id) on delete cascade,
|
||||||
foreign key (rubric_version_id) references rubric_versions(id)
|
foreign key (rubric_version_id) references rubric_versions(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table if not exists publication_records (
|
create table if not exists publication_records (
|
||||||
id text primary key,
|
id text primary key,
|
||||||
job_id text not null,
|
result_version_id text,
|
||||||
revision integer not null,
|
job_id text,
|
||||||
platform text not null,
|
revision integer,
|
||||||
|
publish_target text not null,
|
||||||
url text not null,
|
url text not null,
|
||||||
published_at text not null,
|
published_at text not null,
|
||||||
status text not null,
|
status text not null,
|
||||||
notes text not null,
|
notes text not null,
|
||||||
created_at text not null,
|
created_at text not null,
|
||||||
updated_at text not null,
|
updated_at text not null,
|
||||||
foreign key (job_id, revision)
|
foreign key (result_version_id)
|
||||||
references optimized_articles(job_id, revision) on delete cascade
|
references optimization_result_versions(id) on delete cascade
|
||||||
);
|
);
|
||||||
|
|
||||||
create table if not exists performance_snapshots (
|
create table if not exists performance_snapshots (
|
||||||
@@ -130,9 +187,27 @@ export function initializeSchema(db: Database.Database) {
|
|||||||
create index if not exists idx_scoring_runs_job_revision
|
create index if not exists idx_scoring_runs_job_revision
|
||||||
on scoring_runs(job_id, revision);
|
on scoring_runs(job_id, revision);
|
||||||
|
|
||||||
|
create index if not exists idx_scoring_runs_result_version
|
||||||
|
on scoring_runs(result_version_id);
|
||||||
|
|
||||||
|
create index if not exists idx_optimization_cases_updated_at
|
||||||
|
on optimization_cases(updated_at);
|
||||||
|
|
||||||
|
create index if not exists idx_optimization_cases_case_type
|
||||||
|
on optimization_cases(case_type);
|
||||||
|
|
||||||
|
create index if not exists idx_optimization_cases_status
|
||||||
|
on optimization_cases(status);
|
||||||
|
|
||||||
|
create index if not exists idx_optimization_result_versions_case
|
||||||
|
on optimization_result_versions(case_id, version desc);
|
||||||
|
|
||||||
create index if not exists idx_publication_records_job_revision
|
create index if not exists idx_publication_records_job_revision
|
||||||
on publication_records(job_id, revision);
|
on publication_records(job_id, revision);
|
||||||
|
|
||||||
|
create index if not exists idx_publication_records_result_version
|
||||||
|
on publication_records(result_version_id);
|
||||||
|
|
||||||
create index if not exists idx_performance_snapshots_publication
|
create index if not exists idx_performance_snapshots_publication
|
||||||
on performance_snapshots(publication_id);
|
on performance_snapshots(publication_id);
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
import type { OptimizationFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
||||||
import type { AppRepository } from "./repository";
|
import type { AppRepository } from "./repository";
|
||||||
import {
|
import {
|
||||||
|
archiveOptimizationCase,
|
||||||
createArticleJob,
|
createArticleJob,
|
||||||
createBrandTemplate,
|
createBrandTemplate,
|
||||||
|
createOptimizationCase,
|
||||||
|
createOptimizationResultVersion,
|
||||||
createPublicationRecord,
|
createPublicationRecord,
|
||||||
getArticleJob,
|
getArticleJob,
|
||||||
getBrandTemplate,
|
getBrandTemplate,
|
||||||
@@ -10,10 +13,19 @@ import {
|
|||||||
getLatestOptimizedArticle,
|
getLatestOptimizedArticle,
|
||||||
getLatestQaReport,
|
getLatestQaReport,
|
||||||
getLatestScoringRun,
|
getLatestScoringRun,
|
||||||
|
getLatestScoringRunForResultVersion,
|
||||||
|
getOptimizationCaseDetail,
|
||||||
|
getOptimizationResultVersion,
|
||||||
getPublicationRecord,
|
getPublicationRecord,
|
||||||
|
findResultVersionForArticleRevision,
|
||||||
listBrandTemplates,
|
listBrandTemplates,
|
||||||
|
listOptimizationCases,
|
||||||
listPerformanceSnapshots,
|
listPerformanceSnapshots,
|
||||||
listPublicationRecords,
|
listPublicationRecords,
|
||||||
|
listPublicationRecordsForResultVersion,
|
||||||
|
markOptimizationCaseFailed,
|
||||||
|
restoreOptimizationCase,
|
||||||
|
saveCaseInput,
|
||||||
saveCalibrationEvent,
|
saveCalibrationEvent,
|
||||||
saveFactCard,
|
saveFactCard,
|
||||||
saveOptimizedArticle,
|
saveOptimizedArticle,
|
||||||
@@ -21,6 +33,7 @@ import {
|
|||||||
saveQaReport,
|
saveQaReport,
|
||||||
saveRubricVersion,
|
saveRubricVersion,
|
||||||
saveScoringRun,
|
saveScoringRun,
|
||||||
|
updateOptimizationCaseMetadata,
|
||||||
updateArticleJob,
|
updateArticleJob,
|
||||||
type ArticleJob,
|
type ArticleJob,
|
||||||
type NewArticleJob,
|
type NewArticleJob,
|
||||||
@@ -77,12 +90,18 @@ export function createSqliteRepository(dbPath?: string): AppRepository {
|
|||||||
getLatestScoringRun(jobId, revision) {
|
getLatestScoringRun(jobId, revision) {
|
||||||
return Promise.resolve(getLatestScoringRun(dbPath, jobId, revision));
|
return Promise.resolve(getLatestScoringRun(dbPath, jobId, revision));
|
||||||
},
|
},
|
||||||
|
getLatestScoringRunForResultVersion(resultVersionId) {
|
||||||
|
return Promise.resolve(getLatestScoringRunForResultVersion(dbPath, resultVersionId));
|
||||||
|
},
|
||||||
createPublicationRecord(input) {
|
createPublicationRecord(input) {
|
||||||
return Promise.resolve(createPublicationRecord(dbPath, input));
|
return Promise.resolve(createPublicationRecord(dbPath, input));
|
||||||
},
|
},
|
||||||
listPublicationRecords(jobId) {
|
listPublicationRecords(jobId) {
|
||||||
return Promise.resolve(listPublicationRecords(dbPath, jobId));
|
return Promise.resolve(listPublicationRecords(dbPath, jobId));
|
||||||
},
|
},
|
||||||
|
listPublicationRecordsForResultVersion(resultVersionId) {
|
||||||
|
return Promise.resolve(listPublicationRecordsForResultVersion(dbPath, resultVersionId));
|
||||||
|
},
|
||||||
getPublicationRecord(id) {
|
getPublicationRecord(id) {
|
||||||
return Promise.resolve(getPublicationRecord(dbPath, id));
|
return Promise.resolve(getPublicationRecord(dbPath, id));
|
||||||
},
|
},
|
||||||
@@ -95,5 +114,38 @@ export function createSqliteRepository(dbPath?: string): AppRepository {
|
|||||||
saveCalibrationEvent(event) {
|
saveCalibrationEvent(event) {
|
||||||
return Promise.resolve(saveCalibrationEvent(dbPath, event));
|
return Promise.resolve(saveCalibrationEvent(dbPath, event));
|
||||||
},
|
},
|
||||||
|
createOptimizationCase(input) {
|
||||||
|
return Promise.resolve(createOptimizationCase(dbPath, input));
|
||||||
|
},
|
||||||
|
saveCaseInput(input) {
|
||||||
|
return Promise.resolve(saveCaseInput(dbPath, input));
|
||||||
|
},
|
||||||
|
listOptimizationCases(filters) {
|
||||||
|
return Promise.resolve(listOptimizationCases(dbPath, filters));
|
||||||
|
},
|
||||||
|
getOptimizationCaseDetail(caseId) {
|
||||||
|
return Promise.resolve(getOptimizationCaseDetail(dbPath, caseId));
|
||||||
|
},
|
||||||
|
updateOptimizationCaseMetadata(caseId, changes) {
|
||||||
|
return Promise.resolve(updateOptimizationCaseMetadata(dbPath, caseId, changes));
|
||||||
|
},
|
||||||
|
archiveOptimizationCase(caseId) {
|
||||||
|
return Promise.resolve(archiveOptimizationCase(dbPath, caseId));
|
||||||
|
},
|
||||||
|
restoreOptimizationCase(caseId) {
|
||||||
|
return Promise.resolve(restoreOptimizationCase(dbPath, caseId));
|
||||||
|
},
|
||||||
|
markOptimizationCaseFailed(caseId, input) {
|
||||||
|
return Promise.resolve(markOptimizationCaseFailed(dbPath, caseId, input));
|
||||||
|
},
|
||||||
|
createOptimizationResultVersion(input) {
|
||||||
|
return Promise.resolve(createOptimizationResultVersion(dbPath, input));
|
||||||
|
},
|
||||||
|
getOptimizationResultVersion(versionId) {
|
||||||
|
return Promise.resolve(getOptimizationResultVersion(dbPath, versionId));
|
||||||
|
},
|
||||||
|
findResultVersionForArticleRevision(jobId, revision) {
|
||||||
|
return Promise.resolve(findResultVersionForArticleRevision(dbPath, jobId, revision));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user