Operations

部署与配置

ZhiWu 0.1.0 的部署事实只有一句话:平台整体编排不在本仓,仓库交付的是四个可手工启动的进程 + 一套沙箱 Provisioner 的 Compose 清单 + 一份逐行核对过的环境变量表。 本页把这三件事写清楚,并把每个结论对到仓内文件或 docs/08–docs/13 的核对记录上。

Local

本地开发启动

没有"一键起全栈"。四个进程分别启动,五个数据服务自备。

前置依赖

  • 先把五个数据服务准备好:PostgreSQL(业务表 + LangGraph checkpoint)、Redis(ARQ 队列 / 事件流 / 缓存 / 健康 key)、MinIO(文档 / 附件 / 产物)、Milvus(向量 + BM25 稀疏)、Neo4j(知识图谱)。仓库内没有它们的 compose 定义,见 docs/09-deployment.md 第 2 节。
  • 填 backend/.env:该文件未被 git 跟踪,仓内也没有 .env.example 模板,变量名与默认值以本页环境变量总表为准。
  • 三项密钥必须各 ≥ 32 字符、无首尾空白、彼此不相等:JWT_SECRET_KEY、API_KEY_DERIVATION_SECRET、SANDBOX_PROVISIONER_TOKEN(lifespan 硬校验,backend/server/src/server/utils/auth_utils.py)。
  • 三个持久根目录必须是绝对路径:ZHIWU_USER_DATA_DIR、ZHIWU_SKILL_DATA_DIR、ZHIWU_SKILL_PROJECTION_DIR,否则启动直接 RuntimeError。
  • 工具链:Python ≥ 3.13 + uv(后端与 CLI)、Node + pnpm(前端)、Docker(沙箱 Provisioner)。
1 · 后端 API(:5050)
cd backend
uv sync --all-groups
uv run python server/src/server/main.py
2 · ARQ Worker(另开终端)
cd backend
uv run python server/src/server/worker_main.py
# 并发度:ARQ_MAX_JOBS=10(默认)
# 启动时先 reconcile 收敛遗留任务
3 · 沙箱 Provisioner(:8002)
cd docker/sandbox_provisioner
docker compose up -d            # 或 ./script/startup.sh
curl -s localhost:8002/health
4 · 前端工作台(:5173)
cd web
pnpm install
VITE_API_URL=http://127.0.0.1:5050 \
VITE_MINIO_URL=http://127.0.0.1:9000 \
pnpm dev
!
Vite 代理默认指向容器网络别名。web/vite.config.js 用 loadEnv(mode, process.cwd(), '') 读环境, 代理 target 是 env.VITE_API_URL || 'http://api:5050' 与 env.VITE_MINIO_URL || 'http://minio:9000'—— 两个默认值都是 Compose 网络内的服务名,宿主机直跑后端时解析不到。仓内 web/.env 只预设了 VITE_API_URL=http://127.0.0.1:5050,没有设 VITE_MINIO_URL, 所以 /minio/public/* 仍会去连 http://minio:9000 而失败,表现为头像与公开资源加载不出来; 宿主机直跑需自行补 VITE_MINIO_URL=http://127.0.0.1:9000。来源:docs/09-deployment.md 第 1 节、web/vite.config.js。

仓库交付边界:四条容易踩空的事实

1根目录没有 docker-compose.yml

全仓唯一的完整编排是 docker/sandbox_provisioner/ 下的两套 compose(docker backend 与 kubernetes backend)。 api / worker / web / storage-migrator 没有 Dockerfile、没有编排清单,因此不存在"一条命令起全栈"。 web/AGENTS.md 里的 docker compose exec web … 是失效指令,本地直接 cd web && pnpm …。 来源:docs/09-deployment.md「已确认结论」。

2没有种子用户脚本

backend/scripts/seed_initial_users.py 不存在(该目录只有 gen_invoice_images.py)。 初始 superadmin 走公开端点 POST /api/auth/initialize(首次运行时创建管理员账号), 见 docs/06-api.md 鉴权清单与 docs/00-overview.md 的核对修正。

3schema 迁移有代码、没有镜像

迁移代码在仓内:backend/zhiwu/src/zhiwu/storage_migration.py,入口 cd backend && uv run python -m zhiwu.storage_migration。 但没为它包镜像,部署方需自行写成一次性 Job(先于 api / worker 执行)。 应用进程本身只校验 schema 版本兼容、不建表不改表(pg_manager.require_current_schema(), server/utils/lifespan.py)。来源:docs/09-deployment.md 第 2 节。

4沙箱镜像不是本仓构建的

默认镜像 enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest 属第三方,本仓只通过 SANDBOX_RUNTIME_PROFILE(默认 core)注入 DISABLE_* 开关裁剪能力; 沙箱内 HTTP 端口固定 8080。镜像内预装运行时清单无法从本仓确认。 来源:docker/sandbox_provisioner/app.py、docs/09-deployment.md「已确认结论」。

Compose

沙箱 Provisioner 编排

这是仓库内唯一一份完整编排。docker-compose.yml 用 docker backend, docker-compose.k8s.yml 用 kubernetes backend(集成测试用),两者并存但争用宿主 8002。

