# LLM in Text 仓库指引 (v0.2.0)

本文件适用于整个仓库。进入更深层目录后，子目录中的 AGENTS.md 优先于本文件。

## 项目定位

- 这是一个智能 Markdown 编辑器，前端负责编辑器 UI、上传导出、补全交互和设置状态，后端负责 LLM、OCR、文件转换和 TTS 接口。
- 前端技术栈：Vue 3 + Vite + Milkdown/Crepe + Pinia + Vue Router。
- 后端技术栈：FastAPI + Python + OpenAI-compatible LLM endpoint + Redis Streams。
- 仓库内另有 `InTEX/` iOS 客户端工程，技术栈为 SwiftUI + SwiftData；当前仍是独立的 Xcode 初始工程，尚未接入 Web/Docker 后端。
- 项目版本：v0.2.0（自 b82c6d3 之后的全栈架构升级版本）。

## 功能块系统（核心概念）

- **统一命名**：文档块、PRO 块、上传块统称为"功能块"。
- **禁止嵌套**：所有功能块的 schema 均设 `atom: true, isolating: true`，ProseMirror 层面强制禁止互相嵌套。DocBlockCrepe.vue 的嵌套 Crepe 编辑器不注册任何功能块插件，从架构上杜绝深层嵌套。
- **无数量限制**：一篇文档可包含任意数量的功能块，彼此独立存在。
- **导入自动解析**：从 Markdown 文件导入后，各功能块语法必须通过对应的 Remark/parseMarkdown 解析器自动识别并还原为交互卡片。
- **导出可复原**：通过 toMarkdown/leafText 序列化器将功能块还原为 Markdown 语法，确保导出后再次导入能完整复原。doc_block toMarkdown 输出 legacy HTML tag，`getExportMarkdown()` 中通过 `transformLegacyDocBlocksForExport()` 转换为 fenced code block。

| 类型 | Node Type | Markdown 语法 | Plugin 文件 | Utility 文件 |
|------|-----------|---------------|-------------|--------------|
| 文档块 | `doc_block` | \`\`\`llm-file fenced code / `<doc_type=...>` legacy HTML tag | `plugins/docBlockPlugin.ts` | `utils/docBlock.js` |
| PRO 块 | `pro_block` | `[PRO]` / `[PRO]{指令}` | `plugins/proBlockPlugin.ts` | `utils/proBlock.js` |
| 上传块 | `upload_block` | `{{{}}}` / `{{{upload file type:...}}}` | `plugins/uploadBlockPlugin.ts` | `utils/uploadBlock.js` |

- **已验证**：当前代码完全符合"禁止嵌套、自动解析复原"的要求。修改功能块相关逻辑时需验证：1) schema 的 atom/isolating 属性不被移除；2) Remark/parseMarkdown/toMarkdown 解析链路完整。
- 当前代码中可以确认的主功能是：AI 补全、OCR、文档转 Markdown、TTS/ASR、Markdown/DOCX/PDF 导入导出。
- 历史文档中有一部分 TTS/ASR、Apple Silicon、Whisper、离线模式说明已经落后于当前代码；出现冲突时以实际代码和测试为准。

## 先看哪里

- 项目概览和运行说明：README.md
- 前端入口：src/main.js
- 路由：src/router/index.js
- 编辑器主组件：src/components/MilkdownEditor.vue
- AI 补全核心：src/plugins/copilotPlugin.ts、src/plugins/copilotTypes.ts（类型定义）
- 前端请求层：src/utils/api.js、src/utils/fetch.js（安全 fetch wrapper）
- 前端配置：src/utils/config.js
- 设置状态：src/stores/settings.js
- 后端入口和主路由：backend/main.py
- LLM 和 OCR 调用：backend/llm.py
- Prompt 组装：backend/prompt.py（含 PRO 模式模板）
- TTS 路由：backend/tts_asr.py
- **任务队列系统**：backend/job_system.py（Redis Streams 异步任务管理）
- **Worker 进程入口**：backend/worker.py（消费 Redis Streams 的独立 worker）
- **任务处理器注册表**：backend/job_handlers.py（completion/PRO/web_search/compress/OCR/convert/TTS/ASR 处理器）
- **会话管理**：backend/session_store.py（内存 + PostgreSQL 双后端）
- **API/LLM 审计日志**：backend/audit_store.py（PostgreSQL 持久化）
- **风控配置**：backend/risk_config.py（环境变量驱动的配置数据类）
- **风控引擎**：backend/risk_control.py（速率限制、并发控制、熔断器、预算追踪）
- **验证码路由**：backend/captcha_api.py（FastAPI router）
- **文档存储**：backend/docs_store.py（MIME 类型检测、文本/二进制分类）
- **LLM 策略解析**：backend/llm_policy.py（按 job_type 解析模型配置）
- **网页搜索块组件**：src/components/WebSearchBlockCrepe.vue（可折叠/压缩/删除的搜索结果卡片）
- **网页搜索块插件**：src/plugins/webSearchBlockPlugin.ts（```llm-websearch fenced code 语法）
- **OCR 图片包装器**：src/components/OCRImageWrapper.vue（加载/成功/失败状态覆盖层）
- **验证码组件**：src/components/CaptchaComponent.vue（vue3-captcha 封装）
- **SSE 流式解析**：src/utils/sse.ts（Server-Sent Events 事件解析）
- **文档管理 API**：src/utils/docsApi.js（上传/预览/删除接口客户端）
- **PRO 功能接受追踪**：src/utils/proAccept.js（使用分析和统计）
- **Cookie 策略管理**：src/utils/cookie_policy.js（SameSite/Secure 标志处理）
- **字符串工具**：src/utils/string.ts（长度计算、编码检测）
- **网页搜索上下文提取**：src/utils/webSearch.js（搜索结果 Markdown 构建与解析）
- **测试配置和入口**：pytest.ini、backend/tests/run_tests.py
- **iOS 客户端工程**：InTEX/InTEX.xcodeproj（scheme: `InTEX`）
- **iOS 客户端入口**：InTEX/InTEX/InTEXApp.swift、InTEX/InTEX/ContentView.swift

