Skip to content

feat(transcript): public conversation-history detail page with Feishu OAuth (backend)#310

Open
uestney wants to merge 1 commit into
xvirobotics:mainfrom
uestney:feat/transcript-page-backend
Open

feat(transcript): public conversation-history detail page with Feishu OAuth (backend)#310
uestney wants to merge 1 commit into
xvirobotics:mainfrom
uestney:feat/transcript-page-backend

Conversation

@uestney

@uestney uestney commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an opt-in public conversation-history detail page accessible from Feishu cards via a 📜 查看完整对话 markdown link. Page renders the complete turn history (assistant text + tool calls + tool results) from the Claude jsonl transcript, gated by Feishu OAuth + per-bot open_id whitelist.

This is the backend half. The matching frontend PR (feat/transcript-page-frontend) ships the React TranscriptView component and route registration in web/. Either PR can merge in any order — backend cards still render (just with a dead link until the frontend ships); the frontend page shows a friendly error against an old backend.

What's in this PR

  • Three new HTTP routes (src/api/routes/transcript-routes.ts):
    • GET /api/auth/feishu/login?return=<url> — kicks off Feishu OAuth (passport.feishu.cn).
    • GET /api/auth/feishu/callback?code=...&state=... — verifies HMAC-signed state, exchanges code for open_id, signs an HttpOnly mb_session JWT cookie (7d), 302 to return.
    • GET /api/transcript/:chatId?turn=<n|all> — cookie + whitelist gated; returns parsed transcript JSON.
  • OAuth helper (src/feishu/oauth.ts) — HMAC-signed state, OIDC token exchange, hand-rolled HS256 JWT (no jsonwebtoken dep). METABOT_SESSION_SECRET auto-generated to .env.local on first start.
  • jsonl parser (src/session/transcript-reader.ts) — walks ~/.claude/projects/<workdir>/<sessionId>.jsonl, groups messages by turn (= user message boundaries), splits content[] into text / tool_use / tool_result, matches tool_use_id ↔ next-user-message's tool_result block.
  • Path helpers in src/session/session-registry.tsencodeWorkdir, projectTranscriptDir, sessionJsonlPath (storage-agnostic, no SQLite touched).
  • Card markdown linksrc/feishu/card-builder-v2.ts emits 📜 [查看完整对话历史](<publicBaseUrl>/web/transcript/<chatId>?turn=<n>) after the footer when the bot has transcriptLink set.
  • Per-chat turn counterMessageBridge increments a chatId → turnIndex map each turn, StreamProcessor.setTranscriptLink plumbs it into CardState.
  • Typed configpublicBaseUrl?: string and transcriptAllowOpenIds?: string[] on each bot in bots.json. Without publicBaseUrl, the link is not rendered — fully backwards compatible.
  • Auth exemptions in src/api/http-server.ts/api/auth/feishu/* (browser hits, no Bearer header) and /api/transcript/* (cookie-authed) bypass the global Bearer check.

Whitelist precedence

  1. bots.json → per-bot transcriptAllowOpenIds: string[] (recommended).
  2. Fallback: .envMETABOT_TRANSCRIPT_ALLOW_OPEN_IDS (comma-separated).
  3. Empty/missing → 403 with a friendly page that shows the logged-in user's open_id so the admin can copy/paste it into the whitelist.

Test plan

  • npm run build — green.
  • npm test — 323/323 passing (real tests/ dir; the 10 failures in stale _backup_dev/ are unrelated local-only files not on this branch).
  • npm run lint — 0 errors (2 pre-existing warnings in unrelated files).
  • OAuth round-trip from Feishu mobile + desktop (requires public deployment — being trialled on the SA bot at port 18443 behind a Caddy reverse-proxy to 127.0.0.1:10016).
  • First-login whitelist bootstrap: confirm the 403 page surfaces open_id so admins can self-serve.
  • Backwards compatibility: bot without publicBaseUrl → card renders exactly as before (no link), no warnings.

Not in this PR

  • Frontend TranscriptView React component — separate PR.
  • Real-time refresh / WebSocket — page is historical, REST-only.
  • Comment / annotation / search across turns — future iteration.

… OAuth

Add an opt-in `📜 查看完整对话` link to every Feishu card that opens a
public read-only React page showing the full turn history (assistant
text + tool calls + tool results), gated by Feishu OAuth + open_id
whitelist.

Why:
- Feishu cards are "update-in-place" so intermediate tool calls and
  thinking are overwritten by later progress — users see only the final
  text. The new page replays the complete jsonl transcript.
- OAuth + whitelist (per-bot `transcriptAllowOpenIds`) prevents random
  link sharing from exposing conversations.
- Opt-in via `publicBaseUrl` in `bots.json`: bots without it render
  cards exactly as before (no link, no exposure).

Backend pieces:
- `src/api/routes/transcript-routes.ts` — three endpoints:
  `/api/auth/feishu/login`, `/api/auth/feishu/callback`,
  `/api/transcript/:chatId?turn=<n|all>`. Whitelist + JWT cookie.
- `src/feishu/oauth.ts` — HMAC-signed state, OIDC token exchange,
  hand-rolled HS256 JWT (no jsonwebtoken dep). `METABOT_SESSION_SECRET`
  auto-generated to `.env.local` on first start.
- `src/session/transcript-reader.ts` — parses jsonl content blocks
  (text / tool_use / tool_result), groups by turn (= user message
  boundaries), matches tool_use_id ↔ tool_result.
- `src/session/session-registry.ts` — three pure path helpers
  (`encodeWorkdir`, `projectTranscriptDir`, `sessionJsonlPath`) shared
  with the reader.
- `src/feishu/card-builder-v2.ts` — emits the markdown link when the
  bot has a `transcriptLink` set on its CardState.
- `src/bridge/message-bridge.ts` + `src/engines/claude/stream-processor.ts`
  — per-chat turn counter feeds the transcript URL into CardState.
- `src/config.ts` — typed `publicBaseUrl` + `transcriptAllowOpenIds`
  on the bot config.

Tests: new `tests/oauth.test.ts` + `tests/transcript-reader.test.ts`;
extended `tests/card-builder-v2.test.ts` to assert the markdown link.

Docs: README.md / README_EN.md / CLAUDE.md / .env.example /
bots.example.json all describe the new fields.
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.

1 participant