Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6eaefe7
docs(openspec): 规划 Issue 34 领域模型迁移链
hifizz Aug 21, 2026
78d5523
refactor(domain): define canonical conversation model
hifizz Aug 21, 2026
4f64e78
feat(persistence): normalize conversation entities
hifizz Aug 21, 2026
dde8017
feat(generation): migrate canonical lifecycle
hifizz Aug 21, 2026
67b427a
feat(api): add canonical conversation commands
hifizz Aug 21, 2026
036f891
feat(client): normalize canonical conversation state
hifizz Aug 21, 2026
393cc4f
feat(cutover): enforce canonical conversation authority
hifizz Aug 21, 2026
15ab38f
fix(cutover): validate migration lineage
hifizz Aug 21, 2026
e1664f5
fix(deploy): isolate preview migrations
hifizz Aug 21, 2026
c996848
test(api): verify canonical HTTP against postgres
hifizz Aug 21, 2026
94cb3f4
test(cutover): rehearse local backup restoration
hifizz Aug 21, 2026
5f57dad
feat(cutover): add approved reset path
hifizz Aug 21, 2026
d36567b
docs(cutover): inventory legacy deletion dependencies
hifizz Aug 21, 2026
4fca823
docs(openspec): map legacy conversation supersession
hifizz Aug 21, 2026
e915c43
feat(cutover): enforce forward-only recovery
hifizz Aug 21, 2026
19b9e5a
feat(cutover): add health audit baseline
hifizz Aug 21, 2026
0d0684c
feat(cutover): validate release manifest
hifizz Aug 21, 2026
3f3c8eb
feat(cutover): bind actions to release manifest
hifizz Aug 21, 2026
22ae14f
feat(cutover): retain verified local backups
hifizz Aug 23, 2026
2c92964
docs(cutover): record approved local reset
hifizz Aug 23, 2026
4662984
feat(conversation): retire ThreadTree runtime
hifizz Aug 23, 2026
c1a070e
feat(database): drop legacy conversation tables
hifizz Aug 23, 2026
f3497d6
docs(cutover): record canonical verification
hifizz Aug 23, 2026
e6afc76
docs(openspec): sync canonical conversation specs
hifizz Aug 23, 2026
82f40cf
docs(openspec): archive define-conversation-domain-model
hifizz Aug 23, 2026
507b0f1
docs(openspec): archive normalize-conversation-persistence
hifizz Aug 23, 2026
9c4a901
docs(openspec): archive migrate-generation-lifecycle
hifizz Aug 23, 2026
ae18951
docs(openspec): archive add-conversation-command-api
hifizz Aug 23, 2026
384aa97
docs(openspec): archive normalize-conversation-client-state
hifizz Aug 23, 2026
cda256f
docs(openspec): archive retire-thread-tree-authority
hifizz Aug 23, 2026
ded0557
fix(theme): avoid client-rendered next-themes script
hifizz Aug 24, 2026
da9f89e
fix: 验证重构后的功能,修复问题
hifizz Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ DIRECT_URL=postgres://postgres.xxxx:password@aws-0-region.pooler.supabase.com:54
# 可选:每实例连接数上限(Serverless + 池化下宜小);直连想启用预处理语句设 DB_PREPARE=true。
# DB_POOL_MAX=10
# DB_PREPARE=false
# Vercel Preview 默认禁止在共享数据库上执行 DDL。只有给 Preview 配置了独立数据库时才设为 true。
# VERCEL_PREVIEW_DATABASE_MIGRATIONS=false

# === Issue #34 Conversation 单一权威与切换 ===
# ThreadTree 权威已退役;必须显式使用 canonical,禁止再拆成 client/API/generation 多个开关。
CONVERSATION_AUTHORITY=canonical
# 每次正式发布使用新的不可变 epoch,客户端会在加载实体前与服务端核对。
CONVERSATION_CUTOVER_EPOCH=replace-with-release-epoch
# 受控维护窗口可设为 read-only:拒绝新写入,但允许读取和 Generation Stop/排空。
# CONVERSATION_MAINTENANCE_MODE=off
# 仅本机/测试可用;生产环境设置 true 会启动失败。
# CONVERSATION_ISOLATED_TEST=false

