Skip to content

Claude/feature llm moderation posts - #14

Merged
cyanmint merged 18 commits into
defaultfrom
claude/feature-llm-moderation-posts
May 24, 2026
Merged

Claude/feature llm moderation posts#14
cyanmint merged 18 commits into
defaultfrom
claude/feature-llm-moderation-posts

Conversation

@cyanmint

Copy link
Copy Markdown
Contributor

No description provided.

mycyg and others added 9 commits May 17, 2026 02:25
…fication

Three new modules join the existing activities module on a unified "广场":
- POST: general dynamics — any VERIFIED+ member
- MEDICAL: hospital/doctor reviews — any VERIFIED+ posts, all comment
- RESOURCE: skills on offer or requests for help — VERIFIED+ posts

Activities are tightened to TRUSTED+ to match their higher trust profile.

Every post, comment, and event now passes through containsBlockedTerms
(cheap regex backstop) and then moderateAndClassify, which calls an
Anthropic-compatible /v1/messages endpoint (e.g. Deepseek). The LLM
returns a verdict (pass/flag/reject), a user-facing reason, the true
section the content belongs in, and risk categories. Verdict drives
status: pass->PUBLISHED, flag->PENDING_REVIEW (admin queue),
reject->400 with reason. Section mismatch (e.g. an "event" posted as
POST) is rejected with guidance. The service fails closed: any
network/parsing error becomes a flag so content is parked for human
review rather than silently dropped.

D1 schema 0005 adds the posts table, the moderation result columns
on both posts and events, and rebuilds comments to allow polymorphic
event_id XOR post_id.

Routes added:
- GET/POST /api/posts, GET/PATCH/DELETE /api/posts/:id
- GET/POST /api/posts/:id/comments, PATCH /api/posts/comments/:cid/hide
- GET /api/posts/admin/pending, PATCH /api/posts/:id/review

Frontend adds /posts list (tab-filtered by section), /posts/new,
/posts/[id] (with pending-review banner for the author/admin),
/admin/posts review queue, and a "广场" entry in TopBar/MobileNav.

Vitest covers the LLM parser and fail-closed branches (13 tests).
Integration verified locally end-to-end against Deepseek v4-flash:
benign passes, keyword spam rejected at the door, hookup content
rejected by LLM, event-shaped content from non-TRUSTED rejected with
guidance, MEDICAL+RESOURCE posts published, comments moderated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This revision makes the user experience around posts/events flow purely
through notifications + email. The frontend no longer exposes any
moderation surface or asks the user to pick a section — the LLM
classifies + tags + decides verdict; users just write title/body.

Changes:
- LLM service (worker/src/lib/llm.ts) now produces a controlled tag
  vocabulary alongside verdict/section/categories. The prompt is
  rewritten to treat medical-science / medical-experience as first-class
  citizens (no longer require "specific hospital name") and to be honest
  about the EVENT-tier rule. A new generateXiaoTReply produces a warm,
  bounded reply for question/help posts.
- New retry helper (worker/src/lib/retry.ts) with exponential backoff
  500/1500/3000ms, applied around both moderation and 小T calls. Failures
  still close down to flag→PENDING_REVIEW so nothing slips through.
- Migration 0006 introduces a 小T system user (id 'system-xiao-t',
  handle 'xiao_t') and adds is_bot to comments so the UI can label
  machine-generated replies.
- POST /api/posts always returns 200 with status; the verdict path
  records the row in REJECTED/PENDING_REVIEW state, writes a notification
  in every case, and emails the author for non-PUBLISHED outcomes.
  Same async pattern applied to event creation. Activities no longer
  require an onlineUrl up-front (organizer can share the link later).
- Posts list page now switches by tag (全部 / 提问求助 / 医疗 / 资源 /
  分享). PostForm shrinks to title/body/visibility — the LLM derives
  everything else.
- Notifications page learns POST_APPROVED / POST_PENDING_REVIEW /
  POST_REJECTED / XIAO_T_REPLIED / EVENT_APPROVED / EVENT_PENDING_REVIEW
  / EVENT_REJECTED.
- 22 vitest cases cover parser robustness, retry semantics, fail-closed
  paths, 小T trimming + sustained-failure handling.

Integration tested end-to-end against deepseek-v4-flash:
question-help post → 小T comment landed in seconds, paired with
POST_APPROVED and XIAO_T_REPLIED notifications. Medical-science post
without hospital name now publishes. Hookup content is saved REJECTED,
hidden from public, notified to the author. Online event with no link
publishes correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier draft tried to auto-demote LLM-classified EVENT content to a
regular post. That's the wrong move: activities are a high-trust
channel with its own tier gate, and silently turning an activity
solicitation into a casual post would let GUEST/VERIFIED users smuggle
event coordination into the broader feed.

Now:
- EVENT-classified content from /api/posts is rejected outright with a
  reason that explains the tier requirement and points at the activities
  module. The post is still saved in REJECTED state so the author keeps
  a record and is notified.
