docs: 计划评审修复(Task9 改为配置接线/过渡main照抄jc/词表schema补全/conftest工厂/审计步骤)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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 收尾——引擎合并租户化完成"`
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user