# === 用户系统(better-auth,邮箱注册登录) ===
# BETTER_AUTH_SECRET:任意高熵随机串(如 openssl rand -base64 32)。
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pnpm-debug.log*
next-env.d.ts
.gstack/

# local cutover evidence and database dumps (never commit)
/.local-backups/
/.local-cutover/

# thread-chat e2e 验收脚本的截图输出
e2e/thread-chat/shots/
.vercel
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ project-wide instructions. `CLAUDE.md` is the single source of truth for shared
development commands, workflow rules, architecture, and implementation notes.

If an instruction in this file conflicts with `CLAUDE.md`, follow this file.

## Formatting

- **禁止 Agent 执行 `pnpm format`,也禁止主动执行其他 Prettier 格式化命令。** 仓库的 pre-commit hook 已负责格式化;如果该环节没有执行 format,不补跑、不排查,也不做任何手动格式化。
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Package manager is **pnpm** (pnpm-lock.yaml / pnpm-workspace.yaml).
- `pnpm build` — production build
- `pnpm lint` — ESLint (flat config, eslint.config.mjs)
- `pnpm typecheck` — `tsc --noEmit`
- `pnpm format` — Prettier (with prettier-plugin-tailwindcss for class sorting)
- `pnpm format` — 仅供 pre-commit hook 使用,Agent 禁止主动执行
- `pnpm db:generate` — generate a Drizzle migration from `lib/db/schema.ts`
- `pnpm db:migrate` — apply pending migrations to `DATABASE_URL`
- `pnpm db:push` — push schema directly without a migration file (quick local iteration)
Expand All @@ -27,7 +27,7 @@ To add a shadcn/ui component: `npx shadcn@latest add <name>` (lands in `componen

## Development workflow

- **Don't run `pnpm format` while writing code.** Only check logic correctness during development; formatting happens once, right before committing. (No husky/lint-staged is configured yet, so this is a manual discipline, not an enforced hook — set one up if asked.)
- **禁止 Agent 执行 `pnpm format`,也禁止主动执行其他 Prettier 格式化命令。** 仓库的 pre-commit hook 已负责格式化;如果该环节没有执行 format,不补跑、不排查,也不做任何手动格式化。
- **Run `pnpm typecheck` after each batch of code changes** (a file, or a set of related edits) and fix any errors immediately rather than letting them accumulate.
- **凡是 `import`(含 `import type`)某个包的子路径,该包必须是 `package.json` 里的直接依赖 —— 不要依赖幻影依赖(phantom dependency)。** pnpm 有时会把传递依赖 hoist 到 `node_modules/` 根,使得本地 `pnpm typecheck`/`pnpm build` 侥幸通过,但 Vercel 干净安装 + pnpm 严格解析下不可从项目根解析,构建报 `Cannot find module 'X'`。**本地构建过 ≠ Vercel 构建过。** 判据:import 的包名若不在 `package.json` 的 `dependencies`/`devDependencies` 里,就显式声明它(版本对齐同族包)。运行时才需要的进 `dependencies`,纯类型(`import type`,构建时擦除)进 `devDependencies`。已被 shiki 家族咬过两次:`@shikijs/langs`·`@shikijs/themes`·`@shikijs/transformers`(运行时,`dependencies`)、`@shikijs/types`(类型,`devDependencies`)—— 这个包族把 langs/themes/types 拆成一堆子包,直接 import 任一子路径都得声明。
- **After finishing a module-sized chunk of work, sweep for magic strings and duplicated variables/strings/functions.**
Expand All @@ -42,7 +42,7 @@ Next.js **16** App Router project (React 19, TypeScript, Tailwind CSS **v4**), s
- **shadcn/ui on Base UI, not Radix.** `components.json` uses the `base-rhea` style; primitives in `components/ui/` import from `@base-ui/react` (e.g. `@base-ui/react/button`). Don't reach for `@radix-ui/*` when editing or adding components.
- **The full component kit is already vendored** in `components/ui/` (~60 components), including chat-oriented primitives: `message.tsx`, `message-scroller.tsx`, `bubble.tsx`, `attachment.tsx`, `marker.tsx`. Check for an existing component before adding or writing a new one.
- **Tailwind v4, CSS-first config.** There is no tailwind.config file; theme tokens live as CSS variables in `app/globals.css`. Class merging goes through `cn()` in `lib/utils.ts`.
- **thread-chat 的手写样式** 是独立于 Tailwind 的一层:全部收敛在 `.tc` 作用域(手工命名空间的手稿风设计系统,语义类名、非原子类)。实体规则按功能区块拆在 `app/thread-chat/styles/*.css`,`app/thread-chat/thread-chat.css` 只是按**源码顺序** `@import` 它们的桶文件——改这里务必保持 `@import` 顺序(级联依赖它),且非相邻功能刻意拆成 `*-collapse/-stream/-extras` 等后缀文件以保序(如流式的 `.send.stop` 覆盖必须在 `composer.css` 之后)。设计 token(`--paper/--ink/--d1..d5/字体/尺寸`)的**单一来源**是 `styles/tokens.css`,`theme.ts` 的深度→变量映射依赖此处变量名
- **thread-chat 的手写样式** 是独立于 Tailwind 的一层:全部收敛在 `.tc` 作用域(手工命名空间的手稿风设计系统,语义类名、非原子类)。共享规则按功能区块拆在 `app/thread-chat/styles/*.css`,`app/thread-chat/thread-chat.css` 只是按**源码顺序** `@import` 它们的桶文件;canonical 页面自己的布局扩展位于 `app/thread-chat/canonical/canonical-thread-chat.css`。设计 token(`--paper/--ink/--d1..d5/字体/尺寸`)的单一来源是 `styles/tokens.css`。
- Path aliases: `@/components`, `@/components/ui`, `@/lib`, `@/hooks` (see `components.json` and tsconfig.json).
- Theming via `next-themes` through `components/theme-provider.tsx`, wired up in `app/layout.tsx` (dark mode toggles with the `d` key on the starter page).

Expand All @@ -61,7 +61,7 @@ Key patterns:

## AI backend

`app/api/chat/route.ts` streams from **MiniMax** via `@ai-sdk/openai-compatible` — not real OpenAI. AI SDK is at **v7** (`ai@^7`); note `ai` and `@ai-sdk/react` (`@^4`) track independent version numbers in this ecosystem, they are not in lockstep. Env vars live in `.env.local`: `MINIMAX_API_KEY`, `MINIMAX_BASE_URL`, `LLM_MODEL_ID`.
`app/api/chat/route.ts` 只服务首页 assistant-ui 的线性聊天;它通过统一模型注册表选择 MiniMax、Ark、OpenRouter 等 provider。AI SDK is at **v7** (`ai@^7`); note `ai` and `@ai-sdk/react` (`@^4`) track independent version numbers in this ecosystem, they are not in lockstep. Env vars live in `.env.local`,实际键名由 provider 注册表决定。

MiniMax emits chain-of-thought as literal `<think>...</think>` text rather than a dedicated reasoning stream part. The model is wrapped with `wrapLanguageModel` + `extractReasoningMiddleware({ tagName: "think" })` so it renders as a collapsible reasoning block instead of raw text in the message.

Expand All @@ -72,7 +72,7 @@ Three tools are wired end-to-end as a reference for adding more:

Each tool's custom UI is registered with the `useAssistantTool({ toolName, type, render, ... })` hook from `@assistant-ui/react`, in `components/assistant-ui/{weather,notepad,compare-table}-tool.tsx`. These are null-returning components mounted via `<AssistantTools />` (`components/assistant-ui/tools.tsx`) inside `AssistantRuntimeProvider` in `app/page.tsx`. `useAssistantTool` is marked `@deprecated` in favor of `defineToolkit`/`Tools({ toolkit })` + `useAui({ tools })`, but that path assumes assistant-ui's "use generative" compiler, which isn't set up in this project — keep using `useAssistantTool` for new tools unless that changes.

`/api/chat` also has a **threadChat mode** for the branch-chat page (`app/thread-chat/`): the client sends the full persisted turn identity (`anchorText`, `treeId`, `threadId`, user/assistant message ids, and an app `generationId`) in the body. The route validates that identity against the owner-scoped saved tree before any paid model call. AI SDK v7's `streamText` rejects system-role messages from the client, so system prompts remain server-owned and are built via `buildThreadChatSystem()` (`lib/chat/thread-chat-prompt.ts`, templates in `constants/thread-chat.ts`). In this mode `getWeather`/`compareTable` are excluded and the only backend artifact tool is `createMarkdownArtifact`. Its bilingual semantic description plus a conservative bilingual first-step intent check route requests to create/deliver a Markdown document; concept questions such as “Markdown 是什么?” remain ordinary text answers. `tool-input-start` creates a non-clickable inline progress card immediately; matching `tool-input-delta` chunks are accumulated by call id and parsed with AI SDK `parsePartialJson` to show partial title, real character/line counts, and recent headings. The validated `tool-input-available` event atomically replaces that transient progress with the persisted Markdown Artifact, rendered in the shared drawer through `MarkdownBody`. Never persist `Message.markdownGeneration` or partial tool input: `saveTree` strips the transient field, while the next prompt reconstructs model context only from completed message-owned Artifact title/content. `hasToolCall(createMarkdownArtifact)` terminates the model loop after the tool step so no redundant recap is generated. Additionally, `toUIMessageStreamResponse({ onError })` logs in-stream errors server-side (`[chat] 流内错误:`) for all modes while still masking them to the client.
`/thread-chat` 不使用 `/api/chat`。它通过 `app/api/conversation-commands/*`、`app/api/conversation-generations/*` 和 `app/api/conversations/*` 操作 canonical `Project → Conversation → Thread → Turn/Message/Generation` 实体;服务端从规范 snapshot 编译模型上下文,客户端只保存可丢弃的列/画布 UI 状态。向 `/api/chat` 发送旧 `threadChat` envelope 会返回 `410 legacy_protocol_retired`。

## Database & thread persistence

Expand All @@ -84,4 +84,4 @@ Drizzle ORM + Postgres backs chat history so threads survive page reloads (previ
- Client: `lib/db/index.ts` — a global-singleton `postgres`/drizzle client so dev HMR doesn't exhaust Postgres connections.
- Migrations: `drizzle.config.ts` + `drizzle/` (see `pnpm db:*` scripts above).
- Persistence wiring: `lib/chat/thread-list-adapter.ts` implements assistant-ui's `RemoteThreadListAdapter` (list/rename/archive/delete/initialize/fetch/generateTitle) against `app/api/threads/*` route handlers. `lib/chat/use-thread-history-adapter.ts` implements `ThreadHistoryAdapter.withFormat()` for per-thread message load/append. Both are composed in `app/page.tsx` via `useRemoteThreadListRuntime({ runtimeHook: () => useChatRuntime({ adapters: { history } }), adapter })` — `useChatRuntime`'s own internal remote-thread-list wrapper detects it's nested (`allowNesting: true`) and no-ops, so this composition doesn't conflict with it.
- Branch-tree persistence (`app/thread-chat/`): `branch_trees` stores each branch-conversation tree as **one owner-scoped row of whole-tree JSON** (`state` = full `ThreadTreeState`) — completely separate from the assistant-ui `threads`/`messages` tables. Tree identity lives in the URL (`/thread-chat/{treeId}`, client-generated UUID); localStorage only remembers the last tree id and per-tree workbench UI. Historical null-owner rows are never listed and may be atomically claimed only by opening their exact URL. `branch_generations` is the authoritative per-attempt sidecar: it stores the verified turn snapshot, lifecycle/heartbeat, versioned structured result, and billing state. The browser must strictly save the user message + assistant placeholder before posting `/api/chat`; refreshing only detaches the browser fetch, while the server-owned stream consumer continues the model, finalizes/charges once, and lets tree GET or generation polling merge the current terminal patch by `generationId` CAS. Only the explicit Stop API requests model abort. P0 restores the completed structured answer after refresh; it deliberately does not replay missed token deltas as a live stream (that is a separate resumable-stream/P1 protocol). Titles remain **dual-track**: debounced PUT writes derived `title`, rename PATCH writes `custom_title`, and display uses `coalesce(custom_title, title)`. Before deploying this contract, run `pnpm db:migrate`; do not deploy the new client before the owner/generation APIs and migration.
- **Canonical Conversation persistence** (`app/thread-chat/`): URL 中的 ID 是 `Conversation.id`,不是 Tree ID。`lib/db/schema.ts` 中的 `conversations`、`conversation_threads`、`thread_forks`、`conversation_turns`、`conversation_messages`、`conversation_generations`、Artifact、command/outbox 与反馈表是唯一事实源;关系由 FK、唯一约束和 revision/idempotency CAS 保护。客户端 normalized store 只投影服务端 snapshot,localStorage 仅保存按 Conversation 分隔且可丢弃的 UI workspace。裸 `/thread-chat` 通过认证后的 canonical bootstrap 找到或建立个人 Project/Conversation。旧 `branch_trees`、`branch_generations` 与 `branch_message_feedback` 已由迁移删除;历史 migration 和已验证备份不改写。变更 schema 使用 `pnpm db:generate` 后运行 `pnpm db:migrate`,不得把交互式 `db:push` 当作发布迁移。
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ The project is a Next.js 16 App Router application using React, TypeScript, Tail

| Boundary | Location | Responsibility |
| ------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Core | [`app/thread-chat/core/`](./app/thread-chat/core/) | Tree state, selectors, and the branch-conversation store |
| Legacy core | [`app/thread-chat/core/`](./app/thread-chat/core/) | Issue #34 迁移期间的临时整树权威、选择器与 branch-conversation store;不是目标领域模型 |
| Domain | [`lib/thread-chat/domain/`](./lib/thread-chat/domain/) | 规范 Conversation 实体、不变量、标题与变体选择契约 |
| Legacy import | [`lib/thread-chat/legacy/`](./lib/thread-chat/legacy/) | 遗留 Thread Tree 到规范 Conversation 快照的单向只读投影 |
| Branching | [`app/thread-chat/branching/`](./app/thread-chat/branching/) | `selection/` owns text anchors and selection UI; `assistant/` owns branch-aware assistant rendering |
| Chat | [`app/thread-chat/chat/`](./app/thread-chat/chat/) | `message/`, `composer/`, and `actions/` group the three conversation feature sets |
| Orchestration | [`app/thread-chat/orchestration/`](./app/thread-chat/orchestration/) | `canvas/`, `columns/`, `navigation/`, `artifacts/`, `overlays/`, and `workspace/` compose the workbench |
Expand All @@ -131,6 +133,8 @@ Detailed design material is available in the repository:

- [ChatPDF research](./docs/chatpdf/01-调研报告.md) and [design](./docs/chatpdf/02-设计方案.md)
- [Deep research design](./docs/deep-research/设计说明.md)
- [Conversation 领域术语与迁移边界](./docs/architecture/conversation-domain-glossary.md)
- [Issue #34 后续迁移输入清单](./docs/architecture/conversation-migration-inputs.md)
- [OpenSpec change records](./openspec/changes/)
- [Project development guidance](./CLAUDE.md)

Expand Down
30 changes: 0 additions & 30 deletions app/api/branch-generations/[generationId]/route.ts

This file was deleted.

34 changes: 0 additions & 34 deletions app/api/branch-generations/[generationId]/stop/route.ts

This file was deleted.

Loading
Loading