- DESIGN.md: update Gitea URL in online addresses table - OPS_MANUAL.md: update all Gitea references, add gitea.conf section, update service architecture diagram Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
34 KiB
34 KiB
CareerBot 项目设计文档
1. 项目概述
CareerBot 是一个个人职业展示网站,集成了 AI 智能对话助手。主要面向两类用户:
- 访问者(招聘方):浏览候选人的职业背景,通过 AI 对话深入了解候选人,上传 JD 进行匹配分析
- 管理员(候选人):管理个人资料、教育/工作经历、技能等内容,配置 LLM,管理访问令牌,查看招聘意向消息,生成定制简历
线上地址
| 用途 | 地址 |
|---|---|
| 主页 | http://www.ityb.me/careerbot/ |
| 管理后台 | http://www.ityb.me/careerbot/admin/login |
| Gitea 代码仓库 | https://git.ityb.me |
技术栈
| 层级 | 技术 |
|---|---|
| 后端框架 | Python / FastAPI |
| 模板引擎 | Jinja2 |
| 数据库 | SQLite + SQLAlchemy ORM |
| LLM 集成 | OpenAI 兼容 API (httpx) |
| 实时通信 | Server-Sent Events (SSE) |
| 认证 | JWT (管理员) / Cookie (访问者) |
| 密码加密 | bcrypt |
| 文件处理 | python-docx, PyPDF2, Pillow |
| 反向代理 | Nginx (子路径部署) |
2. 项目结构
CareerBot/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 应用入口,路由挂载,启动初始化
│ ├── config.py # Pydantic Settings 配置(含 BASE_PATH)
│ ├── database.py # SQLAlchemy 引擎、Session、init_db
│ ├── models.py # 全部 ORM 模型定义
│ ├── routers/
│ │ ├── auth.py # 认证:管理员登录、访问令牌验证、匿名访问
│ │ ├── public.py # 公开页面路由和数据 API
│ │ ├── admin.py # 管理后台 CRUD、简历生成、消息管理
│ │ └── chat.py # 聊天 API:对话、历史、招聘意图检测
│ ├── services/
│ │ ├── chat_service.py # 聊天业务逻辑:构建 prompt、处理消息
│ │ ├── llm_service.py # LLM 调用封装:流式/非流式、视觉降级
│ │ └── file_parser.py # 文件解析:txt/docx/pdf/image
│ └── static/
│ ├── css/style.css # 全局样式
│ └── js/
│ ├── main.js # 主页数据加载(个人信息、技能、经历)
│ ├── chat.js # 聊天组件(SSE 流式、提问限制、匿名模式)
│ └── admin.js # 管理后台 CRUD 操作、消息管理
├── templates/
│ ├── index.html # 主页(职业展示 + 聊天组件)
│ ├── login.html # 访问者登录/匿名入口
│ └── admin/
│ ├── login.html # 管理员登录
│ ├── dashboard.html # 仪表盘
│ ├── profile.html # 个人资料管理
│ ├── education.html # 教育经历管理
│ ├── experience.html # 工作经历管理
│ ├── skills.html # 技能管理
│ ├── tokens.html # 访问令牌管理
│ ├── messages.html # 招聘消息查看
│ ├── llm-config.html # LLM 配置
│ └── resume.html # 简历生成
├── init_data.py # 数据库初始化种子数据
├── requirements.txt # Python 依赖
├── start.bat # 启动脚本
└── stop.bat # 停止脚本
3. 系统架构图
┌─────────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ index.html │ │ login.html │ │ admin/*.html │ │
│ │ main.js │ │ (Token/匿名入口) │ │ admin.js │ │
│ │ chat.js │ │ │ │ │ │
│ └──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘ │
└─────────┼──────────────────┼──────────────────────┼─────────────┘
│ HTTP/SSE │ HTTP │ HTTP
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ public.py │ │ auth.py │ │ chat.py │ │ admin.py │ │
│ │ 页面路由 │ │ 认证鉴权 │ │ 聊天API │ │ 管理CRUD │ │
│ │ 数据API │ │ JWT/Cookie │ │ SSE流式 │ │ 简历生成 │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └────┬─────┘ │
│ │ │ │ │ │
│ │ ┌──────┴──────────┐ │ │ │
│ │ │ get_current_ │ │ │ │
│ │ │ admin/visitor │◄───┤ │ │
│ │ └─────────────────┘ │ │ │
│ │ │ │ │
│ ┌─────┴───────────────────────────────┴──────────────┴─────┐ │
│ │ Services Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │chat_service │ │ llm_service │ │ file_parser │ │ │
│ │ │System Prompt │ │ OpenAI API │ │ txt/docx/pdf │ │ │
│ │ │消息处理/历史 │─▶│ 流式/非流式 │ │ image/base64 │ │ │
│ │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ │
│ └───────────────────────────┼──────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────┼──────────────────────────────┐ │
│ │ SQLAlchemy ORM │ │
│ │ Profile │ Skill │ Education │ WorkExperience │ AdminUser │ │
│ │ AccessToken │ LLMConfig │ ChatHistory │ RecruiterMessage │ │
│ └───────────────────────────┼──────────────────────────────┘ │
└──────────────────────────────┼──────────────────────────────────┘
│
┌──────────▼──────────┐
│ SQLite (WAL模式) │
│ careerbot.db │
└─────────────────────┘
│ httpx (异步)
┌──────────▼──────────┐
│ LLM API Provider │
│ (DeepSeek/Qwen/ │
│ OpenAI/Zhipu) │
└─────────────────────┘
3.1 子路径部署架构(BASE_PATH)
CareerBot 部署在域名的 /careerbot 子路径下,通过 BASE_PATH 配置实现全局路径前缀管理:
浏览器请求: http://www.ityb.me/careerbot/api/profile
│
▼
Nginx (:80)
location /careerbot/ → proxy_pass http://127.0.0.1:8000/careerbot/
│
▼
FastAPI (uvicorn :8000)
app.include_router(public.router, prefix="/careerbot")
→ 匹配路由: @router.get("/api/profile")
→ 实际路径: /careerbot/api/profile
前缀注入机制:
| 层级 | 方式 | 说明 |
|---|---|---|
| Python 路由 | app.include_router(prefix=BASE) |
所有路由自动加前缀,路由装饰器不变 |
| 静态文件 | app.mount(f"{BASE}/static", ...) |
CSS/JS/图片通过 /careerbot/static/ 访问 |
| Jinja2 模板 | templates.env.globals["base"] |
模板中用 {{ base }} 引用前缀 |
| 前端 JS | window.BASE_PATH |
在模板中注入,JS 文件通过全局变量使用 |
| RedirectResponse | url=f"{BASE}/login" |
Python 重定向使用完整路径 |
| DB 存储路径 | photo_url = f"{BASE}/uploads/..." |
持久化数据中的 URL 包含前缀 |
配置位置:
app/config.py→BASE_PATH: str = "/careerbot"- 修改此值即可将应用迁移到其他子路径
4. 数据模型(类图)
┌─────────────────────┐ ┌─────────────────────────┐
│ Profile │ │ AdminUser │
├─────────────────────┤ ├─────────────────────────┤
│ id: int (PK) │ │ id: int (PK) │
│ name: str │ │ email: str (unique) │
│ phone: str │ │ password_hash: str │
│ location: str │ └─────────────────────────┘
│ birthday: str │
│ party: str │ ┌─────────────────────────┐
│ education_level: str│ │ AccessToken │
│ email: str │ ├─────────────────────────┤
│ photo_url: str? │ │ id: int (PK) │
│ self_summary: text │ │ token: str (unique) │
└─────────────────────┘ │ created_at: datetime │
│ is_active: bool │
┌─────────────────────┐ │ note: str? │
│ Skill │ │ max_questions: int [=9] │
├─────────────────────┤ │ used_questions: int [=0] │
│ id: int (PK) │ └─────────────────────────┘
│ category: str │
│ content: text │ ┌─────────────────────────┐
│ sort_order: int │ │ LLMConfig │
└─────────────────────┘ ├─────────────────────────┤
│ id: int (PK) │
┌─────────────────────┐ │ api_url: str │
│ Education │ │ api_key: str │
├─────────────────────┤ │ model_name: str │
│ id: int (PK) │ │ max_tokens: int [=2048] │
│ start_date: str │ │ temperature: float [=0.7]│
│ end_date: str │ │ system_prompt: text? │
│ school: str │ │ is_active: bool │
│ major: str │ │ max_questions_per_session│
│ degree: str │ └─────────────────────────┘
│ details: text │
│ sort_order: int │ ┌─────────────────────────┐
└─────────────────────┘ │ ChatHistory │
├─────────────────────────┤
┌─────────────────────┐ │ id: int (PK) │
│ WorkExperience │ │ session_id: str (索引) │
├─────────────────────┤ │ role: str │
│ id: int (PK) │ │ content: text │
│ start_date: str │ │ created_at: datetime │
│ end_date: str │ └─────────────────────────┘
│ company: str │
│ position: str │ ┌─────────────────────────┐
│ company_intro: text │ │ RecruiterMessage │
│ responsibilities: t │ ├─────────────────────────┤
│ achievements: text │ │ id: int (PK) │
│ sort_order: int │ │ session_id: str (索引) │
└─────────────────────┘ │ visitor_label: str │
│ company: str │
│ contact: str │
│ intent: str │
│ summary: text │
│ is_read: bool │
│ created_at: datetime │
└─────────────────────────┘
模型分组关系:
候选人内容数据: Profile ──┬── Skill (1:N)
├── Education (1:N)
└── WorkExperience (1:N)
访问控制: AdminUser (管理员)
AccessToken (访问者令牌) ── 关联 max_questions / used_questions
AI 配置: LLMConfig (LLM连接配置 + 默认提问次数)
对话数据: ChatHistory (按 session_id 分组)
RecruiterMessage (招聘意图检测结果)
5. 认证与访问控制
5.1 三种访问身份
┌───────────────┬───────────────────┬──────────────┬──────────────┐
│ 身份 │ 认证方式 │ 浏览内容 │ 对话限制 │
├───────────────┼───────────────────┼──────────────┼──────────────┤
│ 管理员 │ JWT (Bearer Token)│ 管理后台 │ 无限制 │
│ Token 访问者 │ Cookie (visitor_ │ 主页+对话 │ 按token配额 │
│ │ token = 实际token)│ │ (默认9次) │
│ 匿名访问者 │ Cookie (visitor_ │ 主页+对话 │ 3次 │
│ │ token=__anonymous__)│ │ │
└───────────────┴───────────────────┴──────────────┴──────────────┘
5.2 访问者认证流程
访问 / (主页)
│
▼
有 visitor_token Cookie?
│
┌────┴────┐
│ No │ Yes
▼ ▼
重定向 token == "__anonymous__"?
/login │
│ ┌────┴────┐
▼ │ Yes │ No
登录页 │ ▼
│ │ DB查询token有效?
│ │ │
│ │ ┌────┴────┐
│ │ │ Yes │ No
│ │ │ ▼
│ │ │ 重定向 /login
│ ▼ ▼
│ 渲染 index.html
│
├─ 输入Token → POST /api/verify-token → set cookie → 跳转 /
└─ 匿名访问 → POST /api/anonymous-entry → set cookie(__anonymous__) → 跳转 /
5.3 管理员认证
- 登录:
POST /api/admin/login→ 验证 email + bcrypt 密码 → 返回 JWT - 鉴权: 所有
/api/admin/*接口通过get_current_admin依赖校验 JWT - JWT 存储: 前端
localStorage.admin_token,请求时放入Authorization: BearerHeader - 有效期: 480 分钟(8 小时)
6. 核心流程
6.1 智能对话流程
用户发送消息 (chat.js)
│
│ POST /api/chat (FormData: session_id, message, file?)
▼
┌─── chat.py ─────────────────────────────────────────┐
│ │
│ 1. 身份识别: get_current_visitor │
│ ├─ AccessToken记录 → 检查 used_questions < max │
│ └─ None (匿名) → 查 ChatHistory 计数 < 3 │
│ │
│ 2. 超限 → 返回限制提示的 SSE │
│ 未超限 → 继续 │
│ │
│ 3. Token用户: used_questions += 1, db.commit() │
│ │
│ 4. 调用 chat_service.process_message() │
│ │ │
│ ┌──▼── chat_service.py ──────────────────────┐ │
│ │ │ │
│ │ a. 解析上传文件 (file_parser) │ │
│ │ ├─ txt/docx/pdf → 提取文本 │ │
│ │ └─ 图片 → base64编码 │ │
│ │ │ │
│ │ b. 查询最近20条 ChatHistory │ │
│ │ │ │
│ │ c. 构建 messages: │ │
│ │ [system_prompt, ...history, user_msg] │ │
│ │ │ │
│ │ d. 调用 llm_service.chat_completion_stream │ │
│ │ │ │
│ │ e. 保存 user/assistant 消息到 ChatHistory │ │
│ │ (错误消息不保存) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 5. SSE 流式返回 → chat.js 逐块渲染 │
│ │
│ 6. BackgroundTask: 招聘意图检测 │
│ (独立DB Session, 不影响主流程) │
└──────────────────────────────────────────────────────┘
6.2 LLM 调用流程(含视觉降级)
chat_completion_stream(messages, db)
│
▼
读取 LLMConfig (is_active=True)
│
├─ 无配置 → yield "AI助手尚未配置"
│
▼
构建请求:
URL: {api_url}/v1/chat/completions
Headers: Authorization: Bearer {api_key}
Body: { model, messages, max_tokens, temperature, stream: true }
│
▼
发送流式请求 (httpx.AsyncClient.stream)
│
┌────┴────────────────┐
│ 成功 │ 400 错误 且 消息含图片
│ │
▼ ▼
逐块 yield 视觉降级处理:
content _strip_images(messages)
├─ 移除 image_url 内容
├─ 保留文本部分
└─ 追加提示: "模型不支持图片, 请改用txt/docx/pdf"
│
▼
重试 (不含图片的messages)
│
┌────┴────┐
│ 成功 │ 失败
▼ ▼
逐块 yield 错误信息
yield
6.3 招聘意图检测流程
聊天 SSE 流结束
│
▼ BackgroundTask (独立DB Session)
_check_recruiter_intent()
│
▼
构建检测 prompt:
System: "你是一个精确的信息提取助手,只输出JSON格式。"
User: "分析以下对话,判断访问者是否表达了:
1.招聘意愿 2.面试意愿 3.公司/联系方式"
+ 用户消息(截取500字) + AI回复(截取500字)
│
▼
调用 chat_completion (非流式) → LLM 返回 JSON
│
▼
解析 JSON:
{"detected": true/false, "intent": "...",
"company": "...", "contact": "...", "summary": "..."}
│
┌────┴────┐
│detected │ not detected
│= true │
▼ ▼
创建 忽略
RecruiterMessage
记录到DB
│
▼
管理后台 Messages 页面显示
侧边栏红色未读数徽章
6.4 简历生成流程
管理员上传 JD (文件或文本)
│
▼ POST /api/admin/resume/generate
解析 JD 内容 (file_parser)
│
▼
从 DB 读取候选人全部资料:
Profile + Skills + Education + WorkExperience
│
▼
构建 messages:
System: "你是专业简历撰写顾问..." (详见 §7.3)
User: "=== JD === ... === 候选人背景 === ..."
│
▼
SSE 流式生成 Markdown 格式简历
│
▼
前端渲染 Markdown 预览
│
▼ POST /api/admin/resume/download
Markdown → Word 文档 (python-docx)
- 解析 #/##/### 标题
- 解析 -/* 列表
- 解析 **粗体**
- 设置页边距、对齐
│
▼
下载 resume.docx
7. LLM 集成详细设计
7.1 架构设计
CareerBot 采用 OpenAI 兼容 API 格式 调用 LLM,通过 httpx 发送 HTTP 请求,不使用官方 SDK。这样可以灵活切换国内外各种兼容 OpenAI 格式的 LLM 提供商。
┌──────────────────────────────────────────────────┐
│ llm_service.py │
│ │
│ get_active_config(db) ← 从DB读取LLM配置 │
│ │ │
│ ▼ │
│ _build_url_and_headers() │
│ URL自动补全: {base_url}/v1/chat/completions │
│ Headers: Authorization: Bearer {api_key} │
│ │ │
│ _build_body() │
│ { model, messages, max_tokens, temperature } │
│ │ │
│ ┌────┴────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ chat_completion chat_completion_stream │
│ (非流式,同步返回) (流式,AsyncGenerator) │
│ │ │ │
│ │ ┌────────────────┤ │
│ │ │ 视觉降级逻辑 │ │
│ │ │ _has_image_content() 检测 │
│ │ │ _strip_images() 移除图片 │
│ │ │ 自动重试 │
│ │ └────────────────┘ │
└──────┴──────────────────────────────────────────┘
支持的 LLM 提供商(管理后台预设按钮):
| 提供商 | Base URL | 默认模型 |
|---|---|---|
| 通义千问 | https://dashscope.aliyuncs.com/compatible-mode |
qwen-plus |
| DeepSeek | https://api.deepseek.com |
deepseek-chat |
| 智谱 AI | https://open.bigmodel.cn/api/paas |
glm-4-flash |
| OpenAI | https://api.openai.com |
gpt-4o |
7.2 LLM 配置参数
在管理后台 LLM Config 页面配置,存储在 llm_config 表:
| 参数 | 说明 | 默认值 |
|---|---|---|
api_url |
LLM API 基础地址 | - |
api_key |
API 密钥 | - |
model_name |
模型名称 | - |
max_tokens |
单次最大生成 token 数 | 2048 |
temperature |
生成随机性 (0~1) | 0.7 |
system_prompt |
自定义额外系统指令 | 空 |
max_questions_per_session |
新Token默认提问次数 | 9 |
7.3 Prompt 设计
7.3.1 智能对话 System Prompt
由 chat_service.build_system_prompt() 动态构建,每次对话请求实时从数据库读取,确保内容始终最新。
结构:
你是{候选人姓名}的个人AI职业助手,专门帮助招聘者了解{姓名}的职业背景、技能和工作经验。
=== 个人基本信息 ===
姓名: ... 学历: ... 所在地: ... 邮箱: ...
个人总结: ...
=== 技能特长 ===
- 类别1: 内容...
- 类别2: 内容...
=== 教育经历 ===
时间 | 学校 | 专业 | 学位
详情: ...
=== 工作经历 ===
时间 | 公司 | 职位
公司简介: ...
工作职责: ...
工作成就: ...
=== 回答规则 ===
1. 只回答与招聘、职业、工作能力、技术背景相关的问题
2. 对于无关问题,礼貌拒绝并引导回职业话题
3. 严格基于以上信息回答,不编造经历
4. 使用专业、友好的语气
5. 如果招聘者上传了JD,详细分析岗位匹配度,突出亮点/优势,客观指出不足
6. 根据招聘者使用的语言选择中文或英文回答
=== 额外指令 === ← 仅当管理员配置了自定义 system_prompt 时追加
{自定义内容}
7.3.2 招聘意图检测 Prompt
由 chat.py._check_recruiter_intent() 使用,非流式调用,后台静默执行:
System: "你是一个精确的信息提取助手,只输出JSON格式。"
User:
你是一个信息提取助手。分析以下对话,判断访问者是否表达了以下任何意愿:
1. 招聘意愿(想要招聘候选人)
2. 面试意愿(想要邀请候选人面试)
3. 留下了公司信息或联系方式
如果检测到以上任何一项,请用以下JSON格式回复:
{"detected": true, "intent": "...", "company": "...", "contact": "...", "summary": "..."}
如果没有检测到,回复:
{"detected": false}
访问者消息: {截取前500字}
AI助手回复: {截取前500字}
7.3.3 简历生成 Prompt
由 admin.py.generate_resume() 使用,流式调用:
System:
你是一位专业的简历撰写顾问。请根据提供的JD和候选人的完整背景信息,
生成一份针对该职位量身定制的中文简历。
要求:
1. 必须包含候选人的全部教育经历和工作经历,不得遗漏
2. "个人优势"和"自我评价"部分要针对JD定制
3. 每段工作经历重点突出与JD相关的职责和成就
4. 技能部分优先展示JD要求的技能
5. 使用专业、简洁的语言
6. 输出格式使用Markdown
简历结构:
# 个人信息
# 求职意向(根据JD推断)
# 个人优势(3-5条,针对JD定制)
# 核心技能
# 工作经历(全部,按时间倒序)
# 教育背景(全部,按时间倒序)
User:
=== 目标职位描述(JD) ===
{JD内容}
=== 候选人完整背景 ===
{从DB读取的全部资料}
7.4 LLM 能力说明
当前版本没有为 LLM 配置 Function Calling / Tool Use 能力。
LLM 在本项目中扮演的角色是纯文本生成,有三个使用场景:
| 场景 | 调用方式 | 输入 | 输出 |
|---|---|---|---|
| 智能对话 | 流式 (SSE) | system prompt + 对话历史 + 用户消息(可含文件文本) | 自然语言回复 |
| 招聘意图检测 | 非流式 | 当轮对话(用户+AI各500字) | JSON 结构化数据 |
| 简历生成 | 流式 (SSE) | JD + 候选人全部资料 | Markdown 格式简历 |
没有使用的 LLM 高级能力:
- Function Calling / Tool Use
- RAG (检索增强生成)
- Agent 多步推理
- 图片生成
已实现的降级策略:
- 视觉模型不可用时自动降级:检测到 400 错误 + 消息含图片 → 移除图片内容 → 用文本重试 → 提示用户改用 txt/docx/pdf 格式
8. 提问次数限制机制
┌─────────────────────────────────────────────────┐
│ 提问次数限制 │
├─────────────────────────────────────────────────┤
│ │
│ Token 用户: │
│ 限制存储: AccessToken.max_questions (默认9) │
│ 已用计数: AccessToken.used_questions │
│ 检查方式: 服务端每次请求 check + increment │
│ 持久性: 跨会话、跨页面刷新持久有效 │
│ 管理: 管理后台可修改 max/used,可重置 │
│ │
│ 匿名用户: │
│ 限制: 固定 3 次 (ANONYMOUS_MAX_QUESTIONS) │
│ 计数: 服务端查 ChatHistory 中 session_id 的 │
│ user 消息数量 │
│ 持久性: 绑定 session_id (sessionStorage) │
│ │
│ 默认值来源: │
│ 新Token的 max_questions │
│ ← LLMConfig.max_questions_per_session │
│ ← 若未配置则 fallback 到 9 │
│ │
└─────────────────────────────────────────────────┘
9. SSE 流式通信协议
前后端通过 Server-Sent Events 实现流式对话,使用 POST + fetch ReadableStream(非 EventSource,因为需要 POST 发送 FormData)。
数据格式
data: {"content": "回复文本块..."} ← 正常内容块
data: {"content": "...", "limit_reached": true} ← 超限提示
data: {"content": "", "done": true} ← 结束标记
前端处理逻辑 (chat.js)
// 0. BASE_PATH 由模板注入: window.BASE_PATH = "{{ base }}"
const BASE_PATH = window.BASE_PATH || '';
// 1. POST FormData (session_id + message + file?)
const response = await fetch(BASE_PATH + '/api/chat', { method: 'POST', body: formData });
// 2. 读取 ReadableStream
const reader = response.body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// 3. 解析 SSE 行,提取 data: JSON
// 4. 逐块拼接到气泡元素的 textContent
}
10. 文件处理能力
file_parser.py 支持以下格式:
| 格式 | 处理方式 | 返回 | 大小限制 |
|---|---|---|---|
.txt |
多编码尝试 (utf-8 → gbk → gb2312 → utf-16) | {"text": "..."} |
10MB |
.docx |
python-docx 提取段落文本 | {"text": "..."} |
10MB |
.pdf |
PyPDF2 逐页提取文本 | {"text": "..."} |
10MB |
.png/.jpg/.gif/.webp |
base64 编码 | {"base64_image": "...", "mime_type": "..."} |
10MB |
| 其他 | - | {"error": "不支持的文件格式"} |
- |
11. 运维脚本
start.bat
- 检查端口 8000 是否被占用
- 首次运行自动执行
init_data.py初始化数据库 - 启动
uvicorn --host 0.0.0.0 --port 8000 --reload
stop.bat
- 查找占用端口 8000 的所有进程
- 逐个终止进程
12. 关键设计决策
| 决策 | 原因 |
|---|---|
| 直接使用 bcrypt 而非 passlib | passlib 与 bcrypt 5.0 存在不兼容问题 |
| httpx 而非 openai SDK | 灵活支持国内 LLM 厂商,无需依赖特定 SDK |
| SQLite + WAL | 轻量部署,WAL 模式支持并发读取 |
| 无 Alembic 迁移 | 项目初期用 create_all 自动建表,新增列手动 ALTER TABLE |
| Jinja2 < 3.1.6 | 3.1.6 存在模板缓存 unhashable dict bug |
| POST + ReadableStream 而非 EventSource | EventSource 仅支持 GET,无法发送 FormData 附件 |
| BackgroundTask 做意图检测 | 避免阻塞 SSE 流或导致连接异常 |
| 意图检测用独立 DB Session | BackgroundTask 执行时请求级 Session 已关闭 |
| FAB 按钮用内层 span flex | Chrome 对 button 元素的 flex 布局有渲染 bug |
| 子路径部署 + BASE_PATH | 支持同域名多项目部署,一处配置全局生效 |
| Jinja2 globals + window.BASE_PATH | 模板和静态 JS 统一获取路径前缀,避免硬编码 |