from __future__ import annotations """Build the user-focused DeerFlow maintenance handbook. This deliberately reuses the established Word style helpers and diagrams from the original builder, but changes the information architecture for a normal Docker deployment: no 1Panel-, ARM-, or platform-specific deployment content. """ from pathlib import Path import build_manual as b OUT = Path(__file__).resolve().parent / "DeerFlow系统运维手册-用户重点版.docx" def set_running_furniture(doc): """Replace the prior deployment-specific running header/footer.""" header_p = doc.sections[0].header.paragraphs[0] header_p.clear() header_p.alignment = b.WD_ALIGN_PARAGRAPH.RIGHT run = header_p.add_run("DeerFlow 系统运维手册 | 用户重点版") b.set_run_font(run, size=8.5, color=b.GRAY) footer_p = doc.sections[0].footer.paragraphs[0] footer_p.clear() footer_p.alignment = b.WD_ALIGN_PARAGRAPH.CENTER run = footer_p.add_run("内部运维资料 · 第 ") b.set_run_font(run, size=8.5, color=b.GRAY) b.add_field(footer_p, "PAGE", "1") run = footer_p.add_run(" 页") b.set_run_font(run, size=8.5, color=b.GRAY) def add_cover(doc, diagrams): p = doc.add_paragraph() p.paragraph_format.space_before = b.Inches(0.78) p.paragraph_format.space_after = b.Pt(10) run = p.add_run("DeerFlow") b.set_run_font(run, size=16, bold=True, color=b.BLUE) p = doc.add_paragraph(style="Title") p.paragraph_format.space_before = b.Pt(16) run = p.add_run("系统运维手册") b.set_run_font(run, size=30, bold=True, color=b.DARK_BLUE) p = doc.add_paragraph(style="Subtitle") run = p.add_run("通用 Docker 容器版 · 配置、资产与故障处理优先") b.set_run_font(run, size=15, color=b.GRAY) b.add_figure(doc, diagrams["architecture"], "系统访问与数据流概览", "浏览器经由 Web 容器访问前端和后端服务的流程", width=6.1) b.add_callout( doc, "本版重点", "以常规 Docker 容器运行环境为基准;将常见报错、config.yaml、.env、技能/智能体和前端管理设置提前,便于业务管理员直接查用。", "note", ) b.add_table( doc, ["文档版本", "适用范围", "更新日期"], [["V1.1", "当前 DeerFlow Docker 部署包及本项目定制功能", "2026-08-08"]], [1800, 5000, 2560], font_size=9.2, ) b.add_para(doc, "密级:内部运维资料。密钥、密码、IP、数据库账号均必须使用占位符;不要写入本手册。", italic=True, align=b.WD_ALIGN_PARAGRAPH.CENTER, after=0) def add_navigation(doc): p = doc.add_paragraph(style="Title") p.paragraph_format.page_break_before = True run = p.add_run("先用目录找到答案") b.set_run_font(run, size=24, bold=True, color=b.DARK_BLUE) b.add_para(doc, "本手册按“先排错、再配置、再维护”的顺序编排。打开 Word 后可使用左侧导航窗格按标题跳转;目录页码没有更新时,按 Ctrl+A 后按 F9。") toc = doc.add_paragraph() toc.paragraph_format.space_after = b.Pt(8) b.add_field(toc, r'TOC \o "1-3" \h \z \u', "目录将在 Word 中自动更新") b.add_callout(doc, "最常用的五处", "页面或服务异常先看第 1 章;模型与 max_tokens 看第 2 章;登录、数据库和接口地址看第 3 章;技能/智能体问题看第 4 章;菜单、敏感词、主题看第 5 章。", "ok") b.add_h2(doc, "阅读约定") for text in [ "“部署目录”指存放 docker-compose.yml、.env、config 和 data 的当前运行目录。所有命令都应在此目录执行。", "前端和后端不是同一个容器端口。用户通常只访问 APP_PORT;Web/Nginx 会把 /deerflow 请求转发到后端。", "改配置前先备份原文件;改完只重启受影响服务;保存日志、健康检查和实际功能验证结果。", "本文中的 <尖括号内容> 都是待替换占位符,不能原样复制到生产环境。", ]: b.add_bullet(doc, text) def add_chapter_common_errors(doc): b.add_h1(doc, "1. 常见报错与第一时间怎么处理") b.add_para(doc, "先收集状态再修改配置。大多数问题并非代码故障,而是当前目录错误、环境变量未重新加载、后端未启动、数据库连不上、权限不足或资产被过滤。") b.add_h2(doc, "1.1 先执行这组无损检查") b.add_code(doc, """ # 必须在部署目录执行 pwd sudo docker compose ps sudo docker compose logs --tail=200 backend sudo docker compose logs --tail=200 web curl -fsS http://127.0.0.1:/deerflow/health """, "Shell") b.add_callout(doc, "先不要做", "不要执行 docker system prune -a;不要删除 data 后重启;不要创建同名技能或智能体覆盖可能的恢复线索;不要把完整 backend.env 发到群里。", "danger") b.add_h2(doc, "1.2 容器、端口和磁盘") b.add_table( doc, ["现象/报错", "优先检查", "处理方式"], [ ["address already in use", "APP_PORT 已被其他进程占用", "用 ss -lntp 查占用;修改部署目录 .env 的 APP_PORT,然后重建 web。"], ["容器不断 Restarting", "backend 的最后 200 行日志", "重点查看 YAML、环境变量、数据库 URL、挂载权限或启动命令;修复后再重建 backend。"], ["no space left on device", "磁盘、inode、data 和日志目录", "先清理可确认的旧日志或无用缓存;不要在未备份时清理 data 或盲目 prune。"], ["permission denied", "宿主机挂载目录所有者和容器运行用户", "核对 compose volume 与目录权限;只对目标目录做最小权限修正,不使用 chmod -R 777。"], ["Web 访问失败", "docker compose ps、APP_PORT、主机防火墙", "确认 web 容器正常、端口监听,并放行实际访问端口。"], ], [2200, 2650, 4510], font_size=8.8, ) b.add_h2(doc, "1.3 前端、Nginx 和接口") b.add_table( doc, ["现象", "原因方向", "解决"], [ ["502 Bad Gateway", "backend 未启动或 Web 无法访问 backend:8001", "先查 backend 日志和 Compose 网络;后端恢复后再刷新前端。"], ["刷新二级页面 404", "Nginx 的 SPA 回退缺失", "恢复 Nginx 的 try_files 到 /index.html 配置并重启 web。"], ["页面白屏", "runtime-config.js 语法或地址错误、缓存未刷新", "浏览器 Console/Network 定位;恢复配置,重启 web,使用 Ctrl+F5。"], ["所有 API 都失败", "VITE_BACKEND_BASE_URL 或 VITE_LANGGRAPH_BASE_URL 配错", "优先恢复 /deerflow 和 /deerflow/api 相对路径;见第 3.5 节。"], ["CORS / Mixed Content", "前后端不同源或 HTTPS 页面调用 HTTP 接口", "优先同源代理;必须跨域时在后端配置允许 Origin 并统一 HTTPS。"], ], [2100, 3000, 4260], font_size=8.8, ) b.add_h2(doc, "1.4 登录、模型、数据库和资产") b.add_table( doc, ["现象/状态", "最常见原因", "首要动作"], [ ["401 / invalid_api_key", "模型 Key 缺失、变量名不一致或已过期", "对照 config.yaml 的 $变量名 与 backend.env;更新后强制重建 backend。"], ["404 model not found", "model 写成显示名或 base_url 路径不兼容", "使用服务商实际 model ID;核对 API 是否要求 /v1。"], ["429 / timeout", "并发、配额、模型服务或网络问题", "降低并发/重试,检查模型服务和网络连通性。"], ["401 / 403 登录或发布失败", "Token 过期、角色/所有者不足、广场未启用", "重新登录;核对 system_role、资产所有者和目标广场。"], ["技能/智能体看不到", "目录未挂载、数据库切换、过滤、权限或发布状态", "按第 4.6 节逐步排查,不要立即新建同名资产。"], ["MySQL Access denied / timeout", "账号、密码 URL 编码、host 授权、网络或端口", "查看第 3.4 节连接格式和第 7 章数据库恢复原则。"], ], [2250, 3150, 3960], font_size=8.65, ) def add_chapter_model(doc, diagrams): b.add_h1(doc, "2. config.yaml:模型、max_tokens 与后端配置") b.add_para(doc, "模型条目由 config/config.yaml 管理;密钥由 config/backend.env 管理。两处的变量名必须完全一致。新增模型时先写密钥,再写模型条目,再验证后端日志和实际对话。") b.add_figure(doc, diagrams["model_flow"], "图 2-1 模型配置与验证闭环", "从密钥、模型条目到后端验证和前端选择的流程", width=6.35) b.add_h2(doc, "2.1 新增或修改一个 OpenAI 兼容模型") b.add_number(doc, "备份 config/backend.env 和 config/config.yaml,并记录要替换的模型名称。") b.add_number(doc, "在 backend.env 写入密钥,变量名仅供后端读取。") b.add_code(doc, """ # config/backend.env INTRANET_MODEL_API_KEY=<服务商分配的密钥> """, "ENV") b.add_number(doc, "在 config.yaml 的 models: 下增加或修改模型条目。name 是系统引用名;model 是服务商真实模型 ID;display_name 是前端显示名。") b.add_code(doc, """ models: - name: intranet-model display_name: 内网模型 use: langchain_openai:ChatOpenAI model: <服务商实际模型ID> api_key: $INTRANET_MODEL_API_KEY base_url: https://<模型服务地址>/v1 request_timeout: 600.0 max_retries: 2 max_tokens: 4096 temperature: 0.7 supports_vision: false supports_thinking: false """, "YAML") b.add_number(doc, "强制重建 backend 以重新读取 backend.env;随后查看启动日志、健康检查和前端模型列表。") b.add_code(doc, """ sudo docker compose up -d --force-recreate backend sudo docker compose logs --tail=300 -f backend curl -fsS http://127.0.0.1:/deerflow/health """, "Shell") b.add_h2(doc, "2.2 字段说明与常见误配") b.add_table( doc, ["字段", "用途", "常见问题"], [ ["name", "系统内部模型名称,配置引用以它为准", "重名会覆盖或引用混乱;不要随意改已被智能体使用的 name。"], ["display_name", "前端显示名称", "改显示名不等于改 model;可用于给业务人员更易懂的名字。"], ["use", "适配器,例如 langchain_openai:ChatOpenAI", "适配器与接口协议不匹配会导致初始化失败。"], ["model", "模型服务商要求的真实 ID", "误填显示名经常造成 404 model not found。"], ["api_key", "使用 $变量名 从 backend.env 读取密钥", "写明文、变量拼错或漏掉 $ 都会导致鉴权失败。"], ["base_url", "接口基地址", "有的服务要求 /v1,有的路径已包含 /v1;以服务商接口文档为准。"], ["request_timeout / max_retries", "单次等待时间与失败重试次数", "超时不等于模型不存在,先检查服务和网络。"], ["max_tokens", "单次生成输出上限", "不是输入+输出总上下文窗口,不能超过服务商允许的最大输出。"], ], [1850, 2850, 4660], font_size=8.45, ) b.add_h2(doc, "2.3 如何添加或调整 max_tokens") b.add_para(doc, "在目标模型条目中添加或修改 max_tokens: <整数>。它控制单次回答最多生成多少 token。回答被截断时可在服务商允许范围内逐步增加;成本或响应时间异常时应适度降低。") b.add_table( doc, ["现象", "判断", "处理"], [ ["回答结尾被截断", "max_tokens 太小,或服务端提前结束", "先查看 finish_reason;确认服务商上限后逐步提高。"], ["400 invalid max_tokens", "超过模型限制或字段不兼容", "按服务商模型上限修改;必要时删除该字段验证兼容性。"], ["长文本仍失败", "上下文不足,不只是输出上限", "缩短输入、启用摘要或选择更大上下文模型。"], ["费用/耗时异常", "输出限制过高或提示词过长", "降低 max_tokens;优化提示词和业务限流。"], ], [2000, 3000, 4360], font_size=8.9, ) b.add_h2(doc, "2.4 后端模型变更后的验收") for text in [ "YAML 使用空格缩进,不能使用 Tab;密钥变量名必须同时存在于 backend.env 和 config.yaml。", "修改 backend.env 后必须使用 docker compose up -d --force-recreate backend;普通 restart 不会重新读取环境文件。", "只修改 config.yaml 时可重启 backend,但仍应查看日志确认配置解析成功。", "先进行短问答验证,再验证长文本、工具调用、视觉输入或思考模式(如果该模型声明支持)。", "记录模型 name、服务地址、变更人、验证时间;不要把 API Key 写入变更单或截图。", ]: b.add_bullet(doc, text) def add_chapter_config(doc, diagrams): b.add_h1(doc, "3. .env、backend.env、数据库与前端接口地址") b.add_para(doc, "部署目录的运行配置分为三类:.env 控制 Compose 与对外端口;backend.env 保存后端密钥和连接串;config.yaml 定义模型、登录、数据库模式等;runtime-config.js 控制前端运行时接口地址。不要进入容器直接改文件。") b.add_figure(doc, diagrams["config_layers"], "图 3-1 配置分层与生效方式", "部署层、后端层、前端运行时层和管理页面配置的关系", width=6.35) heading = b.add_h2(doc, "3.1 .env:项目名、镜像标签、访问端口") heading.paragraph_format.page_break_before = True b.add_code(doc, """ # 部署目录 .env COMPOSE_PROJECT_NAME=deerflow DEERFLOW_TAG=<当前镜像包对应标签> APP_PORT=8090 TZ=Asia/Shanghai """, "ENV") b.add_table( doc, ["变量", "作用", "修改后的操作"], [ ["COMPOSE_PROJECT_NAME", "Compose 项目名称,用于隔离容器、网络和卷名称", "确认未与其他项目冲突;通常不在运行中随意修改。"], ["DEERFLOW_TAG", "docker-compose.yml 使用的镜像标签", "应与已导入镜像的标签一致;镜像升级时核对交付清单和 SHA256。"], ["APP_PORT", "用户访问 Web/Nginx 的宿主机端口", "修改后执行 docker compose up -d --force-recreate web;确认新端口开放。"], ["TZ", "容器日志和页面显示时区", "重建受影响容器后生效。"], ], [1900, 3600, 3860], font_size=8.85, ) b.add_h2(doc, "3.2 backend.env:JWT、模型密钥和数据库连接") b.add_code(doc, """ # config/backend.env AUTH_JWT_SECRET=<至少32字节的随机值> INTRANET_MODEL_API_KEY=<模型服务密钥> MYSQL_DATABASE_URL=mysql+asyncmy://:@:3306/?charset=utf8mb4 """, "ENV") b.add_callout(doc, "密钥规则", "AUTH_JWT_SECRET 是签发登录令牌的服务端密钥,不是用户登录密码。修改它会让所有已登录会话失效。backend.env 的任何改动都要强制重建 backend 才会重新读取。", "warn") b.add_h2(doc, "3.3 config.yaml:登录密码与数据库模式") b.add_code(doc, """ # config/config.yaml(示意;保留现有其他配置) auth_login: username_login: enabled: true database: backend: mysql # 使用默认文件数据库时为 sqlite mysql_url: $MYSQL_DATABASE_URL """, "YAML") b.add_para(doc, "用户名登录的共享密码或账号管理项应遵循当前 config.yaml 的 auth_login 配置;单个用户的 login_password、system_role 和资产所有权以应用用户管理/数据库记录为准。不要把用户密码与 AUTH_JWT_SECRET 混为一谈。") b.add_h2(doc, "3.4 切换或排查外部 MySQL") b.add_number(doc, "冻结可能影响资产写入的操作,完整备份 data/backend、当前配置和现有数据库。") b.add_number(doc, "创建数据库和最小权限账号,确认字符集 utf8mb4,并允许容器所在网络访问。") b.add_number(doc, "密码中 @、:、/、#、% 等字符需要 URL 编码;HOST 必须是容器能访问的地址,不能把容器内 127.0.0.1 当成外部数据库。") b.add_number(doc, "同时更新 backend.env 的 MYSQL_DATABASE_URL 与 config.yaml 的 database.backend/mysql_url,强制重建 backend 并持续查看迁移日志。") b.add_number(doc, "分别用管理员、lqq 和普通用户验证登录、技能、智能体、菜单、敏感词和发布状态。SQLite 与 MySQL 不会自动双向同步。") b.add_h2(doc, "3.5 前端接口地址与端口") b.add_code(doc, """ // config/runtime-config.js:推荐同源相对路径 window.__DEERFLOW_RUNTIME_CONFIG__ = { VITE_BACKEND_BASE_URL: "/deerflow", VITE_LANGGRAPH_BASE_URL: "/deerflow/api", VITE_AIPI_BASE_URL: "<可选的业务接口地址>", }; """, "JavaScript") b.add_para(doc, "推荐保持相对路径。浏览器会自动访问当前域名和 APP_PORT,再由 Nginx 转到 backend:8001,这样最少遇到 CORS、证书与混合内容问题。确需使用独立后端域名时,应同步配置允许 Origin 和 HTTPS。") b.add_code(doc, """ # 修改前端端口或运行时配置后的推荐操作 sudo docker compose up -d --force-recreate web sudo docker compose ps sudo docker compose logs --tail=200 web # 浏览器使用 Ctrl+F5;在 Network 检查 runtime-config.js 和 /deerflow/health """, "Shell") def add_chapter_assets(doc, diagrams): b.add_h1(doc, "4. 技能与智能体:放置、发布、丢失排查") b.add_para(doc, "技能和智能体能否在页面出现,取决于文件、数据库元数据、当前登录身份、发布状态和目录挂载是否一致。优先在前端创建/上传;仅在批量迁移或前端不可用时手工导入。") b.add_figure(doc, diagrams["persistence_map"], "图 4-1 技能和智能体的持久化关系", "技能文件、智能体文件、业务数据库和运行时缓存共同决定资产可见性", width=6.35) heading = b.add_h2(doc, "4.1 技能放在哪里") heading.paragraph_format.page_break_before = True b.add_code(doc, """ data/skills/ ├── public/ │ └── / │ ├── SKILL.md │ ├── scripts/ # 可选 │ ├── references/ # 可选 │ └── assets/ # 可选 └── custom/ └── / └── SKILL.md """, "目录结构") b.add_table( doc, ["目录", "含义", "注意"], [ ["data/skills/public", "系统共享/公共技能文件分类", "放入 public 不等于已发布到广场;仍需有正确元数据、权限和发布状态。"], ["data/skills/custom", "用户自定义技能目录", "适合从前端创建或上传的技能;应保留完整目录而不是只复制一个文件。"], ["SKILL.md", "技能说明、使用条件、操作规范的核心文件", "避免写入真实 API Key、数据库密码、内网敏感地址或个人信息。"], ["scripts/references/assets", "脚本、参考资料和附件", "脚本必须可执行且依赖完整;上传前检查许可证、体积和敏感信息。"], ], [2200, 2800, 4360], font_size=8.7, ) b.add_h2(doc, "4.2 智能体放在哪里") b.add_code(doc, """ data/backend/agents// ├── config.yaml # 模型、工具、技能、行为参数 └── SOUL.md # 角色、提示词、表达和规则 """, "目录结构") b.add_para(doc, "智能体文件只是一部分。名称、所有者、可见性、广场和发布状态等业务元数据通常在 SQLite 或 MySQL 中。因此恢复或迁移智能体时,必须同时备份/恢复 data/backend 与对应数据库。") b.add_h2(doc, "4.3 推荐的创建、上传和发布流程") b.add_figure(doc, diagrams["publish_flow"], "图 4-2 从创建到发布广场", "在界面创建、验证、发布、权限校验直至生效的流程", width=6.35) for text in [ "使用普通业务用户或管理员进入技能管理/智能体管理,创建或上传资产并填写名称、说明、依赖和使用范围。", "完成本地实际验证:技能要验证工具调用、脚本依赖和文件权限;智能体要验证所选模型、技能/工具和首轮问答。", "在资产详情或卡片中点击“发布”,选择一个启用的目标广场。资产所有者或 admin 才能发布/下架。", "发布后用普通用户验证广场可见性;重新发布到其他广场通常是迁移,不是多份复制。", "若只需要个人使用,保持私有或不发布;不要通过手工复制文件代替发布流程。", ]: b.add_number(doc, text) b.add_h2(doc, "4.4 手工导入时的安全做法") b.add_code(doc, """ # 示例:先将完整技能目录复制到 custom,再确认容器内可见 sudo mkdir -p data/skills/custom/ # 将经过审核的文件复制到上面的目录后执行: sudo docker compose exec backend sh -lc 'find /app/backend/skills/custom -maxdepth 3 -type f -print' sudo docker compose restart backend """, "Shell") b.add_callout(doc, "手工导入后的边界", "手工复制只解决文件存在的问题,不会自动补齐所有者、可见性或发布元数据。导入后应通过界面核对资产记录;若无记录,按应用支持流程创建或恢复元数据。", "warn") b.add_h2(doc, "4.5 发布按钮或广场不可用") b.add_table( doc, ["问题", "检查项", "处理"], [ ["看不到“发布”按钮", "当前用户是否资产所有者或 admin", "切换到所有者/admin;不要只凭文件目录判断权限。"], ["选择不到广场", "广场是否启用、是否存在默认/私有范围限制", "由 admin 在发布广场管理中启用或创建目标广场。"], ["发布后普通用户看不到", "广场、私有状态、筛选、缓存和角色", "确认实际发布到哪个广场;退出重登并清除筛选;检查发布 API 401/403。"], ["智能体能看见但模型不可用", "config.yaml 中模型 name 是否仍存在", "恢复同名模型条目或将智能体改到已验证模型后重启 backend。"], ], [2200, 3000, 4160], font_size=8.7, ) b.add_h2(doc, "4.6 技能或智能体“丢失”专项排查") b.add_figure(doc, diagrams["lost_asset_flow"], "图 4-3 资产丢失排查顺序", "从服务目录、身份、挂载、数据库、过滤到恢复验证的排查流程", width=6.35) b.add_number(doc, "确认当前部署目录和服务:执行 pwd、docker compose ps,排除在旧目录启动了另一套服务。") b.add_number(doc, "确认登录身份:重新登录,核对用户名、system_role、资产所有者和是否处于独立会话/嵌入会话。") b.add_number(doc, "同时检查宿主机和容器内文件。宿主机有而容器没有,重点检查 docker-compose.yml 的 volume 映射与当前工作目录。") b.add_code(doc, """ find data/skills -maxdepth 3 -type f -print | sort find data/backend/agents -maxdepth 3 -type f -print | sort sudo docker compose exec backend sh -lc 'find /app/backend/skills -maxdepth 3 -type f -print | sort' sudo docker compose exec backend sh -lc 'find /app/backend/.deer-flow/agents -maxdepth 3 -type f -print | sort' """, "Shell") b.add_number(doc, "确认数据库未切换:查看 config.yaml 的 database.backend 和 backend.env 的 MYSQL_DATABASE_URL。切换数据库不会自动搬迁或同步资产记录。") b.add_number(doc, "检查过滤、广场和权限:清除搜索条件,切换“我的/公共/已发布/私有”等视图,并用 admin 核验发布记录。") b.add_number(doc, "查看 backend 日志。文件和数据库都缺失时,停止写入,从备份恢复;重启 backend、重新登录并实际调用验证。") def add_chapter_ui(doc, diagrams): b.add_h1(doc, "5. 前端管理:菜单名称、敏感词、主题与账号") b.add_para(doc, "菜单、主题、敏感词和广场主要通过应用管理页面维护,属于数据库中的全局设置。保存后通常立即生效,但应使用管理员和普通用户分别验证。") b.add_figure(doc, diagrams["ui_map"], "图 5-1 常用管理入口", "菜单、外观、技能、智能体和用户管理入口关系", width=6.35) b.add_h2(doc, "5.1 配置菜单名称、排序和隐藏") b.add_number(doc, "使用 system_role=admin 的 DeerFlow 管理员登录,打开左下角“设置和更多”中的“菜单管理”。") b.add_number(doc, "找到目标一级、二级或三级菜单,修改显示名称 label、启用状态、排序或父级;保存后刷新页面验证。") b.add_number(doc, "菜单停用会隐藏该节点及其子树;修改名称不要改真实 path、路由 ID 或代码常量。") b.add_number(doc, "用普通用户检查左侧菜单、顶部导航和直接访问链接;记录修改前名称、目的和回退方式。") b.add_callout(doc, "安全边界", "隐藏菜单只移除导航入口,不等于阻止用户通过已知 URL 或 API 访问。真正禁用功能还需后端 API 权限、路由守卫和角色权限共同生效。", "danger") b.add_h2(doc, "5.2 配置敏感词:必须用哪个用户") b.add_para(doc, "敏感词功能是“展示层脱敏映射”:原始词 -> 替换词。它不会修改数据库里的原文,也不会改变 URL、路由 path 或节点 ID。") b.add_callout(doc, "登录要求", "敏感词管理前端入口、路由守卫和页面级校验都要求当前显示用户名严格等于 lqq。即使 admin 拥有后端写权限,也应使用 lqq 登录来维护页面。邮箱账号时,@ 前缀必须为 lqq。", "warn") b.add_number(doc, "用 lqq 登录,进入“设置和更多 -> 敏感词管理”。") b.add_number(doc, "新增原始词与替换词,设置启用状态;需要初始规则时可使用“导入种子词”合并缺失映射。") b.add_number(doc, "保存的是整套规则;修改前先导出/截图并经业务确认,避免替换词冲突或过度脱敏。") b.add_number(doc, "刷新页面,检查菜单、导航、标题和普通内容的显示;用普通用户复核最终效果。") b.add_h2(doc, "5.3 主题色、布局和嵌入页面") b.add_table( doc, ["配置目标", "操作入口/变量", "验证"], [ ["个人主题", "设置 -> 外观,用户自己的选择通常优先", "新开页面检查浅/深主题、文字对比度和焦点状态。"], ["系统默认与主题库", "admin 在外观设置中配置默认主题、主题名称、主色和启用状态", "用没有个人覆盖的新测试用户验证。"], ["强制紧凑侧边栏", "runtime-config.js 的 VITE_FORCE_COLLAPSED_SIDEBAR", "重启 web 并刷新浏览器后检查导航。"], ["iframe 嵌入", "URL 参数 embed=1、theme=...、hideJump=1、isolate=1", "在嵌入页验证无全局导航、主题和独立会话行为。"], ], [2200, 3500, 3660], font_size=8.7, ) b.add_callout(doc, "主题验收", "不仅看按钮颜色。还要检查正文、表格、弹窗、下拉框、禁用、hover、focus、错误和成功状态的对比度;不要用品牌主色铺满大面积背景。", "note") heading = b.add_h2(doc, "5.4 必备账号与角色") heading.paragraph_format.page_break_before = True b.add_table( doc, ["账号类型", "至少准备", "主要用途", "注意"], [ ["DeerFlow admin", "2 个", "菜单、主题、广场、用户和全局资产管理", "system_role 必须为 admin;保留一个应急管理员。"], ["lqq", "1 个", "敏感词管理前端入口", "显示用户名/邮箱前缀必须严格为 lqq。"], ["普通测试用户", "1 个", "验收菜单隐藏、资产可见性和广场发布", "不能只用管理员验收,管理员会掩盖权限问题。"], ["模型/数据库服务账号", "按环境", "调用外部服务", "遵循最小权限和定期轮换,不与个人账号共用。"], ], [2100, 1250, 3300, 2710], font_size=8.65, ) def add_chapter_operations(doc): b.add_h1(doc, "6. 日常启停、日志和健康检查") b.add_para(doc, "一键脚本和 Docker Compose 命令都应从部署目录运行。日常 restart 适合重启进程;任何环境变量文件改动都必须通过 force-recreate 重建受影响容器。") b.add_h2(doc, "6.1 常用命令速查") b.add_code(doc, """ # 整套服务 sudo bash restart.sh sudo bash stop.sh sudo bash logs.sh sudo bash diagnose.sh # 只处理后端或前端 sudo docker compose restart backend sudo docker compose up -d --force-recreate backend # backend.env 改动后 sudo docker compose restart web sudo docker compose up -d --force-recreate web # .env 的 APP_PORT / Web 配置改动后 sudo docker compose ps """, "Shell") b.add_h2(doc, "6.2 如何打印日志") b.add_code(doc, """ # 全部服务日志;Ctrl+C 仅停止跟踪,不停止服务 sudo bash logs.sh # 只看后端或 Web/Nginx sudo bash logs.sh backend sudo bash logs.sh web # Compose 原生命令:最后 500 行并持续跟踪 sudo docker compose logs --tail=500 -f backend sudo docker compose logs --tail=500 -f web """, "Shell") b.add_h2(doc, "6.3 健康检查和变更后验收") b.add_code(doc, """ curl -fsS http://127.0.0.1:/deerflow/health curl -I http://127.0.0.1:/ sudo docker compose ps """, "Shell") for text in [ "容器 Up 不等于业务可用:健康接口成功后仍要登录一次、发起一次对话并验证修改的功能。", "backend.env、.env 改动:force-recreate;config.yaml 改动:至少 restart backend;runtime-config.js 改动:重启 web 并强制刷新浏览器。", "变更完成后继续观察后端日志至少 5 分钟,确认没有反复重启、401/429/500、数据库错误或模型初始化失败。", ]: b.add_bullet(doc, text) def add_chapter_backup(doc): b.add_h1(doc, "7. 数据库、备份、恢复与升级") b.add_para(doc, "一次可恢复的备份至少包含 config、data 和当前业务数据库。只保存 Docker 镜像无法恢复用户、技能、智能体、发布广场、菜单、主题与敏感词。外部 MySQL 必须单独导出。") b.add_h2(doc, "7.1 必须备份的对象") b.add_table( doc, ["对象", "作用", "建议频率", "恢复价值"], [ ["config 与 .env", "模型、登录、数据库、接口地址和运行配置", "每次变更后", "恢复服务连接和端口配置。"], ["data/backend", "文件数据库、智能体文件及运行数据", "每日 + 变更前", "恢复智能体、用户和运行相关资产。"], ["data/skills", "public/custom 技能文件和附件", "每日 + 技能变更后", "恢复技能内容和脚本。"], ["data/knowledge", "知识库文件", "按使用频率", "恢复知识资产。"], ["外部 MySQL dump", "资产元数据、用户、菜单、主题、广场等", "每日 + 变更前", "恢复数据库层资产与全局设置。"], ], [1900, 3150, 1850, 2460], font_size=8.5, ) b.add_h2(doc, "7.2 文件和数据库备份示例") b.add_code(doc, """ # 文件备份:确保备份目录位于独立存储,不在当前 data 内 cd <部署目录> sudo tar -czf <备份目录>/deerflow-files-$(date +%F-%H%M).tar.gz .env config data sha256sum <备份目录>/deerflow-files-*.tar.gz # MySQL 备份:密码不要直接写到命令历史,优先使用受控 defaults-extra-file mysqldump --single-transaction --routines --triggers \ -h -u > <备份目录>/deerflow-mysql-$(date +%F-%H%M).sql """, "Shell") b.add_h2(doc, "7.3 恢复与升级原则") for text in [ "恢复前先确认备份版本、SHA256、数据模式和目标目录;停止或冻结写入,避免恢复过程中产生新分叉数据。", "先恢复到隔离或测试环境验证,再进行正式切换;恢复后检查目录权限、数据库连接、健康接口、登录、模型、技能、智能体和广场。", "升级时保留旧镜像、旧配置和完整备份;新版本使用独立测试端口验证,确认无误再切换。", "SQLite 与 MySQL 的资产数据不会自动双向同步。任何切换、迁移或恢复都要有明确来源和一次性合并计划。", ]: b.add_bullet(doc, text) b.add_callout(doc, "高风险操作", "删除部署目录、删除 data、覆盖未知数据库、docker system prune -a 和递归修改所有者都可能造成不可逆损失。执行前必须明确目标、验证备份可用并获得审批。", "danger") def add_chapter_security_handover(doc): b.add_h1(doc, "8. 巡检、安全与交接") b.add_h2(doc, "8.1 每日与每周巡检") b.add_table( doc, ["频率", "检查内容", "合格标准"], [ ["每日", "docker compose ps、/deerflow/health、磁盘/inode、backend/web 日志", "服务正常、无重启循环、无连续 5xx/401/429、空间充足。"], ["每周", "备份是否完成、最近恢复演练、模型配额与超时、资产发布与权限变更", "备份可校验;高频失败有负责人;变更有记录。"], ["每次变更后", "端口、日志、健康、登录、实际业务功能、普通用户权限", "功能验收通过且可回退。"], ], [1500, 3900, 3960], font_size=8.75, ) b.add_h2(doc, "8.2 最低安全要求") for text in [ "AUTH_JWT_SECRET、模型 API Key、数据库 URL/密码只保存在受控的 backend.env 或密钥系统中,不进入 Git、截图、聊天记录和前端 runtime-config.js。", "管理员、lqq、普通测试用户和服务账号分开管理;定期轮换密码/密钥,离职或交接时立即回收不再需要的权限。", "对外只开放必要的 APP_PORT;后端 8001 默认不直接暴露。数据库地址、模型服务和业务接口应采用最小网络范围。", "对外发送日志前删除 Authorization、Cookie、Token、API Key、数据库 URL、手机号和身份信息,只保留时间、状态码、trace ID 与关键报错。", ]: b.add_bullet(doc, text) b.add_h2(doc, "8.3 交接清单") b.add_table( doc, ["项目", "交接时填写"], [ ["生产访问地址 / APP_PORT", ""], ["当前部署目录与 Compose 项目名", ""], ["镜像版本 / SHA256", ""], ["数据库模式、地址与库名(不写密码)", ""], ["DeerFlow admin、lqq、普通测试用户责任人", ""], ["模型服务与密钥保管责任人", ""], ["备份位置、保留期与最近恢复演练日期", ""], ["监控、告警和问题升级联系人", ""], ], [3500, 5860], font_size=9.1, ) def add_appendices(doc): b.add_h1(doc, "附录 A:最短命令清单") b.add_code(doc, """ cd <部署目录> sudo docker compose ps sudo docker compose logs --tail=500 -f backend sudo docker compose logs --tail=500 -f web sudo docker compose restart backend sudo docker compose up -d --force-recreate backend sudo docker compose restart web sudo docker compose up -d --force-recreate web curl -fsS http://127.0.0.1:/deerflow/health """, "Shell") heading = b.add_h1(doc, "附录 B:问题升级信息模板") heading.paragraph_format.page_break_before = False b.add_code(doc, """ 【问题标题】 【发生时间】YYYY-MM-DD HH:mm:ss(时区) 【访问地址/端口】<不含密码> 【当前账号角色】admin / lqq / 普通用户(不要提供密码) 【操作步骤】1... 2... 3... 【实际现象】 【期望结果】 【HTTP 状态码 / trace ID】 【容器状态】docker compose ps 【已脱敏日志】backend/web 最近 200 行 【最近变更】模型、数据库、菜单、主题、镜像或配置 【已采取动作】以及结果 """, "故障单模板") heading = b.add_h1(doc, "附录 C:核心事实速记") heading.paragraph_format.page_break_before = True b.add_table( doc, ["主题", "结论"], [ ["前端和后端端口", "容器内部不同:Web 80、Backend 8001;用户通常经由 APP_PORT 访问 Web。"], ["配置文件", ".env 管端口/Compose;backend.env 管密钥和连接串;config.yaml 管模型/登录/数据库;runtime-config.js 管前端地址。"], ["环境变量生效", "backend.env 或 .env 改动后必须 force-recreate 受影响容器;普通 restart 不会重新读取。"], ["模型与 max_tokens", "模型条目在 config.yaml;Key 在 backend.env;max_tokens 是单次输出上限,不是上下文窗口。"], ["技能位置", "data/skills/public 与 data/skills/custom;目录文件存在不等于已经发布。"], ["智能体位置", "data/backend/agents/;还依赖数据库元数据、所有者、可见性和发布状态。"], ["敏感词用户", "页面维护需要显示用户名严格为 lqq;admin 不能替代该前端入口条件。"], ["菜单隐藏", "只隐藏导航入口,不等于禁止直接 URL 或 API,需要后端权限配合。"], ["资产丢失", "先查目录、挂载、数据库、身份、过滤和日志,最后才恢复;不要急着创建同名资产。"], ["完整备份", "config + data + 外部 MySQL dump + 镜像版本/SHA256;只备份镜像不足以恢复业务资产。"], ], [3100, 6260], font_size=8.7, ) def build_document(diagrams): doc = b.Document() b.configure_document(doc) set_running_furniture(doc) doc.core_properties.title = "DeerFlow 系统运维手册(用户重点版)" doc.core_properties.subject = "DeerFlow 常见报错、模型、配置、技能、智能体、前端管理与恢复" doc.core_properties.author = "DeerFlow 运维团队" doc.core_properties.keywords = "DeerFlow, 运维, Docker, config.yaml, 技能, 智能体" add_cover(doc, diagrams) add_navigation(doc) add_chapter_common_errors(doc) add_chapter_model(doc, diagrams) add_chapter_config(doc, diagrams) add_chapter_assets(doc, diagrams) add_chapter_ui(doc, diagrams) add_chapter_operations(doc) add_chapter_backup(doc) add_chapter_security_handover(doc) add_appendices(doc) doc.save(OUT) return OUT if __name__ == "__main__": b.ensure_dirs() diagrams = b.make_diagrams() print(build_document(diagrams))