# 2026-04-13 ## 背景 - 用户确认在现有 `fquiz` 系统内先落地 AI 聊天功能(先方案确认,再开发),要求最小改动闭环。 ## 改动 - 后端新增聊天域(FastAPI + SQLAlchemy): - 新增模型:`api/app/models/chat.py` - `chat_sessions` - `chat_messages` - 新增 Schema:`api/app/schemas/chat.py` - 新增服务: - `api/app/services/chat_service.py`(会话/消息读写与发送主流程) - `api/app/services/llm_gateway.py`(模型路由解析 + OpenAI-compatible 调用) - 新增路由:`api/app/api/v1/chat.py` - `GET /api/v1/chat/sessions` - `POST /api/v1/chat/sessions` - `GET /api/v1/chat/sessions/{session_id}/messages` - `POST /api/v1/chat/sessions/{session_id}/messages` - 入口接入: - `api/app/api/router.py` 挂载 `chat` 路由 - `api/app/models/__init__.py`、`api/app/core/database.py` 注册聊天模型确保 `create_all` 生效 - 聊天模型与密钥策略: - 复用模型管理路由规则,解析顺序: - `CAPABILITY: chat.default` - `GLOBAL: __global__` - 仅命中 `ENABLED` 且存在激活密钥记录的模型。 - 运行时 key 从环境变量读取,不反解库内 hash: - `LLM_PROVIDER_API_KEYS`(`openai=sk-...` 或 JSON)。 - 配置与部署模板: - `api/app/core/config.py` 新增: - `llm_provider_api_keys` - `llm_request_timeout_seconds` - `chat_context_message_limit` - `chat_default_system_prompt` - `.env.example`、`docker-compose.yml`、`.github/workflows/main.yml` 同步新增上述变量透传。 - `api/requirements.txt` 新增 `httpx==0.28.1`。 - RBAC 与菜单: - `api/app/services/seed_service.py` 新增权限 `chat.use`。 - 新增后台菜单 `admin.chat`(`/admin/chat`)。 - `admin` 默认菜单绑定新增 `admin.chat`。 - `api/app/services/admin_service.py` 把 `admin.chat` 加入内置受保护菜单集合。 - 前端(Next.js App Router + TanStack Query): - 新增页面:`web/src/app/admin/chat/page.tsx` - 会话列表 - 消息列表 - 发送消息 - 错误与反馈展示 - 新增类型:`web/src/types/auth.ts`(ChatSession/ChatMessage 等)。 - 后台首页增加入口卡片:`web/src/app/admin/page.tsx`。 - 首页快捷入口增加 `AI 聊天` 按钮:`web/src/app/page.tsx`。 ## 验证 - 后端语法编译: - `python3 -m compileall api/app` 通过 - 前端 lint: - `npm run lint:web` 通过 - 前端类型检查: - `cd web && npx tsc --noEmit` 通过 ## 风险与备注 - 当前为非流式回复,长回答时前端等待感较明显。 - 当前默认按 OpenAI-compatible `/chat/completions` 调用,若其他 Provider 接口不兼容需二期扩展适配层。 - 聊天表由 `create_all` 自动创建,适用于当前仓库口径;后续若引入正式迁移体系需补充迁移脚本。 ## 补充记录(2026-04-13 / web 构建修复) - 问题:`docker compose build web` 在 `RUN npm run build` 失败,堆栈指向 `web/src/app/layout.tsx` 中 `next/font/google`(`Space Grotesk` / `Manrope` / `JetBrains Mono`)拉取 `fonts.googleapis.com` 失败。 - 处理: - `web/src/app/layout.tsx` 移除 `next/font/google` 依赖与变量注入。 - `web/src/app/globals.css` 在 `:root` 补充 `--font-heading` / `--font-body` / `--font-mono` 本地字体回退栈,保持现有样式变量接口不变。 - 验证: - `cd web && npm run build` 通过。 - `docker compose build web` 通过。 - 风险: - 构建不再依赖外网字体下载,但渲染字体会按运行环境已安装字体回退,字形可能与 Google Fonts 原始效果有轻微差异。 ## 补充记录(2026-04-13 / 首页类型构建修复) - 问题:`docker compose build web` 在 `RUN npm run build` 的 TypeScript 阶段失败,报错集中在 `web/src/app/page.tsx`,`ButtonRootProps/InputRootProps` 不接受 `role/id/onPress` 等常用属性。 - 根因:`@heroui/react` 在当前仓库依赖组合(Next.js 16 + React 19 + TS 严格检查)下导出类型不完整,导致封装组件 `web/src/components/ui.tsx` 的 `Button/Input/Checkbox` 类型约束与页面实际用法不一致。 - 处理: - `web/src/app/page.tsx`:认证模式切换按钮从 `role="tab"+aria-selected` 改为 `aria-pressed` toggle 语义,避免不被组件类型支持的属性。 - `web/src/app/page.tsx`:将首页 `Button` 用法改为原生 `button`,保持现有 `btn-*` 样式类不变。 - `web/src/components/ui.tsx`:`Button/Input/Checkbox` 改为原生语义封装(保留 `btn-*` / `control` 约定),并为 `Checkbox` 兼容 `isSelected`、`onValueChange` 与布尔回调 `onChange`。 - 验证: - `cd web && npm run build` 通过(包含 `Running TypeScript ... Finished TypeScript`)。 - 风险: - 首页表单控件不再走 HeroUI 行为层,交互细节(如组件内建动画/状态样式)由本地样式类主导;当前页面功能不受影响,但若后续回切 HeroUI 需先升级或修复其类型声明。 ## 补充记录(2026-04-13 / HeroUI 类型阻断修复-2) - 问题:`docker compose build web` 在 `RUN npm run build` 的 TypeScript 阶段再次失败,先报 `web/src/components/ui.tsx` 中 `withDefaultButtonClass(className)` 参数不兼容(`className` 可能是函数),随后暴露 `ListBox/Modal/Table` 类型不接受 `children`。 - 根因:`@heroui/react` 在当前依赖组合下的导出类型与页面实际用法持续不一致,`Button/Input` 与容器类组件(`ListBox/Modal/Table`)都存在声明缺口。 - 处理: - `web/src/components/ui.tsx`:`Button/Input/TextArea/Checkbox` 全部切为原生语义封装,并兼容现有调用参数: - `Button` 兼容 `onPress`、`isDisabled` - `Checkbox` 兼容 `isSelected`、`defaultSelected`、`onValueChange`、布尔 `onChange` - `web/src/components/ui.tsx`:`ListBox/ListBoxItem/Modal/Table` 保持 HeroUI 运行时组件,但新增宽松 TS 包装类型,避免 `children` 和 `onSelectionChange` 的类型阻断。 - 验证: - `cd web && npm run build` 通过(`Running TypeScript ... Finished TypeScript`)。 - 风险: - `ListBox/Modal/Table` 的包装类型放宽后,编译期约束下降;后续若升级 HeroUI 并修复官方声明,建议回收宽松包装并恢复严格类型。 ## 补充记录(2026-04-13 / HeroUI Table 集合上下文运行时修复) - 问题:用户在前端运行时遇到 `cannot be rendered outside a collection.`,堆栈来自压缩产物,页面实际白屏。 - 根因:`web/src/app/admin/todos/page.tsx` 中 `Table.Header/Table.Body/Table.Column/Table.Row` 直接挂在 `Table` 下;当前 HeroUI 版本中 `Table` 仅是容器根节点,必须通过 `Table.Content` 承载 `react-aria` 的真正表格上下文,否则集合子节点会在运行时抛错。 - 处理: - `web/src/app/admin/todos/page.tsx`:将表格结构调整为 `Table -> Table.Content -> Table.Header/Table.Body`,其余业务逻辑与样式不变。 - 验证: - `npm run lint:web` 通过。 - `cd web && npx tsc --noEmit` 通过。 - `cd web && npm run build` 通过(含 `Generating static pages ... /admin/todos`)。 - 风险: - 仅修复 `admin/todos` 当前命中页面;若后续新增 HeroUI `Table` 页面,仍需遵循同一组合约束避免复发。 ## 补充记录(2026-04-13 / web 字体构建回归修复) - 问题:用户再次反馈 `docker compose build web` 在 `RUN npm run build` 失败,堆栈指向 `web/src/app/layout.tsx` 的 `[next]/internal/font/google/space_grotesk...`。 - 根因:`layout.tsx` 回归为 `next/font/google`(`Space_Grotesk` / `Manrope` / `JetBrains_Mono`)编译期下载字体,受限网络环境下失败。 - 处理: - `web/src/app/layout.tsx` 删除 `next/font/google` 导入与字体变量注入,仅保留 `h-full antialiased`。 - `web/src/app/globals.css` 在 `:root` 显式补齐 `--font-heading` / `--font-body` / `--font-mono` 本地字体回退栈。 - 验证: - `cd web && npm run build` 通过。 - `docker compose build web` 通过(`[builder 5/5] RUN npm run build` 完成,镜像 `fquiz-web` 构建成功)。 - 风险: - 字体外观按运行环境本地字体回退,可能与 Google Fonts 设计稿存在轻微字形差异。