From 6bb8354b8d35939a989debef947e079fd468e4ce Mon Sep 17 00:00:00 2001 From: Yuuuu <1569752928@qq.com> Date: Tue, 31 Mar 2026 14:23:55 +0800 Subject: [PATCH] Strengthen AI system charter and governance constraints --- AGENTS.md | 81 +- CLAUDE.md | 1 + .../api/ai_steward_api/routes_conflicts.py | 23 +- backend/app/api/ai_steward_api/schemas.py | 28 +- .../mixin_creation_normalize_persist.py | 55 + .../services/executive_briefing_service.py | 135 +- .../tests/test_ai_steward_conflicts_api.py | 118 ++ ...t_ai_steward_project_from_text_contract.py | 13 + backend/tests/test_executive_briefing_api.py | 199 +- frontend/src/api/aiSteward.ts | 7 + .../pages/ai-config/sections/KnowledgeTab.tsx | 9 +- .../components/BriefingDetailDrawer.tsx | 47 +- .../components/BriefingPrintDocument.tsx | 41 +- .../components/BriefingSectionCard.tsx | 118 +- .../feedback-center/FeedbackDetail.tsx | 34 +- .../feedback-center/FeedbackList.tsx | 9 +- .../feedback-center/MetricsPanel.tsx | 21 +- .../components/feedback-center/TrendPanel.tsx | 16 +- .../ai-steward/sections/ConflictsTab.tsx | 4 +- .../ai-steward/sections/CreateProjectTab.tsx | 23 +- .../ai-steward/sections/FeedbackCenterTab.tsx | 5 +- .../KnowledgeGraphContextBoardPage.tsx | 7 +- .../ModelRoutingConfigPage.tsx | 31 +- yesterday_tasks_20260331.csv | 1710 +++++++++++++++++ yesterday_tasks_20260331.xlsx | Bin 0 -> 73085 bytes 25 files changed, 2594 insertions(+), 141 deletions(-) create mode 100644 backend/tests/test_ai_steward_conflicts_api.py create mode 100644 yesterday_tasks_20260331.csv create mode 100644 yesterday_tasks_20260331.xlsx diff --git a/AGENTS.md b/AGENTS.md index 1d4c1e1b..23e25d81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ frontend/ # React 前端 --- -## 8. 服务边界约束(禁止越界) +## 8. 服务边界约束与 AI 系统宪章(禁止越界) - 数据库写操作必须通过 Alembic migration,禁止直接 `ALTER TABLE` - 新增接口必须遵循响应包络:`{code, message, data}`,不得裸返回 @@ -88,6 +88,83 @@ frontend/ # React 前端 - 禁止模型兜底回复:模型失败/超时/空响应/结构化解析失败时,必须显式报错并保留真实失败原因,禁止返回固定兜底文案冒充成功结果 - 业务语义治理:凡是会决定 `project.create / signal_source.onboard / followup.* / query vs execute` 的中文关键词、正则、`looks_like_*`、`*_HINTS/*_KEYWORDS`,只能定义在 `backend/app/services/ai_pmo/workflow_semantic_planner/` 或 prompt/catalog 配置层;其它目录只允许做文本清洗、ID/URL/repo/locator 抽取与结构化结果解析 +### 8.1 系统定位 + +- PMFlow 的 AI 不是聊天功能,而是事实驱动的决策、生成与执行系统。 +- 系统目标不是输出“看起来完整”的答案,而是输出可验证、可追溯、可执行的结果。 +- 任何 AI 能力都必须服务于业务闭环,不得脱离业务事实自由发挥。 + +### 8.2 核心原则 + +- 真源优先:内部已验证事实优先于反馈记录,反馈记录优先于外部知识,外部知识优先于模型推断。 +- 证据优先:先找证据,再下结论;证据不足时必须显式标注待确认。 +- 读写分离:检索、分析、生成与写入必须分层,禁止一条链路混做。 +- 结果可审计:所有关键输出必须保留来源、时间、操作者、模型与路由信息。 +- 不确定显式化:模型不能把推断包装成事实,不能把假设写成结论。 +- 租户隔离:所有事实、反馈、草案、执行记录必须天然支持 tenant 隔离。 + +### 8.3 标准工作链 + +- Prompt 负责定义角色、结构、口径和输出格式。 +- RAG 负责优先获取内部事实,再补充外部知识与背景材料。 +- 外部检索只负责补行业经验、技术最佳实践和公共信息。 +- LLM 负责在约束内合成结果,不负责凭空创造事实。 +- Executor 负责真正落库、改状态、发起写操作。 +- Feedback Center 负责记录偏差、纠偏和复盘,不负责替代事实源。 +- Knowledge Graph 负责沉淀结构化事实、关系和证据,不负责流程日志。 +- Task Brief 负责承载方案草案,不负责替代最终任务实体。 + +### 8.4 四层对象边界 + +- 事实层:项目、任务、信号、功能目录、知识图谱、关系与证据。 +- 评审层:反馈中心、审核记录、原因码、修订历史、质量统计。 +- 生成层:需求分析、MVP 建议、方案评审稿、任务包、草案。 +- 执行层:项目创建、任务创建、更新、删除、信号接入、状态流转。 + +### 8.5 生成层约束 + +- 所有生成类输出必须至少区分事实、推断、建议、待确认项四类内容。 +- 生成层可以组织信息、给出建议,但不能直接改变业务状态。 +- 生成层不能替代事实层,也不能伪装成执行结果。 +- 生成层的目标是可交付,不是看起来完整。 + +### 8.6 执行层约束 + +- 所有写操作必须经过受控执行器。 +- LLM 和 RAG 都不能直接调用数据库写接口或业务写接口。 +- 写操作必须具备幂等性、审计日志、权限校验、租户隔离与失败回滚能力。 +- 高风险动作必须走确认门禁,不能直接自动执行。 + +### 8.7 冲突处理 + +- 内部事实与外部知识冲突时,以内部事实和当前系统约束为准。 +- 反馈记录与当前执行结果冲突时,以当前执行结果为当前状态,以反馈记录为修正依据。 +- 任务草案与最终执行结果冲突时,以最终执行结果为准,草案只作为历史版本保留。 +- 模型输出与证据冲突时,以证据为准,模型输出必须修正。 + +### 8.8 新业务接入门槛 + +- 新业务必须先回答五个问题:事实源在哪、输出模板在哪、检索范围在哪、执行器在哪、反馈回流在哪。 +- 新业务必须明确自身属于事实层、评审层、生成层还是执行层。 +- 新业务必须注册到统一路由、统一 prompt registry 和统一执行门禁。 +- 新业务如果无法明确这五项,不得进入正式接入流程。 + +### 8.9 禁止事项 + +- 禁止让 LLM 直接成为写操作入口。 +- 禁止让 RAG 直接承担执行职责。 +- 禁止让反馈中心替代事实源。 +- 禁止让知识图谱承担流程垃圾桶职责。 +- 禁止让任务包替代最终任务实体。 +- 禁止在无证据条件下输出确定性结论。 +- 禁止绕过统一路由、统一审计和统一租户隔离。 + +### 8.10 业务语义补充 + +- 涉及 `project.create / signal_source.onboard / followup / query vs execute` 的判断,必须由统一语义规划层负责。 +- 涉及中文关键词、正则、`looks_like_*`、`*_HINTS/*_KEYWORDS` 的规则,只能在统一语义规划目录或 prompt catalog 中定义。 +- 其它模块只允许做清洗、抽取、结构化解析,不得私自定义业务语义。 + ## 9. 新增代码的参考模式 - 新增后端接口:参考 `backend/app/api/sstc.py` 的结构 @@ -190,4 +267,4 @@ frontend/ # React 前端 ### ✅ 文件操作前必须: 1. 先列出将要操作的文件路径 2. 明确告知用户操作后果 -3. 等待用户输入"确认"后才能执行 \ No newline at end of file +3. 等待用户输入"确认"后才能执行 diff --git a/CLAUDE.md b/CLAUDE.md index e25a7c87..3f8813eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,7 @@ - `AGENTS.md`:Codex/Coding Agent 的主规则入口(优先级最高的项目级约束) - `CLAUDE.md`:面向 Claude 风格的项目说明文档(补充背景) - 建议:规则放 `AGENTS.md`,架构/目录说明放 `CLAUDE.md` +- 当前 AI 行为的系统宪章已在 `AGENTS.md` 中升级为“事实层 / 评审层 / 生成层 / 执行层”四层分治;所有新增 AI 业务默认按该宪章接入,`CLAUDE.md` 仅保留结构与背景说明 ## 2. 项目定位 - 项目名:`PMFlow / Neural Sync` diff --git a/backend/app/api/ai_steward_api/routes_conflicts.py b/backend/app/api/ai_steward_api/routes_conflicts.py index 371588cb..0ee19dc4 100644 --- a/backend/app/api/ai_steward_api/routes_conflicts.py +++ b/backend/app/api/ai_steward_api/routes_conflicts.py @@ -4,6 +4,27 @@ from .shared import * from .helpers import _parse_related_users, _utc_now_naive from .schemas import CognitiveConflictResponse, ConflictDetectRequest +CONFLICT_TYPE_LABELS = { + "skill_mismatch": "技能错配", + "quality_drop": "质量下降预警", + "schedule_delay": "进度延期预警", + "unknown": "待识别冲突", + "risk": "风险预警", +} + + +def _display_conflict_type(value: str | None) -> str: + raw = str(value or "").strip() + if not raw: + return "待识别冲突" + mapped = CONFLICT_TYPE_LABELS.get(raw.lower()) + if mapped: + return mapped + if any(separator in raw for separator in ("_", ":", "/", "-", ".")): + return "其他冲突" + return raw + + @router.get("/cognitive-conflicts", response_model=List[CognitiveConflictResponse]) async def get_cognitive_conflicts( current_user: User = Depends(get_current_user), @@ -41,6 +62,7 @@ async def get_cognitive_conflicts( CognitiveConflictResponse( id=str(c.id), type=c.conflict_type, + display_type=_display_conflict_type(c.conflict_type), description=c.description, severity="high" if "critical" in (c.description or "").lower() else "medium", related_users=_parse_related_users(c.involved_user_ids), @@ -250,4 +272,3 @@ async def check_task_charter_mismatch( raise except Exception as e: raise HTTPException(status_code=500, detail=f"一致性检测失败: {str(e)}") - diff --git a/backend/app/api/ai_steward_api/schemas.py b/backend/app/api/ai_steward_api/schemas.py index e8019df7..11f38ca9 100644 --- a/backend/app/api/ai_steward_api/schemas.py +++ b/backend/app/api/ai_steward_api/schemas.py @@ -11,6 +11,27 @@ class ProjectFromTextRequest(BaseModel): max_length=500 ) +class ProjectFromTextSignalSource(BaseModel): + registry_id: int | str | None = None + id: int | str | None = None + project_id: int | str | None = None + signal_id: int | str | None = None + source_kind: str + source_name: str + source_identifier: str + display_source_kind: Optional[str] = None + technical_identifier: Optional[str] = None + description: Optional[str] = None + requested_via: Optional[str] = None + status: Optional[str] = None + onboarding_status: Optional[str] = None + section: Optional[str] = None + title: Optional[str] = None + priority: Optional[str] = None + origin: Optional[str] = None + raw_context: Optional[Dict[str, Any]] = None + config: Optional[Dict[str, Any]] = None + class ProjectFromTextResponse(BaseModel): """零配置项目创建响应""" project: Dict[str, Any] @@ -18,13 +39,14 @@ class ProjectFromTextResponse(BaseModel): charter: Dict[str, Any] tasks: List[Dict[str, Any]] risks: List[Dict[str, Any]] - signal_sources: List[Dict[str, Any]] = Field(default_factory=list) + signal_sources: List[ProjectFromTextSignalSource] = Field(default_factory=list) analysis: Dict[str, Any] class CognitiveConflictResponse(BaseModel): """认知冲突响应""" id: str type: str + display_type: str description: str severity: str resolution_suggestion: Optional[str] = None @@ -114,6 +136,10 @@ class ExecutiveBriefingMetric(BaseModel): class ExecutiveBriefingItem(BaseModel): id: str title: str + display_title: str + display_subtitle: str + display_source: str + technical_identifier: Optional[str] = None detail: str detail_lines: List[str] = Field(default_factory=list) meta: List[Dict[str, str]] = Field(default_factory=list) diff --git a/backend/app/services/ai_steward_service/mixin_creation_normalize_persist.py b/backend/app/services/ai_steward_service/mixin_creation_normalize_persist.py index 34b3a37c..a479ef07 100644 --- a/backend/app/services/ai_steward_service/mixin_creation_normalize_persist.py +++ b/backend/app/services/ai_steward_service/mixin_creation_normalize_persist.py @@ -2,6 +2,60 @@ from __future__ import annotations from .shared import * +PROJECT_SIGNAL_SOURCE_KIND_LABELS = { + "custom": "自定义来源", + "github": "GitHub 仓库", + "feishu": "飞书来源", + "wecom": "企业微信来源", + "webhook": "Webhook 来源", + "browser": "浏览器来源", + "manual": "人工录入", + "scheduled": "定时来源", + "url": "网页来源", + "user_insight": "用户洞察来源", + "social_media": "社媒来源", + "data_analysis": "数据分析来源", + "live_gift_analytics": "直播礼物分析来源", + "market": "市场行情来源", + "policy": "政策监测来源", + "long": "长连接来源", +} + +PROJECT_SIGNAL_SOURCE_KIND_RE = re.compile(r"^[a-z0-9][a-z0-9:._/-]*[a-z0-9]$", re.IGNORECASE) + + +def _looks_like_technical_identifier(value: Any) -> bool: + raw = str(value or "").strip() + if not raw or " " in raw or any("\u4e00" <= char <= "\u9fff" for char in raw): + return False + if not PROJECT_SIGNAL_SOURCE_KIND_RE.match(raw): + return False + return any(separator in raw for separator in ("_", ":", "/", "-", ".")) + + +def _display_source_kind(value: Any) -> str: + raw = str(value or "").strip() + if not raw: + return "未分类来源" + mapped = PROJECT_SIGNAL_SOURCE_KIND_LABELS.get(raw.lower()) + if mapped: + return mapped + if _looks_like_technical_identifier(raw): + return "其他来源" + return raw + + +def _enrich_signal_sources(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + enriched: list[dict[str, Any]] = [] + for item in items: + cloned = dict(item) + technical_identifier = str(cloned.get("source_identifier") or "").strip() or None + cloned["display_source_kind"] = _display_source_kind(cloned.get("source_kind")) + cloned["technical_identifier"] = technical_identifier + enriched.append(cloned) + return enriched + + class AIStewardCreationPersistMixin: async def _normalize_project_creation_draft( self, @@ -183,6 +237,7 @@ class AIStewardCreationPersistMixin: requirement_items=normalized["analysis"].get("requirement_items") if isinstance(normalized["analysis"].get("requirement_items"), list) else [], default_source_kind="custom", ) + signal_sources = _enrich_signal_sources(signal_sources) charter_data = normalized["charter"] charter = TruthNode( diff --git a/backend/app/services/executive_briefing_service.py b/backend/app/services/executive_briefing_service.py index 465b3ec1..071f31b9 100644 --- a/backend/app/services/executive_briefing_service.py +++ b/backend/app/services/executive_briefing_service.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import re from datetime import datetime, timedelta from typing import Any, Optional from zoneinfo import ZoneInfo @@ -74,7 +75,36 @@ SOURCE_KIND_LABELS = { "scheduled": "定时来源", "manual": "人工录入", "feishu": "飞书来源", + "wecom": "企业微信来源", + "github": "GitHub 仓库", + "browser": "浏览器来源", + "url": "网页来源", + "custom": "自定义来源", + "user_insight": "用户洞察来源", + "social_media": "社媒来源", + "data_analysis": "数据分析来源", + "live_gift_analytics": "直播礼物分析来源", + "market": "市场行情来源", + "policy": "政策监测来源", + "long": "长连接来源", } +CONFLICT_TYPE_LABELS = { + "skill_mismatch": "技能错配", + "quality_drop": "质量下降预警", + "schedule_delay": "进度延期预警", + "risk": "风险预警", + "unknown": "待识别冲突", +} +BRIEFING_KIND_LABELS = { + "signal": "项目信号", + "signal_source": "信号源", + "capacity": "人员负载", + "task": "项目任务", + "notification": "项目风险", + "conflict": "协作冲突", + "briefing": "事项", +} +TECHNICAL_IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9:._/-]*[a-z0-9]$", re.IGNORECASE) def _deepcopy_dict(value: Any) -> dict[str, Any]: @@ -98,6 +128,19 @@ def _safe_text(value: Any, *, fallback: str = "-") -> str: return text or fallback +def _contains_cjk(text: str) -> bool: + return any("\u4e00" <= char <= "\u9fff" for char in text) + + +def _looks_like_technical_identifier(value: Any) -> bool: + text = str(value or "").strip() + if not text or _contains_cjk(text) or " " in text: + return False + if not TECHNICAL_IDENTIFIER_RE.match(text): + return False + return any(separator in text for separator in ("_", ":", "/", "-", ".")) + + def _iso_or_none(value: Optional[datetime]) -> Optional[str]: if value is None: return None @@ -122,7 +165,24 @@ def _label_source_kind(value: Any) -> str: raw = str(value or "").strip() if not raw: return "-" - return SOURCE_KIND_LABELS.get(raw.lower(), raw) + mapped = SOURCE_KIND_LABELS.get(raw.lower()) + if mapped: + return mapped + if _looks_like_technical_identifier(raw): + return "其他来源" + return raw + + +def _label_conflict_type(value: Any) -> str: + raw = str(value or "").strip() + if not raw: + return "待识别冲突" + mapped = CONFLICT_TYPE_LABELS.get(raw.lower()) + if mapped: + return mapped + if _looks_like_technical_identifier(raw): + return "其他冲突" + return raw def _target( @@ -252,6 +312,12 @@ class ExecutiveBriefingService: return False if not str(item.get("id") or "").strip(): return False + if not str(item.get("display_title") or "").strip(): + return False + if not str(item.get("display_subtitle") or "").strip(): + return False + if not str(item.get("display_source") or "").strip(): + return False if not isinstance(item.get("detail_lines"), list): return False if not isinstance(item.get("meta"), list): @@ -479,7 +545,10 @@ class ExecutiveBriefingService: for item in items[:3]: if not isinstance(item, dict): continue - lines.append(f"- {_safe_text(item.get('title'))}:{_safe_text(item.get('detail'))}") + lines.append( + f"- {_safe_text(item.get('display_title') or item.get('title'))}" + f":{_safe_text(item.get('display_subtitle') or item.get('detail'))}" + ) return "\n".join(lines).strip() async def _get_config_row(self) -> Optional[Integration]: @@ -600,11 +669,16 @@ class ExecutiveBriefingService: @staticmethod def _pick_display_text(*values: Any) -> Optional[str]: + fallback_text: Optional[str] = None for value in values: text = str(value or "").strip() - if text: + if not text: + continue + if fallback_text is None: + fallback_text = text + if not _looks_like_technical_identifier(text): return text - return None + return fallback_text @classmethod def _is_reportable_signal(cls, *, project_id: Optional[int], title: Optional[str]) -> bool: @@ -651,6 +725,18 @@ class ExecutiveBriefingService: entries.append({"label": label_text, "value": value_text}) return entries + @staticmethod + def _display_subtitle(*values: Any) -> str: + for value in values: + text = str(value or "").strip() + if text and not _looks_like_technical_identifier(text): + return text + for value in values: + text = str(value or "").strip() + if text: + return text + return "-" + async def _get_project_name(self, project_id: Optional[int]) -> Optional[str]: if project_id is None: return None @@ -710,12 +796,32 @@ class ExecutiveBriefingService: related_user_ids: Optional[list[int]] = None, occurred_at: Optional[datetime] = None, target: Optional[dict[str, Any]] = None, + display_title: Optional[str] = None, + display_subtitle: Optional[str] = None, + display_source: Optional[str] = None, + technical_identifier: Optional[str] = None, ) -> dict[str, Any]: cleaned_detail_lines = [line for line in detail_lines if str(line or "").strip()] cleaned_meta = [entry for entry in (meta or []) if str(entry.get("label") or "").strip() and str(entry.get("value") or "").strip()] + safe_title = _safe_text(title, fallback="待处理事项") + safe_display_title = _safe_text(display_title or safe_title, fallback="待处理事项") + safe_display_subtitle = self._display_subtitle( + display_subtitle, + detail, + *cleaned_detail_lines, + ) + safe_display_source = _safe_text( + display_source or BRIEFING_KIND_LABELS.get(str(kind or "").strip().lower(), "事项"), + fallback="事项", + ) + safe_technical_identifier = str(technical_identifier or "").strip() or None return { "id": str(item_id), - "title": _safe_text(title, fallback="待处理事项"), + "title": safe_title, + "display_title": safe_display_title, + "display_subtitle": safe_display_subtitle, + "display_source": safe_display_source, + "technical_identifier": safe_technical_identifier, "detail": detail or "|".join(cleaned_detail_lines[:3]) or "-", "detail_lines": cleaned_detail_lines, "meta": cleaned_meta, @@ -821,6 +927,7 @@ class ExecutiveBriefingService: if not self._is_reportable_signal_source(title=title_text, source_identifier=row.source_identifier): continue kind_label = _label_source_kind(row.source_kind) + status_label = _label_status(row.onboarding_status) items.append( await self._build_brief_item( item_id=f"signal_source:{row.id}", @@ -829,19 +936,25 @@ class ExecutiveBriefingService: kind="signal_source", detail_lines=self._detail_lines( kind_label, - _safe_text(row.source_identifier), - f"状态 {_label_status(row.onboarding_status)}", + f"接入状态 {status_label}", + _safe_text(row.description, fallback=""), ), meta=self._meta_entries( ("来源类型", kind_label), - ("标识", _safe_text(row.source_identifier)), - ("接入状态", _label_status(row.onboarding_status)), + ("接入状态", status_label), ("负责人", await self._get_user_name(user_id)), ("项目", await self._get_project_name(project_id)), ), project_id=project_id, user_id=user_id, occurred_at=row.created_at, + display_title=_safe_text(row.source_name, fallback=title_text or "未命名信号源"), + display_subtitle=self._display_subtitle( + f"{kind_label} · 接入状态 {status_label}", + row.description, + ), + display_source=kind_label, + technical_identifier=str(row.source_identifier or "").strip() or None, target=self._project_target( project_id, params={ @@ -1132,6 +1245,10 @@ class ExecutiveBriefingService: project_id=normalized_project_id, related_user_ids=related_user_ids, occurred_at=row.created_at, + display_title=_label_conflict_type(row.conflict_type), + display_subtitle=self._display_subtitle(row.description), + display_source="协作冲突", + technical_identifier=str(row.conflict_type or "").strip() if _looks_like_technical_identifier(row.conflict_type) else None, target=self._project_target( normalized_project_id, params={"stage": "signal"}, diff --git a/backend/tests/test_ai_steward_conflicts_api.py b/backend/tests/test_ai_steward_conflicts_api.py new file mode 100644 index 00000000..8a815246 --- /dev/null +++ b/backend/tests/test_ai_steward_conflicts_api.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +import pytest_asyncio +from fastapi import FastAPI +from httpx import ASGITransport, AsyncClient +from sqlalchemy import JSON +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from app.api.ai_steward_api import routes_conflicts +from app.core.database import get_db +from app.dependencies.auth import get_current_user +from app.dependencies.tenant import get_tenant_id +from app.models.base import Base +from app.models.cognitive_conflict import CognitiveConflict +from app.models.truth_node import TruthNode +from app.models.user import User + + +def _use_sqlite_json_types() -> None: + User.__table__.c.assigned_domains.type = JSON() + TruthNode.__table__.c.content.type = JSON() + CognitiveConflict.__table__.c.involved_user_ids.type = JSON() + + +@pytest_asyncio.fixture +async def db_session(): + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + _use_sqlite_json_types() + async with engine.begin() as conn: + await conn.run_sync( + lambda sync_conn: Base.metadata.create_all( + sync_conn, + tables=[ + User.__table__, + TruthNode.__table__, + CognitiveConflict.__table__, + ], + ) + ) + maker = async_sessionmaker(engine, expire_on_commit=False) + async with maker() as session: + yield session + await engine.dispose() + + +def _build_app(*, db_session) -> FastAPI: + app = FastAPI() + app.include_router(routes_conflicts.router) + + async def _fake_get_db(): + yield db_session + + app.dependency_overrides[get_db] = _fake_get_db + app.dependency_overrides[get_tenant_id] = lambda: 1 + app.dependency_overrides[get_current_user] = lambda: SimpleNamespace( + id=9001, + tenant_id=1, + name="管理员", + username="admin", + role="ADMIN", + ) + return app + + +@pytest.mark.asyncio +async def test_cognitive_conflicts_return_display_type(db_session) -> None: + user = User( + tenant_id=1, + username="tester", + email="tester@example.com", + password_hash="hash", + name="测试用户", + role="ADMIN", + status="active", + is_active=True, + ) + db_session.add(user) + await db_session.flush() + + truth_node = TruthNode( + tenant_id=1, + node_type="charter", + parent_id=None, + project_id=7, + signal_id=None, + created_by=user.id, + title="项目宪章", + content={}, + version=1, + status="active", + ) + db_session.add(truth_node) + await db_session.flush() + + db_session.add( + CognitiveConflict( + tenant_id=1, + truth_node_id=truth_node.id, + conflict_type="skill_mismatch", + description="技能要求与成员能力不匹配", + involved_user_ids=[user.id], + status="active", + ) + ) + await db_session.commit() + + app = _build_app(db_session=db_session) + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/ai-steward/cognitive-conflicts") + + assert response.status_code == 200 + payload = response.json() + assert payload[0]["type"] == "skill_mismatch" + assert payload[0]["display_type"] == "技能错配" diff --git a/backend/tests/test_ai_steward_project_from_text_contract.py b/backend/tests/test_ai_steward_project_from_text_contract.py index 1c226f51..1da433b2 100644 --- a/backend/tests/test_ai_steward_project_from_text_contract.py +++ b/backend/tests/test_ai_steward_project_from_text_contract.py @@ -7,6 +7,7 @@ from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from app.api.ai_steward_api import routes_capture +from app.api.ai_steward_api.schemas import ProjectFromTextResponse from app.core.database import get_db from app.dependencies.auth import get_current_user from app.dependencies.tenant import get_tenant_id @@ -43,6 +44,8 @@ async def test_project_from_text_returns_enveloped_signal_sources(monkeypatch) - "source_kind": "custom", "source_name": "核心痛点 - 内容本地化", "source_identifier": "custom:core-pain-content-localization", + "display_source_kind": "自定义来源", + "technical_identifier": "custom:core-pain-content-localization", "description": "翻译公司贵、慢、不地道", "section": "核心痛点", } @@ -76,6 +79,16 @@ async def test_project_from_text_returns_enveloped_signal_sources(monkeypatch) - assert payload["message"] == "success" assert payload["data"]["project"]["id"] == "11" assert payload["data"]["signal_sources"][0]["source_name"] == "核心痛点 - 内容本地化" + assert payload["data"]["signal_sources"][0]["display_source_kind"] == "自定义来源" + assert payload["data"]["signal_sources"][0]["technical_identifier"] == "custom:core-pain-content-localization" + + +def test_project_from_text_response_schema_exposes_signal_source_display_fields() -> None: + schema = ProjectFromTextResponse.model_json_schema() + signal_source = schema["$defs"]["ProjectFromTextSignalSource"] + + assert "display_source_kind" in signal_source["properties"] + assert "technical_identifier" in signal_source["properties"] @pytest.mark.asyncio diff --git a/backend/tests/test_executive_briefing_api.py b/backend/tests/test_executive_briefing_api.py index 335d31ee..536a2393 100644 --- a/backend/tests/test_executive_briefing_api.py +++ b/backend/tests/test_executive_briefing_api.py @@ -119,7 +119,21 @@ async def test_executive_briefing_snapshot_and_live_are_enveloped(db_session, mo "title": "今日需拍板", "summary": "有 2 项需要拍板", "metrics": [{"label": "需拍板", "value": 2, "tone": "warning"}], - "items": [], + "items": [ + { + "id": "conflict:1", + "title": "skill_mismatch", + "display_title": "技能错配", + "display_subtitle": "技能要求与成员能力不匹配", + "display_source": "协作冲突", + "technical_identifier": "skill_mismatch", + "detail": "技能要求与成员能力不匹配", + "detail_lines": ["技能要求与成员能力不匹配"], + "meta": [], + "tone": "warning", + "kind": "conflict", + } + ], "drilldown_tab": "signals", } ], @@ -145,6 +159,10 @@ async def test_executive_briefing_snapshot_and_live_are_enveloped(db_session, mo assert live_payload["code"] == 0 assert live_payload["data"]["mode"] == "live" assert live_payload["data"]["sections"][0]["key"] == "decisions" + assert live_payload["data"]["sections"][0]["items"][0]["display_title"] == "技能错配" + assert live_payload["data"]["sections"][0]["items"][0]["display_subtitle"] == "技能要求与成员能力不匹配" + assert live_payload["data"]["sections"][0]["items"][0]["display_source"] == "协作冲突" + assert live_payload["data"]["sections"][0]["items"][0]["technical_identifier"] == "skill_mismatch" @pytest.mark.asyncio @@ -268,6 +286,84 @@ async def test_executive_briefing_service_formats_management_summary_text(db_ses assert "P0 信号无人接" in text +@pytest.mark.asyncio +async def test_executive_briefing_pending_signal_sources_include_display_fields(db_session) -> None: + owner = User( + tenant_id=1, + username="pm", + email="pm@example.com", + password_hash="hash", + name="产品经理", + role="ADMIN", + status="active", + is_active=True, + ) + db_session.add(owner) + await db_session.flush() + + project = Project( + tenant_id=1, + name="cashflow", + title="现金流看板", + status="active", + ) + db_session.add(project) + await db_session.flush() + + db_session.add( + SignalSourceRegistry( + tenant_id=1, + project_id=project.id, + created_by_user_id=owner.id, + source_kind="github", + source_name="现金流日报仓库", + source_identifier="daily_cashflow_analysis", + description="每日现金流分析脚本仓库", + requested_via="ai_steward", + status="pending", + onboarding_status="pending", + config={}, + raw_context={}, + ) + ) + await db_session.commit() + + service = ExecutiveBriefingService(db_session, tenant_id=1) + items = await service._list_pending_signal_sources(limit=5) + + assert len(items) == 1 + assert items[0]["display_title"] == "现金流日报仓库" + assert items[0]["display_source"] == "GitHub 仓库" + assert items[0]["technical_identifier"] == "daily_cashflow_analysis" + assert "daily_cashflow_analysis" not in items[0]["display_subtitle"] + + +def test_snapshot_payload_without_display_fields_is_not_current() -> None: + payload = { + "hero": {"title": "简报中心"}, + "sections": [ + { + "key": "decisions", + "title": "今日需拍板", + "summary": "有 1 项需要拍板", + "items": [ + { + "id": "conflict:1", + "title": "skill_mismatch", + "detail": "技能要求与成员能力不匹配", + "detail_lines": ["技能要求与成员能力不匹配"], + "meta": [], + "tone": "warning", + "kind": "conflict", + } + ], + } + ], + } + + assert ExecutiveBriefingService._snapshot_payload_is_current(payload) is False + + @pytest.mark.asyncio async def test_executive_briefing_service_builds_detailed_signal_items_and_target_params(db_session) -> None: owner = User( @@ -623,3 +719,104 @@ async def test_executive_briefing_capacity_ratio_matches_displayed_hours(db_sess assert overload_item["title"] == "杨宇" assert overload_item["detail_lines"][0] == "159% 负载" assert any(meta["label"] == "负载" and meta["value"] == "159%" for meta in overload_item["meta"]) + + +@pytest.mark.asyncio +async def test_executive_briefing_localizes_conflict_and_signal_source_codes(db_session) -> None: + owner = User( + tenant_id=1, + username="boss", + email="boss@example.com", + password_hash="hash", + name="李雷", + role="BOSS", + status="active", + is_active=True, + ) + db_session.add(owner) + await db_session.flush() + + project = Project( + tenant_id=1, + name="cashflow-forecast", + title="现金流平台", + description="现金流预测与预警", + owner_id=owner.id, + status="active", + ) + db_session.add(project) + await db_session.flush() + + truth_node = TruthNode( + tenant_id=1, + project_id=project.id, + created_by=owner.id, + node_type="risk", + title="风险节点", + content={"summary": "风险节点"}, + version=1, + status="active", + ) + db_session.add(truth_node) + await db_session.flush() + + db_session.add( + SignalSourceRegistry( + tenant_id=1, + project_id=project.id, + created_by_user_id=owner.id, + source_kind="user_insight", + source_name="用户洞察数据源", + source_identifier="ai-customer-service-insight", + description="用户洞察相关信号源", + requested_via="ai_steward", + status="pending", + onboarding_status="pending", + config={}, + raw_context={}, + ) + ) + db_session.add_all( + [ + CognitiveConflict( + tenant_id=1, + truth_node_id=truth_node.id, + conflict_type="quality_drop", + description="质量下降预警", + involved_user_ids=[], + status="active", + ), + CognitiveConflict( + tenant_id=1, + truth_node_id=truth_node.id, + conflict_type="schedule_delay", + description="任务超时预警", + involved_user_ids=[], + status="active", + ), + ] + ) + await db_session.commit() + + service = ExecutiveBriefingService(db_session, tenant_id=1) + payload = await service.build_brief_payload( + mode="live", + baseline_generated_at=None, + live_generated_at=datetime(2026, 3, 30, 8, 30, 0), + ) + + decisions = next(section for section in payload["sections"] if section["key"] == "decisions") + input_health = next(section for section in payload["sections"] if section["key"] == "input_health") + + source_item = next(item for item in input_health["items"] if item["kind"] == "signal_source") + assert source_item["display_source"] == "用户洞察来源" + assert source_item["display_subtitle"].startswith("用户洞察来源") + assert "user_insight" not in source_item["display_subtitle"] + + conflict_titles = { + item["technical_identifier"]: item["display_title"] + for item in decisions["items"] + if item["kind"] == "conflict" + } + assert conflict_titles["quality_drop"] == "质量下降预警" + assert conflict_titles["schedule_delay"] == "进度延期预警" diff --git a/frontend/src/api/aiSteward.ts b/frontend/src/api/aiSteward.ts index 054b9713..c5b0fe50 100644 --- a/frontend/src/api/aiSteward.ts +++ b/frontend/src/api/aiSteward.ts @@ -31,6 +31,8 @@ export interface ProjectFromTextSignalSource { source_kind: string; source_name: string; source_identifier: string; + display_source_kind?: string | null; + technical_identifier?: string | null; description?: string | null; requested_via?: string; status?: string; @@ -89,6 +91,7 @@ export interface SignalSourceRepairResponse { export interface CognitiveConflict { id: string; type: string; + display_type: string; description: string; severity: string; related_users: string[]; @@ -135,6 +138,10 @@ export interface BriefingMetaEntry { export interface ExecutiveBriefingItem { id: string; title: string; + display_title: string; + display_subtitle: string; + display_source: string; + technical_identifier?: string | null; detail: string; detail_lines: string[]; meta: BriefingMetaEntry[]; diff --git a/frontend/src/pages/ai-config/sections/KnowledgeTab.tsx b/frontend/src/pages/ai-config/sections/KnowledgeTab.tsx index 13c24aeb..292cf3d0 100644 --- a/frontend/src/pages/ai-config/sections/KnowledgeTab.tsx +++ b/frontend/src/pages/ai-config/sections/KnowledgeTab.tsx @@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { TabsContent } from "@/components/ui/tabs"; import { formatBeijingDateTime } from "@/lib/datetime"; +import { toZhKnowledgeDocStatus, toZhKnowledgeFileType, toZhKnowledgeSourceType } from "@/lib/display"; import type { KnowledgeDoc } from "../types"; interface KnowledgeTabProps { @@ -96,7 +97,7 @@ export default function KnowledgeTab({ 添加中… ) : ( - "Add URL" + "添加 URL" )} @@ -118,7 +119,9 @@ export default function KnowledgeTab({
{doc.title}
-
类型: {doc.file_type} · 来源: {doc.source_type}
+
+ 类型: {toZhKnowledgeFileType(doc.file_type)} · 来源: {toZhKnowledgeSourceType(doc.source_type)} +
上传时间: {formatBeijingDateTime(doc.created_at)} {doc.last_fetched_at ? ` · 最近抓取: ${formatBeijingDateTime(doc.last_fetched_at)}` : ""} @@ -128,7 +131,7 @@ export default function KnowledgeTab({
- {doc.status === "processing" ? "processing" : doc.status === "complete" ? "complete" : "failed"} + {toZhKnowledgeDocStatus(doc.status)} {doc.source_type === "url" && (
))}
+ {item.technical_identifier && ( +
+ {formatTechnicalIdentifierLabel(item.technical_identifier)} +
+ )} {renderActionButtons(item, runtimeKey)}
)} diff --git a/frontend/src/pages/ai-steward/components/BriefingPrintDocument.tsx b/frontend/src/pages/ai-steward/components/BriefingPrintDocument.tsx index 935131db..bfb316e0 100644 --- a/frontend/src/pages/ai-steward/components/BriefingPrintDocument.tsx +++ b/frontend/src/pages/ai-steward/components/BriefingPrintDocument.tsx @@ -3,7 +3,13 @@ import type { ExecutiveBriefingHero, ExecutiveBriefingSection, } from "@/pages/ai-steward/types"; -import { groupBriefingItemsByProject } from "./BriefingSectionCard"; +import { formatTechnicalIdentifierLabel } from "@/lib/display"; +import { + getBriefingItemDisplaySource, + getBriefingItemDisplaySubtitle, + getBriefingItemDisplayTitle, + groupBriefingItemsByProject, +} from "./BriefingSectionCard"; export interface PrintableBriefingPayload { brief_date: string; @@ -40,25 +46,6 @@ function toneLabel(tone: string): string { } } -function itemKindLabel(kind?: string | null): string { - switch (kind) { - case "signal": - return "项目信号"; - case "signal_source": - return "信号源"; - case "capacity": - return "人员负载"; - case "task": - return "项目任务"; - case "notification": - return "项目风险"; - case "conflict": - return "协作冲突"; - default: - return "事项"; - } -} - function modeLabel(mode: PrintableBriefingPayload["mode"]): string { return mode === "live" ? "实时版" : "晨报快照"; } @@ -118,6 +105,9 @@ function renderItemMeta(item: ExecutiveBriefingSection["items"][number]): Array< return item.meta; } const fallback: Array<{ label: string; value: string }> = []; + if (item.display_source) { + fallback.push({ label: "来源", value: item.display_source }); + } if (item.project_name) { fallback.push({ label: "项目", value: item.project_name }); } @@ -134,7 +124,7 @@ function renderItemDetailLines(item: ExecutiveBriefingSection["items"][number]): if (Array.isArray(item.detail_lines) && item.detail_lines.length) { return item.detail_lines; } - const detail = String(item.detail || "").trim(); + const detail = getBriefingItemDisplaySubtitle(item); return detail ? [detail] : []; } @@ -163,14 +153,14 @@ function renderItems(section: ExecutiveBriefingSection): string { const detailLines = renderItemDetailLines(item); return `
-
+
- ${escapeHtml(item.title)} - ${escapeHtml(itemKindLabel(item.kind))} + ${escapeHtml(getBriefingItemDisplayTitle(item))} + ${escapeHtml(getBriefingItemDisplaySource(item))}
${toneLabel(item.tone)}
-
${escapeHtml(item.detail)}
+
${escapeHtml(getBriefingItemDisplaySubtitle(item))}
${meta .map( @@ -183,6 +173,7 @@ function renderItems(section: ExecutiveBriefingSection): string { ) .join("")}
+ ${item.technical_identifier ? `
${escapeHtml(formatTechnicalIdentifierLabel(item.technical_identifier))}
` : ""}
${detailLines .map((line) => `
${escapeHtml(line)}
`) diff --git a/frontend/src/pages/ai-steward/components/BriefingSectionCard.tsx b/frontend/src/pages/ai-steward/components/BriefingSectionCard.tsx index 8935e062..e15679f2 100644 --- a/frontend/src/pages/ai-steward/components/BriefingSectionCard.tsx +++ b/frontend/src/pages/ai-steward/components/BriefingSectionCard.tsx @@ -3,6 +3,11 @@ import { ChevronDown, ChevronRight, PanelRightOpen } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { + shouldHideTechnicalIdentifierFromPrimaryText, + toZhConflictType, + toZhSourceKind, +} from "@/lib/display"; import { cn } from "@/lib/utils"; import type { ExecutiveBriefingItem, ExecutiveBriefingSection } from "@/pages/ai-steward/types"; @@ -71,6 +76,91 @@ function getMetaValue(item: ExecutiveBriefingItem, label: string): string | null return value || null; } +function normalizeText(value?: string | null): string { + return String(value || "").trim(); +} + +function localizeSignalSourceLabel(value?: string | null): string { + const raw = normalizeText(value); + if (!raw) return "信号源"; + const localized = toZhSourceKind(raw); + if (shouldHideTechnicalIdentifierFromPrimaryText(localized)) { + return "其他来源"; + } + return localized; +} + +function sanitizeSignalSourceSubtitle(item: ExecutiveBriefingItem, rawSubtitle: string): string { + const segments = rawSubtitle + .split(/[||]/) + .map((segment) => segment.trim()) + .filter(Boolean); + + if (!segments.length) return rawSubtitle; + + const technicalIdentifier = normalizeText(item.technical_identifier); + const sanitized = segments + .filter((segment) => { + if (technicalIdentifier && segment === technicalIdentifier) return false; + return !shouldHideTechnicalIdentifierFromPrimaryText(segment); + }) + .map((segment, index) => (index === 0 ? localizeSignalSourceLabel(segment) : segment)); + + return sanitized.join("|") || rawSubtitle; +} + +export function getBriefingItemDisplayTitle(item: ExecutiveBriefingItem): string { + const displayTitle = normalizeText(item.display_title); + const rawTitle = normalizeText(item.title); + + if (item.kind === "conflict") { + const localized = toZhConflictType(displayTitle || item.technical_identifier || rawTitle); + if (localized !== "-") { + return localized; + } + } + + if (displayTitle && !shouldHideTechnicalIdentifierFromPrimaryText(displayTitle)) { + return displayTitle; + } + if (rawTitle && !shouldHideTechnicalIdentifierFromPrimaryText(rawTitle)) { + return rawTitle; + } + if (item.kind === "signal_source") { + return "未命名信号源"; + } + return displayTitle || rawTitle || "其他事项"; +} + +export function getBriefingItemDisplaySubtitle(item: ExecutiveBriefingItem): string { + const subtitle = normalizeText(item.display_subtitle) || normalizeText(item.detail); + if (!subtitle) return "展开后查看详细内容"; + if (item.kind === "signal_source") { + return sanitizeSignalSourceSubtitle(item, subtitle); + } + if (item.kind === "conflict" && shouldHideTechnicalIdentifierFromPrimaryText(subtitle)) { + const localized = toZhConflictType(item.technical_identifier || subtitle); + if (localized !== "-") { + return localized; + } + } + return subtitle; +} + +export function getBriefingItemDisplaySource(item: ExecutiveBriefingItem): string { + const displaySource = normalizeText(item.display_source); + if (item.kind === "signal_source") { + return localizeSignalSourceLabel(displaySource || item.technical_identifier); + } + if (item.kind === "conflict") { + return "协作冲突"; + } + if (displaySource && !shouldHideTechnicalIdentifierFromPrimaryText(displaySource)) { + return displaySource; + } + return itemKindLabel(item.kind); +} + function itemKindLabel(kind?: string | null): string { switch (kind) { case "signal": @@ -96,14 +186,15 @@ export function groupBriefingItemsByProject(items: ExecutiveBriefingItem[]): Bri const projectId = typeof item.project_id === "number" ? item.project_id : null; const projectName = String(item.project_name || "").trim(); const ownerName = String(item.owner_name || "").trim(); - const sourceIdentifier = getMetaValue(item, "标识"); const workloadValue = getMetaValue(item, "负载"); - const fallbackTitle = String(item.title || "").trim() || "其他事项"; + const fallbackTitle = getBriefingItemDisplayTitle(item); + const fallbackSubtitle = getBriefingItemDisplaySubtitle(item); + const sourceLabel = getBriefingItemDisplaySource(item); let key = ""; let title = ""; let categoryLabel = ""; - let previewText = String(item.detail || "").trim() || "展开后查看详细内容"; + let previewText = fallbackSubtitle; if (projectId !== null && projectName) { key = `project:${projectId}`; @@ -111,18 +202,17 @@ export function groupBriefingItemsByProject(items: ExecutiveBriefingItem[]): Bri categoryLabel = "项目"; } else if (item.kind === "signal_source") { key = `signal-source:${item.id}`; - title = sourceIdentifier ? `${fallbackTitle}` : fallbackTitle; - categoryLabel = "信号源"; - previewText = sourceIdentifier ? `来源标识 ${sourceIdentifier}` : previewText; + title = fallbackTitle; + categoryLabel = sourceLabel; } else if (item.kind === "capacity" || item.user_id || ownerName) { key = `user:${item.user_id ?? item.id}`; title = ownerName || fallbackTitle; categoryLabel = "成员"; - previewText = workloadValue ? `${workloadValue}|${String(item.detail || "").trim()}` : previewText; + previewText = workloadValue ? `${workloadValue}|${fallbackSubtitle}` : previewText; } else { key = `item:${item.id}`; title = fallbackTitle; - categoryLabel = itemKindLabel(item.kind); + categoryLabel = sourceLabel; } const existing = groups.get(key); @@ -261,7 +351,9 @@ export default function BriefingSectionCard({ {group.items.length} 条

- {group.previewText || group.items[0]?.detail || "展开后查看详细内容"} + {group.previewText || + (group.items[0] ? getBriefingItemDisplaySubtitle(group.items[0]) : "") || + "展开后查看详细内容"}

@@ -283,13 +375,15 @@ export default function BriefingSectionCard({
-

{item.title}

+

+ {getBriefingItemDisplayTitle(item)} +

- {itemKindLabel(item.kind)} + {getBriefingItemDisplaySource(item)}

- {item.detail} + {getBriefingItemDisplaySubtitle(item)}

展开查看 diff --git a/frontend/src/pages/ai-steward/components/feedback-center/FeedbackDetail.tsx b/frontend/src/pages/ai-steward/components/feedback-center/FeedbackDetail.tsx index 6b8f6262..22da9440 100644 --- a/frontend/src/pages/ai-steward/components/feedback-center/FeedbackDetail.tsx +++ b/frontend/src/pages/ai-steward/components/feedback-center/FeedbackDetail.tsx @@ -1,5 +1,12 @@ import type { FeedbackCenterRecord, FeedbackCenterRecordRevision } from "@/api/feedbackCenter"; import { Badge } from "@/components/ui/badge"; +import { + formatTechnicalIdentifierLabel, + toZhFeedbackFieldName, + toZhFeedbackReasonCode, + toZhFeedbackSourceType, + toZhReviewStatus, +} from "@/lib/display"; interface FeedbackDetailProps { record: FeedbackCenterRecord | null; @@ -42,16 +49,16 @@ export function FeedbackDetail({ record, revisions }: FeedbackDetailProps) {
-

Diff View

+

差异视图

{record.object_title || record.source_title || `记录 #${record.id}`}

- {record.source_type} · 字段 {record.field_name} + {toZhFeedbackSourceType(record.source_type)} · 字段 {toZhFeedbackFieldName(record.field_name)}

- 状态 {record.review_status} + 状态 {toZhReviewStatus(record.review_status)} {record.auto_assigned && ( AI 自动指派 )} @@ -60,7 +67,7 @@ export function FeedbackDetail({ record, revisions }: FeedbackDetailProps) {
-

Meta

+

基础信息

项目

@@ -72,7 +79,7 @@ export function FeedbackDetail({ record, revisions }: FeedbackDetailProps) {

原因码

-

{record.reason_code || "—"}

+

{toZhFeedbackReasonCode(record.reason_code)}

置信度

@@ -81,21 +88,30 @@ export function FeedbackDetail({ record, revisions }: FeedbackDetailProps) {
-

AI Snapshot

+

AI 快照

             {formatJson(record.ai_snapshot)}
           
+
+

技术信息

+
+

{formatTechnicalIdentifierLabel(record.source_type, "技术来源")}

+ {record.reason_code &&

{formatTechnicalIdentifierLabel(record.reason_code, "技术原因码")}

} +

{formatTechnicalIdentifierLabel(record.field_name, "技术字段")}

+
+
+
- {diffCard("AI Before", record.before_value, "before")} - {diffCard("Human After", record.after_value, "after")} + {diffCard("AI 原始结果", record.before_value, "before")} + {diffCard("人工修正结果", record.after_value, "after")}
-

Revisions

+

修订历史

{revisions.length} 条
{!revisions.length &&

暂无修订历史。

} diff --git a/frontend/src/pages/ai-steward/components/feedback-center/FeedbackList.tsx b/frontend/src/pages/ai-steward/components/feedback-center/FeedbackList.tsx index c8d2aade..6205be45 100644 --- a/frontend/src/pages/ai-steward/components/feedback-center/FeedbackList.tsx +++ b/frontend/src/pages/ai-steward/components/feedback-center/FeedbackList.tsx @@ -1,6 +1,7 @@ import type { FeedbackCenterRecord, FeedbackCenterSummaryReason } from "@/api/feedbackCenter"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; +import { toZhFeedbackFieldName, toZhFeedbackSourceType, toZhReviewStatus } from "@/lib/display"; interface FeedbackListProps { items: FeedbackCenterRecord[]; @@ -30,7 +31,7 @@ function sourceLabel(sourceType: string): string { if (sourceType === "task_breakdown" || sourceType === "task_override") return "任务"; if (sourceType === "qa_feedback") return "QA"; if (sourceType === "signal_feedback") return "信号"; - return sourceType || "未知"; + return toZhFeedbackSourceType(sourceType) || "未知"; } export function FeedbackList({ @@ -54,7 +55,7 @@ export function FeedbackList({
-

Queue

+

待处理队列

待复核记录

- {sourceLabel(item.source_type)} · 字段 {item.field_name} + {toZhFeedbackSourceType(item.source_type)} · 字段 {toZhFeedbackFieldName(item.field_name)}

- {item.review_status} + {toZhReviewStatus(item.review_status)}
diff --git a/frontend/src/pages/ai-steward/components/feedback-center/MetricsPanel.tsx b/frontend/src/pages/ai-steward/components/feedback-center/MetricsPanel.tsx index 21abfae4..d695805b 100644 --- a/frontend/src/pages/ai-steward/components/feedback-center/MetricsPanel.tsx +++ b/frontend/src/pages/ai-steward/components/feedback-center/MetricsPanel.tsx @@ -1,6 +1,7 @@ import type { FeedbackCenterSummary } from "@/api/feedbackCenter"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; +import { toZhFeedbackSourceType, toZhReviewStatus } from "@/lib/display"; interface MetricsPanelProps { summary?: FeedbackCenterSummary | null; @@ -19,20 +20,6 @@ const EMPTY_METRICS = { total_revision_count: 0, }; -const SOURCE_LABELS: Record = { - task_breakdown: "任务拆解", - task_override: "任务覆盖", - qa_feedback: "QA 反馈", - signal_feedback: "信号反馈", -}; - -const STATUS_LABELS: Record = { - pending: "待复核", - reviewed: "已复核", - accepted: "已采纳", - rejected: "已拒绝", -}; - function formatPercent(value: number): string { return `${Math.round(value * 100)}%`; } @@ -142,7 +129,7 @@ export function FeedbackMetricsPanel({ summary }: MetricsPanelProps) { const sourceRows = Object.entries(bySourceType) .map(([label, count]) => ({ - label: SOURCE_LABELS[label] || label, + label: toZhFeedbackSourceType(label), count, percent: sourceTotal > 0 ? count / sourceTotal : 0, })) @@ -150,7 +137,7 @@ export function FeedbackMetricsPanel({ summary }: MetricsPanelProps) { const statusRows = Object.entries(byStatus) .map(([label, count]) => ({ - label: STATUS_LABELS[label] || label, + label: toZhReviewStatus(label), count, percent: statusTotal > 0 ? count / statusTotal : 0, })) @@ -192,7 +179,7 @@ export function FeedbackMetricsPanel({ summary }: MetricsPanelProps) {
-

Learning Health

+

学习健康度

学习健康度

把关键采纳指标压成一组可扫读的条形信号,方便快速判断自动化是否在变好。

diff --git a/frontend/src/pages/ai-steward/components/feedback-center/TrendPanel.tsx b/frontend/src/pages/ai-steward/components/feedback-center/TrendPanel.tsx index e0c62e51..0bfa86c5 100644 --- a/frontend/src/pages/ai-steward/components/feedback-center/TrendPanel.tsx +++ b/frontend/src/pages/ai-steward/components/feedback-center/TrendPanel.tsx @@ -8,6 +8,7 @@ import type { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { toZhFeedbackReasonCode, toZhFeedbackSourceType, toZhReviewStatus } from "@/lib/display"; interface TrendPanelProps { summary?: FeedbackCenterSummary | null; @@ -284,7 +285,7 @@ export function FeedbackTrendPanel({
-

Trend Scope

+

趋势范围

趋势分析范围

这里的筛选只影响趋势页,不会改动总览、指标页或任务复核列表。 @@ -373,7 +374,7 @@ export function FeedbackTrendPanel({

-

Decision Focus

+

处理重点

{decisionHeadline}

这里先告诉你近 {windowDays} 天什么值得处理,再给你一键跳转到对应样本,最后再看趋势图做复盘。 @@ -409,10 +410,13 @@ export function FeedbackTrendPanel({ 跳转:{insight.target_section === "task" ? "任务" : insight.target_section === "qa" ? "QA" : insight.target_section === "signal" ? "信号" : "总览"} {insight.target_source_type && ( - {insight.target_source_type} + {toZhFeedbackSourceType(insight.target_source_type)} + )} + {insight.target_review_status && ( + {toZhReviewStatus(insight.target_review_status)} )} {insight.target_reason_code && ( - 原因 {insight.target_reason_code} + 原因 {toZhFeedbackReasonCode(insight.target_reason_code)} )}