- POST/MEDICAL/RESOURCE classification continues to happen
  automatically — users only write title+body, the LLM picks the
  section and the tag. Verified end-to-end against deepseek-v4-flash:
  a hormone-self-test post lands in MEDICAL with medical-science tag;
  a "I can review your résumé" offer lands in RESOURCE with
  resource-offer tag; an event-organising post is rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Posts board now actively refuses to host event coordination, with the
exact response depending on the author's tier and the completeness of
the LLM-extracted event details:

- < TRUSTED: post is not saved. Author is notified (POST_BLOCKED_EVENT)
  with an explanation that activities require TRUSTED+ and a hint to
  ask an organizer to host on their behalf. Email goes out.
- TRUSTED+ AND draft is complete (title/description/category/format/
  start/end, plus city for offline/hybrid): a new activity is created
  in /api/activities directly from the draft. Author gets
  POST_RELOCATED_TO_EVENT pointing at the new event page; no post row
  is created.
- TRUSTED+ AND draft is incomplete: the post is saved in
  PENDING_REVIEW state so the author keeps their text. The
  POST_NEEDS_EVENT_INFO notification lists exactly which fields are
  missing (开始时间、城市…), and an email links back to the post.

To make this work, the LLM prompt was extended with a fifth output
slot — eventDraft / eventDraftComplete / eventDraftMissing — and the
moderation max_tokens was raised from 500 to 1500 so deepseek's
thinking block plus a fully-spec'd draft fit without truncation.
The completeness check is double-validated: even if the model says
complete=true, we recompute and downgrade if obvious gaps remain.

Integration verified end-to-end against deepseek-v4-flash:
"6月21日周日下午两点上海徐家汇桌游聚会" by a TRUSTED user got auto-relocated
to a SOCIAL/OFFLINE activity on 2026-06-21 14:00→16:00 with city=上海,
preciseAddr=徐家汇市民活动中心二楼212室, capacity=20. A vague "想组织
一次聚会" by the same user was held back with a notification listing
[开始时间, 结束时间, 城市] as missing. An EVENT-shaped post by a
VERIFIED-only user was refused outright with no DB row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous EVENT handling rejected VERIFIED users for *any* post the
LLM classified as EVENT — even harmless "anyone been to last week's
hike?" questions. The LLM now distinguishes intent:

- ORGANIZING: author is actively calling people to attend a specific
  new activity (time/place/sign-up signals). Activities are gated to
  TRUSTED+, so the same three-tier flow as before applies: refuse for
  VERIFIED, auto-create activity for TRUSTED+ when the draft is
  complete, hold for completion otherwise.
- DISCUSSING: author is sharing, retrospecting, asking or discussing.
  Treated as a normal POST and saved with the LLM's chosen tag
  (share-life / question-help / announcement / ...).

Default-to-DISCUSSING when the model omits intent — the cost of letting
a conversation through is much lower than the cost of blocking it.

Also:
- max_tokens raised to 9000 so deepseek's thinking block has room to
  reason about the new schema without truncating JSON.
- /posts/:id detail page gets an Edit button (author/admin) and a
  Report button (everyone else). Reports of type POST are accepted by
  the worker (added to VALID_TYPES).
- New /posts/:id/edit page reuses the simplified PostForm in edit
  mode. Saves go through the same LLM moderation flow as creates.

Integration verified end-to-end with deepseek-v4-flash:
- "昨天的桌游聚会很有意思" by VERIFIED → PUBLISHED, tag share-life
- "有没有人参加过北京上周的姐妹徒步活动" by VERIFIED → PUBLISHED, tag question-help
- "组织本周六线下聚会" by VERIFIED → BLOCKED (still gated)
- 32 unit tests pass, including three new ones covering the intent split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two big additions land together.

== LLM auto-handles incoming reports ==
worker/src/lib/llm.ts adds evaluateReport(): given a target's
text + the reporter's reason, returns a verdict (valid|invalid|
uncertain) and an action (hide_target|dismiss|escalate).
worker/src/routes/reports.ts runs this asynchronously after the
report is recorded, then:
- hide_target  -> hides POST (status=HIDDEN), EVENT (CANCELLED) or
                  COMMENT (is_hidden=1); marks report RESOLVED;
                  notifies both reporter (REPORT_AUTO_RESOLVED) and
                  the content author (CONTENT_HIDDEN_BY_REPORT).
- dismiss      -> marks report DISMISSED; notifies the reporter
                  (REPORT_AUTO_DISMISSED).
- escalate     -> leaves the report OPEN for the admin queue with
                  the LLM's hint stashed in resolved_note; notifies
                  the reporter (REPORT_RECEIVED).
USER-type reports always escalate — we never let the model auto-
suspend an account. Failure to reach the LLM also escalates.

Verified end-to-end against deepseek-v4-flash:
- "我不喜欢爬山照片" report on a benign post → DISMISSED
- Report on an obvious "加微信办证" post     → AUTO HIDDEN
- USER-type report                           → escalated, OPEN

== Posts / comments feature completeness ==
- Comment authors can self-delete their own comments
  (DELETE /api/posts/comments/:cid). Nested replies cascade.
