docs: 计划评审修复(Task9 改为配置接线/过渡main照抄jc/词表schema补全/conftest工厂/审计步骤)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 15:04:37 +08:00
parent c82bc79bea
commit c4772a9f3f
@@ -94,21 +94,9 @@ jc/pytest.ini 的 testpaths 已在 Task 1 建
`app.services` → `app.engine.services`,`app.providers` → `app.engine.providers`,`app.api.routes` → `app.engine.routes`,`app.models.database` → `app.engine.models`,`app.config` → `app.engine.config`。在 tests/ 里同样替换。
- [ ] **Step 3: 写最小 app/main.py(过渡版,单租户直跑)**
- [ ] **Step 3: 过渡版 app/main.py —— 照抄 jc main.py 全文(含模块级 engine/async_session/Base 与 lifespan),只改 import 路径与去掉 admin 路由**
```python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.engine.routes.chat import router as ws_router
from app.engine.routes.chat_rest import router as rest_router
app = FastAPI(title="all-kefu (phase1 transitional)")
app.include_router(ws_router)
app.include_router(rest_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
```
注意:jc 的 main.py 里有 lifespan 建表/迁移/sweep 逻辑——本步先不做(Task 5 由 Registry 逐租户做)。若 tests/conftest.py 依赖 jc 的 app 工厂,先按其原样改造指向新 main。
⚠️ **不要写"最小 main"**:`chat.py`/`chat_rest.py`/`lead_push_service.py` 都 `from app.main import async_session`,jc 的 conftest 还 `from app.main import engine`——jc conftest 里文档化过 main→chat_rest 的循环导入次序坑。过渡期保留 jc main.py 的全局 engine/async_session 结构(Task 5 才由 Registry 接管),admin 路由 import 行删除。同时先把 `jc/static/` 整目录拷到 `static/`(Task 10 才改造,但 `StaticFiles(directory="static")` 需要目录先存在)。
- [ ] **Step 4: 跑全量测试建立基线**
@@ -118,7 +106,7 @@ Expected: 除 admin 相关(已删)外全部 PASS。若有 FAIL,逐个修
- [ ] **Step 5: 提交**
```bash
git add -A && git commit -m "feat: jc 引擎主线搬入 app/engine(320 测试基线全绿)"
git add -A && git commit -m "feat: jc 引擎主线搬入 app/engine(315 测试基线全绿:320 去掉 admin 5 用例)"
```
---
@@ -238,15 +226,17 @@ git add app/platform tests/test_config_merge.py && git commit -m "feat: 出厂
```json
{
"price_patterns": ["<PRICE_PATTERNS 每条 pattern 原样>"],
"price_patterns": ["<PRICE_PATTERNS 每条 pattern 原样——防误杀断言就在 pattern 内,一并保留>"],
"deflections": {"en": ["..."], "zh": ["..."], "es": ["..."]},
"no_contact": {"replacement": "<NO_CONTACT_RESPONSE 原文>", "asked_replacement": "<...>"},
"deflections_contact": {"en": ["<DEFLECTIONS_*_CONTACT 已留资变体池>"], "zh": ["..."], "es": ["..."]},
"no_contact": {"web": "<NO_CONTACT_WEB>", "app": "<NO_CONTACT_APP>", "already": "<NO_CONTACT_ALREADY>"},
"triggers": {"boundary": ["..."], "budget": ["..."], "contact_ask": ["..."]},
"anti_kill": ["DN", "PN", "SDR", "PE100", "SCH", "ISO4427", "20ft", "km", "<jc 防误杀清单>"],
"cc_templates": {"en": ["<回显确认模板 EN>"], "zh": ["<ZH>"], "es": ["<ES>"]}
}
```
⚠️ **anti_kill 不设字段**:两个源项目的防误杀都不是独立常量,而是内嵌在正则断言里(如后顾放行、`(?!\s*(公里|km))` 排除)——没有可提取的"清单"。两租户防误杀差异由各自 price_patterns 自带断言体现,CompiledWall 不设 anti_kill。
`seed_profiles/auto/wall.json`:同一结构,price_patterns 用上游 ai-kefu 的中英+$ 词表、anti_kill 换汽车清单(km/年款等)、deflections 去掉 es。
- [ ] **Step 2: 写编译器(先写失败测试)**
@@ -303,9 +293,9 @@ def compile_wall(cfg: dict) -> CompiledWall:
return CompiledWall(
price_patterns=prices,
deflections={k: list(v) for k, v in cfg.get("deflections", {}).items()},
deflections_contact={k: list(v) for k, v in cfg.get("deflections_contact", {}).items()},
no_contact=dict(cfg.get("no_contact", {})),
triggers=triggers,
anti_kill=tuple(cfg.get("anti_kill", [])),
cc_templates={k: list(v) for k, v in cfg.get("cc_templates", {}).items()},
)
```
@@ -335,8 +325,9 @@ git add -A && git commit -m "feat: 词表数据化——boundary/conversation
- [ ] **Step 1: 写两个 tenant.json**
**seed_profiles/jiancai/tenant.json**(纯 JSON,无注释):
```json
// seed_profiles/jiancai/tenant.json
{ "slug": "jiancai", "name": "HESUPIPE 建材", "status": "active",
"domains": ["chat2.agent.hnxinyouhe.com", "localhost.jc"],
"config": { "brand": {"title": "HESUPIPE Support", "color": "#066fc0",
@@ -482,9 +473,11 @@ git add -A && git commit -m "feat: PromptProvider 化 + $DEFLECTION_POOL 单一
**Files:**
- Create: `app/platform/host_middleware.py`
- Modify: `app/engine/routes/chat.py`、`chat_rest.py`、`app/main.py`
- Modify: `app/engine/routes/chat.py`、`chat_rest.py`、`app/main.py`、`tests/conftest.py`
- Test: `tests/test_host_routing.py`
⚠️ conftest 需新增 `build_app(seed_dir, data_dir)` 工厂与 `app_client` 夹具(jc conftest 没有这个):临时目录建租户库 → Registry.load_from_dir → 挂中间件的 app → TestClient(可设 host 头)。后续 Task 9/10 的接线测试复用它。
- [ ] **Step 1: 写失败测试**
```python
@@ -567,33 +560,65 @@ def test_limit_per_ip():
---
### Task 9: 推送异步化(httpx)
### Task 9: email/lead_push/rate_limit 租户配置接线
**背景(评审核实的事实):** jc 现状——email_service 读全局 settings(SMTP_*/NOTIFY_EMAIL,发件名硬编码【HESUPIPE】);lead_push_service 读 settings.PUSH_*;rate_limit 的 key=(ip, device)。既有测试 monkeypatch 的都是 settings,所以这个缺口**测试暴露不了**——必须新增接线测试逼出来。不接线的话,Phase 1 结束时两租户联调会静默错配(auto 用上 jc 的 SMTP/推送目标)。
另:jc 的 lead_push 已是 httpx.AsyncClient(`_do_post`)、glm 已走 executor,零同步 IO 纪律在主线已满足,无需改造(Task 12 做审计确认)。
**Files:**
- Modify: `app/engine/services/lead_push_service.py`
- Test: `tests/test_lead_push_async.py`(新建)+ 既有 push 测试回归
- Modify: `app/engine/services/email_service.py`、`lead_push_service.py`、`rate_limit.py`
- Test: `tests/test_tenant_config_wiring.py`
- [ ] **Step 1: 写失败测试**(AsyncMock httpx):
- [ ] **Step 1: 写失败测试**
```python
import httpx
from unittest.mock import AsyncMock, MagicMock
# tests/test_tenant_config_wiring.py
from types import SimpleNamespace
from app.platform.tenant_config import TenantConfig
async def test_push_uses_httpx_async(monkeypatch):
resp = MagicMock(status_code=200)
resp.json = lambda: {"results": [{"external_key": "k", "status": "accepted"}]}
client = MagicMock()
client.post = AsyncMock(return_value=resp)
monkeypatch.setattr("app.engine.services.lead_push_service._open_client",
lambda: _Ctx(client))
... # dispatch 一条 pending → 断言 client.post awaited 一次、url/headers 正确
def _cfg():
snap = {"email": {"smtp": {"host": "s", "port": 465, "user": "u", "pass": "p"},
"sender_name": "【T】", "notify_emails": ["a@b.c"]},
"push": {"enabled": True, "url": "http://x", "api_key": "k",
"prefix": "t1", "timeout": 5, "max_attempts": 1,
"retry_backoff_base": 1.0}}
return TenantConfig.from_snapshot("t1", "T", ["t1.local"], snap)
def test_email_reads_tenant_smtp(monkeypatch):
import app.engine.config as cfg_mod
monkeypatch.setattr(cfg_mod, "settings",
SimpleNamespace(SMTP_HOST="WRONG", SMTP_PORT=1, SMTP_USER="WRONG",
SMTP_PASS="WRONG", NOTIFY_EMAIL="wrong@x"))
resolved = resolve_email_settings(_cfg()) # 期望读租户段而非全局
assert resolved.notify_emails == ["a@b.c"] and resolved.sender_name == "【T】"
def test_push_reads_tenant_section():
p = resolve_push_settings(_cfg())
assert p.url == "http://x" and p.prefix == "t1"
def test_rate_limit_key_includes_tenant():
from app.engine.services.rate_limit import RateLimiter
lim = RateLimiter()
assert lim.check("t1", "1.1.1.1", "dev1", limit=2) is True
assert lim.check("t1", "1.1.1.1", "dev1", limit=2) is True
assert lim.check("t2", "1.1.1.1", "dev1", limit=2) is True # 换租户不受影响
```
- [ ] **Step 2: 改造**:同步 requests/urllib 调用 → `async with httpx.AsyncClient(timeout=cfg.timeout) as client: await client.post(url, json=payload, headers=...)`;重试矩阵逻辑不变(仍只重试 5xx/传输错误)。**事件循环零同步 IO** 纪律的落点(spec §6.1/§11)。
- [ ] **Step 2: 跑测试确认失败**
- [ ] **Step 3: 既有 test_lead_push_* 回归**(原 mock 点从 sync 库改挂到新 _open_client/AsyncClient 工厂)
Run: `python -m pytest tests/test_tenant_config_wiring.py -v`
Expected: FAIL(resolve_* 函数不存在 / rate_limit 不收 tenant 参数)
- [ ] **Step 4: 提交** `git commit -m "feat: 推送改 httpx.AsyncClient(零同步 IO)"`
- [ ] **Step 3: 实现接线**:email_service/lead_push_service 顶部各加 `resolve_email_settings(config)` / `resolve_push_settings(config)`(读 `config.data` 对应组;email.smtp 为 None 时回退平台默认 = 本阶段 ProcessSettings 的 env 值);服务调用点从读 settings 改为接收经 ConversationManager 传入的租户 config;rate_limit 桶 key 改 `(tenant_slug, ip, device)`、limit 读 `config.engine.rate_limit_per_hour`
- [ ] **Step 4: 既有 email/push 测试回归**:monkeypatch 点从 settings 迁到 resolve_*_settings 返回值(或直接传 config 夹具),断言不变
- [ ] **Step 5: 全量回归 + 提交**
```bash
git add -A && git commit -m "feat: email/push/rate_limit 读租户配置段(服务层 settings 引用清零)"
```
---
@@ -638,7 +663,7 @@ def test_widget_keys_namespaced_by_slug():
**Files:**
- Modify: `static/widget.js`(placeholder 可配置——对齐 ai-kefu 的 input-placeholder 实现,读 brand.placeholder + data-placeholder 覆盖)
- Test: 从 `D:\refactor\ai-kefu\tests\` 拷入 `test_channel_hygiene.py`、`test_chat_rest_ip.py`、`test_lead_push_cc.py`、`test_cc_integration.py`、test_prompt_static.py、test_ip_service_region.py → 修 import 跑绿(行为以 jc 实现为准,断言不适处改断言不改实现)
- Test: 从 `D:\refactor\ai-kefu\tests\` 拷入 `test_channel_hygiene.py`、`test_chat_rest_ip.py`、`test_lead_push_cc.py`、`test_cc_integration.py`、`test_prompt_static.py`、`test_ip_service_region.py`、`test_placeholder_static.py` → 修 import 跑绿(行为以 jc 实现为准,断言不适处改断言不改实现)
- [ ] **Step 1: 拷 6 个测试文件、修 import、跑**
- [ ] **Step 2: 红→绿循环**:失败断言逐条判断是"上游测的旧行为"(改断言)还是"jc 缺的修复"(补实现,注明同步自上游)
@@ -648,10 +673,11 @@ def test_widget_keys_namespaced_by_slug():
### Task 12: Phase 1 收尾
- [ ] **Step 1: 全量回归** `python -m pytest -q` → 全绿,记录用例数(目标 ≥ 320 + 新增 ~20)
- [ ] **Step 2: spec §6.2 合并清单逐项打勾核对**(jc 六项取入/上游吸收/三不带:blog.html、Redis、旧版 whatsapp-cc)
- [ ] **Step 3: 更新 CLAUDE.md**(Phase 1 完成状态、本地起服务方法、测试基线数)
- [ ] **Step 4: 提交** `git commit -m "chore: Phase 1 收尾——引擎合并租户化完成"`
- [ ] **Step 1: 全量回归** `python -m pytest -q` → 全绿,记录用例数(基线 315 + 新增 ~20)
- [ ] **Step 2: 零同步 IO 审计**:`grep -rn "requests\.\|urllib\|time\.sleep" app/` 确认 async 路径无同步 IO(已核实:lead_push_service._do_post 为 httpx.AsyncClient、providers/glm.py 流式走 executor);审计结论写入 CLAUDE.md
- [ ] **Step 3: spec §6.2 合并清单逐项打勾核对**(jc 六项取入/上游吸收/三不带:blog.html、Redis、旧版 whatsapp-cc)
- [ ] **Step 4: 更新 CLAUDE.md**(Phase 1 完成状态、本地起服务方法、测试基线数)
- [ ] **Step 5: 提交** `git commit -m "chore: Phase 1 收尾——引擎合并租户化完成"`
---