Skip to content

fix(monitoring): WeChat Work feedback recording bugs#2108

Open
6mvp6 wants to merge 2 commits intolangbot-app:masterfrom
6mvp6:fix/wecom-feedback-bugs
Open

fix(monitoring): WeChat Work feedback recording bugs#2108
6mvp6 wants to merge 2 commits intolangbot-app:masterfrom
6mvp6:fix/wecom-feedback-bugs

Conversation

@6mvp6
Copy link
Copy Markdown
Contributor

@6mvp6 6mvp6 commented Apr 5, 2026

概要

修复 #2078 引入的企业微信智能机器人反馈功能的 3 个 Bug,以及测试中发现的 2 个额外问题:

  • Bug 1 & 2 — 反馈事件在会话过期后被静默丢弃StreamSessionManager 的 TTL 为 60 秒,而 _handle_feedback_event() 仅在 session 存在时才分发处理函数。用户通常在阅读完回复后才给出反馈(往往超过 60 秒),此时会话已过期,反馈被静默丢弃。此外,对同一消息更改反馈(点赞→点踩)时,由于 feedback_id 的 UNIQUE 约束,原始代码只执行 INSERT 而不做 UPSERT,导致 IntegrityError

  • Bug 3 — 取消反馈(type=3)不生效:缺少处理取消反馈事件的逻辑,反馈记录从未被删除。

  • Bug 4 — inaccurate_reasons 校验错误:企业微信 API 返回整数原因代码(如 [2, 4]),但 FeedbackEvent.inaccurate_reasons 期望 List[str] 类型。Pydantic v2 抛出 ValidationError,导致用户选择负反馈原因时整个反馈记录失败。

  • Bug 5 — 前端时间戳偏差 8 小时useFeedbackData.ts 使用 new Date(item.timestamp) 将服务端的 UTC 时间字符串当作本地时间解析。改用 parseUTCTimestamp(),与其它监控 hooks 保持一致。

修改内容

文件 修改说明
src/langbot/libs/wecom_ai_bot_api/api.py 不再依赖会话存活来分发反馈事件;修复 cleanup() 遗漏的 _feedback_index 清理
src/langbot/pkg/api/http/service/monitoring.py record_feedback():UPSERT 逻辑 + 取消(type=3)删除 + IntegrityError 防御
src/langbot/pkg/platform/sources/wecombot.py 创建 FeedbackEvent 前将整数原因代码转换为字符串
web/src/app/home/monitoring/hooks/useFeedbackData.ts 使用 parseUTCTimestamp() 正确解析 UTC 时间戳