- CommentSection rewritten: nested tree rendering with one-level
  indent, inline Reply / Delete buttons, and a load-more button
  driven by cursor-based pagination (?cursor=created_at).
- Posts list grows a search input that drives ?q=, plus a
  "load more" button driven by ?page= pagination.
- New schema (migration 0007): post_likes, post_bookmarks,
  subscriptions(kind, ref) for following authors or tags.
- Worker endpoints: POST /api/posts/:id/{like,bookmark} toggles,
  GET /api/posts/me/bookmarks, GET /api/posts/me/drafts,
  GET/POST/DELETE /api/users/me/subscriptions.
- Posts board supports drafts via POST /api/posts?draft=1; saves
  status=DRAFT, skips moderation, only visible to the author.
- PostForm gains an image-upload button (uses existing /api/files
  endpoint, inserts ![](url) at cursor) and a "存为草稿" action.
- Detail page renders body via react-markdown + rehype-sanitize
  (links/markdown render natively without any extra plumbing).
- Detail page adds Like, Bookmark, Follow-author and Report
  buttons with optimistic updates and toast feedback.
- New /me/bookmarks and /me/drafts pages reachable from UserMenu.
- /notifications page handles all the new kinds.

37 vitest cases pass (added evaluateReport coverage:
USER-escalates, fail-closed, valid/invalid parsing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audited the posts/reports/likes/bookmarks/subscriptions surfaces and
fixed the issues that actually mattered.

Backend correctness:
- likes/bookmarks toggle now uses INSERT OR IGNORE + meta.changes to
  decide insert-vs-delete, so rapid concurrent clicks can't crash on a
  UNIQUE-violation. Both endpoints also require the post to exist and
  be visible to the viewer (a hidden/rejected post no longer
  accumulates phantom engagement).
- GET /api/posts/:id/comments now gates on the parent post's status +
  visibility. Hidden / draft / pending posts no longer leak their
  comments via direct API call.
- Posts list query folds comment_count and like_count into a single
  LEFT JOIN-on-derived-table instead of running N subqueries per page
  row.
- Visibility WHERE clause now parameterizes viewer.id (was string-
  concatenated); the ADMIN check is also explicit. Same shape as a
  helper returning { sql, params } for clean reuse.
- Reports route: if evaluateReport says hide_target but hideTarget()
  fails (target deleted between report and eval), the report no longer
  silently sits in OPEN. It escalates to admin with a note about the
  failure, and the reporter gets REPORT_RECEIVED rather than a false
  REPORT_AUTO_RESOLVED.
- Auth: system accounts (id starts with 'system-', e.g. xiao_t) cannot
  log in. Magic-link verify + TOTP login + password login each get an
  isSystemUser() guard returning 403. They exist only to author bot
  content.

Frontend polish:
- PostCard renders a like count alongside the comment count, and shows
  a DRAFT badge for the author's own draft entries.
- /me/drafts cards link the whole card body to /posts/:id/edit (draft
  detail page would 404 for non-authors, so detail was the wrong
  landing). Delete moves to a separate ghost button so it isn't a
  swipe-target inside the link.
- Like / bookmark / subscribe handlers gain a useRef-based "pending"
  lock so rapid double-clicks can't fire concurrent toggles and end
  the UI out-of-sync. Servers reconcile to whatever the response says.
- CommentSection caps visual nesting at depth 4 — deeper replies still
  render but stop indenting so the layout doesn't slide off the edge.
- MobileNav adds safe-area bottom padding, truncate on labels, and
  min-w-0 columns so 5 items fit on 320px screens without overflow.
- Notification page replaces the raw n.kind fallback with a friendly
  "你收到一条系统通知" + a console.warn so new kinds don't ship looking
  like SHOUTY_SNAKE_CASE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The events list query had the same string-concatenation pattern for
viewer.id that posts.ts already had — even with viewer.id sourced from
a trusted JWT, mixing concatenation with parameterization invites
future-bug surface area. Lift to the same { sql, params } helper
shape posts.ts uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix invite consumption flow tests and update documentation
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
transcommunity 638140e Commit Preview URL

Branch Preview URL
May 24 2026, 03:34 PM

Removed conditional checks for specific branches in build and deploy jobs.
@cyanmint

Copy link
Copy Markdown
Contributor Author

@copilot resolve all frontend build errors and api backend errors to make new features completely integrate‌

Copilot AI and others added 2 commits May 24, 2026 13:38
…in/posts frontend route

Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/a41c9e14-00b4-4976-9297-34e48a27c246

Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Copilot AI temporarily deployed to github-pages May 24, 2026 15:34 Inactive
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
transcommunity 099ace0 May 24 2026, 04:49 PM

Copilot AI temporarily deployed to github-pages May 24, 2026 16:09 Inactive
@cyanmint
cyanmint marked this pull request as ready for review May 24, 2026 16:17
Copilot AI temporarily deployed to github-pages May 24, 2026 16:49 Inactive
@cyanmint
cyanmint merged commit 2566834 into default May 24, 2026
5 checks passed
@cyanmint
cyanmint deleted the claude/feature-llm-moderation-posts branch May 24, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants