From ec99d17d431c353e87ef8fcbc9dc2c758e27961f Mon Sep 17 00:00:00 2001 From: dolphin Date: Fri, 10 Jul 2026 01:31:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(feedback):=20unify=20task-mode=20=E7=82=B9?= =?UTF-8?q?=E8=B5=9E/=E7=82=B9=E8=B8=A9=20on=20the=20task-result=20ChatMes?= =?UTF-8?q?sage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The linsight task result is already persisted as a category="task" ChatMessage in the unified daily conversation, so rate it through the shared chatmessage feedback (/liked + /chat/comment) instead of a linsight_session_version-specific column/endpoint. All four AI Q&A surfaces now share one storage + rollup path. Backend: - chat_helpers: agent history formatter returns liked + remark (fixes daily + in-conversation task-turn highlight on reload) - workstation_schema: WorkstationMessage carries liked + remark (channel surface) - linsight utils: get_task_feedback_by_version maps session_version -> its task ChatMessage {message_id, liked} - linsight endpoint: session-version-list enriches each version with message_id + liked for the standalone linsight page Frontend: - ResultPanel: rate by messageId via likeChatApi / disLikeCommentApi - TaskTurnPanel forwards messageId + liked; AiMessageBubble passes the task row's message.messageId (hidden on the read-only share view); ExecutionFlow reads them from the enriched session-version store field - drop the now-unused likeLinsightVersion / commentLinsightVersion --- .../linsight/api/endpoints/linsight.py | 13 +++++++++- src/backend/bisheng/linsight/domain/utils.py | 26 +++++++++++++++++++ .../domain/schemas/workstation_schema.py | 6 +++++ .../domain/services/chat_helpers.py | 5 ++++ src/frontend/client/src/api/linsight.ts | 17 ------------ .../src/components/Chat/AiMessageBubble.tsx | 2 ++ .../Linsight/Execution/ExecutionFlow.tsx | 2 +- .../Linsight/Execution/ResultPanel.tsx | 26 ++++++++++--------- .../Linsight/Execution/TaskTurnPanel.tsx | 8 ++++-- src/frontend/client/src/store/linsight.ts | 6 ++++- 10 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/backend/bisheng/linsight/api/endpoints/linsight.py b/src/backend/bisheng/linsight/api/endpoints/linsight.py index 103f42464..ec614e6f4 100644 --- a/src/backend/bisheng/linsight/api/endpoints/linsight.py +++ b/src/backend/bisheng/linsight/api/endpoints/linsight.py @@ -537,7 +537,18 @@ async def get_linsight_session_version_list( model for model in linsight_session_version_models if model.id == shared_version_id ] - return resp_200([model.model_dump() for model in linsight_session_version_models]) + # Unified like/dislike via ChatMessage: the task result is itself a + # category="task" ChatMessage and the verdict lives on that row. Attach each + # version's linked task message_id + liked so the standalone linsight page can + # rate through the shared /liked endpoint and re-highlight on reload (same as + # the in-conversation task turn). + version_dumps = [model.model_dump() for model in linsight_session_version_models] + feedback_map = await linsight_execute_utils.get_task_feedback_by_version(session_id) + for dump in version_dumps: + info = feedback_map.get(dump.get("id")) + dump["message_id"] = info["message_id"] if info else None + dump["liked"] = info["liked"] if info else 0 + return resp_200(version_dumps) # Get task execution details diff --git a/src/backend/bisheng/linsight/domain/utils.py b/src/backend/bisheng/linsight/domain/utils.py index ffea34752..85e36b29a 100644 --- a/src/backend/bisheng/linsight/domain/utils.py +++ b/src/backend/bisheng/linsight/domain/utils.py @@ -387,6 +387,32 @@ async def persist_task_turn_message(session_model: LinsightSessionVersion) -> Ch ) +async def get_task_feedback_by_version(session_id: str) -> dict[str, dict]: + """Map each linsight session_version id -> its task ChatMessage feedback. + + The task result is a bot ``ChatMessage`` (``category="task"``) in the + conversation ``session_id`` carrying ``extra.linsight_session_version_id``. + The like/dislike verdict is stored on that ChatMessage row (unified with + daily / knowledge / channel), so the standalone linsight page rates and + echoes the highlight via the shared chatmessage feedback instead of a + linsight-specific column. + + Returns ``{session_version_id: {"message_id": int, "liked": int}}``. + """ + rows = await ChatMessageDao.aget_messages_by_chat_id(chat_id=session_id, category_list=["task"], limit=1000) + result: dict[str, dict] = {} + for row in rows: + if not row.is_bot: + continue + try: + svid = json.loads(row.extra or "{}").get("linsight_session_version_id") + except (json.JSONDecodeError, TypeError): + svid = None + if svid: + result[svid] = {"message_id": row.id, "liked": row.liked or 0} + return result + + async def persist_task_user_turn(chat_id: str, user_id: int, question: str, files: list | None = None) -> ChatMessage: """F035 Track J (TJ-3): persist the task user turn into the unified conversation. diff --git a/src/backend/bisheng/workstation/domain/schemas/workstation_schema.py b/src/backend/bisheng/workstation/domain/schemas/workstation_schema.py index 0dd0f81f3..149ef629f 100644 --- a/src/backend/bisheng/workstation/domain/schemas/workstation_schema.py +++ b/src/backend/bisheng/workstation/domain/schemas/workstation_schema.py @@ -31,6 +31,10 @@ class WorkstationMessage(BaseModel): # to the execution detail for lazy-loading. Both absent on normal turns. category: str | None = None linsightSessionVersionId: str | None = None + # like/dislike echo: 0 none / 1 up / 2 down, plus the dislike reason. The + # frontend re-highlights the rated state on reload. + liked: int | None = None + remark: str | None = None @field_validator("messageId", mode="before") @classmethod @@ -70,6 +74,8 @@ class WorkstationMessage(BaseModel): source=message.source, category=message.category, linsightSessionVersionId=extra.get("linsight_session_version_id"), + liked=message.liked, + remark=message.remark, ) diff --git a/src/backend/bisheng/workstation/domain/services/chat_helpers.py b/src/backend/bisheng/workstation/domain/services/chat_helpers.py index ddbeb8e6f..36b74a1ea 100644 --- a/src/backend/bisheng/workstation/domain/services/chat_helpers.py +++ b/src/backend/bisheng/workstation/domain/services/chat_helpers.py @@ -219,6 +219,11 @@ def _message_base_fields(msg: ChatMessage) -> dict: "flow_id": msg.flow_id, "source": msg.source, "sender": msg.sender, + # like/dislike echo: 0 none / 1 up / 2 down, plus the dislike reason. The + # frontend re-highlights the rated state on reload. Daily answers and task + # results are both ChatMessage rows, so they share this one path. + "liked": msg.liked, + "remark": msg.remark, "create_time": msg.create_time.isoformat() if msg.create_time else None, } diff --git a/src/frontend/client/src/api/linsight.ts b/src/frontend/client/src/api/linsight.ts index 880e9cef7..d9e703de0 100644 --- a/src/frontend/client/src/api/linsight.ts +++ b/src/frontend/client/src/api/linsight.ts @@ -48,23 +48,6 @@ export function startLinsight(versionId: string): Promise { }); } -// 灵思任务结果点赞/点踩(liked: 0 未评 / 1 赞 / 2 踩)。 -// 落库到 linsight_session_version.liked,并汇总进 message_session(后端待接入,见 PRD)。 -export function likeLinsightVersion(versionId: string, liked: number): Promise { - return request.post('/api/v1/linsight/workbench/feedback', { - session_version_id: versionId, - liked - }); -} - -// 灵思点踩原因,落库到 linsight_session_version.execute_feedback(后端待接入)。 -export function commentLinsightVersion(versionId: string, comment: string): Promise { - return request.post('/api/v1/linsight/workbench/feedback', { - session_version_id: versionId, - comment - }); -} - // F035 多轮对话:在已完成的同一会话里追加新一轮(复用同一 session_version + agent thread,保留上下文) export function continueLinsight(session_version_id: string, question: string): Promise { return request.post('/api/v1/linsight/workbench/continue', { diff --git a/src/frontend/client/src/components/Chat/AiMessageBubble.tsx b/src/frontend/client/src/components/Chat/AiMessageBubble.tsx index d0cbd34d4..763c03bf2 100644 --- a/src/frontend/client/src/components/Chat/AiMessageBubble.tsx +++ b/src/frontend/client/src/components/Chat/AiMessageBubble.tsx @@ -603,6 +603,8 @@ function AssistantBubble({
diff --git a/src/frontend/client/src/components/Linsight/Execution/ResultPanel.tsx b/src/frontend/client/src/components/Linsight/Execution/ResultPanel.tsx index 3d86b7c5e..709d62349 100644 --- a/src/frontend/client/src/components/Linsight/Execution/ResultPanel.tsx +++ b/src/frontend/client/src/components/Linsight/Execution/ResultPanel.tsx @@ -12,21 +12,23 @@ import { Outlined } from 'bisheng-icons'; import type { ReactNode } from 'react'; import { useLocalize } from '~/hooks'; import { MessageFeedbackButtons } from '~/components/Chat/MessageFeedbackButtons'; -import { likeLinsightVersion, commentLinsightVersion } from '~/api/linsight'; +import { likeChatApi, disLikeCommentApi } from '~/api/apps'; import { INK } from './execTokens'; interface ResultPanelProps { /** the terminal deliverable (typically ) */ children: ReactNode; - /** linsight session_version id — the feedback target */ - versionId?: string; - /** persisted 点赞/点踩 verdict: 0 none / 1 up / 2 down */ + /** task-result ChatMessage id — the like/dislike target. The task result is a + category="task" ChatMessage, so feedback reuses the shared /liked + + /chat/comment endpoints (same as daily / knowledge / channel). */ + messageId?: string; + /** persisted like/dislike verdict: 0 none / 1 up / 2 down */ liked?: number; - /** show 点赞/点踩 (off for read-only / share view) */ + /** show like/dislike (off for read-only / share view) */ allowFeedback?: boolean; } -export function ResultPanel({ children, versionId, liked, allowFeedback }: ResultPanelProps) { +export function ResultPanel({ children, messageId, liked, allowFeedback }: ResultPanelProps) { const localize = useLocalize(); // peak-end (§2.6): a DoubleCheck Ink "task completed" header marks the // terminal state and lifts the deliverable out of the homogeneous flow; body @@ -45,15 +47,15 @@ export function ResultPanel({ children, versionId, liked, allowFeedback }: Resul
{children} - {/* 点赞/点踩 — the task result lives in linsight_session_version, so - persist via the linsight feedback endpoint keyed by version id - (best-effort: optimistic UI, backend wiring per PRD). */} - {allowFeedback && versionId && ( + {/* like/dislike — the task result is a category="task" ChatMessage, so + rate it through the shared chatmessage feedback endpoints keyed by + message id (rollup to message_session is maintained backend-side). */} + {allowFeedback && messageId && (
{ void likeLinsightVersion(versionId, l).catch(() => {}); }} - onDislikeComment={(c) => { void commentLinsightVersion(versionId, c).catch(() => {}); }} + onLike={(l) => likeChatApi(messageId, l)} + onDislikeComment={(c) => disLikeCommentApi(messageId, c)} />
)} diff --git a/src/frontend/client/src/components/Linsight/Execution/TaskTurnPanel.tsx b/src/frontend/client/src/components/Linsight/Execution/TaskTurnPanel.tsx index 2c7742e4f..dcc648485 100644 --- a/src/frontend/client/src/components/Linsight/Execution/TaskTurnPanel.tsx +++ b/src/frontend/client/src/components/Linsight/Execution/TaskTurnPanel.tsx @@ -47,6 +47,10 @@ import { findPendingUserInput, hasRenderableTimeline, isTaskRunning, isTaskStart interface TaskTurnPanelProps { /** linsight session_version id holding this turn's execution detail */ versionId: string; + /** task-result ChatMessage id — the like/dislike target (this turn's bot row) */ + messageId?: string; + /** persisted like/dislike verdict on the task result: 0 none / 1 up / 2 down */ + liked?: number; /** chat id of the hosting conversation (for the history lazy-load) */ conversationId?: string; /** final answer text (fallback shown before the panel hydrates) */ @@ -58,7 +62,7 @@ interface TaskTurnPanelProps { onPreviewFile?: (file: ArtifactFile) => void; } -export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = false, onPreviewFile }: TaskTurnPanelProps) { +export function TaskTurnPanel({ versionId, messageId, liked, conversationId, answer, readOnly = false, onPreviewFile }: TaskTurnPanelProps) { const localize = useLocalize(); const { getLinsight, switchAndUpdateLinsight } = useLinsightManager(); // WS pump — self-guards on status===Running, so mounting it for a completed @@ -250,7 +254,7 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa document link opens it directly in ChatView's inline workspace panel (preview), replacing the legacy right-side drawer. */} {completed && ( - +