测试计划

  • 发送消息 → 点赞 → 仪表盘显示 type=1 记录,时间正确
  • 同一消息改为点踩 → 仪表盘更新为 type=2 记录
  • 选择负反馈原因 → 仪表盘显示原因代码(如 ["1","3"]
  • 取消反馈 → 仪表盘该记录消失
  • 验证时间戳与服务器本地时间一致(无 8 小时偏差)
  • 验证日志显示会话过期后仍能处理反馈

- Fix feedback events silently dropped when stream session expires:
  dispatch feedback handlers regardless of session availability
- Fix IntegrityError on repeated feedback (like→dislike) for same
  message: implement UPSERT logic in record_feedback()
- Fix cancel feedback (type=3) not removing records: add delete logic
- Fix inaccurate_reasons validation error: convert int reason codes
  to strings before creating FeedbackEvent (Pydantic expects List[str])
- Fix feedback timestamps 8 hours off in frontend: use parseUTCTimestamp
  instead of new Date() for UTC timestamp parsing
- Fix StreamSessionManager.cleanup missing _feedback_index cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 5, 2026 17:34
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. bug? Bug或Bug修复相关 / maybe a bug eh: Improve enhance: 现有功能的改进 / improve current features IM: wecom 企业微信 适配器相关 / WeCom and WeComCS adapter related javascript Pull requests that update Javascript code m: Platform 机器人管理相关 / Bots management m: Session 会话和消息模块 / Sessions management labels Apr 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes multiple issues in the WeChat Work (WeCom) bot feedback pipeline so feedback isn’t dropped after stream session expiry, supports updating existing feedback records, supports cancel feedback events, normalizes negative feedback reason types, and corrects frontend timestamp parsing.

Changes:

  • Backend: always dispatch feedback events even when the stream session has expired; clean up StreamSessionManager._feedback_index during session cleanup.
  • Backend: implement update/insert behavior for repeated feedback IDs and delete-on-cancel (type=3) in monitoring persistence.
  • Platform + Web: normalize inaccurate_reasons to strings and parse backend timestamps as UTC on the monitoring UI.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/langbot/libs/wecom_ai_bot_api/api.py Dispatch feedback handlers without requiring an active stream session; cleanup feedback index on session expiry.
src/langbot/pkg/api/http/service/monitoring.py Add cancel-delete and update/insert logic for MonitoringFeedback records.
src/langbot/pkg/platform/sources/wecombot.py Convert WeCom integer reason codes to strings before constructing FeedbackEvent.
web/src/app/home/monitoring/hooks/useFeedbackData.ts Parse feedback timestamps as UTC via shared parseUTCTimestamp() helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1247 to +1251
existing_row = existing_result.first()

if existing_row:
# UPDATE existing record
existing = existing_row[0] if isinstance(existing_row, tuple) else existing_row
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existing_result.first() returns a SQLAlchemy Row (not a tuple), so existing = existing_row[0] if isinstance(existing_row, tuple) else existing_row will set existing to a Row. Accesses like existing.bot_id / existing.id will then raise AttributeError, breaking feedback updates. Prefer existing = existing_row[0] here (since the query is select(MonitoringFeedback)) or use existing = existing_result.scalars().first() and then read fields from the model instance.

Suggested change
existing_row = existing_result.first()
if existing_row:
# UPDATE existing record
existing = existing_row[0] if isinstance(existing_row, tuple) else existing_row
existing = existing_result.scalars().first()
if existing:
# UPDATE existing record

Copilot uses AI. Check for mistakes.
Comment on lines +1232 to +1240
# Handle cancel feedback (type=3): delete existing record
if feedback_type == 3:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(MonitoringFeedback).where(
MonitoringFeedback.feedback_id == feedback_id
)
)
return None

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record_feedback is annotated as returning str, but the cancel path (feedback_type == 3) returns None. Please update the return type annotation/docstring (e.g., str | None) and ensure callers (if any) handle the None return value consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +1292 to +1309
try:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(MonitoringFeedback).values(record_data)
)
return record_id
except Exception:
# UNIQUE constraint conflict (concurrent feedback for same feedback_id)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(MonitoringFeedback)
.where(MonitoringFeedback.feedback_id == feedback_id)
.values(
timestamp=now,
feedback_type=feedback_type,
feedback_content=feedback_content,
inaccurate_reasons=reasons_json,
)
)
return feedback_id
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The insert conflict handling uses except Exception, which will also swallow non-unique DB failures (connection issues, SQL syntax, etc.) and incorrectly treat them as a UNIQUE conflict. Catch sqlalchemy.exc.IntegrityError specifically (and ideally verify it’s the feedback_id unique constraint) and re-raise/log other exceptions. Also, the fallback currently return feedback_id, which is not the record’s primary key id (and differs from the documented return value). Consider re-selecting the row to return existing.id, or use a real UPSERT that can return the row id.

Copilot uses AI. Check for mistakes.
Comment on lines +919 to +928
# Dispatch feedback event regardless of session availability
for handler in self._message_handlers.get('feedback', []):
try:
await handler(
feedback_id=feedback_id,
feedback_type=feedback_type,
feedback_content=feedback_content,
inaccurate_reasons=inaccurate_reasons,
session=session,
)
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback handlers are now dispatched even when no session is found (passing session=None). This is a behavior change vs. the previous implementation and can break handlers/callbacks that assume session fields are always present. Consider documenting this explicitly (or adjusting handler contracts) so downstream handlers know session may be None and should guard accordingly.

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug? Bug或Bug修复相关 / maybe a bug eh: Improve enhance: 现有功能的改进 / improve current features IM: wecom 企业微信 适配器相关 / WeCom and WeComCS adapter related javascript Pull requests that update Javascript code m: Platform 机器人管理相关 / Bots management m: Session 会话和消息模块 / Sessions management size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants