feat: 搭建阶段 0 与阶段 1 基础工程
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@cross-ai/domain",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --dts --format esm,cjs --clean",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "tsc -p tsconfig.json --noEmit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export const platforms = ["tmall", "jd"] as const;
|
||||
export type PlatformId = (typeof platforms)[number];
|
||||
|
||||
export const searchRequirements = ["none", "recommended", "required"] as const;
|
||||
export type SearchRequirement = (typeof searchRequirements)[number];
|
||||
|
||||
export const taskStatuses = [
|
||||
"Draft",
|
||||
"Searching",
|
||||
"AwaitingConfirmation",
|
||||
"NoSelection",
|
||||
"Running",
|
||||
"Completed",
|
||||
"PartialCompleted",
|
||||
"Blocked",
|
||||
"Failed"
|
||||
] as const;
|
||||
export type TaskStatus = (typeof taskStatuses)[number];
|
||||
|
||||
export const taskStages = [
|
||||
"precheck",
|
||||
"search",
|
||||
"confirmation",
|
||||
"session_check",
|
||||
"crawl",
|
||||
"normalize",
|
||||
"analyze",
|
||||
"publish"
|
||||
] as const;
|
||||
export type TaskStage = (typeof taskStages)[number];
|
||||
|
||||
export const platformStatuses = [
|
||||
"Pending",
|
||||
"SearchBlocked",
|
||||
"Searching",
|
||||
"NoResult",
|
||||
"AwaitingSelection",
|
||||
"Skipped",
|
||||
"Selected",
|
||||
"Blocked",
|
||||
"Running",
|
||||
"Completed",
|
||||
"Failed"
|
||||
] as const;
|
||||
export type PlatformStatus = (typeof platformStatuses)[number];
|
||||
|
||||
export const executionStatuses = [
|
||||
"completed",
|
||||
"blocked",
|
||||
"failed",
|
||||
"skipped",
|
||||
"no_result"
|
||||
] as const;
|
||||
export type ExecutionStatus = (typeof executionStatuses)[number];
|
||||
|
||||
export const reportableTaskStatuses = ["Completed", "PartialCompleted"] as const;
|
||||
export type ReportableTaskStatus = (typeof reportableTaskStatuses)[number];
|
||||
|
||||
export const confidenceLevels = ["high", "medium", "low"] as const;
|
||||
export type ConfidenceLevel = (typeof confidenceLevels)[number];
|
||||
|
||||
export const sampleFlags = ["sufficient", "insufficient", "partial"] as const;
|
||||
export type SampleFlag = (typeof sampleFlags)[number];
|
||||
|
||||
export const evidenceSourceTypes = ["product", "review"] as const;
|
||||
export type EvidenceSourceType = (typeof evidenceSourceTypes)[number];
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./enums";
|
||||
export * from "./models";
|
||||
export * from "./platforms";
|
||||
export * from "./presentation";
|
||||
export * from "./state-machine";
|
||||
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
PlatformId,
|
||||
PlatformStatus,
|
||||
SearchRequirement,
|
||||
TaskStage,
|
||||
TaskStatus
|
||||
} from "./enums";
|
||||
|
||||
export interface CreateTaskInput {
|
||||
query: string;
|
||||
perLinkLimit: number;
|
||||
taskTotalLimit: number;
|
||||
}
|
||||
|
||||
export interface CandidateRecord {
|
||||
candidateId: string;
|
||||
platform: PlatformId;
|
||||
title: string;
|
||||
price: number;
|
||||
priceLabel: string;
|
||||
storeName: string;
|
||||
productUrl: string;
|
||||
imageUrl: string;
|
||||
salesHint: string;
|
||||
specLabel: string;
|
||||
highlights: string[];
|
||||
}
|
||||
|
||||
export interface PlatformRunRecord {
|
||||
platform: PlatformId;
|
||||
searchRequirement: SearchRequirement;
|
||||
status: PlatformStatus;
|
||||
reason?: string | undefined;
|
||||
candidateCount: number;
|
||||
selectedCandidateIds: string[];
|
||||
lastUpdatedAt: string;
|
||||
}
|
||||
|
||||
export interface TaskEventRecord {
|
||||
eventId: string;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SessionReadinessRecord {
|
||||
platform: PlatformId;
|
||||
ready: boolean;
|
||||
searchRequirement: SearchRequirement;
|
||||
lastPreparedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TaskRecord {
|
||||
taskId: string;
|
||||
query: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
perLinkLimit: number;
|
||||
taskTotalLimit: number;
|
||||
taskStatus: TaskStatus;
|
||||
taskStage: TaskStage;
|
||||
platformRuns: PlatformRunRecord[];
|
||||
platformCandidates: Record<PlatformId, CandidateRecord[]>;
|
||||
events: TaskEventRecord[];
|
||||
reportVersions: number[];
|
||||
defaultReportVersion?: number | undefined;
|
||||
latestSuccessfulReportVersion?: number | undefined;
|
||||
}
|
||||
|
||||
export interface HistoryTaskRecord {
|
||||
taskId: string;
|
||||
query: string;
|
||||
taskStatus: TaskStatus;
|
||||
updatedAt: string;
|
||||
hasReport: boolean;
|
||||
defaultReportVersion?: number | undefined;
|
||||
failedPlatforms: PlatformId[];
|
||||
blockedPlatforms: PlatformId[];
|
||||
}
|
||||
|
||||
export interface ConfirmTaskPayload {
|
||||
selections: Array<{
|
||||
platform: PlatformId;
|
||||
candidateIds: string[];
|
||||
}>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { PlatformId, SearchRequirement } from "./enums";
|
||||
|
||||
export interface PlatformCatalogEntry {
|
||||
id: PlatformId;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
description: string;
|
||||
searchRequirement: SearchRequirement;
|
||||
recoveryHint: string;
|
||||
}
|
||||
|
||||
export const platformCatalog = [
|
||||
{
|
||||
id: "tmall",
|
||||
label: "天猫",
|
||||
shortLabel: "TM",
|
||||
description: "推荐保持会话以提升搜索稳定性。",
|
||||
searchRequirement: "recommended",
|
||||
recoveryHint: "建议先预热会话,再执行候选搜索。"
|
||||
},
|
||||
{
|
||||
id: "jd",
|
||||
label: "京东",
|
||||
shortLabel: "JD",
|
||||
description: "搜索前必须具备有效会话。",
|
||||
searchRequirement: "required",
|
||||
recoveryHint: "需要先完成会话准备,否则系统会标记为 SearchBlocked。"
|
||||
}
|
||||
] as const satisfies readonly PlatformCatalogEntry[];
|
||||
|
||||
export const platformCatalogMap = Object.fromEntries(
|
||||
platformCatalog.map((entry) => [entry.id, entry])
|
||||
) as Record<PlatformId, PlatformCatalogEntry>;
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { PlatformStatus, TaskStatus } from "./enums";
|
||||
|
||||
export type StatusTone =
|
||||
| "neutral"
|
||||
| "info"
|
||||
| "progress"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "blocked"
|
||||
| "danger"
|
||||
| "empty";
|
||||
|
||||
export const taskStatusToneMap: Record<TaskStatus, StatusTone> = {
|
||||
Draft: "neutral",
|
||||
Searching: "info",
|
||||
AwaitingConfirmation: "info",
|
||||
NoSelection: "empty",
|
||||
Running: "progress",
|
||||
Completed: "success",
|
||||
PartialCompleted: "warning",
|
||||
Blocked: "blocked",
|
||||
Failed: "danger"
|
||||
};
|
||||
|
||||
export const platformStatusToneMap: Record<PlatformStatus, StatusTone> = {
|
||||
Pending: "neutral",
|
||||
SearchBlocked: "blocked",
|
||||
Searching: "info",
|
||||
NoResult: "empty",
|
||||
AwaitingSelection: "info",
|
||||
Skipped: "neutral",
|
||||
Selected: "progress",
|
||||
Blocked: "blocked",
|
||||
Running: "progress",
|
||||
Completed: "success",
|
||||
Failed: "danger"
|
||||
};
|
||||
|
||||
export const taskSpine = [
|
||||
{ id: "input", label: "输入" },
|
||||
{ id: "confirmation", label: "确认" },
|
||||
{ id: "execution", label: "执行" },
|
||||
{ id: "report", label: "报告" }
|
||||
] as const;
|
||||
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
ExecutionStatus,
|
||||
PlatformStatus,
|
||||
TaskStatus
|
||||
} from "./enums";
|
||||
import type { PlatformRunRecord } from "./models";
|
||||
|
||||
export function mapPlatformStatusToExecutionStatus(
|
||||
status: PlatformStatus
|
||||
): ExecutionStatus {
|
||||
switch (status) {
|
||||
case "Completed":
|
||||
return "completed";
|
||||
case "SearchBlocked":
|
||||
case "Blocked":
|
||||
return "blocked";
|
||||
case "Failed":
|
||||
return "failed";
|
||||
case "Skipped":
|
||||
return "skipped";
|
||||
case "NoResult":
|
||||
return "no_result";
|
||||
default:
|
||||
throw new Error(`Platform status ${status} cannot be published to a report.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveTaskStatusFromConfirmedPlatforms(
|
||||
platformRuns: PlatformRunRecord[]
|
||||
): TaskStatus {
|
||||
const confirmedRuns = platformRuns.filter(
|
||||
(run) => run.selectedCandidateIds.length > 0
|
||||
);
|
||||
|
||||
if (confirmedRuns.length === 0) {
|
||||
return "NoSelection";
|
||||
}
|
||||
|
||||
const completedCount = confirmedRuns.filter(
|
||||
(run) => run.status === "Completed"
|
||||
).length;
|
||||
const hasBlocked = confirmedRuns.some(
|
||||
(run) => run.status === "Blocked" || run.status === "SearchBlocked"
|
||||
);
|
||||
const hasFailed = confirmedRuns.some((run) => run.status === "Failed");
|
||||
|
||||
if (completedCount === confirmedRuns.length) {
|
||||
return "Completed";
|
||||
}
|
||||
|
||||
if (completedCount > 0 && (hasBlocked || hasFailed)) {
|
||||
return "PartialCompleted";
|
||||
}
|
||||
|
||||
if (completedCount === 0 && hasBlocked) {
|
||||
return "Blocked";
|
||||
}
|
||||
|
||||
return "Failed";
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
deriveTaskStatusFromConfirmedPlatforms,
|
||||
mapPlatformStatusToExecutionStatus,
|
||||
type PlatformRunRecord
|
||||
} from "../src/index";
|
||||
|
||||
function createRun(
|
||||
status: PlatformRunRecord["status"],
|
||||
selected = true
|
||||
): PlatformRunRecord {
|
||||
return {
|
||||
platform: "tmall",
|
||||
searchRequirement: "recommended",
|
||||
status,
|
||||
candidateCount: 1,
|
||||
selectedCandidateIds: selected ? ["candidate-1"] : [],
|
||||
lastUpdatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
describe("deriveTaskStatusFromConfirmedPlatforms", () => {
|
||||
it("returns NoSelection when no platform has confirmed links", () => {
|
||||
expect(
|
||||
deriveTaskStatusFromConfirmedPlatforms([createRun("Skipped", false)])
|
||||
).toBe("NoSelection");
|
||||
});
|
||||
|
||||
it("returns Completed when all confirmed platforms are completed", () => {
|
||||
expect(
|
||||
deriveTaskStatusFromConfirmedPlatforms([
|
||||
createRun("Completed"),
|
||||
createRun("Completed")
|
||||
])
|
||||
).toBe("Completed");
|
||||
});
|
||||
|
||||
it("returns PartialCompleted when at least one confirmed platform is completed and another is blocked", () => {
|
||||
expect(
|
||||
deriveTaskStatusFromConfirmedPlatforms([
|
||||
createRun("Completed"),
|
||||
createRun("Blocked")
|
||||
])
|
||||
).toBe("PartialCompleted");
|
||||
});
|
||||
|
||||
it("returns Blocked when all confirmed platforms are blocked", () => {
|
||||
expect(
|
||||
deriveTaskStatusFromConfirmedPlatforms([createRun("SearchBlocked")])
|
||||
).toBe("Blocked");
|
||||
});
|
||||
|
||||
it("returns Failed when all confirmed platforms fail", () => {
|
||||
expect(
|
||||
deriveTaskStatusFromConfirmedPlatforms([createRun("Failed")])
|
||||
).toBe("Failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapPlatformStatusToExecutionStatus", () => {
|
||||
it("maps publishable platform statuses", () => {
|
||||
expect(mapPlatformStatusToExecutionStatus("Completed")).toBe("completed");
|
||||
expect(mapPlatformStatusToExecutionStatus("SearchBlocked")).toBe("blocked");
|
||||
expect(mapPlatformStatusToExecutionStatus("Failed")).toBe("failed");
|
||||
expect(mapPlatformStatusToExecutionStatus("Skipped")).toBe("skipped");
|
||||
expect(mapPlatformStatusToExecutionStatus("NoResult")).toBe("no_result");
|
||||
});
|
||||
|
||||
it("rejects running states", () => {
|
||||
expect(() => mapPlatformStatusToExecutionStatus("Running")).toThrow(
|
||||
/cannot be published/i
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@cross-ai/report-schema",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --dts --format esm,cjs --clean",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cross-ai/domain": "file:../domain",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
confidenceLevels,
|
||||
evidenceSourceTypes,
|
||||
executionStatuses,
|
||||
platforms,
|
||||
reportableTaskStatuses,
|
||||
sampleFlags
|
||||
} from "@cross-ai/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
export const SourceScopeSchema = z.object({
|
||||
platforms: z.array(z.enum(platforms)).min(1),
|
||||
link_count: z.number().int().nonnegative(),
|
||||
review_count: z.number().int().nonnegative()
|
||||
});
|
||||
|
||||
export const InsightCardSchema = z
|
||||
.object({
|
||||
card_id: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
statement: z.string().min(1),
|
||||
confidence: z.enum(confidenceLevels),
|
||||
sample_flag: z.enum(sampleFlags),
|
||||
source_scope: SourceScopeSchema,
|
||||
evidence_ids: z.array(z.string().min(1))
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.sample_flag !== "insufficient" && value.evidence_ids.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Non-insufficient insights must include evidence_ids.",
|
||||
path: ["evidence_ids"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const EvidenceSchema = z.object({
|
||||
evidence_id: z.string().min(1),
|
||||
platform: z.enum(platforms),
|
||||
source_type: z.enum(evidenceSourceTypes),
|
||||
source_url: z.string().url(),
|
||||
review_ref: z.string().nullable(),
|
||||
snippet: z.string().min(1),
|
||||
captured_at: z.string().datetime()
|
||||
});
|
||||
|
||||
export const PlatformInsightSchema = z.object({
|
||||
platform: z.enum(platforms),
|
||||
execution_status: z.enum(executionStatuses),
|
||||
selected_link_count: z.number().int().nonnegative(),
|
||||
price_range: z
|
||||
.object({
|
||||
min: z.number().nonnegative(),
|
||||
max: z.number().nonnegative()
|
||||
})
|
||||
.nullable(),
|
||||
selling_points: z.array(InsightCardSchema),
|
||||
positive_themes: z.array(InsightCardSchema),
|
||||
negative_themes: z.array(InsightCardSchema),
|
||||
store_diff_notes: z.array(InsightCardSchema)
|
||||
});
|
||||
|
||||
export const ReportSchema = z.object({
|
||||
report_id: z.string().min(1),
|
||||
report_version: z.number().int().positive(),
|
||||
task_id: z.string().min(1),
|
||||
generated_at: z.string().datetime(),
|
||||
task_status: z.enum(reportableTaskStatuses),
|
||||
summary: z.object({
|
||||
headline: z.string().min(1),
|
||||
key_points: z.array(z.string().min(1)).min(1),
|
||||
limitations: z.array(z.string().min(1))
|
||||
}),
|
||||
product_snapshot: z.object({
|
||||
query: z.string().min(1),
|
||||
normalized_product_name: z.string().min(1),
|
||||
platform_count: z.number().int().nonnegative(),
|
||||
selected_link_count: z.number().int().nonnegative(),
|
||||
review_sample_count: z.number().int().nonnegative(),
|
||||
analysis_time_range: z.object({
|
||||
start: z.string().datetime(),
|
||||
end: z.string().datetime()
|
||||
})
|
||||
}),
|
||||
platform_insights: z.array(PlatformInsightSchema),
|
||||
cross_platform_insights: z.array(InsightCardSchema),
|
||||
recommendations: z.array(InsightCardSchema),
|
||||
evidence_index: z.array(EvidenceSchema),
|
||||
quality_flags: z.object({
|
||||
sample_insufficient: z.boolean(),
|
||||
partial_platform_failure: z.boolean(),
|
||||
blocked_platforms: z.array(z.enum(platforms)),
|
||||
failed_platforms: z.array(z.enum(platforms))
|
||||
})
|
||||
});
|
||||
|
||||
export type ReportSnapshot = z.infer<typeof ReportSchema>;
|
||||
|
||||
export function parseReportSnapshot(input: unknown): ReportSnapshot {
|
||||
return ReportSchema.parse(input);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseReportSnapshot } from "../src/index";
|
||||
|
||||
describe("ReportSchema", () => {
|
||||
it("accepts a valid report snapshot", () => {
|
||||
const report = parseReportSnapshot({
|
||||
report_id: "report-1",
|
||||
report_version: 1,
|
||||
task_id: "task-1",
|
||||
generated_at: "2026-04-02T12:00:00.000Z",
|
||||
task_status: "Completed",
|
||||
summary: {
|
||||
headline: "天猫样本较完整,京东待预热。",
|
||||
key_points: ["天猫完成候选确认并产出报告。"],
|
||||
limitations: ["京东本次未进入搜索。"]
|
||||
},
|
||||
product_snapshot: {
|
||||
query: "iPhone 15 Pro",
|
||||
normalized_product_name: "iPhone 15 Pro",
|
||||
platform_count: 2,
|
||||
selected_link_count: 1,
|
||||
review_sample_count: 60,
|
||||
analysis_time_range: {
|
||||
start: "2026-04-02T11:40:00.000Z",
|
||||
end: "2026-04-02T12:00:00.000Z"
|
||||
}
|
||||
},
|
||||
platform_insights: [
|
||||
{
|
||||
platform: "tmall",
|
||||
execution_status: "completed",
|
||||
selected_link_count: 1,
|
||||
price_range: { min: 7999, max: 7999 },
|
||||
selling_points: [
|
||||
{
|
||||
card_id: "card-1",
|
||||
title: "卖点稳定",
|
||||
statement: "标题与详情页都突出影像能力。",
|
||||
confidence: "high",
|
||||
sample_flag: "sufficient",
|
||||
source_scope: {
|
||||
platforms: ["tmall"],
|
||||
link_count: 1,
|
||||
review_count: 60
|
||||
},
|
||||
evidence_ids: ["evidence-1"]
|
||||
}
|
||||
],
|
||||
positive_themes: [],
|
||||
negative_themes: [],
|
||||
store_diff_notes: []
|
||||
},
|
||||
{
|
||||
platform: "jd",
|
||||
execution_status: "blocked",
|
||||
selected_link_count: 0,
|
||||
price_range: null,
|
||||
selling_points: [],
|
||||
positive_themes: [],
|
||||
negative_themes: [],
|
||||
store_diff_notes: []
|
||||
}
|
||||
],
|
||||
cross_platform_insights: [
|
||||
{
|
||||
card_id: "card-2",
|
||||
title: "跨平台覆盖有限",
|
||||
statement: "本轮仅天猫形成可发布洞察。",
|
||||
confidence: "medium",
|
||||
sample_flag: "partial",
|
||||
source_scope: {
|
||||
platforms: ["tmall", "jd"],
|
||||
link_count: 1,
|
||||
review_count: 60
|
||||
},
|
||||
evidence_ids: ["evidence-1"]
|
||||
}
|
||||
],
|
||||
recommendations: [
|
||||
{
|
||||
card_id: "card-3",
|
||||
title: "优先补齐京东会话",
|
||||
statement: "建议在下一次任务前先完成京东会话准备。",
|
||||
confidence: "medium",
|
||||
sample_flag: "partial",
|
||||
source_scope: {
|
||||
platforms: ["jd"],
|
||||
link_count: 0,
|
||||
review_count: 0
|
||||
},
|
||||
evidence_ids: ["evidence-1"]
|
||||
}
|
||||
],
|
||||
evidence_index: [
|
||||
{
|
||||
evidence_id: "evidence-1",
|
||||
platform: "tmall",
|
||||
source_type: "product",
|
||||
source_url: "https://example.com/tmall/iphone-15-pro",
|
||||
review_ref: null,
|
||||
snippet: "详情页强调 5 倍长焦与钛金属材质。",
|
||||
captured_at: "2026-04-02T11:58:00.000Z"
|
||||
}
|
||||
],
|
||||
quality_flags: {
|
||||
sample_insufficient: false,
|
||||
partial_platform_failure: false,
|
||||
blocked_platforms: [],
|
||||
failed_platforms: []
|
||||
}
|
||||
});
|
||||
|
||||
expect(report.report_version).toBe(1);
|
||||
expect(report.platform_insights).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("rejects a strong insight without evidence ids", () => {
|
||||
expect(() =>
|
||||
parseReportSnapshot({
|
||||
report_id: "report-2",
|
||||
report_version: 1,
|
||||
task_id: "task-2",
|
||||
generated_at: "2026-04-02T12:00:00.000Z",
|
||||
task_status: "Completed",
|
||||
summary: {
|
||||
headline: "invalid",
|
||||
key_points: ["invalid"],
|
||||
limitations: []
|
||||
},
|
||||
product_snapshot: {
|
||||
query: "x",
|
||||
normalized_product_name: "x",
|
||||
platform_count: 1,
|
||||
selected_link_count: 1,
|
||||
review_sample_count: 1,
|
||||
analysis_time_range: {
|
||||
start: "2026-04-02T11:40:00.000Z",
|
||||
end: "2026-04-02T12:00:00.000Z"
|
||||
}
|
||||
},
|
||||
platform_insights: [],
|
||||
cross_platform_insights: [
|
||||
{
|
||||
card_id: "card-1",
|
||||
title: "缺少证据",
|
||||
statement: "这里没有 evidence ids。",
|
||||
confidence: "high",
|
||||
sample_flag: "sufficient",
|
||||
source_scope: {
|
||||
platforms: ["tmall"],
|
||||
link_count: 1,
|
||||
review_count: 1
|
||||
},
|
||||
evidence_ids: []
|
||||
}
|
||||
],
|
||||
recommendations: [],
|
||||
evidence_index: [],
|
||||
quality_flags: {
|
||||
sample_insufficient: false,
|
||||
partial_platform_failure: false,
|
||||
blocked_platforms: [],
|
||||
failed_platforms: []
|
||||
}
|
||||
})
|
||||
).toThrow(/evidence_ids/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user