feat: send summary notify tip to groups when smart summary completes#291
feat: send summary notify tip to groups when smart summary completes#291pkuWMH wants to merge 1 commit into
Conversation
…ininglamp-OSS#289) When a user completes a smart summary, a tip message (grey system text, contentType=21) is sent to each summarized group/thread, notifying other members: "XXX 总结了群聊内容". This mirrors the screenshot notification pattern (contentType=20) where the client sends a tip directly via WuKongIM SDK. Changes: - Add SummaryNotifyContent (type=21) in dmworkbase, modeled after ScreenshotContent (type=20) - Register type=21 as a system message in module.tsx and Conversation - Add sendSummaryNotifyTip() utility in dmworksummary that iterates source groups/threads and sends the tip via WKSDK - Trigger the tip from SummaryDetailPage (WS + fallback poll paths) and ChatSummaryHistory (chat-window flow) on COMPLETED transition - Add i18n keys (zh-CN / en-US) - Add unit tests (6 cases, all passing) Closes Mininglamp-OSS#289
lml2468
left a comment
There was a problem hiding this comment.
Review: octo-web #291 — feat: send summary notify tip to groups when smart summary completes
Commit: 2bd3432
Verdict: CHANGES_REQUESTED
Findings
- P1
packages/dmworksummary/src/pages/SummaryDetailPage.tsx:283/packages/dmworksummary/src/pages/SummaryDetailPage.tsx:353/packages/dmworksummary/src/components/ChatSummaryHistory.tsx:119: the notify side effect is triggered by whichever authorized client observes the task transition, not by the task creator and not by a durable once-only owner.sendSummaryNotifyTip()stamps the message withWKApp.loginInfo.uid/name, and backend contract verification shows creators and explicit participants can list/get/batch-status the same task. For BY_PERSON tasks, a participant who has the detail/list open can emit a group tip saying they summarized the chat; multiple participants, tabs, or devices can also each send duplicate tips. The per-componenthasSentNotifyTip/notifiedTaskIdsguards only dedupe one mounted component instance. This needs a single authoritative sender, preferably backend-side/idempotent, or at minimum backend-providedcreator_idplus a durable notification state so only the creator path can send exactly once. - P1
packages/dmworksummary/src/utils/sendSummaryNotifyTip.ts:27: every group/thread source is notified without checking the summary's visibility policy. The verified backend contract grants summary access only to the creator or explicit participants; source-group membership alone is denied. So a private/participant-only summary over a group/thread will still post a tip into that whole group/thread, leaking that the summary was run to users who cannot read the summary. Please gate the target list by backend-visible notification policy, for example only group-visible summary modes, or return explicit notify targets from the summary API.
Verification done
- Files read at HEAD SHA:
packages/dmworkbase/src/Components/Conversation/index.tsx,packages/dmworkbase/src/Messages/SummaryNotify/index.tsx,packages/dmworkbase/src/Service/Const.ts,packages/dmworkbase/src/i18n/locales/en-US.json,packages/dmworkbase/src/i18n/locales/zh-CN.json,packages/dmworkbase/src/index.tsx,packages/dmworkbase/src/module.tsx,packages/dmworksummary/src/components/ChatSummaryHistory.tsx,packages/dmworksummary/src/pages/SummaryDetailPage.tsx,packages/dmworksummary/src/utils/__tests__/sendSummaryNotifyTip.test.ts,packages/dmworksummary/src/utils/sendSummaryNotifyTip.ts. - Callsites checked:
sendSummaryNotifyTiponly fromSummaryDetailPageWS/fallback paths andChatSummaryHistorypolling path;summaryNotifyregistration/render/system-message usage;SourceTypecreation inSummaryCreatePage,ChatSummaryNewModal, andchannelType.ts; content-type 21 references. - Backend API contract: verified against
Mininglamp-OSS/octo-smart-summarymain:ListSummaries,GetSummary, andBatchStatusauthorize creator OR explicit participant; tests confirm participant access and source-group-member denial; source type mapping is frontend1=group, 2=thread, 3=DMwith thread mapped to channel type 5. I also confirmedGetSummaryreturns permissions but not a creator id suitable for frontend creator-only notification gating. - Error handling: WuKongIM send errors are swallowed as best-effort, which is appropriate for non-critical notify tips.
- i18n: both zh-CN and en-US strings are present.
- Tests run:
pnpm --filter @dmwork/summary exec vitest run src/utils/__tests__/sendSummaryNotifyTip.test.ts src/components/__tests__/ChatSummaryHistory.test.tsxpassed (2 files / 17 tests);NODE_OPTIONS='--localstorage-file=/tmp/octo-web-291-localstorage.json' pnpm --filter @dmwork/summary exec vitest runpassed (16 files / 140 tests). Full@octo/baseVitest and rawtsc --noEmitare not clean in this temp checkout due existing/unrelated React/type-resolution issues.
Summary
The rendering, content-type registration, i18n, and best-effort send failure behavior are in the right shape. The blocker is ownership/visibility: summary completion is a shared task-state transition, not a local user action like screenshot capture, so client-side observers cannot safely be the notification sender.
REVIEW_STATE: CHANGES_REQUESTED
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #291 (octo-web)
Summary
This PR adds a grey system tip ("X summarized the chat") that is broadcast to each summarized group/thread when a smart-summary task completes. The new message type (summaryNotify, contentType=21) and its cell renderer are modeled cleanly after the existing screenshot (type=20) pattern, the registration wiring is correct, and 6 unit tests for the send utility pass.
However, the trigger and attribution model is incorrect. The tip is sent from client-side polling and attributed to whoever's browser happens to observe the COMPLETED transition — not to the task creator. This produces a user-visible correctness bug (wrong name broadcast to a group) and a duplicate-message bug. I'm requesting changes on those grounds.
Blocking issues
P1 — Tip is attributed to the observer, not the summary creator
packages/dmworksummary/src/utils/sendSummaryNotifyTip.ts:20-21,38-39
const uid = WKApp.loginInfo.uid;
const name = WKApp.loginInfo.name || '';
...
msg.fromUID = uid;
msg.fromName = name;from_uid / from_name are taken from the currently logged-in user, with no check that this user actually created the summary. The two call sites
(ChatSummaryHistory.tsx:124 and SummaryDetailPage.tsx:285,355) also pass detail.sources with no creator guard, and SummaryDetail / SummaryListItem in types/summary.ts:116-160 expose only creator_name? — there is no creator UID field, so the client could not filter "am I the creator?" even if it tried.
Consequences:
- If user B opens the chat summary panel (
ChatSummaryHistory) while a summary that user A created reaches COMPLETED, B's client broadcasts "B summarized the chat" to the group. Wrong person. - For a scheduled summary (
TriggerType.SCHEDULED, no human creator), the name shown is simply whoever happened to be watching.
The PR description shows the intended UX as "吴明辉 总结了群聊内容" (the creator). The implementation cannot guarantee that — it shows the observer. This is a public, in-group, wrong-attribution message and should be fixed before merge.
P1 — Duplicate tips: no cross-client / server-side idempotency
ChatSummaryHistory.tsx:33,108-120 (notifiedTaskIds) and SummaryDetailPage.tsx:95,283-285,353-355 (hasSentNotifyTip) dedup only within a single component instance in a single browser tab. There is no shared/server-side "tip already sent" record (sendSummaryNotifyTip calls WKSDK.chatManager.send directly with no API round-trip; summaryApi.batchStatus tracks status only).
So a single task completion fans out to one tip per observing client:
- Multiple group members each viewing the summary panel → N tips.
- Same user with the panel open in two tabs → 2 tips.
- Panel + detail page open simultaneously → up to 2 tips from one user.
Combined with the attribution bug above, a group can receive several "X summarized the chat" lines with different names for the same summary. This is spammy and confusing enough to block.
P1 — Delivery depends on a client observing the transition
All three send sites fire only from client-side polling while the relevant component is mounted (doPoll, handleStatusChangeEvent, doFallbackPollOnce). There is no server push / background trigger. If a summary (especially a scheduled one) completes while no one has the summary panel/detail open, the tip is never sent.
Taken together, the three issues point to the same root cause: this notification is emitted from the client instead of the backend. The PR description states "no backend changes needed," but the desired semantics — send exactly one tip, attributed to the creator, reliably on completion — are only achievable server-side (or via a backend-issued system message). I'd recommend moving the emit to the backend on the summary-completed event; the client-side SummaryNotifyContent decoder/renderer added here is the right and reusable half of the change.
Non-blocking (nits / suggestions)
- Unused import.
ChatSummaryHistory.tsx:7importsSourceType, which is not referenced in the file.noUnusedLocalsisfalsein the shared tsconfig so this won't break the build, but it will likely trip thenext/eslint lint step and is dead code — drop it. - Best-effort error swallowing is reasonable here, but note that because failures are silently ignored (
sendSummaryNotifyTip.ts:42-44), a partial multi-source send (e.g. tip lands in group A but throws for thread B) is invisible. Acceptable for a non-critical tip; just be aware there's no retry. - Test coverage gap. The 6 tests validate
sendSummaryNotifyTipin isolation with a mockedloginInfo, so they cannot catch the attribution/duplicate problems above — those live in the call-site/trigger logic, which is untested. If the design stays client-side, add tests around the dedup/trigger paths. - Renderer/wiring LGTM.
SummaryNotify/index.tsx,Const.ts:70,module.tsx(cell + SDK decoder registration),Conversation/index.tsx:1815(treated as system message),index.tsxexport, and the i18n keys all mirror the screenshot pattern correctly. contentType=21 has no collision.
Verdict
The new message type and rendering are well-built, but the notification's attribution, deduplication, and delivery are all incorrect under realistic multi-user / scheduled scenarios because the emit happens client-side. These are user-facing correctness bugs, so requesting changes.
Backend contract review (OCT-32)The PR says "no backend changes needed," but the current client-only approach has three correctness/security problems that the backend ( 1. Wrong author —
|
lml2468
left a comment
There was a problem hiding this comment.
Code Review Verdict: COMMENT (PASS-WITH-RISK)
Reviewer: review-lead embedding code-reviewer persona
Scope: full diff (11 files, +253/-1)
Strengths
- Cleanly mirrors the existing
ScreenshotContent(type=20) pattern — registration, decoder, cell renderer, conversation system-message treatment all consistent. - Two-point dedup (
hasSentNotifyTiponSummaryDetailPage,notifiedTaskIdsonChatSummaryHistory) reasoned about and reset on taskId change. - i18n keys added in both
zh-CNanden-US. - Best-effort error swallowing is acceptable for a UI notification path.
Findings (non-blocking, worth a follow-up)
-
Cross-component dedup gap —
hasSentNotifyTipandnotifiedTaskIdsare component-local. If a user hasSummaryDetailPageopen whileChatSummaryHistory's batch poll also flips a task toCOMPLETED, both paths can firesendSummaryNotifyTipfor the same task. Result: duplicate tip messages in the group. Consider lifting dedup into a module-levelSet<number>keyed by task_id, or routing both paths through a single emitter. -
SourceTypeimport looks unused inChatSummaryHistory.tsx— lineimport { TaskStatus, SourceType } from '../types/summary';addsSourceTypebut I don't see it referenced inside this diff hunk. If it's truly unused, lint should catch it. -
sendSummaryNotifyTipdoes extragetSummaryDetailround-trip — when multiple tasks flip to COMPLETED in one batch poll, each one fires a sequentialawait summaryApi.getSummaryDetail(taskId). Low frequency in practice but aPromise.allover the fetches would be a cheap win. -
fromNamepayload field is partially defensive —tipgetter preferschannelManager.getChannelInfoand falls back tothis.fromName. That's fine, but worth a one-line comment so future readers don't thinkfromNameis the canonical display. -
Hardcoded
21in test mock —SummaryNotifyContenttest mock returns21literally. Minor: re-exportMessageContentTypeConst.summaryNotifyand use it to keep the source-of-truth single.
Risk Summary
None of the above are correctness blockers. The duplicate-notification risk (finding 1) is user-visible if it triggers, so a short follow-up issue would be welcome.
lml2468
left a comment
There was a problem hiding this comment.
QA Verdict: COMMENT (PASS-WITH-RISK)
Reviewer: review-lead embedding qa-engineer persona
Scope: test additions + CI status check
Coverage added
6 unit tests on sendSummaryNotifyTip:
- group / thread / DM source routing ✅
- multi-source dispatch ✅
- empty-list no-op ✅
- error-swallow path ✅
These cover the utility function's branches well.
Gaps
-
No integration test for
SummaryDetailPage.hasSentNotifyTipdedup — the flag is set + reset around taskId changes. A regression that flipped its lifecycle (e.g. failing to reset on unmount) would not be caught by the current suite. -
No integration test for
ChatSummaryHistory.notifiedTaskIdsdedup — same concern. A test that polls twice in a row with the same task inCOMPLETEDstate would lock in the contract. -
No
decodeJSON/encodeJSONround-trip test onSummaryNotifyContent— the wire format{ type, from_uid, from_name }is the cross-platform contract with iOS/Android. A round-trip test is cheap insurance. -
No test for cross-component duplicate (see code-review finding #1). Even an asserted reproducer would document the known limitation.
CI signal (worth investigating before merge)
gh pr view 291 reports:
check-sprint / check-sprint— FAILURE (likely a sprint-label policy check, not code; worth confirming)code-reviewStatusContext — FAILURE (a prior automated/manual code-review run flagged something; needs the author to address or explicitly justify)
I did not check out and run pnpm test in this tick (size/L + 15-min budget). PR body claims dmworksummary suite is 140/140 green locally. Recommend the author either re-run CI to confirm test workflow status (none visible in the current statusCheckRollup) or attach a fresh local pass before merge.
Recommendation
Functional path looks safe. Add the three integration tests above and resolve the two CI red checks before merge.
lml2468
left a comment
There was a problem hiding this comment.
Security Verdict: APPROVE (CLEARED)
Reviewer: review-lead embedding security-engineer persona
Scope: STRIDE pass + SBOM diff + authN/authZ
STRIDE pass
| Threat | Assessment |
|---|---|
| Spoofing | from_uid / from_name set from WKApp.loginInfo client-side. Backend must validate sender vs session token — that's the standard trust model for client-sent messages (same as existing ScreenshotContent type=20). No new spoofing surface. |
| Tampering | Tip travels over the same WuKongIM channel as user messages; integrity protections inherited. No serialization quirks (plain JSON object). |
| Repudiation | Tip is attributable via from_uid — fine. |
| Information disclosure | Tip leaks from_uid + from_name to group members. Both are already visible in the group context (membership listing). No new disclosure. The PR explicitly excludes summary content from the notification — good. |
| Denial of service | sendSummaryNotifyTip loops over sources[] with no rate limit. Bounded by user's group/thread memberships (typically small). Acceptable for a UI-triggered action; not externally-attacker-controllable. |
| Elevation of privilege | None. User can only send the tip to channels they're already a member of (sources are derived from their own summary task). |
Dependency / SBOM
No new dependencies. Uses existing wukongimjssdk and @octo/base re-exports. No dependencies-changed label, consistent with diff.
AuthN / AuthZ
- No new authentication paths.
- No new permission gates needed — the tip rides on existing channel-send permissions the user already has by virtue of being summary-task owner over those sources.
Crypto
No crypto changes. Tip is not E2E content; rendered as system message.
Privacy
- "{{name}} summarized the chat" reveals only that the user ran a summary. No summary content is exposed.
- Skipping DM sources (
source_type=3) is the correct privacy default — don't notify yourself.
Conclusion
No security blockers. Standard client-message trust model applies, server-side sender validation assumed (unchanged from existing pattern). CLEARED.
Aggregate Verdict: RISKED — needs-human-review3 reviewer verdicts collected:
Why RISKED (not APPROVED)
Additional human concernGitHub reports Next stepHuman reviewer to:
Aggregated by |
Summary
When a user completes a smart summary, a tip message (grey system text) is sent to each summarized group/thread, notifying other members:
This mirrors the screenshot notification pattern — the client sends a tip directly via WuKongIM SDK, no backend changes needed.
Closes #289
Changes
New:
SummaryNotifyContent(contentType=21)packages/dmworkbase/src/Messages/SummaryNotify/index.tsxModeled after
ScreenshotContent(type=20):from_uidandfrom_namewk-message-system(grey centered text)"吴明辉 总结了群聊内容"/"Test User summarized the chat"New:
sendSummaryNotifyTip()utilitypackages/dmworksummary/src/utils/sendSummaryNotifyTip.tssources[]from the summary taskIntegration points
SummaryDetailPageChatSummaryHistoryBoth use dedup flags (
hasSentNotifyTip/notifiedTaskIds) to ensure tips are sent exactly once per task.Registration
Const.ts:summaryNotify = 21module.tsx: Cell renderer + SDK content decoder registeredConversation/index.tsx: Treated as system message (no bubble, no avatar)index.tsx: Exported from@octo/baseTesting
6 unit tests added and passing:
Full
dmworksummarytest suite: 140/140 tests passing.Not included
octo-ios/octo-android)