## 稳定事实

- **功能块禁止嵌套**：`doc_block`、`pro_block`、`upload_block` 的 schema 均设 `atom: true, isolating: true`，ProseMirror 层面强制禁止互相嵌套。DocBlockCrepe.vue 的嵌套 Crepe 编辑器仅注册 copilotPlugin + hiddenText*，不注册任何功能块插件。修改时不得移除 atom/isolating 属性或在嵌套编辑器中引入功能块插件。
- **功能块导入解析**：各功能块的 Remark 插件（`docBlockRemark`, `proBlockRemark`, `uploadBlockRemark`）负责从 Markdown AST 识别对应语法并转换为节点。doc_block Remark 同时支持 fenced code (`llm-file`) 和 legacy HTML tag (`<doc_type=...>`)。解析链路必须保持完整，否则导入后无法自动复原。
- **功能块导出序列化**：doc_block 的 `toMarkdown` runner 输出 legacy HTML tag，但 `getExportMarkdown()` 中通过 `transformLegacyDocBlocksForExport()` 转换为 fenced code block。pro_block/upload_block 的 toMarkdown/leafText 直接输出标准语法。修改时需验证导出后再次导入能完整复原。
- **Web Search 块语法**：web_search_block 使用 fenced code ```llm-websearch date=... 语法，与 doc_block(llm-file)、pro_block([PRO])、upload_block({{{}}}) 并列。触发语法为 [WEBSEARCH]（不区分大小写），由 `webSearchBlockPlugin.ts` 处理。
- **Store 同步格式**：`scheduleMarkdownSync()` emit markdown 不做转换（doc_block 为 legacy HTML tag），store 中始终是 legacy format。`syncInitialMarkdown()` / `getExportMarkdown()` 负责格式转换。
- **上传块生命周期**：upload_block 是临时占位符，文件上传后被替换为 image node（图片）或 doc_block（文档），不会与 pro_block 共存。
- **PRO block escape/unescape**：`escapeProBlockContent()` / `unescapeProBlockSyntax()` 处理 `\`, `]`, `}`, newline，确保指令 round-trip 正确。
- **补全接口当前不是 SSE**；前端用普通 POST 请求拿 JSON 响应。
- **任务队列架构（v0.2.0 新增）**：后端从同步端点转向 Redis Streams 异步队列。`job_system.py` 定义 JOB_TYPES（completion/pro_completion/web_search/compress/ocr/convert/tts/asr），`worker.py` 消费队列，`job_handlers.py` 注册各类型处理器。所有任务支持并发控制、速率限制和熔断器模式。
- **会话追踪**：`session_store.py` 提供 InMemorySessionStore（开发）和 PostgresSessionStore（生产），通过 session_hash + ip_hash 追踪请求身份。
- **风控系统**：`risk_config.py` + `risk_control.py` 实现速率限制（滑动窗口）、并发控制、熔断器模式和预算追踪，所有阈值通过环境变量配置。
- 前端会生成 X-Request-Id，并在请求被中止时额外调用 /v1/completions/cancel。
- 文档超过 32 KB 时，AI 补全会在前端和插件层被禁用。
- OCR 文本和文档块内容会被注入补全上下文，但这些内容属于隐藏上下文，不应被直接当作用户可见文本重复输出。
- /v1/convert 当前支持 txt、docx、pptx、pdf，非 txt 文件通过 MarkItDown 转成 Markdown，之后会清理图片标记。
- **AI 开关是全局广播状态**：MilkdownEditor.vue 通过 `llm-in-text:copilot-toggle` 同步主编辑器、文档块嵌套编辑器、网页搜索块嵌套编辑器；修 ghost text 时要同时检查这三处。
- **设置项统一使用 country**：前后端请求、prompt、store、设置面板只使用 `country`。
- **DOCX/PDF 导出改为纯前端**：不再依赖 `/v1/export/pdf`。当前策略是先展开所有功能块，再从编辑器 HTML 构建导出内容；`src/utils/richExport.js` 负责 HTML -> PDF / DOCX。
- **上传单文件限制统一为 100MB**：前端校验和后端 OCR 风控上限都按 100MB 处理。
- **视频解析策略**：上传视频时，后端 `/v1/ocr` 接收 `media_type=video`，视频画面走 OCR 模型，音轨通过 ffmpeg 抽取后走 ASR 模型，最终合并为“视频画面 OCR + 视频音频 ASR”文本。
- **OCR 明确关闭思考**：backend/llm.py 的 OCR payload 显式下发 `options.think = False` 与 `temperature = 0`。
- **LLM/PRO/OCR 模型统一**：`LLM_MODEL`、`PRO_LLM_MODEL`、`VLM_MODEL` 的当前默认值和 Docker 运行值统一为 `Nex-N2-mini-mlx-OptiQ-8bit-MTP`。网页搜索模型仍可通过 `RISK_WEB_SEARCH_MODEL` 独立配置。
- **OCR 完成状态不等于识别正确**：Redis 任务、审计入库和 SSE `done` 只能证明链路完成。2026-06-27 的清晰文本图片真实测试中，Nex OCR 仍可能返回未识别文本；用户已明确暂停继续修复 OCR 精度，不要把链路完成误报为 OCR 正常。
- **审计 SQL 参数必须严格对齐**：`llm_call_audit` 当前包含 `queue_ms`、`run_ms`、`total_ms`，修改 INSERT 时必须保持列数、`%s` 占位符数和参数数一致；`backend/tests/test_audit_store.py` 覆盖此回归。
- **TTS/ASR 当前真实实现**：backend/tts_asr.py 统一通过 `LLM_BASE_URL` + `LLM_API_KEY` 调用 OpenAI-compatible Speech API，默认模型为 `Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit` 与 `Qwen3-ASR-0.6B-8bit`。VoiceDesign 请求字段必须使用复数 `instructions`；空声音描述必须回退到 `TTS_DEFAULT_INSTRUCTIONS`，不能向上游发送空指令。
- **六按钮收缩菜单**：MilkdownEditor.vue 的 upload/import/export/AI/template/clear 六个动作统一收进右下角 `.more-actions`。视口宽度 `>= 520px` 横向展开，窄屏纵向展开；再次点击主按钮、再次点击已打开的 export/template、点击 backdrop 或按 Escape 都必须完整收缩回右下角省略号。
- **iOS 工程边界**：`InTEX/` 当前是 SwiftUI + SwiftData 示例骨架，包含 `InTEX`、`InTEXTests`、`InTEXUITests` 三个 target。不要声称它已接入编辑器或 Docker API；提交时禁止加入 `xcuserdata`、`*.xcuserstate` 和 DerivedData。

## 常用命令

- 前端安装：npm install
- 前端开发：npm run dev
- 前端构建：npm run build
- 后端安装：pip install -r backend/requirements.txt
- 后端启动：python backend/main.py
- 可选启动方式：uvicorn backend.main:app --reload --port 8001
- iOS 工程检查：xcodebuild -project InTEX/InTEX.xcodeproj -list
- iOS 模拟器构建：优先使用 XcodeBuildMCP；命令行回退可用 `xcodebuild -project InTEX/InTEX.xcodeproj -scheme InTEX -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO build`
- 全量测试：pytest
- 常用窄测试：
	- pytest backend/tests/test_main_endpoints.py -v
	- pytest backend/tests/test_main_cancel.py -v
	- pytest backend/tests/test_prompt.py -v
	- pytest backend/tests/test_llm.py -v

## Docker 部署约定

- 本机部署目录固定在 `/Users/allenyuan/lit/` 下，不在仓库外再散落数据库或 Docker 持久化目录。
- 当前推荐的部署工作目录是 `/Users/allenyuan/lit/llm-in-text/`；把仓库同步到该目录后，从该目录执行 `docker compose up -d --build`。
- 不再使用 `/Volumes/New Volume/lit/` 部署本项目；迁移时可放弃旧 PostgreSQL 数据，从新部署目录初始化空数据库。
- **docker-compose.yml 定义的服务**：api（FastAPI）、worker（任务消费者）、frontend（Nginx）、postgres（审计+会话存储）、redis（任务队列）、searxng（网页搜索后端）、firecrawl（网页内容抓取）。每次 `docker compose up --build` 后，必须验证所有目标服务（api/worker/frontend/postgres/redis）是否处于 Up 状态。
- **前端网络硬约定**：前端 API 必须调用 `https://api.imageteach.tech:8002/` 反向代理，不要让前端调用 Docker 内 `api` 服务、本机 `localhost:8001`、`localhost:8081` 或同源 `/v1` 代理。网络和反向代理由外部配置处理，除非用户明确要求，不要新增或恢复前端到 Docker 后端的代理。
- 每次修改会影响 Docker 运行效果的代码后，不能只停留在本地测试；必须同步更新当前 Docker 环境中的代码，并验证容器内代码已经变化。
- 首选更新方式：
	1. 在仓库根目录执行 `docker compose up -d --build`。
	2. 执行 `docker compose ps` 确认 `api`、`worker`、`frontend` 等目标服务已重新创建并处于 Up。
	3. 对关键修复点执行容器内验证，例如 `docker compose exec -T worker sh -lc "python - <<'PY'\nfrom pathlib import Path\nprint('_normalize_preferences' in Path('/app/backend/prompt.py').read_text())\nPY"`。
