Files
ai-kefu-admin/CLAUDE.md
T
浅念 6839cb94d2 fix: 终审修复——站点停用接线/prefix查重补创建reset/默认凭据警示+取舍补录
- I-1 站点停用全链接线: Site 数据类加 status 字段; DB 源 _load_one 与
  文件源 read_tenant 均只输出 active 站点(停用站点域名不进 registry
  索引 → 该域名 404;全停用=合法运营状态输出空 sites;文件源无 status
  键默认 active 兼容)。E2E: PATCH 站点 disabled → 其域名 404、复启用
  恢复;源级直测两源过滤。
- I-3 prefix 跨租户查重补两口: db.other_push_prefixes 共用助手
  (MAX(version) 逐租户当前前缀,排除自身);创建向导(出厂档案前缀被占
  → 400 提示改 blank/改占用方)与 reset push(重置回出厂撞车 → 400 提示
  走 PUT)接入;PUT 原有查重改用同一助手。rollback 维持豁免(历史不可变
  语义优先,取舍清单记档)。既有测试第二租户改 blank profile 避撞。
- I-4 默认凭据警示: lifespan 启动检测 ADMIN_PASSWORD 仍为 Settings 字段
  默认值(非硬编码第二份,防双源漂移)→ logger.warning 醒目提示;
  CLAUDE.md Phase 4 部署记档补 ADMIN_PASSWORD 生产要求。
- I-2/M-2 取舍补录: 密钥掩码未实现(GET config/settings 明文返回
  api_key/smtp 密码,Phase 3 前须定 __SET__ 哨兵方案);站点管理权限现为
  仅超管(Phase 3 对接时决策放开或改 spec);取舍清单第 2 条同步刷新
  (rollback 现为唯一豁免口)。

全量回归: 571 passed(基线 566 + 新增 5)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 12:40:21 +08:00

