Skip to content

feat: send summary notify tip to groups when smart summary completes#291

Open
pkuWMH wants to merge 1 commit into
Mininglamp-OSS:mainfrom
pkuWMH:feat/summary-notify-tip
Open

feat: send summary notify tip to groups when smart summary completes#291
pkuWMH wants to merge 1 commit into
Mininglamp-OSS:mainfrom
pkuWMH:feat/summary-notify-tip

Conversation

@pkuWMH

@pkuWMH pkuWMH commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

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.tsx

Modeled after ScreenshotContent (type=20):

  • Decodes/encodes from_uid and from_name
  • Renders as wk-message-system (grey centered text)
  • i18n: "吴明辉 总结了群聊内容" / "Test User summarized the chat"

New: sendSummaryNotifyTip() utility

packages/dmworksummary/src/utils/sendSummaryNotifyTip.ts

  • Iterates sources[] from the summary task
  • Sends tip to group (ChannelTypeGroup=2) and thread (ChannelTypeCommunityTopic=5) sources
  • Skips DM sources (no need to notify yourself)
  • Best-effort: errors are silently ignored

Integration points

Location When
SummaryDetailPage WS event or fallback poll detects status → COMPLETED
ChatSummaryHistory Batch poll detects status → COMPLETED (chat-window flow)

Both use dedup flags (hasSentNotifyTip / notifiedTaskIds) to ensure tips are sent exactly once per task.

Registration

  • Const.ts: summaryNotify = 21
  • module.tsx: Cell renderer + SDK content decoder registered
  • Conversation/index.tsx: Treated as system message (no bubble, no avatar)
  • index.tsx: Exported from @octo/base

Testing

6 unit tests added and passing:

  • ✅ Sends tip to group sources
  • ✅ Sends tip to thread sources
  • ✅ Skips DM sources
  • ✅ Handles multiple sources (3 groups + 1 DM skip)
  • ✅ Does nothing for empty sources
  • ✅ Silently ignores send errors

Full dmworksummary test suite: 140/140 tests passing.

Not included

  • ❌ iOS/Android rendering (separate PRs needed for octo-ios / octo-android)
  • ❌ Group-level notification toggle (not needed for v1)
  • ❌ Summary content in the notification (only notifies the action)

…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
@pkuWMH pkuWMH requested a review from a team as a code owner June 5, 2026 14:28
@github-actions github-actions Bot added the size/L PR size: L label Jun 5, 2026

@lml2468 lml2468 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 with WKApp.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-component hasSentNotifyTip / notifiedTaskIds guards only dedupe one mounted component instance. This needs a single authoritative sender, preferably backend-side/idempotent, or at minimum backend-provided creator_id plus 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: sendSummaryNotifyTip only from SummaryDetailPage WS/fallback paths and ChatSummaryHistory polling path; summaryNotify registration/render/system-message usage; SourceType creation in SummaryCreatePage, ChatSummaryNewModal, and channelType.ts; content-type 21 references.
  • Backend API contract: verified against Mininglamp-OSS/octo-smart-summary main: ListSummaries, GetSummary, and BatchStatus authorize creator OR explicit participant; tests confirm participant access and source-group-member denial; source type mapping is frontend 1=group, 2=thread, 3=DM with thread mapped to channel type 5. I also confirmed GetSummary returns 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.tsx passed (2 files / 17 tests); NODE_OPTIONS='--localstorage-file=/tmp/octo-web-291-localstorage.json' pnpm --filter @dmwork/summary exec vitest run passed (16 files / 140 tests). Full @octo/base Vitest and raw tsc --noEmit are 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 yujiawei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:7 imports SourceType, which is not referenced in the file. noUnusedLocals is false in the shared tsconfig so this won't break the build, but it will likely trip the next/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 sendSummaryNotifyTip in isolation with a mocked loginInfo, 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.tsx export, 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.

@lml2468

lml2468 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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 (octo-smart-summary) is the right place to fix. I verified each against the service source.

1. Wrong author — from_uid is the observer, not the creator