- 如果 `docker compose build` 因 Docker Hub、镜像源、网络 token 超时等外部原因无法拉基础镜像，仍然必须更新正在运行的 Docker 环境。可用应急方式：
	1. 先执行 `npm run build` 生成最新前端产物。
	2. 用 `docker cp` 将改动后的后端文件复制到 `api` 和 `worker` 容器的 `/app/backend/`，必要时将 `dist/` 复制到 `frontend` 容器的 `/usr/share/nginx/html/`。
	3. 执行 `docker compose restart api worker frontend` 重启受影响服务。
	4. 执行 `docker commit llm-in-text-api-1 llm-in-text-api:latest`、`docker commit llm-in-text-worker-1 llm-in-text-worker:latest`；如果更新了前端，也执行 `docker commit llm-in-text-frontend-1 llm-in-text-frontend:latest`。
	5. 执行 `docker compose up -d --no-build --force-recreate api worker frontend`，确保新容器来自已更新镜像。
	6. 再次用 `docker compose exec -T ...` 验证容器内文件和行为，不能只看本地文件。
- Docker 持久化数据统一落在部署目录内的 `docker-data/`，包括 PostgreSQL、Redis 和任务共享临时目录。
- Docker BuildKit 导出缓存统一落在 `docker-data/build-cache/{frontend,api,worker}`。`docker-compose.yml` 的 `cache_from/cache_to` 与 Dockerfile 的 apt/pip/npm cache mount 必须保留；首次构建填充缓存，后续相同源码构建应全部命中缓存。
- `.dockerignore` 必须排除 `backend/models`、`backend/.env`、`docker-data`、`node_modules`、`.git` 等大目录或敏感文件。`backend/models` 是约 4.9GB 的旧本地模型目录，当前 API-based TTS/ASR 不应把它发送进构建上下文。
- 容器内访问宿主机模型服务时，不要继续使用 `localhost`；应改成 `host.docker.internal` 之类的容器可达地址。
- 当前 Docker 部署的 `backend/requirements.docker.txt` 已包含 OCR、转换、队列和基础 API 依赖；`backend/Dockerfile` 额外安装 `ffmpeg` 以支持视频拆音轨。
- **Worker 容器**：worker.py 作为独立服务运行，通过 Redis Streams 消费任务队列。修改 job_handlers.py 或 worker.py 后需要验证 worker 容器内的代码已更新，可通过 `docker compose exec -T worker sh -lc "python -c 'from backend.job_handlers import get_handler; print(get_handler(\"completion\").__name__)'"` 验证。
- **Redis Streams 架构**：任务队列使用 Redis Streams，支持并发控制、速率限制和熔断器。job_system.py 定义 JOB_TYPES 和队列配置，worker.py 注册处理器并运行事件循环。
- 修改 Docker 相关文件时，除了代码本身，还要同步检查：
	- `docker-compose.yml`
	- `backend/Dockerfile`
	- `backend/requirements.docker.txt`
	- `Dockerfile.frontend`
	- `docker/nginx.conf`
	- `backend/.env.example` 与实际部署用 `backend/.env`