配置项docker-compose.yml(docker backend)docker-compose.k8s.yml(kubernetes backend)
项目名 / 服务名 zhiwu-sandbox-provisioner · 服务 provisioner zhiwu-sandbox-provisioner-k8s · 服务 provisioner
container_name zhiwu-sandbox-provisioner zhiwu-sandbox-provisioner-k8s
build args BASE_IMAGE ← PROVISIONER_BASE_IMAGE,默认 docker.m.daocloud.io/library/python:3.13-slim;PIP_INDEX_URL 默认阿里云源 同上两个 build args,默认值一致
image zhiwu-sandbox-provisioner:local zhiwu-sandbox-provisioner:local(同一镜像)
command 镜像默认 uvicorn app:app --host 0.0.0.0 --port 8002 --env-file sandbox.env 显式覆盖为不带 --env-file 的 uvicorn,配置全走 environment,避免 sandbox.env 里 PROVISIONER_BACKEND=docker 干扰
端口 ${PORT:-8002}:8002 8002:8002(写死)
公共环境变量 SANDBOX_PROVISIONER_TOKEN(compose 内含默认值,生产必须覆盖,≥32 字符且与 backend/.env 一致)、PROVISIONER_PUBLIC_URL(返回给 backend 的代理基址,默认 http://127.0.0.1:8002) 同样两项,PROVISIONER_PUBLIC_URL 写死 http://127.0.0.1:8002
backend 专用变量 由镜像内 sandbox.env 提供(PROVISIONER_BACKEND=docker) PROVISIONER_BACKEND=kubernetes、KUBECONFIG_PATH=/app/kubeconfig/kubeconfig.k8s.yaml、K8S_NAMESPACE=zhiwu-know、NODE_HOST=k8s.orb.local、SANDBOX_IMAGE=all-in-one-sandbox:zhiwu-local、SANDBOX_CONTAINER_PORT=8080、SANDBOX_HEALTH_TIMEOUT_SECONDS=300
挂载 /var/run/docker.sock:/var/run/docker.sock(高权限)、../../user-data:/app/user-data、../../skill-projections:/app/skill-projections、./sandbox.env:/app/sandbox.env:ro ./kubeconfig.k8s.yaml:/app/kubeconfig/kubeconfig.k8s.yaml:ro、./sandbox.env:/app/sandbox.env:ro(不挂 docker.sock)
extra_hosts host.docker.internal:host-gateway(保证 Linux 部署同样可用) 同左
restart unless-stopped unless-stopped

来源:docker/sandbox_provisioner/docker-compose.yml、docker/sandbox_provisioner/docker-compose.k8s.yml、docker/sandbox_provisioner/Dockerfile。

!
两套 compose 争用宿主 8002。切换 backend 前必须先 down 掉另一套: docker compose -f docker-compose.yml down 再起 k8s 版,反之亦然。 backend/.env 的 SANDBOX_PROVISIONER_URL 始终指向宿主 8002,因此"起的是哪一套"决定了 backend 走 docker 还是 kubernetes。 k8s 版还需要本地 kubeconfig.k8s.yaml——它被 docker/sandbox_provisioner/.gitignore 显式排除(含客户端证书/私钥),新环境要自行生成; 基础设施模板在 docker/sandbox_provisioner/test/k8s-infra.yaml(Namespace + 两个静态绑定 PV/PVC)。

挂载为什么长这样

  • /app/user-data 与 /app/skill-projections 不是给 provisioner 自己写的:它的 _resolve_host_paths 会按这两个容器内路径反查宿主 bind 源,再把宿主真实路径转挂给沙箱容器的 /home/gem/user-data、/home/gem/skills。反查失败就会表现为"沙箱与工作区文件不同步"。
  • ./sandbox.env 以只读挂载,既是 uvicorn --env-file 的配置来源,也是 provisioner load_sandbox_env 注入沙箱容器环境变量的来源——所以即使 k8s 版去掉了 --env-file,仍保留这份只读挂载。
  • /var/run/docker.sock 等价于宿主 root 权限,仅限受控主机;需要更强隔离时选 kubernetes backend(见 docs/11-security.md 第 7 节)。

↻script/startup.sh

封装 docker compose up -d --build,随后做 60 × 0.5s 的健康探测,并给宿主目录 chmod。日常起停用这个。

■script/shutdown.sh

对应的停止入口(封装 docker compose down),避免遗留 provisioner 容器占住 8002。

▤script/sandbox_local.sh

在宿主机直接跑 provisioner(不经容器),用于调试;script/run/ 是运行产物目录(当前只有 provisioner.log,未被 git 跟踪),不是镜像构建输入。

Scale

依赖服务与扩缩容

应用侧可以水平扩,数据侧一件都不在仓库里。

数据服务用途仓库内是否含编排关键变量
PostgreSQL业务表 + LangGraph checkpoint(同一 DSN 共用)否POSTGRES_URL(无默认)
RedisARQ 队列、Run 事件 Stream、配置缓存、登录限速、Worker 健康 key否REDIS_URL,默认 redis://redis:6379/0
MinIO文档 / 图片 / 公开附件(桶 knowledgebases、kb-images、public)否MINIO_URI / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_PUBLIC_URL
Milvus每 KB 一个 collection:向量 + BM25 稀疏,支持 hybrid 与图向量库否MILVUS_URI / MILVUS_TOKEN / MILVUS_DB
Neo4j知识图谱抽取与召回否NEO4J_URI / NEO4J_USERNAME / NEO4J_PASSWORD
API:无状态,直接多副本

session 不存服务端,JWT 自包含(HS256,claims 只有 sub/exp/iss/aud),任意副本可服务任意请求。 来源:docs/09-deployment.md 第 4 节、utils/auth_utils.py。

Worker:可多实例,单写者由 DB 保证

租约 CAS 抢占 + 部分唯一索引 uq_agent_runs_one_active_per_thread 让同一 (uid, agent_slug, thread) 只能有一个非终态 Run;单并发度看 ARQ_MAX_JOBS(默认 10), 实例启动先跑 reconcile 收敛遗留任务。来源:services/run_worker.py、models_business.py。

沙箱:docker backend 用 per-sandbox 网络池;k8s backend 交给 K8s 调度

provisioner 本身是单点 HTTP 服务(:8002),三种 backend 分支在 app.py; 沙箱容器按 runtime scope 一个,扩缩由 backend 决定而不是应用决定。

数据层:按各自标准方案,仓库未含运维清单

没有资源配额、没有 HPA/Ingress/PDB、没有应用侧 Deployment(k8s/ 是空目录)。 官方推荐规格与备份策略需外部确认——这一点在 docs/09-deployment.md 里明确列为待确认项。

备份基线

  • 必须备:PostgreSQL(业务 + checkpoint)、MinIO 桶 knowledgebases / kb-images、宿主目录 user-data/、skill-sources/、skill-projections/、Milvus 与 Neo4j 数据。
  • 可再生但重建耗时:skill-projections/ 与内置 Skill 可凭 DB + 源目录重建(投影 fail-closed 设计支持重同步);Milvus 索引可凭 PG 表 knowledge_chunks 走 re-index 端点重建。
  • 频率 / 保留期 / 恢复演练:仓内无任何依据可推导。
×
仓内确实没有任何备份脚本。根 script/ 是空目录且未被 git 跟踪,全仓无 *backup* 文件、无 cron 定义、 无快照或对象生命周期配置。备份与恢复属运维 P0 补齐项,需外部确认频率与保留期。来源:docs/09-deployment.md「已确认结论」。
Probes

健康检查与探活

五个对象、四种机制。就绪探针与存活探针是分开的两个端点。

对象端点 / 机制默认参数来源
API 进程 GET /api/system/health(存活)、GET /api/system/ready(就绪,聚合依赖探测) 就绪探测超时 READINESS_PROBE_TIMEOUT_SECONDS=2、缓存 READINESS_CACHE_TTL_SECONDS=1 routers/system_router.py、services/readiness_service.py
Sandbox Provisioner GET :8002/health —(script/startup.sh 起后按 60 × 0.5s 探测) docker/sandbox_provisioner/app.py
沙箱容器 provisioner 创建容器后轮询沙箱 :8080 健康接口 SANDBOX_HEALTH_TIMEOUT_SECONDS 默认 60(k8s 版 compose 覆盖为 300) docker/sandbox_provisioner/app.py、docker-compose.k8s.yml
ARQ Worker 周期性向 Redis 写 health check key,外部读 key 判活 WORKER_HEALTH_INTERVAL_SECONDS=5(health_check_interval / health_check_key) services/run_worker.py、services/run_queue_service.py
OCR 引擎 GET /api/system/ocr/health 权限 get_required_user(登录即可,非 admin) routers/system_router.py

补充:MCP 连通性用 POST /api/system/mcp-servers/{slug}/test 主动验证,不属于常态探活。 来源:docs/09-deployment.md 第 3 节、docs/10-observability.md 第 3 节。

Environment

环境变量总表

按域拆开。是否必填以代码校验逻辑为准,默认值逐个取自 os.getenv(NAME, default) 第二参数 / get_int_env / or 兜底分支, 来源 docs/08-configuration.md 第 2 节(已逐行核对)。 注意:环境变量名与 system_options 字段完全解耦——SILICONFLOW_API_KEY、MILVUS_* 不会被系统配置项回退读取。

基础与环境

变量用途必填默认值
ZHIWU_ENV运行环境(development / production);影响 CORS 默认值、reload 行为与密钥兜底策略。仅 {prod, production} 视为生产否development
ZHIWU_INSTANCE_ID实例标识,构成 JWT issuer zhiwu-know:{instance_id};变更后旧 token 因 issuer 不匹配全部失效生产必填开发缺失时自动生成 instance-{hex16} 并只写回进程环境(不落盘);生产缺失直接启动失败
ZHIWU_CODE_REVISION代码版本标识,用于 /api/system/info 与 Run manifest 展示否空串
ZHIWU_CORS_ORIGINSCORS 白名单,逗号分隔;含 * 时关闭 credentials生产必填production 默认空;development 默认 http://localhost:5173、http://127.0.0.1:5173
HOST_IP / RUNNING_IN_DOCKER容器 / 宿主网络感知:设置了 RUNNING_IN_DOCKER 时,把 MinIO endpoint 的主机名改写为 HOST_IP否HOST_IP 缺省按 localhost 处理
ZHIWU_BRAND_FILE_PATH品牌定制文件路径;相对路径会锚定包内 config/static/否未配置时依次回退 info.local.yaml → info.template.yaml

目录与存储根

变量用途必填默认值 / 校验
ZHIWU_USER_DATA_DIR用户工作区持久根(宿主卷,同时被 provisioner 反查转挂给沙箱)是兜底相对路径 user-data,但 require_absolute_storage_roots() 对空值/相对值直接抛 RuntimeError
ZHIWU_SKILL_DATA_DIRSkill 共享源根(内置 Skill 同步落点)是兜底 skill-sources,同样受绝对路径校验
ZHIWU_SKILL_PROJECTION_DIR按 uid 的 Skill 只读投影根是兜底 skill-projections,同样受绝对路径校验
ZHIWU_RUNTIME_DIR运行期临时目录(日志文件写在这里的 logs/ 下)否{tempdir}/zhiwu-runtime-{pid}——不配就每进程一个目录、重启即换,多实例不共享
!
这三个"是"不是建议。相对路径或空值在 lifespan 的 required 组件里就会终止启动; 而 ZHIWU_RUNTIME_DIR 没配时 /api/system/logs 基本读不到有意义的日志。 来源:zhiwu/config/__init__.py、server/utils/lifespan.py。

数据服务

变量用途必填默认值
POSTGRES_URLPG DSN,业务库与 LangGraph checkpoint 共用(环境变量名由 KB_DATABASE_URL_ENV 指定)是无默认,缺失即报错(sandbox provider 侧读取时兜底空串,但不改变"必须配"的事实)
POSTGRES_POOL_SIZE / POSTGRES_MAX_OVERFLOW / POSTGRES_POOL_TIMEOUT_SECONDS业务库 SQLAlchemy 异步引擎连接池否10 / 20 / 30s(非法值由 get_int_env 直接 raise)
LANGGRAPH_POSTGRES_POOL_SIZE / LANGGRAPH_POSTGRES_POOL_TIMEOUT_SECONDScheckpoint 库独立连接池否10 / 30s
REDIS_URL / REDIS_MAX_CONNECTIONS / REDIS_SOCKET_TIMEOUT / REDIS_CONNECT_TIMEOUTRedis:队列、事件 Stream、缓存、限速、健康 key是redis://redis:6379/0;连接上限 32;两个 timeout 未配置时传 None(走 redis-py 内置值)
MILVUS_URI / MILVUS_TOKEN / MILVUS_DBMilvus(KB collection 与图向量库同值)KB 功能必填http://localhost:19530 / "" / zhiwu
MINIO_URI / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_PUBLIC_URLMinIO 对象存储与公开访问前缀是http://minio:9000 / minioadmin / minioadmin / /minio——后两项是硬编码弱默认凭证
NEO4J_URI / NEO4J_USERNAME / NEO4J_PASSWORDNeo4j 图谱图谱功能必填bolt://localhost:7687 / neo4j / 0123456789——密码是硬编码弱默认

安全与认证

变量用途必填默认值 / 校验
JWT_SECRET_KEYJWT HS256 签名密钥生产必填生产缺失或等于公开默认值 zhiwu_know_secure_key → 启动失败;开发缺失自动生成临时 token_hex(32)(重启即变,旧 token 全失效)
API_KEY_DERIVATION_SECRETAPI Key 的 HMAC 派生密钥(支持可重放派生同 key)是(所有环境)无兜底:缺失时 _validate_configured_security_secrets 抛 ValueError
SANDBOX_PROVISIONER_TOKEN与 Provisioner 通信的 Bearer token;compose 侧与 backend/.env 两侧必须一致是(所有环境)lifespan 与 provider.py 各校验一次:≥ 32 字符、无首尾空白、三项彼此不相等(hmac.compare_digest 交叉检查)
OIDC_ENABLEDSSO 总开关;为 false 时直接返回 enabled=False,不读其余任何 OIDC 变量否false
OIDC_ISSUER_URL / OIDC_CLIENT_ID / OIDC_CLIENT_SECRET / OIDC_AUTHORIZATION_ENDPOINT / OIDC_TOKEN_ENDPOINT / OIDC_USERINFO_ENDPOINT / OIDC_END_SESSION_ENDPOINT / OIDC_REDIRECT_URIauthorization code 流程的 issuer / 客户端凭据 / 各端点(开关之外 20 个变量的发现与凭据子集)启用 SSO 时必填默认空串
OIDC_PROVIDER_NAME / OIDC_SCOPES / OIDC_DEFAULT_ROLE / OIDC_DEFAULT_DEPARTMENT登录按钮显示名、请求 scope、新用户默认角色与默认部门否OIDC登录 / openid profile email / user / OIDC用户
OIDC_EMAIL_CLAIM / OIDC_NAME_CLAIM / OIDC_USERNAME_CLAIM / OIDC_DEPARTMENT_CLAIM从 userinfo 取字段的 claim 名否email / name / preferred_username / department
OIDC_AUTO_CREATE_USER / OIDC_USE_RAW_USERNAME / OIDC_FETCH_DEPARTMENT_INFO / OIDC_FORCE_PROMPT_LOGIN布尔开关:自动建号 / 用原始用户名 / 拉部门信息 / 每次强制跳登录页否true / false / false / true

模型与检索供应商

变量用途必填默认值 / 行为
SILICONFLOW_API_KEYSiliconFlow(内置默认供应商)的对话 / embed / rerank,同时是 DeepSeek-OCR 解析器的默认密钥源用内置供应商时必填无默认
WEB_SEARCH_PROVIDER联网搜索 provider 选择否只接受 doubao / tavily,非白名单值记 warning 后忽略;未设置时按字典序自动探测第一个已配置密钥的 provider
TAVILY_API_KEY / DOUBAO_SEARCH_API_KEY对应 provider 的密钥用搜索时必填无默认;无 provider 且无密钥 → 不注册搜索工具
NOTION_API_KEY / NOTION_TOKENNotion 知识库连接器凭证用 Notion KB 时必填NOTION_TOKEN 优先,回退 NOTION_API_KEY;两者都缺且未传凭证 → 拒连

OCR 与文档解析

变量用途必填默认值
RAPIDOCR_MODEL_DIRRapidOCR 模型目录(默认 OCR 引擎)否未设置时由 RapidOCR 使用包内模型
PADDLEX_URIPP-StructureV3 自托管服务地址用该引擎时必填http://localhost:8080(注意与沙箱 8080 端口概念无关)
PADDLEOCR_API_URL / PADDLEOCR_API_TOKENPaddleOCR 云服务(PaddleOCR-VL 与 PP-OCRv6 共用)用该引擎时必填URL 回退模块常量 DEFAULT_PADDLEOCR_API_URL;token 无默认
MINERU_API_URI / MINERU_API_KEY / MINERU_TIMEOUTMinerU 解析服务(自托管 / 官方云)与超时用该引擎时必填http://localhost:30001;无默认 key;超时 1800s;官方云缺 MINERU_API_KEY 直接抛 DocumentParserException
OFFICE_PREVIEW_TIMEOUT_SECONDSOffice 文档预览转换超时否60

沙箱

变量用途必填默认值
SANDBOX_PROVIDER沙箱提供者选择否provisioner——只支持这一个值,其余值报错
SANDBOX_PROVISIONER_URLProvisioner 基址是(宿主直跑需覆盖)http://sandbox-provisioner:8002(容器服务名;本地 compose 部署可用 http://127.0.0.1:8002)
SANDBOX_PROVISIONER_CREATE_TIMEOUT_SECONDS / SANDBOX_PROVISIONER_CREATE_ATTEMPTS / SANDBOX_PROVISIONER_DELETE_TIMEOUT_SECONDS / SANDBOX_PROVISIONER_DELETE_ATTEMPTS创建 / 删除沙箱的容错窗口否20 / 4 / 20 / 3
SANDBOX_KEEPALIVE_INTERVAL_SECONDS向 provisioner touch 保活周期否30
SANDBOX_VIRTUAL_PATH_PREFIX沙箱虚拟路径前缀(会被规范化为绝对路径);工作区根否/home/gem/user-data;技能根 /home/gem/skills 与个人技能 {prefix}/agents/skills 为常量
SANDBOX_WORKDIR_PROBEbind mount 工作目录探测强度否required(可选 optional / disabled)
SANDBOX_EXEC_TIMEOUT_SECONDS / SANDBOX_MAX_OUTPUT_BYTES单条命令超时 / 输出上限否180 / 262144

Worker 与 SSE

变量用途必填默认值
ARQ_MAX_JOBS单 Worker 进程并发任务数否10
ZHIWU_JOB_TIMEOUT_SECONDS单个 ARQ 任务超时否3600(长任务如图谱构建被 cancelled 时首先调这个)
TASKER_DEFAULT_TIMEOUT_SECONDS通用任务器默认超时否21600(6h)
RUN_SSE_POLL_INTERVAL_SECONDS / RUN_SSE_HEARTBEAT_SECONDS / RUN_SSE_MAX_CONNECTION_MINUTESSSE 读事件轮询间隔 / 心跳 / 最长连接时长否1.0 / 15 / 30
RUN_CANCEL_KEY_TTL_SECONDS / RUN_EVENTS_STREAM_TTL_SECONDS / RUN_EVENTS_STREAM_MAXLEN取消标位 TTL / 事件 Stream 存活 / Stream 裁剪上限否1800 / 7200 / 0(0 = 不裁剪)
WORKER_HEALTH_INTERVAL_SECONDSWorker 健康上报周期(Redis health key)否5
READINESS_PROBE_TIMEOUT_SECONDS / READINESS_CACHE_TTL_SECONDS依赖就绪探测超时 / 结果缓存否2 / 1

追踪

变量用途必填默认值
LANGFUSE_ENABLEDLangfuse 追踪总开关否默认 true(只有显式设为其他值才关闭)
LANGFUSE_BASE_URL追踪上报的基座地址否https://litefuse.cloud(外部 SaaS)
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY追踪项目凭据上报时必填无默认;不配则无副作用,配了就会向外部上报

其它

变量用途必填默认值
ZHIWU_DATASET_PERSIST_BATCH_SIZE评估数据集批量落库大小否1(带 max(1, …) 下界)
MYSQL_HOST / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE / MYSQL_PORT / MYSQL_DATABASE_DESCRIPTION内置 mysql-reporter Skill 脚本的连接信息。它们不来自任何 env 文件:真值存在 DB 表 agent_envs(用户级沙箱环境变量),建沙箱时由 load_user_agent_env(uid) 注入,仅对新建沙箱生效用该 Skill 时四项必填端口 3306;缺失抛 MySQLConnectionError
!
三条已核对的坑,配环境变量时最容易中招:
  1. MINIO_BUCKET 是失效变量:只出现在 backend/.env 里,全仓(backend/zhiwu、backend/server、docker/、zhiwu-cli)无任何读取点;桶名由代码常量 KB_BUCKETS(knowledgebases / kb-images)与 PUBLIC_READ_BUCKETS(public)决定,改它得改代码。
  2. 登录锁定阈值 5 次 / 300 秒是代码常量(MAX_LOGIN_FAILED_ATTEMPTS、LOGIN_LOCK_DURATION_SECONDS,storage/postgres/models_business.py),全仓没有 getenv("MAX_LOGIN_FAILED_ATTEMPTS"),也没有管理端解锁端点——想调只能改代码,且锁定期内 JWT 鉴权的所有接口都返 423。
  3. Neo4j 与 MinIO 有硬编码弱默认凭证(neo4j/0123456789、minioadmin/minioadmin),代码不会因为你没配就报错,只会安静地用默认值连上去;docker-compose.yml 里的 SANDBOX_PROVISIONER_TOKEN 同样带内置默认值,生产必须全部覆盖。
In-DB Options

数据库内系统配置项

环境变量管"连接与密钥",config_options 表管"模型与引擎"。两件事不要混,也不要指望环境变量能覆盖后者。

第一优先:DB 存储值

解析顺序是 DB 存储值 > 字段 environment 指向的环境变量 > 代码默认值(backend/zhiwu/src/zhiwu/config/options.py)。

第二优先:环境变量

只有声明了 environment 映射的 option 才吃环境变量(OCR host opts 一类);system_options 的 5 个字段没有 environment 映射,只能由 DB 覆盖。

第三:代码默认值

入库由启动时 ensure_options_in_db() 幂等同步(advisory lock pg_advisory_xact_lock(94721801));params 由代码定义,管理员只改 value。

缓存层

解析结果进 Redis,OPTION_CACHE_TTL_SECONDS = 300,key 前缀 zhiwu:config_option: + 版本号;含 sensitive 字段的 option 标记为 cacheable=False 不进缓存,且 option.get(db) 显式传会话时直接走 DB、绕过缓存。

system_options(key = system_options,params.internal=True)

字段用途代码默认值消费方 / 注意
default_model默认对话模型siliconflow-cn:deepseek-ai/DeepSeek-V4-FlashAgent 上下文兜底、Run 创建、思维导图 / 示例问题生成、POST /api/chat/call 未传 model_spec 时
fast_model快速响应模型与 default_model 同值⚠ 后端无任何消费方(全仓 fast_model 只出现在定义处)。唯一链路在前端:BasicSettings 配置它,会话组件把它作为 meta.model_spec 传给 POST /api/chat/call 生成自动标题;不影响摘要 / 评估等内部任务
embed_model默认 Embedding 模型siliconflow-cn:BAAI/bge-m3Milvus KB 创建时 chunk_parser_config.setdefault("embed_model_id", …);换模型会让老 collection 维度不符
reranker默认 Re-Rankersiliconflow-cn:BAAI/bge-reranker-v2-m3⚠ key 名是 reranker 而不是 reranker_model;它与 Milvus KB 自己的配置字段 reranker_model 不打通——检索精排不会回退到本项
default_ocr_engine默认 OCR 引擎rapid_ocrOCR 服务、附件解析、ocr_parse_file 工具

其它 option(经 /api/system/config/options 管理)

option key字段环境变量回退备注
mineru_ocr_host_optsserver_urlMINERU_API_URI自托管 MinerU 服务地址
mineru_official_api_optsapi_key(sensitive)MINERU_API_KEY敏感项 → cacheable=False,不走 Redis 缓存;接口只返回来源 + 脱敏预览(serialize_option)
pp_structure_v3_ocr_host_optsserver_urlPADDLEX_URIPP-StructureV3 自托管服务
paddleocr_api_optsapi_url / api_token(sensitive)PADDLEOCR_API_URL / PADDLEOCR_API_TOKENPaddleOCR-VL 与 PP-OCRv6 共用
remote_skill_source_policyallowed_hosts(list[str])无默认 github.com、modelscope.cn,精确匹配域名;空列表 = 关闭远程安装

A入口一 · /api/system/config

GET 登录用户可读,POST 与 POST /update 需 admin。 这一组读写的是 system_options 单条记录里的 5 个字段(上表)。 来源:docs/08-configuration.md 第 3 节、routers/system_router.py。

B入口二 · /api/system/config/options/{key}

GET /options 与 PUT /options/{key} 均为 admin,读写 OPTION_DEFINITIONS 里的其它选项; list_options() 显式排除 system_options,所以这组接口不会返回上面那 5 个字段。两组入口互不覆盖。

Security

安全基线

下面是当前代码事实,不是目标状态。标"待收敛"的是已知项。

认证方式

方式机制生命周期 / 注意
密码登录argon2 哈希($argon2 前缀校验)+ JWT HS256,claims 只有 sub/exp/iss/aud(aud=zhiwu-know-api,iss=zhiwu-know:{ZHIWU_INSTANCE_ID})⚠ 有效期硬编码 7 天,无 refresh、无黑名单 / 会话表;吊销只能等过期或换 JWT_SECRET_KEY(代价是全体重登);ZHIWU_INSTANCE_ID 变更后旧 token 因 issuer 不匹配全部失效
API Key生成 zhiwukey_<random>,DB 只存 sha256(key) + 12 字符前缀;中间件按前缀分流有完整 is_enabled / revoked_at / expires_at / last_used_at;明文只在创建时返回一次;派生型 Key 可由 API_KEY_DERIVATION_SECRET 重放
OIDC SSOauthorization code 流程(login-url / callback / exchange-code),由 OIDC_* 变量组驱动取决于 IdP;开关关闭时不读其余变量
CLI 浏览器设备码cli_auth_sessions:CLI 取 user_code → Web 端 approve → CLI 换 token会话一次性;能力由 /api/system/discovery 的 browser_login 声明
ImpersonatePOST /api/auth/impersonate/{user_id},仅 superadmin,禁止模拟 superadmin 账号⚠ 写 operation_logs 并 logger.warning,但签发的 token 不带任何"模拟会话"claim,下游无法区分;无二次确认

◈RBAC 与资源级权限

角色三档:superadmin / admin / user(users.role)。 守卫实现是逐路由 FastAPI 依赖注入,没有全局中间件:get_required_user(登录且已绑部门,否则 400)→ get_admin_user → get_superadmin_user。 资源权限由 share_config(JSON)定义 read / manage scope,可授 global / department / user 级,角色上限 role_ceiling 约束可达范围;部门是 departments 树。 来源:zhiwu/permissions/resource_permission.py、docs/11-security.md「已确认结论」。

✋工具审批 human-in-the-loop

敏感工具集合固定 SENSITIVE_BACKEND_TOOLS = {write_file, edit_file, execute}; 按用户 tool_approval_mode(default / always / never)启用 HumanInTheLoopMiddleware。 命中时 Run 进入 interrupted,前端 approve / edit / reject 后以 run_type=resume 从 checkpoint 续跑。 当前 Project workdir 内的写入豁免审批;子智能体默认禁用敏感工具与 install_skill。

▣沙箱边界

  • CompositeBackend 路径守卫:readable / writable roots + 虚拟路径 /home/gem/*,越界直接拒
  • 文件工具七件套没有 delete(agents/backends/composite.py)
  • 技能投影 fail-closed:同步异常时清空投影而不是留旧内容
  • 远程 Skill 安装:一次性沙箱执行 npx skills add + 域名白名单 + draft 两段确认,且 inherit_env=False(不继承宿主环境变量)
  • MCP:用户配置仅 sse / streamable_http,stdio 强制仅内置(启动时自动禁用历史用户的 stdio),杜绝任意命令执行面

⚑密钥管理现状

  • 三项密钥 lifespan 硬校验(≥32 字符、无首尾空白、彼此不等);API_KEY_DERIVATION_SECRET 与 SANDBOX_PROVISIONER_TOKEN 在所有环境缺失都是启动硬失败
  • 生产应接部署平台的 secret 管理注入,不要把 backend/.env 打进镜像(该文件未被 git 跟踪,仓内亦无 .env.example)
  • ⚠ 已知待收敛项:MCP env / headers 与模型供应商 api_key 在管理端接口明文返回,只靠 get_admin_user 角色守卫,无字段级打码;普通用户走字段白名单
  • ⚠ /docs 与 /openapi.json 未按 ZHIWU_ENV 关闭
  • 暴破防护三层:内存滑窗(60s / 每 IP 10 次)+ Redis 滑窗(600s,IP+账号 10 次、单 IP 全局 30 次)+ 账号锁定(5 次 / 300s,返 423 + X-Lock-Remaining)。客户端 IP 优先取 X-Forwarded-For 首段,生产必须由反向代理覆盖 / 剥离 XFF,否则可伪造绕过限速
×
两条部署侧要自己承担的风险。 其一,docker/sandbox_provisioner/docker-compose.yml 把 /var/run/docker.sock 挂进 provisioner 容器, 等价于授予宿主 root,只能跑在受控主机上;需要更强隔离就切 kubernetes backend(docs/11-security.md 第 7 节)。 其二,核对时发现开发机本地 .git/config 的 remote.origin.url 把仓库用户名与密码内嵌在 HTTPS URL 里—— 这不是仓库内容,但任何能读 .git/config 或执行 git remote -v 的人都拿到拉取权限,且该 URL 可能已被写进日志与 IDE 输出; 建议改用 SSH 或 credential helper,并视该密码已失效、在服务端轮换。 backend/.env 与 docker/sandbox_provisioner/kubeconfig.k8s.yaml(含客户端私钥)都已在 .gitignore 内,别让流水线把它们打进产物。
Observability

可观测性

没有 Prometheus / OTel 指标端点,也没有告警规则——观测靠 trace_id 日志、Langfuse 追踪和 DB 聚合三件事。

信号机制 / 来源运维要点
日志与 trace_id loguru + contextvars 的 8 位短 ID(run_id / task_id / uuid 前 8 位归一化,占位 -);TraceIdMiddleware 在 ASGI 最外层注入并支持从入站提取 一次 HTTP 请求或一次 ARQ 任务内所有业务日志共享同一 trace_id;API 与 Worker 两侧各自独立生成、不要求一致。格式含 [T:trace_id]、file:line,业务字段另有 run_id / thread_id / uid / agent_slug
日志输出目标 两路:运行态文件 {ZHIWU_RUNTIME_DIR}/logs/zhiwu-{上海当日日期}.log + sys.stderr(带色) 容器里被日志采集器接住的只有 stderr 这一路;文件名在模块导入时按当天日期固化,进程跨天不会切到新日期名(同一路径上仍会 rotation 出历史文件)
日志级别与保留 utils/logging_config.py:level="DEBUG"、rotation="10 MB"、retention="30 days"、compression="zip"、enqueue=True ⚠ 级别硬编码 DEBUG、不可配置:全仓无 LOG_LEVEL / ZHIWU_LOG_* 环境变量,生产与开发同样 DEBUG 落盘;只有 httpx / openai / neo4j / urllib3 被桥接为 WARNING 降噪。长期留存需外挂采集
在线日志查询 GET /api/system/logs?levels=INFO,ERROR(admin) 读文件尾部 1000 行(硬编码,无分页);只有 levels 一个参数,按 " - LEVEL - " 子串匹配,loguru 追加的多行 traceback 会被丢弃;响应里 scope:"api" 是硬编码标签,不是按进程过滤——两侧共享 ZHIWU_RUNTIME_DIR 时会混写进同一文件
追踪(Langfuse) LangfuseTracingSession 在 Run 开始时建根 span、写 trace 级属性并抓 trace_id,langfuse CallbackHandler 以 trace_context 挂入模型调用;trace_id 持久化在 agent_runs.langfuse_trace_id API 侧 GET /api/agent/runs/{run_id}/langfuse 返回追踪链接;默认 enabled=true 且基座默认外部 SaaS litefuse.cloud,自托管需改 LANGFUSE_BASE_URL
审计 模型 / 工具生命周期写成 messages 行,message_type = model_audit / tool_audit(带 run_id / request_id / operation_id / sequence / extra_metadata);工具审计同时向 tool_calls 写兼容投影行 ⚠ 没有独立审计表(36 表清单已确认);写入按 lifecycle 事件串行开短事务、重复 start/finish 幂等收敛。读取端 GET /api/chat/thread/{tid}/audits 权限是 superadmin(不是 admin)。管理操作另写 operation_logs
Run 计时 agent_runs.prepared_at / first_model_request_at / first_output_at + 终态时间 由此算排队时延、首字延迟、总耗时;"首字慢"先看这几个差值,再判 worker 并发与供应商限流
Token 用量 TokenUsageMiddleware 每次主模型调用写 token_usage JSON,按模型分桶 成本核算靠它 + /api/dashboard/stats;工具调用统计在 tool_calls 表,会话统计在 conversation_stats
告警 仓库内未发现告警规则 / 通知器实现 需自建阈值:worker health key 心跳缺失、agent_runs 的 failed / lease_expired 比率、首字延迟 P95、模型 429 计数

来源:docs/10-observability.md 第 1–5 节与「已确认结论」。未引入 Prometheus / OTel 属事实而非 TODO。

Development

开发指南

后端是 uv workspace(成员 server 与 zhiwu:前者只做进程壳,后者是核心库);前端是 Vue 3 + pnpm。测试不需要任何外部数据服务。

环境搭建
cd backend && uv sync --all-groups   # workspace: server, zhiwu
# 填 backend/.env(git 未跟踪,仓内无 .env.example)
uv run python server/src/server/main.py         # API :5050
uv run python server/src/server/worker_main.py  # Worker(另终端)

cd web && pnpm install && pnpm dev              # :5173
cd docker/sandbox_provisioner && ./script/startup.sh
cd zhiwu-cli && uv tool install --editable .    # 全局命令 zhiwu
测试与质量门
# 后端:pytest 用例都在 backend/test/unit/**
cd backend && uv run pytest test/unit -x -q

# 前端单测(注意 runner)
cd web && pnpm run test:unit     # node --test,不是 vitest
cd web && pnpm run lint:check    # 只读 gate;pnpm lint 可自动修复
cd web && pnpm run build

# 后端静态检查:ruff(dev 依赖 ruff>=0.16.8)
i
跑单测不需要本地 Milvus / Neo4j / PostgreSQL / Redis。 backend/test/conftest.py 只做两件事:把 backend/ 加进 sys.path、注册仅 unit 一个 marker (auth / integration / e2e / slow 四行全被注释掉);backend/pyproject.toml 没有 [tool.pytest.ini_options],也没有 testcontainers。 实测 uv run pytest test/unit -q 零外部服务可跑完(一处陈旧用例失败属测试与代码不同步)。 集成 / e2e 体系尚未建立,需真实 Milvus / Neo4j 的验证只能手工连本地服务。 前端 test:unit 的实体是 node --test --test-concurrency=1 "test/**/*.test.js" "test/**/*.spec.js",package.json 里没有 vitest 依赖。 来源:docs/12-development-guide.md「已确认结论」。

扩展点:四件事各自的入口

要做的事入口文件 / 位置要点
新增 Agent 后端图 backend/zhiwu/src/zhiwu/agents/buildin/<name>/graph.py(参考 chatbot/) context / state / prompt / graph 四件套,继承 BaseAgent,实现中间件栈与 _build_graph;注册 backend_id 后 /api/agent/backends 可见,agents.backend_id 指向它即可被选用
新增 Skill 内置:backend/zhiwu/src/zhiwu/agents/skills/buildin/<slug>/SKILL.md(+ 可选 scripts/) frontmatter 写 name / slug / description;启动 lifespan 自动同步入库(bump version)。改的是入 git 的真源,不是 skill-sources/(后者是运行期拷贝目录、已被 .gitignore 排除)。上传 / 远程安装走 /api/skills/import/prepare 或 /remote/prepare → /install-drafts/{id}/confirm;脚本在沙箱内以 /home/gem/skills/<slug> 路径执行
新增知识库实现 backend/zhiwu/src/zhiwu/knowledge/implementations/<type>.py + knowledge/runtime.py 实现 knowledge/base.py 的 KnowledgeBase 抽象(parse / index / query 等),再 KnowledgeBaseFactory.register(...)(现例 MilvusKB / DifyKB / NotionKB);GET /api/knowledge/types 自动可选
接入 MCP 页面 / API:POST /api/system/mcp-servers;内置 stdio:agents/mcp/service.py 的 _DEFAULT_MCP_SERVERS 用户配置只允许 sse / streamable_http,配 headers 鉴权;POST /{slug}/test 验连通、/{slug}/tools 按需 toggle、/{slug}/tools/refresh 清进程内 slug:config_hash 缓存。新增内置 stdio 需谨慎——它在 Worker 进程内执行命令

⌁调试入口

  • 终端日志带 [T:trace_id];在线查 GET /api/system/logs
  • LANGFUSE_ENABLED=true 后按 Run 看完整 trace(/api/agent/runs/{id}/langfuse)
  • LangGraph 状态:GET /api/chat/thread/{tid}/state 直查 checkpoint
  • Swagger:/api/docs(开发环境)

◇API 兼容性与 CLI

URL 无版本号,兼容性靠响应字段增量演进。GET /api/system/discovery(公开)返回 name / version / api_prefix、capabilities.features.knowledge 与 12 个 CLI 能力字段 (min_cli_version + browser_login、api_key_auth、remote_config、agent_list、kb_* 等)。 这些值是硬编码字面量,不随配置或运行状态变化;CLI 执行前读它校验版本(要求 ≥ 0.1.0)与能力标志, 不满足直接报 ServerCompatibilityError 退出。加端点时记得同步 routers/__init__.py 与 docs/06-api.md。

Troubleshooting

常见故障与排查

从 docs/13-faq-troubleshooting.md 提炼的十条高频项。先定位是哪一层:启动 → 鉴权 → Run → 沙箱 → RAG → 日志。

1启动即报安全密钥缺失 / 相同

症状:进程秒退,日志提示密钥校验失败。
原因:JWT_SECRET_KEY、API_KEY_DERIVATION_SECRET、SANDBOX_PROVISIONER_TOKEN 任一为空、短于 32 字符、带首尾空白或彼此相等;开发环境只对 JWT_SECRET_KEY 自动生成临时值,另两项任何环境缺失都是硬失败。
处置:配三个互不相同的 ≥ 32 字符强随机值(openssl rand -hex 32)。

2启动报存储根目录错误

症状:lifespan 阶段 RuntimeError。
原因:ZHIWU_USER_DATA_DIR 等三个目录是相对路径或空值。
处置:改成绝对路径;顺带把 ZHIWU_RUNTIME_DIR 也固定下来,否则日志散落在带 pid 的临时目录里。

3schema 版本不兼容拒绝启动

症状:应用进程启动时校验 schema 失败。
原因:应用只校验兼容、不建表不改表,迁移归 storage-migrator。
处置:先跑一次性迁移(仓内 uv run python -m zhiwu.storage_migration,镜像由部署方自打),再起 api / worker。

4423 登录被锁定,连已登录页面也报错

症状:登录返 423 + X-Lock-Remaining,其他鉴权接口也 423。
原因:连续 5 次密码错 → 锁 300s;且锁定检查在 get_current_user 的 JWT 分支,作用于所有鉴权请求。
处置只能等自然过期——仓内没有任何管理端解锁 / 重置端点,阈值也没有环境变量入口。紧急旁路:API Key 分支提前 return,不经过锁定检查。

5429 登录受限

症状:登录接口返 429。
原因:两层 IP 滑窗——内存层 60s / 每 IP 10 次,Redis 层 600s(IP+账号 10 次、单 IP 全局 30 次)。
处置:等窗口过期;无清除端点,必要时直接删 zhiwu:login-failure:ip:* / …:ipacct:* key。生产要确认反代已覆盖 XFF,否则限速按代理 IP 计数。

6消息一直 queued / Run 卡 running

症状:提交后无输出。
原因:Worker 没跑或队列堆积;或同线程已有活跃 Run(部分唯一索引);Run 卡住多为 Worker 崩溃后租约未过期。
处置:查 Redis health key 是否每 5s 刷新、GET /api/agent/thread/{tid}/active_run、agent_run_attempts.heartbeat_at / lease_expires_at 是否停更;等租约过期会被其他实例接管。等 Run 时用 Last-Event-ID 重连 SSE 续传。

7沙箱创建超时 / 工作目录不同步

症状:execute 或文件工具失败,或沙箱里看不到刚写的文件。
原因:provisioner 不可达(两套 compose 抢了同一个 8002)、/var/run/docker.sock 权限、镜像缺失、两端 token 不一致;不同步多为宿主 bind 源反查失败。
处置:GET :8002/health → 看 provisioner 日志 → 核对 SANDBOX_PROVISIONER_URL 与 token → 确认 compose 里 user-data / skill-projections 相对挂载路径与真实宿主目录一致 → 用 SANDBOX_WORKDIR_PROBE 打开探测;重试次数看 SANDBOX_PROVISIONER_CREATE_ATTEMPTS。

8文档停在 error_parsing / 检索无结果

症状:KB 不索引,或查询空返回。
原因:OCR / 解析引擎不可用(MinerU、PaddleOCR 服务地址或密钥未配、超时);检索侧是状态未到 indexed、相似度阈值过滤或 search_mode 选择不当。
处置:GET /api/system/ocr/health,把 default_ocr_engine 先换回本地 rapid_ocr;用 /databases/{kb_id}/query-test 调试、调 query-params;换 embed 模型导致维度不符时新建 KB 重索引。

9技能工具不可见 / 头像加载失败

症状:模型说没有某技能,或前端图片 404。
原因:技能需模型先 read_file SKILL.md 才激活,未激活就不放依赖工具;投影 fail-closed 会在同步异常时清空 skill-projections/<uid>/。头像 404 多为宿主机直跑没设 VITE_MINIO_URL,代理仍去打 http://minio:9000。
处置:检查 prompt 摘要是否含该技能、重新触发同步、补 VITE_MINIO_URL=http://127.0.0.1:9000。

10日志查不到 / MCP 用户 stdio 消失

症状:/api/system/logs 返回空或只有旧内容;历史用户配置的 stdio MCP 不见了。
原因:该端点只读当前 API 进程自己 的日志文件(文件名按当天固化),Worker 未共享 ZHIWU_RUNTIME_DIR 时写在另一个带 pid 的目录;stdio 是启动策略强制禁用的安全迁移。
处置:进容器 tail 文件,或让两侧共用同一 ZHIWU_RUNTIME_DIR(会互相混写);MCP 改用 sse / streamable_http 形态。

提示:docs/13-faq-troubleshooting.md 明确标注各条"原因"为代码逆向推导,尚无真实运维案例印证。

This Site

本站部署:Cloudflare Pages 与 Workers

说明站本身是 site/ 下的原生静态 HTML / CSS / JS——零构建步骤、无外部 CDN 依赖、无 bundler。 同一份产物可以走 Pages(纯静态托管)或 Workers(Static Assets 绑定 + 少量边缘路由)。

路径一 · Cloudflare Pages

Wrangler CLI(推荐)
cd site
npx wrangler pages deploy . --project-name=zhiwu-site

在 site/ 目录执行,产出目录就是当前目录,没有 build command。 Dashboard 路径等价:连 Git 仓库 → Root directory 设 site → Build command 留空 → Output directory 填 site(或连仓库根时填 .,取决于 Root 设置)。

site/_headers 与 site/_redirects 只对 Pages 生效:它们必须放在被部署目录的根(也就是 site/ 下), Pages 用它配缓存与安全头;走 Workers 时这两个文件不会被读取,同类规则要搬到 Worker 脚本或 Cloudflare 的 Rules 里。 当前 _headers 已给全站加 CSP / nosniff / frame-ancestors,并给 /assets/* 打一年不可变缓存、 给 /*.html 打 max-age=0, must-revalidate;_redirects 只保留两条历史别名 (/docs/* → /deploy、/api-docs → /api)。

不要再往 _redirects 里写 /capabilities → /capabilities.html 这类规则。 Pages 本身就会把 /x.html 规范化成 /x(308)并在无扩展名路径上找回同名 .html: 自己再补一条正向 301,两个方向就互相指回去,浏览器 ERR_TOO_MANY_REDIRECTS、页面打不开。 clean URL 是平台白送的,不需要脚本或规则去实现。

路径二 · Cloudflare Workers

Assets 绑定 + Worker 脚本
cd site
npm install
npx wrangler deploy

# 本地预览(两种方式都不需要构建)
python3 -m http.server 8788
# Workers 路径的本地联调用:
npx wrangler dev

Workers 路线靠 site/wrangler.jsonc 的静态资源声明: assets.directory = "./"(即 site/ 自身)、assets.binding = "ASSETS"、 html_handling = "auto-trailing-slash"、not_found_handling = "404-page"(未命中时回 site/404.html)、 run_worker_first = true。因为开了 run_worker_first,每个请求先进 site/worker/index.js:只有 /api/*(/api/health、/api/site)与 /docs/*、/api-docs 这两条别名(Workers 不读 _redirects,所以在脚本里补齐以保持两条路线一致) 由脚本处理,其余一律 env.ASSETS.fetch(request) 交回静态资源—— 脚本里同样不能对无扩展名路径手动改写 .html,否则会和 auto-trailing-slash 的规范化形成同一个重定向环。

部署命令:cd site && npm install && npx wrangler deploy;本地联调 npx wrangler dev (npm run preview 是同一条)。账号信息不写在配置里(wrangler.jsonc 里没有 account_id 字段;留空字符串不是"自动探测",会让部署直接失败): 由 npx wrangler login 写入本地凭据,或用环境变量 CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN 提供。若不需要边缘路由,把 run_worker_first 改为 false 可让请求完全绕过 Worker,但那样 /api/health 会返回 404.html 而不是 JSON。

选哪个

维度PagesWorkers + Static Assets
适用纯内容站(本站默认选择)需要在边缘加路由、重定向逻辑或鉴权时
构建无;直接上传目录无;wrangler deploy 打包 assets + 脚本
_headers / _redirects生效不生效,需在 Worker 或 Rules 里实现
成本模型静态请求额度为主每次请求都执行 JS,计入 Worker 请求数
预览每次部署产生 <hash>.zhiwu-site.pages.dev 预览域名;连 Git 时每个 PR 一个预览wrangler versions upload / 版本部署产生预览

⌗自定义域名与预览

  • Pages:项目 Settings → Custom domains 添加域名,Cloudflare 会写 CNAME 并在托管 zone 内自动开代理;*.pages.dev 子域默认存在,可用 Branch settings 控制生产分支。
  • Workers:在 Worker 的 Triggers → Custom domains 绑定,或在 wrangler.jsonc 里声明 routes 后随部署生效。
  • 两种路线都保留免费的 pages.dev / workers.dev 域名,预览部署不会覆盖生产。

!部署前要自己确认的两件事

  • wrangler.jsonc 的 name 与 Pages 的 --project-name 必须与实际 Cloudflare 账号下的项目名一致;不一致时 pages deploy 会新建项目、wrangler deploy 会新建 Worker,容易留下孤儿。
  • 先 npx wrangler login(或提供 CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID)。本站是公开内容,不需要 secrets;若后续给 Worker 加鉴权,密钥走 wrangler secret put,不要写进 wrangler.jsonc。
i
与 ZhiWu 平台部署的区别。本节只讲说明站的上线;ZhiWu 平台本体是自托管的四个进程 + 五个数据服务, 不在 Cloudflare 上跑(见本页本地开发启动与依赖服务与扩缩容)。