sendSummaryNotifyTip stamps the tip with WKApp.loginInfo.uid (whichever member's client first observes status → COMPLETED). The test asserts exactly this (expect(msg.fromUID).toBe('user_001') = the logged-in observer). So "X 总结了群聊内容" will name a random online viewer, not the person who ran the summary.

The data already exists backend-side. octo-smart-summary stores CreatorID on the task (internal/model/model.go:104, set to effectiveUID on create at task.go:230). Scheduled summaries also carry a creator (the schedule owner). The web DTO (SummaryDetail) just doesn't expose it.

  • Minimal contract change: add creator_id (and keep creator_name) to the task detail + list-item response DTOs so the client can stamp the real author. This unblocks the client immediately.

2. Private-summary leak — fan-out ignores the access policy

The service already defines the authoritative visibility rule: canAccessTask = task creator OR explicit participant (internal/api/handler/task.go:41-78). The tip, however, is broadcast to the entire group/thread channel (ChannelTypeGroup / ChannelTypeCommunityTopic), i.e. to every member regardless of whether they may access the task. That leaks the existence + authorship of a creator/participant-only summary to non-participants.

A client cannot safely gate this — it doesn't hold the policy. Backend must own the notify-policy decision. Options, in order of preference:

3. Duplicate fan-out — dedup is per-client only

hasSentNotifyTip / notifiedTaskIds are in-memory per component instance. Every online member polling/observing COMPLETED fires its own tip → N duplicate system messages in the channel. Only a server-side idempotent emit (one tip per task, keyed on task id at the COMPLETED transition) actually solves this. Per-client flags can't.

Recommendation

Option (a) server-side emit resolves all three at once and is the correct long-term design. Caveat: octo-smart-summary has no IM/WuKongIM outbound path today — it would need to call octo-server's message-send (e.g. a system/bot sender) on completion. That's a real but bounded addition.

If you need to unblock the client now, the smallest backend change is expose creator_id + a backend-computed notifiable flag in the task DTO (fixes #1 and #2); #3 still requires server-side emit to be fully correct.

I'm filing a backend follow-up issue for the octo-smart-summary work and will link it here. Backend verdict on the client-only PR as-is: do not merge — it ships a private-data leak (#2) and a misattributed author (#1).

@lml2468 lml2468 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 (hasSentNotifyTip on SummaryDetailPage, notifiedTaskIds on ChatSummaryHistory) reasoned about and reset on taskId change.
  • i18n keys added in both zh-CN and en-US.
  • Best-effort error swallowing is acceptable for a UI notification path.

Findings (non-blocking, worth a follow-up)

  1. Cross-component dedup gaphasSentNotifyTip and notifiedTaskIds are component-local. If a user has SummaryDetailPage open while ChatSummaryHistory's batch poll also flips a task to COMPLETED, both paths can fire sendSummaryNotifyTip for the same task. Result: duplicate tip messages in the group. Consider lifting dedup into a module-level Set<number> keyed by task_id, or routing both paths through a single emitter.

  2. SourceType import looks unused in ChatSummaryHistory.tsx — line import { TaskStatus, SourceType } from '../types/summary'; adds SourceType but I don't see it referenced inside this diff hunk. If it's truly unused, lint should catch it.

  3. sendSummaryNotifyTip does extra getSummaryDetail round-trip — when multiple tasks flip to COMPLETED in one batch poll, each one fires a sequential await summaryApi.getSummaryDetail(taskId). Low frequency in practice but a Promise.all over the fetches would be a cheap win.

  4. fromName payload field is partially defensivetip getter prefers channelManager.getChannelInfo and falls back to this.fromName. That's fine, but worth a one-line comment so future readers don't think fromName is the canonical display.

  5. Hardcoded 21 in test mockSummaryNotifyContent test mock returns 21 literally. Minor: re-export MessageContentTypeConst.summaryNotify and 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 lml2468 added review:done:code:comment code-reviewer APPROVE-WITH-NITS and removed review:running:code code-reviewer review in progress labels Jun 27, 2026

@lml2468 lml2468 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  1. No integration test for SummaryDetailPage.hasSentNotifyTip dedup — 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.

  2. No integration test for ChatSummaryHistory.notifiedTaskIds dedup — same concern. A test that polls twice in a row with the same task in COMPLETED state would lock in the contract.

  3. No decodeJSON / encodeJSON round-trip test on SummaryNotifyContent — the wire format { type, from_uid, from_name } is the cross-platform contract with iOS/Android. A round-trip test is cheap insurance.

  4. 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-sprintFAILURE (likely a sprint-label policy check, not code; worth confirming)
  • code-review StatusContext — 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 lml2468 added review:done:qa:comment qa-engineer PASS-WITH-RISK and removed review:running:qa qa-engineer review in progress labels Jun 27, 2026

@lml2468 lml2468 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@lml2468 lml2468 added review:done:security:approve security-engineer CLEARED and removed review:running:security security-engineer review in progress labels Jun 27, 2026
@lml2468

lml2468 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Aggregate Verdict: RISKED — needs-human-review

3 reviewer verdicts collected:

  • qa: COMMENT (PASS-WITH-RISK) — 6 unit tests on sendSummaryNotifyTip cover the utility branches well, but SummaryDetailPage.hasSentNotifyTip dedup lifecycle and ChatSummaryHistory.notifiedTaskIds reset behavior lack integration coverage. Regression risk if either flag's lifecycle silently breaks.
  • security: APPROVE (CLEARED) — STRIDE pass clean. No new spoofing/tampering surface beyond the existing ScreenshotContent (type=20) trust model. Backend remains the authority for sender validation.
  • code: COMMENT (APPROVE-WITH-NITS) — Cleanly mirrors ScreenshotContent pattern; dedup reasoning is sound; i18n present in zh-CN/en-US. Minor nits noted in the per-role verdict.

Why RISKED (not APPROVED)

qa raised a with-risk flag (untested dedup lifecycle in the two integration points). Per pr-tick §7 aggregation rules, any risk / with-risk without changes ⇒ RISKED, requiring human review before merge.

Additional human concern

GitHub reports mergeable=CONFLICTING against main — needs a rebase/merge before merge is even possible. Independent of the review outcome.

Next step

Human reviewer to:

  1. Decide whether the qa-flagged dedup-lifecycle risk is acceptable for v1 or warrants the requested integration test.
  2. Resolve the merge conflict against main.
  3. Merge if both gates pass.

Aggregated by review-lead (autopilot pr-tick-octo-web). Agent will not merge — human action required.

@lml2468 lml2468 added review:complete 3 verdicts aggregated, awaiting human merge needs-human-review labels Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human-review review:complete 3 verdicts aggregated, awaiting human merge review:done:code:comment code-reviewer APPROVE-WITH-NITS review:done:qa:comment qa-engineer PASS-WITH-RISK review:done:security:approve security-engineer CLEARED size/L PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 智能总结完成后向被总结的群发送 tip 通知消息

3 participants