diff --git a/project-docs/V2-ai-quality-improvement-linked-changes-2026-06-23.md b/project-docs/V2-ai-quality-improvement-linked-changes-2026-06-23.md new file mode 100644 index 00000000..69230685 --- /dev/null +++ b/project-docs/V2-ai-quality-improvement-linked-changes-2026-06-23.md @@ -0,0 +1,67 @@ +# V2 AI 质量改进项关联真实变更记录 - 2026-06-23 + +## 背景 + +上一轮已经完成 Prompt 发布/回滚闭环。本轮继续推进“AI 质量问题归因 -> 改进动作 -> 真实配置变更 -> 效果回看”的闭环,避免质量改进项只停留在文字状态。 + +## 本轮完成内容 + +1. 改进项增加真实关联字段 + - `linked_prompt_template_id` + - `linked_prompt_template_version` + - `linked_knowledge_base_id` + - `linked_import_batch_id` + +2. AI Platform 自动推断关联对象 + - Prompt 维度改进项会尝试关联对应 Prompt 模板和版本。 + - 知识库维度改进项会关联知识库 ID 和该知识库最新导入批次。 + - 创建接口仍支持前端或 SDK 显式传入关联字段。 + +3. 持久化兼容 + - SQLAlchemy 模型增加对应字段。 + - 自动兼容迁移会给已有 `ai_quality_improvement_actions` 表补列。 + - 内存仓库与 SQL 仓库均支持创建、更新、列表、看板返回关联字段。 + +4. OpenAPI 与 SDK 同步 + - `AiQualityImprovementActionCreate`、`AiQualityImprovementActionUpdate`、`AiQualityImprovementAction` 同步新增关联字段。 + - Python SDK 支持创建和更新改进项时传入关联字段。 + +5. 管理后台质量运营页增强 + - 页面加载 Prompt 模板和知识库导入批次候选。 + - 从 Prompt 归因生成改进项时,自动关联 Prompt 模板 ID 和版本。 + - 从知识库归因生成改进项时,自动关联知识库 ID 和最新导入批次。 + - 改进项卡片展示“关联变更”,并提供跳转 Prompt 中心入口。 + - 新增 `/api/ai-platform/knowledge-import-batches` 前端代理。 + +## 验证结果 + +- `python -m pytest yuqei-ai-platform/services/ai-platform-api/tests -q` + - 26 passed +- `python -m pytest yuqei-ai-sdk-python/tests -q` + - 2 passed +- `npm run web:typecheck` + - passed +- `npm run web:build` + - passed +- OpenAPI YAML 解析 + - passed + +## 当前闭环价值 + +现在质量运营路径变成: + +1. 质量看板发现某个模型、Prompt、知识库或标签异常集中。 +2. 管理员从归因卡片生成改进项。 +3. 改进项自动挂上相关 Prompt 版本或知识库导入批次。 +4. 管理员去 Prompt 中心发布新版本,或补充知识库导入批次。 +5. 后续 AI 调用继续进入质量看板,改进项显示处理后异常率、质量分、待处理标签变化。 + +这让“调整 Prompt”“补知识库”开始具备可追溯的配置变更记录。 + +## 下一步建议 + +下一步可以进入“改进动作执行页”: + +- 对 `tune_prompt` 改进项,提供直接创建 Prompt 草稿的入口。 +- 对 `improve_knowledge` 改进项,提供上传/导入知识文档入口。 +- 改进项完成时自动要求填写“实际变更对象”,防止误标完成。 diff --git a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml index 186902be..5727d793 100644 --- a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml +++ b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml @@ -1666,6 +1666,22 @@ components: root_cause_hint: type: string nullable: true + linked_prompt_template_id: + type: string + nullable: true + maxLength: 64 + linked_prompt_template_version: + type: string + nullable: true + maxLength: 64 + linked_knowledge_base_id: + type: string + nullable: true + maxLength: 64 + linked_import_batch_id: + type: string + nullable: true + maxLength: 64 evaluation_days: type: integer minimum: 1 @@ -1690,6 +1706,22 @@ components: resolution_note: type: string nullable: true + linked_prompt_template_id: + type: string + nullable: true + maxLength: 64 + linked_prompt_template_version: + type: string + nullable: true + maxLength: 64 + linked_knowledge_base_id: + type: string + nullable: true + maxLength: 64 + linked_import_batch_id: + type: string + nullable: true + maxLength: 64 AiQualityImprovementAction: allOf: - $ref: "#/components/schemas/AiQualityImprovementActionCreate" diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/models.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/models.py index 8f38063b..a8dfe8d3 100644 --- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/models.py +++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/models.py @@ -172,6 +172,10 @@ class AiQualityImprovementActionModel(Base): source_label_codes_json: Mapped[str] = mapped_column(Text, default="[]") source_run_ids_json: Mapped[str] = mapped_column(Text, default="[]") root_cause_hint: Mapped[str | None] = mapped_column(Text, nullable=True) + linked_prompt_template_id: Mapped[str | None] = mapped_column(String(64), nullable=True) + linked_prompt_template_version: Mapped[str | None] = mapped_column(String(64), nullable=True) + linked_knowledge_base_id: Mapped[str | None] = mapped_column(String(64), nullable=True) + linked_import_batch_id: Mapped[str | None] = mapped_column(String(64), nullable=True) evaluation_days: Mapped[int] = mapped_column(Integer, default=14) baseline_total_runs: Mapped[int] = mapped_column(Integer, default=0) baseline_affected_runs: Mapped[int] = mapped_column(Integer, default=0) diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/repository.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/repository.py index 187e4cb2..884ba70a 100644 --- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/repository.py +++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/repository.py @@ -335,6 +335,10 @@ class AiQualityImprovementActionCreate(BaseModel): source_label_codes: list[str] = Field(default_factory=list) source_run_ids: list[str] = Field(default_factory=list) root_cause_hint: str | None = None + linked_prompt_template_id: str | None = Field(default=None, max_length=64) + linked_prompt_template_version: str | None = Field(default=None, max_length=64) + linked_knowledge_base_id: str | None = Field(default=None, max_length=64) + linked_import_batch_id: str | None = Field(default=None, max_length=64) evaluation_days: int = Field(default=14, ge=1, le=90) @@ -343,6 +347,10 @@ class AiQualityImprovementActionUpdate(BaseModel): owner: str | None = Field(default=None, max_length=128) priority: str | None = Field(default=None, pattern="^(low|medium|high|critical)$") resolution_note: str | None = None + linked_prompt_template_id: str | None = Field(default=None, max_length=64) + linked_prompt_template_version: str | None = Field(default=None, max_length=64) + linked_knowledge_base_id: str | None = Field(default=None, max_length=64) + linked_import_batch_id: str | None = Field(default=None, max_length=64) class AiQualityImprovementAction(AiQualityImprovementActionCreate): @@ -866,6 +874,11 @@ class InMemoryAiPlatformRepository: now = datetime.now(UTC) with self._lock: baseline = _calculate_improvement_action_stats(payload, self.ai_run_audits) + payload = _with_inferred_action_links( + payload, + prompt_templates=self.prompt_templates.values(), + knowledge_import_batches=self.knowledge_import_batches.values(), + ) action = AiQualityImprovementAction( id=f"qa-action-{uuid4().hex[:12]}", title=payload.title or _default_improvement_action_title(payload), @@ -906,6 +919,14 @@ class InMemoryAiPlatformRepository: update["priority"] = payload.priority if payload.resolution_note is not None: update["resolution_note"] = payload.resolution_note + if payload.linked_prompt_template_id is not None: + update["linked_prompt_template_id"] = payload.linked_prompt_template_id + if payload.linked_prompt_template_version is not None: + update["linked_prompt_template_version"] = payload.linked_prompt_template_version + if payload.linked_knowledge_base_id is not None: + update["linked_knowledge_base_id"] = payload.linked_knowledge_base_id + if payload.linked_import_batch_id is not None: + update["linked_import_batch_id"] = payload.linked_import_batch_id updated = action.model_copy(update=update) self.quality_improvement_actions[action_id] = updated return _materialize_improvement_action(updated, self.ai_run_audits) @@ -956,15 +977,15 @@ class SqlAlchemyAiPlatformRepository: self._vector_search_backend = vector_search_backend if auto_create: Base.metadata.create_all(self._engine) - self._ensure_prompt_template_columns() + self._ensure_compat_columns() - def _ensure_prompt_template_columns(self) -> None: + def _ensure_compat_columns(self) -> None: with self._engine.begin() as connection: dialect = self._engine.dialect.name - existing_columns: set[str] + prompt_columns: set[str] if dialect == "sqlite": rows = connection.execute(text("PRAGMA table_info(prompt_templates)")).mappings().all() - existing_columns = {str(row["name"]) for row in rows} + prompt_columns = {str(row["name"]) for row in rows} else: rows = connection.execute( text( @@ -972,15 +993,38 @@ class SqlAlchemyAiPlatformRepository: "where table_name = 'prompt_templates'" ) ).mappings().all() - existing_columns = {str(row["column_name"]) for row in rows} - if "status" not in existing_columns: + prompt_columns = {str(row["column_name"]) for row in rows} + if "status" not in prompt_columns: connection.execute(text("ALTER TABLE prompt_templates ADD COLUMN status VARCHAR(32) DEFAULT 'draft'")) - if "rollback_from_version" not in existing_columns: + if "rollback_from_version" not in prompt_columns: connection.execute(text("ALTER TABLE prompt_templates ADD COLUMN rollback_from_version VARCHAR(32)")) - if "published_at" not in existing_columns: + if "published_at" not in prompt_columns: column_type = "TIMESTAMP" if dialect == "sqlite" else "TIMESTAMP WITH TIME ZONE" connection.execute(text(f"ALTER TABLE prompt_templates ADD COLUMN published_at {column_type}")) + if dialect == "sqlite": + rows = connection.execute(text("PRAGMA table_info(ai_quality_improvement_actions)")).mappings().all() + action_columns = {str(row["name"]) for row in rows} + else: + rows = connection.execute( + text( + "select column_name from information_schema.columns " + "where table_name = 'ai_quality_improvement_actions'" + ) + ).mappings().all() + action_columns = {str(row["column_name"]) for row in rows} + action_column_specs = { + "linked_prompt_template_id": "VARCHAR(64)", + "linked_prompt_template_version": "VARCHAR(64)", + "linked_knowledge_base_id": "VARCHAR(64)", + "linked_import_batch_id": "VARCHAR(64)", + } + for column_name, column_type in action_column_specs.items(): + if column_name not in action_columns: + connection.execute( + text(f"ALTER TABLE ai_quality_improvement_actions ADD COLUMN {column_name} {column_type}") + ) + def upsert_provider_config(self, payload: ProviderConfigCreate) -> ProviderConfig: now = datetime.now(UTC) with self._session_factory.begin() as session: @@ -1589,6 +1633,17 @@ class SqlAlchemyAiPlatformRepository: ).all() runs = [_ai_run_audit_from_model(row) for row in audit_rows] baseline = _calculate_improvement_action_stats(payload, runs) + payload = _with_inferred_action_links( + payload, + prompt_templates=[ + _prompt_template_from_model(row) + for row in session.scalars(select(PromptTemplateModel)).all() + ], + knowledge_import_batches=[ + _knowledge_import_batch_from_model(row) + for row in session.scalars(select(KnowledgeImportBatchModel)).all() + ], + ) row = AiQualityImprovementActionModel( id=f"qa-action-{uuid4().hex[:12]}", dimension=payload.dimension, @@ -1603,6 +1658,10 @@ class SqlAlchemyAiPlatformRepository: source_label_codes_json=_encode_json_list(payload.source_label_codes), source_run_ids_json=_encode_json_list(payload.source_run_ids), root_cause_hint=payload.root_cause_hint, + linked_prompt_template_id=payload.linked_prompt_template_id, + linked_prompt_template_version=payload.linked_prompt_template_version, + linked_knowledge_base_id=payload.linked_knowledge_base_id, + linked_import_batch_id=payload.linked_import_batch_id, evaluation_days=payload.evaluation_days, baseline_total_runs=baseline["total_runs"], baseline_affected_runs=baseline["affected_runs"], @@ -1632,6 +1691,14 @@ class SqlAlchemyAiPlatformRepository: row.priority = payload.priority if payload.resolution_note is not None: row.resolution_note = payload.resolution_note + if payload.linked_prompt_template_id is not None: + row.linked_prompt_template_id = payload.linked_prompt_template_id + if payload.linked_prompt_template_version is not None: + row.linked_prompt_template_version = payload.linked_prompt_template_version + if payload.linked_knowledge_base_id is not None: + row.linked_knowledge_base_id = payload.linked_knowledge_base_id + if payload.linked_import_batch_id is not None: + row.linked_import_batch_id = payload.linked_import_batch_id row.updated_at = now row.resolved_at = now if payload.status in {"resolved", "false_positive", "closed"} else None @@ -1926,6 +1993,10 @@ def _quality_improvement_action_from_model(row: AiQualityImprovementActionModel) source_label_codes=[str(item) for item in _decode_json_list(row.source_label_codes_json)], source_run_ids=[str(item) for item in _decode_json_list(row.source_run_ids_json)], root_cause_hint=row.root_cause_hint, + linked_prompt_template_id=row.linked_prompt_template_id, + linked_prompt_template_version=row.linked_prompt_template_version, + linked_knowledge_base_id=row.linked_knowledge_base_id, + linked_import_batch_id=row.linked_import_batch_id, evaluation_days=row.evaluation_days, baseline_total_runs=row.baseline_total_runs, baseline_affected_runs=row.baseline_affected_runs, @@ -1938,6 +2009,61 @@ def _quality_improvement_action_from_model(row: AiQualityImprovementActionModel) ) +def _with_inferred_action_links( + payload: AiQualityImprovementActionCreate, + *, + prompt_templates: Iterable[PromptTemplate], + knowledge_import_batches: Iterable[KnowledgeImportBatch], +) -> AiQualityImprovementActionCreate: + update: dict[str, str] = {} + if payload.dimension == "prompt_template" and not payload.linked_prompt_template_id: + prompt = _find_prompt_template_link(payload.key, prompt_templates) + if prompt is not None: + update["linked_prompt_template_id"] = prompt.id + update["linked_prompt_template_version"] = prompt.version + if payload.dimension == "knowledge_base" or payload.linked_knowledge_base_id: + if payload.dimension == "knowledge_base" and not payload.linked_knowledge_base_id and payload.key != "no_knowledge_source": + update["linked_knowledge_base_id"] = payload.key + knowledge_base_id = payload.linked_knowledge_base_id or update.get("linked_knowledge_base_id") + if knowledge_base_id and not payload.linked_import_batch_id: + batch = _find_latest_import_batch_link(knowledge_base_id, knowledge_import_batches) + if batch is not None: + update["linked_import_batch_id"] = batch.id + return payload.model_copy(update=update) if update else payload + + +def _find_prompt_template_link( + key: str, + prompt_templates: Iterable[PromptTemplate], +) -> PromptTemplate | None: + candidates = [ + template + for template in prompt_templates + if template.id == key or template.name == key or f"{template.name}:{template.version}" == key + ] + if not candidates: + return None + return sorted( + candidates, + key=lambda item: item.published_at or item.updated_at, + reverse=True, + )[0] + + +def _find_latest_import_batch_link( + knowledge_base_id: str, + batches: Iterable[KnowledgeImportBatch], +) -> KnowledgeImportBatch | None: + candidates = [ + batch + for batch in batches + if batch.knowledge_base_id == knowledge_base_id + ] + if not candidates: + return None + return sorted(candidates, key=lambda item: item.updated_at, reverse=True)[0] + + def _ensure_utc(value: datetime) -> datetime: if value.tzinfo is None: return value.replace(tzinfo=UTC) diff --git a/yuqei-ai-platform/services/ai-platform-api/tests/test_platform_data.py b/yuqei-ai-platform/services/ai-platform-api/tests/test_platform_data.py index 018b2765..50fd5457 100644 --- a/yuqei-ai-platform/services/ai-platform-api/tests/test_platform_data.py +++ b/yuqei-ai-platform/services/ai-platform-api/tests/test_platform_data.py @@ -326,6 +326,53 @@ def test_quality_attribution_can_create_and_resolve_improvement_action() -> None assert dashboard_action["effect_summary"] +def test_quality_improvement_action_links_prompt_and_knowledge_changes() -> None: + repository = InMemoryAiPlatformRepository() + repository.seed_defaults() + prompt = repository.get_prompt_template("legal_qa.default") + assert prompt is not None + batch = repository.create_knowledge_import_batch( + KnowledgeImportBatchCreate( + knowledge_base_id="laws-cn", + source_name="civil-code-refresh", + source_type="law_import", + ) + ) + + prompt_action = repository.create_quality_improvement_action( + AiQualityImprovementActionCreate( + dimension="prompt_template", + key=prompt.id, + label="legal_qa.default", + action_type="tune_prompt", + evaluation_days=7, + ) + ) + assert prompt_action.linked_prompt_template_id == prompt.id + assert prompt_action.linked_prompt_template_version == prompt.version + + knowledge_action = repository.create_quality_improvement_action( + AiQualityImprovementActionCreate( + dimension="knowledge_base", + key="laws-cn", + label="laws-cn", + action_type="improve_knowledge", + evaluation_days=7, + ) + ) + assert knowledge_action.linked_knowledge_base_id == "laws-cn" + assert knowledge_action.linked_import_batch_id == batch.id + + updated = repository.update_quality_improvement_action( + knowledge_action.id, + AiQualityImprovementActionUpdate( + status="in_progress", + linked_import_batch_id="kb-batch-manual", + ), + ) + assert updated.linked_import_batch_id == "kb-batch-manual" + + def test_knowledge_document_can_be_added_and_searched() -> None: client = make_client() diff --git a/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py b/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py index 9da66e56..bbbddbba 100644 --- a/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py +++ b/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py @@ -345,6 +345,10 @@ class AiQualityImprovementActionCreate(BaseModel): source_label_codes: list[str] = Field(default_factory=list) source_run_ids: list[str] = Field(default_factory=list) root_cause_hint: str | None = None + linked_prompt_template_id: str | None = None + linked_prompt_template_version: str | None = None + linked_knowledge_base_id: str | None = None + linked_import_batch_id: str | None = None evaluation_days: int = 14 @@ -353,6 +357,10 @@ class AiQualityImprovementActionUpdate(BaseModel): owner: str | None = Field(default=None, max_length=128) priority: str | None = None resolution_note: str | None = None + linked_prompt_template_id: str | None = None + linked_prompt_template_version: str | None = None + linked_knowledge_base_id: str | None = None + linked_import_batch_id: str | None = None class AiQualityImprovementAction(AiQualityImprovementActionCreate): diff --git a/yuqei-ai-sdk-python/tests/test_ai_platform.py b/yuqei-ai-sdk-python/tests/test_ai_platform.py index cacc237f..a7517e25 100644 --- a/yuqei-ai-sdk-python/tests/test_ai_platform.py +++ b/yuqei-ai-sdk-python/tests/test_ai_platform.py @@ -115,6 +115,10 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None: "source_label_codes": ["source_limited"], "source_run_ids": ["run-1"], "root_cause_hint": "Model quality issues are concentrated.", + "linked_prompt_template_id": "prompt-1", + "linked_prompt_template_version": "v2", + "linked_knowledge_base_id": "laws-cn", + "linked_import_batch_id": "kb-batch-1", "evaluation_days": 7, "baseline_total_runs": 1, "baseline_affected_runs": 1, @@ -621,6 +625,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None: assert payload["dimension"] == "model" assert payload["action_type"] == "tune_model" assert payload["source_label_codes"] == ["source_limited"] + assert payload["linked_prompt_template_id"] == "prompt-1" return httpx.Response(200, json=quality_action_payload()) if request.url.path == "/api/v1/audit/quality-improvement-actions": query = parse_qs(request.url.query.decode()) @@ -636,6 +641,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None: assert request.method == "PATCH" assert payload["status"] == "resolved" assert payload["resolution_note"] == "Provider config was tuned." + assert payload["linked_import_batch_id"] == "kb-batch-2" return httpx.Response( 200, json=quality_action_payload( @@ -802,10 +808,13 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None: priority="high", source_label_codes=["source_limited"], source_run_ids=["run-1"], + linked_prompt_template_id="prompt-1", evaluation_days=7, ) ) assert created_action.status == "open" + assert created_action.linked_prompt_template_id == "prompt-1" + assert created_action.linked_import_batch_id == "kb-batch-1" assert created_action.baseline_affected_runs == 1 assert created_action.effect_status == "unchanged" listed_actions = client.list_quality_improvement_actions( @@ -823,6 +832,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None: AiQualityImprovementActionUpdate( status="resolved", resolution_note="Provider config was tuned.", + linked_import_batch_id="kb-batch-2", ), ) assert updated_action.status == "resolved" diff --git a/yuqei-contract-platform/apps/contract-web/src/app/admin/ai-quality-dashboard/page.tsx b/yuqei-contract-platform/apps/contract-web/src/app/admin/ai-quality-dashboard/page.tsx index 4f5f5810..7c14c564 100644 --- a/yuqei-contract-platform/apps/contract-web/src/app/admin/ai-quality-dashboard/page.tsx +++ b/yuqei-contract-platform/apps/contract-web/src/app/admin/ai-quality-dashboard/page.tsx @@ -84,6 +84,10 @@ type QualityImprovementAction = { source_label_codes: string[]; source_run_ids: string[]; root_cause_hint?: string | null; + linked_prompt_template_id?: string | null; + linked_prompt_template_version?: string | null; + linked_knowledge_base_id?: string | null; + linked_import_batch_id?: string | null; evaluation_days: number; baseline_total_runs: number; baseline_affected_runs: number; @@ -112,6 +116,24 @@ type QualityDashboard = { improvement_actions?: QualityImprovementAction[]; }; +type PromptTemplateOption = { + id: string; + name: string; + version: string; + status: string; + published_at?: string | null; + updated_at: string; +}; + +type KnowledgeImportBatchOption = { + id: string; + knowledge_base_id: string; + source_name: string; + source_type: string; + status: string; + updated_at: string; +}; + type LoadState = | { status: "loading" } | { status: "ready"; dashboard: QualityDashboard } @@ -181,6 +203,8 @@ export default function AiQualityDashboardPage() { const [days, setDays] = useState(14); const [actionMessage, setActionMessage] = useState(null); const [actionBusyKey, setActionBusyKey] = useState(null); + const [promptOptions, setPromptOptions] = useState([]); + const [importBatchOptions, setImportBatchOptions] = useState([]); const [filters, setFilters] = useState({ status: "", owner: "", @@ -209,8 +233,24 @@ export default function AiQualityDashboardPage() { } } + async function loadLinkOptions() { + const [promptResponse, batchResponse] = await Promise.allSettled([ + fetch("/api/ai-platform/prompt-templates", { cache: "no-store" }), + fetch("/api/ai-platform/knowledge-import-batches?limit=50", { cache: "no-store" }) + ]); + if (promptResponse.status === "fulfilled" && promptResponse.value.ok) { + const payload = await promptResponse.value.json().catch(() => []); + setPromptOptions(Array.isArray(payload) ? payload : []); + } + if (batchResponse.status === "fulfilled" && batchResponse.value.ok) { + const payload = await batchResponse.value.json().catch(() => []); + setImportBatchOptions(Array.isArray(payload) ? payload : []); + } + } + useEffect(() => { void loadDashboard(days); + void loadLinkOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -249,6 +289,7 @@ export default function AiQualityDashboardPage() { priority: inferPriority(item), source_label_codes: item.top_label_codes, root_cause_hint: item.root_cause_hint, + ...inferActionLinkPayload(item, promptOptions, importBatchOptions), evaluation_days: days }) }); @@ -257,6 +298,7 @@ export default function AiQualityDashboardPage() { throw new Error(payload?.message ?? `HTTP ${response.status}`); } setActionMessage("已生成改进项,可以在下方队列继续处理。"); + await loadLinkOptions(); await loadDashboard(days); } catch (error) { setActionMessage(error instanceof Error ? error.message : "生成改进项失败"); @@ -600,8 +642,14 @@ function QualityActionItem({ 变化 {formatSignedPercent(action.affected_rate_delta)} {action.resolution_note && 处理说明:{action.resolution_note}} + {action.effect_summary && 效果复盘:{action.effect_summary}}
+ {action.linked_prompt_template_id && ( + + 查看 Prompt + + )} {action.status === "open" && (