128 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — all-kefu 工作约定
## 项目定位
本项目 all-kefu 是**统一多租户 AI 客服平台**:一套引擎 + 租户配置包,消除"每客户复制一套代码"的旧模式。项目由 ai-kefu / ai-kefu-jc 两个复制分叉项目合并而来,**jc 为主线**(代码以 ai-kefu-jc 为基础迁移,再做租户化改造)。
## 硬约束
- **uvicorn 必须以 `--workers 1` 运行**。内存中的 token 缓存、限流状态、配置缓存、每租户 engine 缓存均假设单进程,多 worker 会导致状态割裂。任何启动脚本/部署配置不得改动此项。
- **Windows 环境**:跑 python / pytest 前设置 `PYTHONUTF8=1`(GBK 控制台遇 emoji / 中文 payload 会直接崩,血泪教训)。
## 本地开发访问方式
- 路由按 Host 解析租户:`/` 与 `/static` 也需要已知租户域名(未知 Host 一律 404,唯一豁免 `/health`,供部署健康检查 curl 用)。
- 本地访问二选一:改 hosts 把 `chat.agent.hnxinyouhe.com` / `chat2.agent.hnxinyouhe.com` 指到 `127.0.0.1`;或 curl 显式带 `-H "Host: chat2.agent.hnxinyouhe.com"`。
- uvicorn 起在 8018:`PYTHONUTF8=1 .venv/Scripts/python -m uvicorn app.main:app --port 8018 --workers 1`。
## 开发流程
- 改动 `seed_profiles` 后必须跑全量 pytest(出厂配置影响面大)。
- 全量回归命令:`PYTHONUTF8=1 .venv/Scripts/python -m pytest -q`(根目录与 tests/ 子目录均可跑,pytest.ini 已锚定根路径)。
## Phase 1 完成状态(2026-10-12 收尾核对)
**测试基线:459 passed**(根目录与 tests/ 子目录各跑一遍,数字一致;唯一 warning 为 starlette TestClient 对 httpx 的弃用提示,无害)。此为 Phase 1 时点数字;现行回归基线以 Phase 2 完成状态段为准(566)。
**零同步 IO 审计**(`grep -rn "requests\.\|urllib\|time\.sleep" app/`):
- 命中数 **0**——app/ 内无 requests 调用、无 urllib、无 time.sleep。
- 全库唯一 HTTP 出站点为 `app/engine/services/lead_push_service.py` `_do_post`,使用 `httpx.AsyncClient`(async with 上下文),异步路径成立。
- `app/engine/providers/glm.py` 的同步 token 生成与兜底同步调用分别经 `loop.run_in_executor` / `asyncio.to_thread` 下卸,事件循环不被阻塞。
- `app/engine/services/rate_limit.py` 的 `import time` 仅用 `time.time()` 读时钟(纯 CPU,非 IO);app/ 源码内无 `open()` 文件读取。
**spec §6.2 合并清单核对**(逐项,全部在位):
- 取自 jc:cc_completion 完整判定树+状态机+extractor `cc_reply`+e164 出站渲染(`app/engine/services/cc_completion.py`,conversation.py 双门控消费);多语言墙(€/£、西/葡/俄/阿词表在 `seed_profiles/jiancai/wall.json`、ES 迂回池 boundary_filter+wall.json、建材防误杀白名单为 pattern 级 comment+后顾断言 DN800/SDR11/PN16/"euro pallets" 排除);dual-contact 分步双要(cc_settled 串行门控邮箱轻补)+`_email_asked`;CC 强制墙(conversation.py cc_wall 裁决段)+零号码墙回显豁免收窄(boundary_filter.py:旧豁免移除,改 allow_number 回显确认轮白名单+防自锁线);XFF 服务端优先(chat_rest `_server_ip`/chat.py 握手期提取,均取第一段)、阿语数字归一(contact_pipeline 捕获源头 translate)、`resolve_region_code`(ip_service.py)。
- 上游吸收:input-placeholder 可配置化(品牌配置组,public.py 下发,widget/chat data-placeholder 管道);7 个测试文件(test_channel_hygiene / test_chat_rest_ip / test_lead_push_cc / test_cc_integration / test_ip_service_region / test_prompt_static / test_placeholder_static,commit d1aed8e)。
- 三不带:blog.html(static/ 无此文件)、Redis(app+scripts 零命中)、旧版 whatsapp-cc-completion(零命中,jc 版 cc_completion.py 取代)。
**当前能力一览**:多租户引擎(TenantRuntime 隔离边界,engine 不感知 platform);站点模型(tenant_config.Site,一租户多站点覆盖);词表/提示词/品牌全配置化(seed_profiles 出厂包 + 文件级 registry_reload 热加载,Phase 2 升级为 platform.db);Host 路由(host_middleware,未知 Host 404);WS 防串库(租户化 client key + 会话站点标);WS 每 IP 连接上限(conn_limiter,租户级 ws_conn_limit 配置)。
## Phase 2 完成状态(2026-10-13 收尾核对)
**测试基线:566 passed**(根目录与 tests/ 子目录各跑一遍,数字一致;唯一 warning 同 Phase 1——starlette TestClient 对 httpx 的弃用提示,无害)。后续任务回归以此为准。
**零同步 IO 审计复查**(grep requests / urllib / time.sleep / open 于 app/):命中数 **0**——无 requests/urllib 导入、无 time.sleep、无 open() 调用;`import time` 仅 platform/auth.py 与 engine/services/rate_limit.py 两处,均只读 time.time() 时钟。scrypt 登录路径 CPU 开销注记在案(auth.py 模块 docstring:单次 ~50ms,只在登录/改密路径,不在每请求热路径——依赖只查内存 token 表)。
**安全自查**(读码 + 定向测试 28 条全绿:test_auth / test_admin_mounting / test_avatar_upload / test_host_routing):
- **密码/token 不进日志**:app/platform/ 全层**零 logger 调用**(干净由构造保证);main.py 启动日志仅静态串 + slug/db 路径;引擎层(Phase 1 既有)日志参数为会话内容摘录/session_id/notify_emails,无凭据字段。
- **gitignore 覆盖复核**(git check-ignore 实证全命中):`tenants/*.db{,-wal,-shm}`(租户库实际落 `tenants/<slug>.db`)、`platform.db{,-wal,-shm}`(仓库根)、`data/`(含 `data/uploads/` 上传卷)。
- **登录限流在 /api/platform 豁免路径仍生效**:host_middleware 对 `/api/platform/` 前缀的豁免只跳过**租户解析**,认证守护在路由依赖;登录限流先于密码验证(锁定期内正确密码也 429)。测试钉位:test_auth.py::test_login_lockout_after_5_fails + test_admin_mounting.py::test_platform_api_bypasses_tenant_resolution / test_platform_prefix_not_open / test_prefix_match_precise。
- **上传目录不可执行**:上传只认魔数(png/jpg/webp)+ uuid 重命名落盘——.py 文件无进无出;avatar 下发(public.py)取 PurePosixPath.name 并显式拒绝 `/` `\` 与目录点(防 Windows 反斜杠穿透),FileResponse content-type 由扩展名映射(只可能 image/*)。
**平台层能力一览**(Phase 2 交付;engine 仍不感知 platform):
- **认证**:scrypt 版本化哈希(stdlib 零新依赖,偏离 spec bcrypt/argon2 有注记)、内存 token 8h TTL、登录限流(username+IP 滑动窗 5 次/15 分钟)、改密(must_change_password 引导)、角色两级(超管 / 租户管理员)+ 409 语义。
- **租户管理**:创建向导(消费出厂档案,blank=jc 墙提示词)、站点与域名子资源 CRUD、停用(聊天面 503、后台仍可见)、slug 不可变。
- **配置版本管线**:分组校验 → 整份版本快照(MAX+1)→ 回滚 / 重置出厂 / 试金石(wall/test)→ registry_reload 热生效。
- **上传**:头像 multipart(魔数校验 / 2MB 上限 / uuid 重命名),写配置版本 + 三级解析链下发(uploads → seed 资产 → 静态兜底)。
- **数据视图**:会话列表(筛选/分页)/ 详情 / 删除 + 推送日志——租户 scope 强制(跨租户 404)。
- **统计**:卡片 / 漏斗 / 趋势 / 分布 / 推送健康——逐租户库查询 + 应用层合并(**禁 ATTACH**,spec §7.6);指标口径表定死在 stats.py docstring。
- **DB 源生产装配**:TENANT_SOURCE=db 默认(DbTenantSource),FileTenantSource 保留为测试/档案/冷备形态;bootstrap_platform.py 一键引导。
**admin API 速览**(全部 `/api/platform` 前缀;Host 中间件豁免该前缀的租户解析,认证由路由依赖守护——require_super 超管 / _tenant_guard 租户管理员):
- 认证:`POST /auth/login`、`POST /auth/logout`、`POST /auth/change-password`
- 租户:`GET|POST /tenants`、`GET|PATCH /tenants/{slug}`、`POST /tenants/{slug}/sites`、`PATCH /tenants/{slug}/sites/{site_slug}`、`POST|DELETE /tenants/{slug}/sites/{site_slug}/domains[/{domain}]`
- 配置:`GET /tenants/{slug}/config`、`PUT /tenants/{slug}/config/{group}`、`POST /tenants/{slug}/config/{group}/reset`、`GET /tenants/{slug}/config/versions[/{version}]`、`POST /tenants/{slug}/config/rollback`、`POST /tenants/{slug}/wall/test`
- 上传:`POST /tenants/{slug}/brand/avatar`
- 数据:`GET /tenants/{slug}/conversations[/{conv_id}]`、`DELETE /tenants/{slug}/conversations/{conv_id}`、`GET /tenants/{slug}/push-logs`
- 统计:`GET /stats`(超管,`?tenant=` 单租户下钻)、`GET /tenants/{slug}/stats`
- 平台设置:`GET|PUT /settings`
## Phase 2 沉淀的取舍与 Phase 3 清理单
**实现取舍(API 冻结 / Phase 3 前端对接前须知)**:
1. **PUT config 整组替换语义**——前端必须整组读-改-写(Task 6;做部分更新需先 GET 全组再改再 PUT)。
2. **rollback 仍绕过 prefix 跨租户查重**(终审 I-3 已收口 PUT/创建向导/reset 三口;rollback 单独保留豁免 = 历史不可变语义优先——回滚到含已占前缀的历史版本属数据修复场景;如仍需收口,在 rollback 前补跑查重)。
3. **并发保存同租户撞 UNIQUE → 500**(单管理员场景可接受;收口 = 捕 IntegrityError 转 409)。
4. **统计 API 无 `?site=` 筛选**(spec §7.6 提及;Phase 3 前端需要时补)。
5. **登录限流反代形态退化为按用户名**(限流键 username + request.client.host,无 XFF 解析;可信代理解析归部署批次)。
6. **avatar 旧文件不清理**(换头像后旧文件留盘;孤儿累积无害)。
7. **停用租户管理员存量 token 最长存活 8h**(TTL 限窗)——即时锁死需在 validate 时回查租户状态。
8. **400(预检撞车)vs 409(竞态 IntegrityError)并存**——API 冻结前统一。
9. **limiter 全量清扫**(token/登录限流的内存键永不回访,长跑进程缓慢累积;收口 = 定期清扫)。
10. **lead_push_service 的 settings re-export 锚**(app.engine.config 的 settings 在 lead_push_service 顶部 re-export,22 处测试 monkeypatch 依赖此锚——改动前先迁测试)。
11. **密钥掩码未实现**——GET config/settings 返回明文 api_key/smtp 密码;Phase 3 前端对接前必须定掩码哨兵方案(GET 吐 `__SET__` + PUT 收哨兵保留旧值)(出处:终审审查 I-2)。
12. **站点管理权限现为仅超管**(spec §7.3 写租户管理员同左)——Phase 3 对接时决策放开或改 spec(出处:终审审查 M-2)。
**Phase 3 清理单**(Phase 1/2 审查挂账滚动合并;~~删除线~~ 项已在 Phase 2 完成):
1. ~~FakeAI 三份拷贝收敛进 conftest~~(Task 10 完成:conftest.py 单一定义,tests/ 零拷贝)。
2. ~~rate_limit 与 ws_conn_limit 的 0 语义统一 + 配置类型校验~~(Task 10 完成)。
3. ~~name_to_region_extras / context_seed_map 等散装 resolver 收拢为 config_views 单入口~~(Task 10 完成:app/platform/config_views.py)。
4. ~~avatar 从 seed_profiles 迁 uploads 卷~~(Task 7 完成:上传 + 三级解析链)。
5. **AIService.from_runtime 类方法**——组装正典化(出处:引擎改造审查)。
6. **boundary_filter 视图常量退役**——admin 重写后旧测试迁移时(出处:引擎改造审查)。
7. **push enabled 布尔空槽语义**——admin 配置显式化时解决(出处:push 审查)。
8. **contact.template_languages 运行期接线**——后台批次(出处:配置 schema 审查)。
9. **话术含引号/换行的转义**——编辑器批次(`_format_deflection_pool` docstring 有注)。
10. **产品决策项**:国名纠正 e164 变化时是否补发更正邮件(上游行为更优);REST/WS 服务端 IP 口径是否统一(出处:上游吸收审查)。
11. **/health 分级健康**——Phase 4 部署批次(进程存活之外按依赖分级上报;出处:终审审查 M-3)。
12. **PromptProvider.from_dir 的 FileNotFoundError 带路径上下文**——提示词目录缺失时异常信息含实际路径(出处:终审审查 M-4)。
## Phase 3 计划编写注意
- conftest 三坑纪律延续:`async_db` 夹具用 NullPool;TestClient 必须走 portal 桥接事件循环;禁止 `asyncio.run` 直开租户 session_factory(三个坑都在 tests/conftest.py 有既成解法,照抄勿发明)。
- `build_app` / `app_client` / `TenantTestClient` 夹具已就绪,新测试优先复用而非自建 app 实例;**平台层测试用 `db_source_app` / `super_client` 夹具**(conftest.py:DB 源装配 + 超管登录态客户端,Phase 2 全部平台路由测试的既成姿势)。
- 引擎侧读配置一律经 **config_views 单入口**(app/platform/config_views.py:resolve_email/push/ai_settings、resolve_contact_flags、context_seed_map、name_to_region_extras)——新消费点不再自写 resolver。
- 统计前端照 **stats.py docstring 的指标口径表**展示(会话数不要求有消息、线索=三选一非空、推送成功率分母口径、北京日界趋势等,歧义处已定死)。
## Phase 4 部署记档(提前锚定,部署批次展开成 docs/deploy-production.md)
- **`pip install -r requirements.txt` 必须执行**:python-multipart(>=0.0.20)为 Phase 2 新依赖——Task 7 实证缺失即头像上传 500(FastAPI multipart 解析依赖它)。
- **bootstrap_platform.py 用法**:`PYTHONUTF8=1 python scripts/bootstrap_platform.py [--db PATH] [--profiles SLUG ...] [--seed DIR]`。两步全幂等:init(schema + 超管引导,凭据取 env/.env 的 ADMIN_USERNAME/ADMIN_PASSWORD)+ 导入出厂租户(默认 auto jiancai 两租户,已有 slug 跳过);**不建租户管理员**(走后台创建向导补)。
- **TENANT_SOURCE=db 默认**(生产装配形态);`file` 为冷备/本地调试回退。
- **admin 域名反代区分**:`/api/platform/*` 前缀豁免租户 Host 解析——平台后台 API 可与租户聊天域名同源反代(按路径分流)或独立域名,两形态皆可;租户聊天面(`/`、`/static`、WS、`/api/public`、`/api/chat*`)仍严格按 Host 解析租户,未知 Host 404。
- **ADMIN_PASSWORD 默认值警示**(终审 I-4):lifespan 启动检测到 ADMIN_PASSWORD 仍是出厂默认(admin123)时打 WARNING 日志——生产部署必须设 ADMIN_PASSWORD 环境变量换掉,且超管首登立即改密(must_change_password 门强制)。
## 文档索引
- 设计文档(spec):`docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md`
- Phase 1 实施计划:`docs/superpowers/plans/2026-10-12-phase1-engine-merge-tenantization.md`
- Phase 2 实施计划:`docs/superpowers/plans/2026-10-13-phase2-platform-layer.md`
- 部署坑手册:Phase 4 补 `docs/deploy-production.md`(尚未存在;Phase 4 部署记档段已锚定要点)