## 代码约定

- 不要把整个仓库当成“全小写+短横线命名”项目。当前实际情况是：
	- Vue 组件和视图多为 PascalCase
	- 前端工具模块多为小写 .js
	- 插件层使用 TypeScript
	- Python 使用 snake_case
- 以就地风格为准，不要顺手做全仓格式统一。
- UI 文案和代理回复默认使用中文。
- 不要修改 milkdown-docs/，它是只读参考资料。
- 不要新增硬编码密钥、空 catch/except、as any、@ts-ignore 之类的扩散式技术债。
- 代理在这个仓库里应优先做局部、可验证的修改，不要做无关重构。

## 调试路径

- 补全问题：
	src/components/MilkdownEditor.vue
	-> src/plugins/copilotPlugin.ts
	-> src/utils/api.js
	-> backend/main.py
	-> backend/prompt.py / backend/llm.py

- OCR 问题：
	src/components/MilkdownEditor.vue
	-> backend/main.py
	-> backend/llm.py

- 文档转换问题：
	src/utils/convert.js
	-> backend/main.py

- TTS 问题：
	src/components/TTSMenu.vue / src/components/TTSPlayer.vue / src/components/MilkdownEditor.vue
	-> src/utils/api.js
	-> backend/tts_asr.py

## 测试和产物

- pytest.ini 对 backend.main、backend.llm、backend.prompt、backend.geoip、backend.prompts、backend.tts_asr 设了覆盖率门槛，低于 90% 会失败。
- 默认测试目录是 backend/tests。
- 常见生成产物包括 dist、htmlcov、.pytest_cache、api_performance_report.md；它们不是源代码。

## 文档注意事项

- README.md 对产品功能有参考价值，但其中补全、TTS/ASR 和部分接口说明已经比代码旧。
- backend/TTS_ASR_MACOS_FIX.md 和 backend/tests/TESTING_GUIDE.md 更适合作为历史背景，不应在与代码冲突时被当成事实来源。
- 修改行为时，优先参考实现代码和对应测试，再决定是否同步普通文档。
