diff --git a/CLAUDE.md b/CLAUDE.md index e560551..3c90de7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ ## Phase 1 完成状态(2026-10-12 收尾核对) -**测试基线:459 passed**(根目录与 tests/ 子目录各跑一遍,数字一致;唯一 warning 为 starlette TestClient 对 httpx 的弃用提示,无害)。收尾提交前基线即此数,后续任务回归以此为准。 +**测试基线:459 passed**(根目录与 tests/ 子目录各跑一遍,数字一致;唯一 warning 为 starlette TestClient 对 httpx 的弃用提示,无害)。此为 Phase 1 时点数字;现行回归基线以 Phase 2 完成状态段为准(566)。 **零同步 IO 审计**(`grep -rn "requests\.\|urllib\|time\.sleep" app/`): @@ -39,28 +39,86 @@ **当前能力一览**:多租户引擎(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 清理单(前序审查挂账,集中列此) +## Phase 2 完成状态(2026-10-13 收尾核对) -1. **FakeAI 三份拷贝收敛进 conftest**——下次吸收测试时先做(出处:上游吸收审查,tests/ 内多处 FakeAI 定义重复)。 -2. **rate_limit 与 ws_conn_limit 的 0 语义统一 + 配置类型校验**——试金石批次(出处:配置接线审查)。 -3. **name_to_region_extras / context_seed_map 等散装 resolver 收拢为 config_views 单入口**(出处:配置接线审查"种子映射消双路径"遗留)。 -4. **AIService.from_runtime 类方法**——组装正典化(出处:引擎改造审查)。 -5. **boundary_filter 视图常量退役**——admin 重写后旧测试迁移时(出处:引擎改造审查)。 -6. **push enabled 布尔空槽语义**——admin 配置显式化时解决(出处:push 审查)。 -7. **contact.template_languages 运行期接线**——后台批次(出处:配置 schema 审查)。 -8. **话术含引号/换行的转义**——编辑器批次(`_format_deflection_pool` docstring 有注)。 -9. **产品决策项**:国名纠正 e164 变化时是否补发更正邮件(上游行为更优);REST/WS 服务端 IP 口径是否统一(出处:上游吸收审查)。 -10. **avatar 从 seed_profiles 迁 uploads 卷**——上传功能落地时(出处:品牌下发审查)。 -11. **/health 分级健康**——Phase 4 部署批次(进程存活之外按依赖分级上报,健康检查可区分"进程活"与"依赖通";出处:终审审查 M-3)。 -12. **PromptProvider.from_dir 的 FileNotFoundError 带路径上下文**——提示词目录缺失时异常信息含实际路径,定位不用猜(出处:终审审查 M-4)。 +**测试基线:566 passed**(根目录与 tests/ 子目录各跑一遍,数字一致;唯一 warning 同 Phase 1——starlette TestClient 对 httpx 的弃用提示,无害)。后续任务回归以此为准。 -## Phase 2 计划编写注意 +**零同步 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 表)。 -- conftest 跨循环纪律:`async_db` 夹具用 NullPool;TestClient 必须走 portal 桥接事件循环;禁止 `asyncio.run` 直开租户 session_factory(三个坑都在 tests/conftest.py 有既成解法,照抄勿发明)。 -- `build_app` / `app_client` / `TenantTestClient` 夹具已就绪,新测试优先复用而非自建 app 实例。 +**安全自查**(读码 + 定向测试 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/.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 跨租户查重**(域名类校验只在 PUT 路径)——如需收口,在 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 依赖此锚——改动前先迁测试)。 + +**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。 ## 文档索引 - 设计文档(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 4 补 `docs/deploy-production.md`(尚未存在) +- Phase 2 实施计划:`docs/superpowers/plans/2026-10-13-phase2-platform-layer.md` +- 部署坑手册:Phase 4 补 `docs/deploy-production.md`(尚未存在;Phase 4 部署记档段已锚定要点) diff --git a/README.md b/README.md index cb83ad6..15183dd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # all-kefu -统一多租户 AI 客服平台:一套引擎 + 租户配置包,消除此前"每客户复制一套代码"的分叉模式。项目由 ai-kefu / ai-kefu-jc 两个复制分叉项目合并而来(jc 为主线),Phase 1 引擎合并与租户化改造已完成。 +统一多租户 AI 客服平台:一套引擎 + 租户配置包,消除此前"每客户复制一套代码"的分叉模式。项目由 ai-kefu / ai-kefu-jc 两个复制分叉项目合并而来(jc 为主线),Phase 1 引擎合并与租户化改造、Phase 2 平台层(platform.db + admin API)已完成。 -**现状能力**(Phase 1 收尾时点): +**现状能力**(Phase 2 收尾时点): - 多租户引擎:TenantRuntime 隔离边界,engine 包不感知平台层,可整体拎走独立部署。 -- 站点模型:一租户多站点(品牌/主题/站点级覆盖),词表、提示词、品牌全部配置化(seed_profiles 出厂包 + 文件级 registry_reload 热加载,Phase 2 升级为 platform.db)。 +- 站点模型:一租户多站点(品牌/主题/站点级覆盖),词表、提示词、品牌全部配置化;配置存 platform.db 版本快照(分组校验 / 回滚 / 热加载),seed_profiles 为出厂档案与测试形态。 - Host 路由:按域名解析租户,未知 Host 404;WS 通道租户化客户端 key 防串库,另有限流与每 IP 连接上限。 - 引擎行为:cc 区号补全判定树、多语言墙(中/西/葡借道/俄/阿)、dual-contact 分步双要、CC 强制墙、lead 邮件推送。 +- 平台层(Phase 2):scrypt 认证 + 内存 token + 登录限流;租户管理(创建向导 / 站点 / 域名 / 停用);配置版本管线(整组替换 / 回滚 / 重置出厂 / 试金石);头像上传(魔数校验 + uuid 重命名);数据视图(会话 / 推送日志 / 删除,租户 scope 强制);统计(卡片 / 漏斗 / 趋势 / 分布 / 推送健康,逐库聚合禁 ATTACH);DB 源生产装配(TENANT_SOURCE=db 默认 + bootstrap_platform.py 引导)。admin API 全在 `/api/platform` 前缀(Host 豁免、路由依赖守护),端点清单见 [CLAUDE.md](CLAUDE.md)。 -**本地跑法与工作约定**:见 [CLAUDE.md](CLAUDE.md)(含 uvicorn 启动命令、Host 访问方式、Windows PYTHONUTF8 要求、全量回归基线 459)。 +**本地跑法与工作约定**:见 [CLAUDE.md](CLAUDE.md)(含 uvicorn 启动命令、Host 访问方式、Windows PYTHONUTF8 要求、全量回归基线 566)。 设计文档(spec):[docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md](docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md)