CareerBot/DESIGN.md
ln0422 f0c4134edd Document dual-domain deployment (trunk + branch1)
- DESIGN.md: add branching strategy table, update online addresses
  with both trunk (career.ityb.me) and branch1 (www.ityb.me) URLs
- OPS_MANUAL.md: update service architecture diagram, ports table
  (add :8001), file structure (add CareerBot-branch1), systemd
  services (add careerbot-branch1.service with env-based DB sharing),
  nginx config (three server blocks + certbot HTTPS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 13:11:14 +08:00

35 KiB
Raw Permalink Blame History

CareerBot 项目设计文档

1. 项目概述

CareerBot 是一个个人职业展示网站,集成了 AI 智能对话助手。主要面向两类用户:

  • 访问者(招聘方):浏览候选人的职业背景,通过 AI 对话深入了解候选人,上传 JD 进行匹配分析
  • 管理员(候选人):管理个人资料、教育/工作经历、技能等内容,配置 LLM管理访问令牌查看招聘意向消息生成定制简历

线上地址

用途 地址 分支 特性
展示版主页(无聊天) https://www.ityb.me/ branch1 隐藏对话按钮,纯展示
完整版主页(含聊天) https://career.ityb.me/careerbot/ main (trunk) 含 AI 对话、JD 匹配、简历生成
管理后台 https://career.ityb.me/careerbot/admin/login main (trunk) 管理员入口
Gitea 代码仓库 https://git.ityb.me - 自托管 Git

分支策略

分支 部署域名 BASE_PATH 用途
maintrunk career.ityb.me /careerbot 主线开发,保留全部功能
branch1 www.ityb.me / ityb.me ""(根路径) 展示变体,隐藏 AI 对话按钮,共享 trunk 的数据库和上传目录

数据层面两分支共用同一 SQLite 数据库和上传目录,后端资料同步,仅前端展示差异。

技术栈

层级 技术
后端框架 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.pyBASE_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: Bearer Header
  • 有效期: 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

  1. 检查端口 8000 是否被占用
  2. 首次运行自动执行 init_data.py 初始化数据库
  3. 启动 uvicorn --host 0.0.0.0 --port 8000 --reload

stop.bat

  1. 查找占用端口 8000 的所有进程
  2. 逐个终止进程

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 统一获取路径前缀,避免硬编码