chore: 服务端品牌与行业残留更名清零(HESUPIPE/DB名/redis字段)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -6,7 +6,7 @@ AI_MODEL=glm-4-flash
|
|||||||
ZHIPU_BASE_URL=https://open.bigmodel.cn/api/coding/paas/v4
|
ZHIPU_BASE_URL=https://open.bigmodel.cn/api/coding/paas/v4
|
||||||
|
|
||||||
# 数据库
|
# 数据库
|
||||||
DATABASE_URL=sqlite+aiosqlite:///./data/cc_ai_kefu.db
|
DATABASE_URL=sqlite+aiosqlite:///./data/hesupipe_ai_kefu.db
|
||||||
|
|
||||||
# 邮件推送
|
# 邮件推送
|
||||||
SMTP_HOST=smtp.example.com
|
SMTP_HOST=smtp.example.com
|
||||||
@@ -23,5 +23,5 @@ PUSH_EXTERNAL_KEY_PREFIX=ai-kefu
|
|||||||
|
|
||||||
# 应用配置
|
# 应用配置
|
||||||
APP_HOST=0.0.0.0
|
APP_HOST=0.0.0.0
|
||||||
APP_PORT=8016
|
APP_PORT=8017
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""REST shell for the HESU app (non-streaming, JSON-in/JSON-out).
|
"""REST shell for the HESUPIPE app (non-streaming, JSON-in/JSON-out).
|
||||||
|
|
||||||
These routes wrap `ConversationManager`, shared with the existing `/ws/chat`
|
These routes wrap `ConversationManager`, shared with the existing `/ws/chat`
|
||||||
widget path. The app uses HTTP because it lacks a WebSocket client and is fine
|
widget path. The app uses HTTP because it lacks a WebSocket client and is fine
|
||||||
|
|||||||
+1
-5
@@ -14,11 +14,7 @@ class Settings(BaseSettings):
|
|||||||
ZHIPU_BASE_URL: str = ""
|
ZHIPU_BASE_URL: str = ""
|
||||||
AI_MODEL: str = "glm-4-flash"
|
AI_MODEL: str = "glm-4-flash"
|
||||||
|
|
||||||
DATABASE_URL: str = "sqlite+aiosqlite:///./data/cc_ai_kefu.db"
|
DATABASE_URL: str = "sqlite+aiosqlite:///./data/hesupipe_ai_kefu.db"
|
||||||
|
|
||||||
REDIS_URL: str = "redis://localhost:6379/0"
|
|
||||||
REDIS_PASSWORD: str = ""
|
|
||||||
SESSION_TTL: int = 86400 # 24 hours
|
|
||||||
|
|
||||||
SMTP_HOST: str = ""
|
SMTP_HOST: str = ""
|
||||||
SMTP_PORT: int = 465
|
SMTP_PORT: int = 465
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ async def lifespan(app: FastAPI):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="AI汽车客服", lifespan=lifespan)
|
app = FastAPI(title="HESUPIPE AI 客服", lifespan=lifespan)
|
||||||
|
|
||||||
from app.api.routes.chat import router as chat_router
|
from app.api.routes.chat import router as chat_router
|
||||||
from app.api.routes.admin import router as admin_router
|
from app.api.routes.admin import router as admin_router
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async def send_lead_email(
|
|||||||
if contact_parts:
|
if contact_parts:
|
||||||
subject += " | " + ", ".join(contact_parts)
|
subject += " | " + ", ".join(contact_parts)
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg["From"] = f"【XYH】 <{settings.SMTP_USER}>"
|
msg["From"] = f"【HESUPIPE】 <{settings.SMTP_USER}>"
|
||||||
msg["To"] = settings.NOTIFY_EMAIL
|
msg["To"] = settings.NOTIFY_EMAIL
|
||||||
|
|
||||||
info_rows = "".join(
|
info_rows = "".join(
|
||||||
@@ -108,7 +108,7 @@ async def send_returning_lead_email(contact_info: dict) -> bool:
|
|||||||
]
|
]
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["Subject"] = f"[线索回访] {' | '.join(contact_parts) or '已知客户'}再次来访"
|
msg["Subject"] = f"[线索回访] {' | '.join(contact_parts) or '已知客户'}再次来访"
|
||||||
msg["From"] = f"【XYH】 <{settings.SMTP_USER}>"
|
msg["From"] = f"【HESUPIPE】 <{settings.SMTP_USER}>"
|
||||||
msg["To"] = settings.NOTIFY_EMAIL
|
msg["To"] = settings.NOTIFY_EMAIL
|
||||||
msg.attach(MIMEText(
|
msg.attach(MIMEText(
|
||||||
f"<html><body><p>该联系方式 24h 内已发过完整询盘,客户又开启了新对话,"
|
f"<html><body><p>该联系方式 24h 内已发过完整询盘,客户又开启了新对话,"
|
||||||
@@ -120,7 +120,7 @@ async def send_correction_email(contact_info: dict, count: int = 1) -> bool:
|
|||||||
"""D4: 联系方式变化(改号/新增/无提示换值)后的轻量更正邮件,每会话上限3封。"""
|
"""D4: 联系方式变化(改号/新增/无提示换值)后的轻量更正邮件,每会话上限3封。"""
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["Subject"] = f"[询盘·更正{count}] 客户联系方式已更新(以本封为准)"
|
msg["Subject"] = f"[询盘·更正{count}] 客户联系方式已更新(以本封为准)"
|
||||||
msg["From"] = f"【XYH】 <{settings.SMTP_USER}>"
|
msg["From"] = f"【HESUPIPE】 <{settings.SMTP_USER}>"
|
||||||
msg["To"] = settings.NOTIFY_EMAIL
|
msg["To"] = settings.NOTIFY_EMAIL
|
||||||
msg.attach(MIMEText(
|
msg.attach(MIMEText(
|
||||||
f"<html><body><p>这是本会话第 {count} 次更正(上限 3 次),请以本封为准:</p>"
|
f"<html><body><p>这是本会话第 {count} 次更正(上限 3 次),请以本封为准:</p>"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Minimal in-memory rate limiter (per (ip, device_id), rolling 1h window).
|
"""Minimal in-memory rate limiter (per (ip, device_id), rolling 1h window).
|
||||||
|
|
||||||
Mirrors hesu's rate_limit.py shape. Only the chat message route uses this;
|
Kept aligned with the reference implementation's rate_limit.py shape. Only the chat message route uses this;
|
||||||
session/history endpoints are cheap and not throttled. State is process-local
|
session/history endpoints are cheap and not throttled. State is process-local
|
||||||
and resets on restart, which is acceptable for a single-worker ai-kefu deploy.
|
and resets on restart, which is acceptable for a single-worker ai-kefu deploy.
|
||||||
|
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ python -m pytest -q && git add -A && git commit -m "feat: 多语言输出墙(€
|
|||||||
- Modify: `app/services/conversation.py`、`app/services/contact_pipeline.py`
|
- Modify: `app/services/conversation.py`、`app/services/contact_pipeline.py`
|
||||||
- Test: `tests/test_contact_pipeline.py`、`tests/test_context_seeding.py`
|
- Test: `tests/test_contact_pipeline.py`、`tests/test_context_seeding.py`
|
||||||
|
|
||||||
- [ ] **Step 1: 先写失败的阿语数字测试**
|
- [x] **Step 1: 先写失败的阿语数字测试**
|
||||||
|
|
||||||
`tests/test_contact_pipeline.py` 追加:
|
`tests/test_contact_pipeline.py` 追加:
|
||||||
|
|
||||||
@@ -353,14 +353,14 @@ def test_arabic_digits_normalized():
|
|||||||
|
|
||||||
(具体 API 名以参考项目实际函数签名为准——`split_validate`/canon 归一化入口。)
|
(具体 API 名以参考项目实际函数签名为准——`split_validate`/canon 归一化入口。)
|
||||||
|
|
||||||
- [ ] **Step 2: 跑测试确认失败**
|
- [x] **Step 2: 跑测试确认失败**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pytest tests/test_contact_pipeline.py -q -k arabic
|
python -m pytest tests/test_contact_pipeline.py -q -k arabic
|
||||||
```
|
```
|
||||||
预期:FAIL(号码原样保留阿语数字)
|
预期:FAIL(号码原样保留阿语数字)
|
||||||
|
|
||||||
- [ ] **Step 3: 实现数字归一化**
|
- [x] **Step 3: 实现数字归一化**
|
||||||
|
|
||||||
`contact_pipeline.py` 顶部加:
|
`contact_pipeline.py` 顶部加:
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ _DIGIT_TRANSLATE = str.maketrans("٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01
|
|||||||
|
|
||||||
在归一化入口(normalize_contacts/digits_only 调用链的最前端)对值做 `v.translate(_DIGIT_TRANSLATE)`。注意:正则 `\d` Unicode 模式本就匹配阿语数字,位数校验能过——本步只解决"存进去的形式"。
|
在归一化入口(normalize_contacts/digits_only 调用链的最前端)对值做 `v.translate(_DIGIT_TRANSLATE)`。注意:正则 `\d` Unicode 模式本就匹配阿语数字,位数校验能过——本步只解决"存进去的形式"。
|
||||||
|
|
||||||
- [ ] **Step 4: conversation.py 建材化**
|
- [x] **Step 4: conversation.py 建材化**
|
||||||
|
|
||||||
1. `BOUNDARY_TRIGGER_RE`:确认已含 MOQ/lead time/delivery time/sample/certificate 类词;补 `container loading|装柜|起订`(以实际正则现状为准增补)
|
1. `BOUNDARY_TRIGGER_RE`:确认已含 MOQ/lead time/delivery time/sample/certificate 类词;补 `container loading|装柜|起订`(以实际正则现状为准增补)
|
||||||
2. context 种子白名单:`vehicle_name/vehicle_id` → `product_name/product_id`
|
2. context 种子白名单:`vehicle_name/vehicle_id` → `product_name/product_id`
|
||||||
@@ -379,7 +379,7 @@ _DIGIT_TRANSLATE = str.maketrans("٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01
|
|||||||
4. 同步更新 `tests/test_context_seeding.py` 的键名
|
4. 同步更新 `tests/test_context_seeding.py` 的键名
|
||||||
5. **话术跨层同步(Task 3 质量审查 Minor 4)**:conversation.py 约 592 行的注入提醒与 boundary_filter.py 的 NO_CONTACT_WEB 仍是 "our manager replies fast!",与 system.md 的 "reply within 12 hours" 口径不一致——统一为 12 小时句(或两版并存亦可,但须明确决定)
|
5. **话术跨层同步(Task 3 质量审查 Minor 4)**:conversation.py 约 592 行的注入提醒与 boundary_filter.py 的 NO_CONTACT_WEB 仍是 "our manager replies fast!",与 system.md 的 "reply within 12 hours" 口径不一致——统一为 12 小时句(或两版并存亦可,但须明确决定)
|
||||||
|
|
||||||
- [ ] **Step 5: 全量测试 + Commit**
|
- [x] **Step 5: 全量测试 + Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pytest -q && git add -A && git commit -m "feat: 会话编排建材化+阿语号码转ASCII"
|
python -m pytest -q && git add -A && git commit -m "feat: 会话编排建材化+阿语号码转ASCII"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import sys
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
DB = sys.argv[1] if len(sys.argv) > 1 else "./data/cc_ai_kefu.db"
|
DB = sys.argv[1] if len(sys.argv) > 1 else "./data/hesupipe_ai_kefu.db"
|
||||||
|
|
||||||
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
|
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
|
||||||
INTL_RE = re.compile(r"\+\d{6,15}")
|
INTL_RE = re.compile(r"\+\d{6,15}")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DB = sys.argv[1] if len(sys.argv) > 1 else "./data/cc_ai_kefu.db"
|
DB = sys.argv[1] if len(sys.argv) > 1 else "./data/hesupipe_ai_kefu.db"
|
||||||
|
|
||||||
PATTERNS = [
|
PATTERNS = [
|
||||||
(r"\$\s?\d", "美元符"),
|
(r"\$\s?\d", "美元符"),
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ def test_asks_contact_fragment_followup_still_pass():
|
|||||||
def test_warranty_commitment_violates():
|
def test_warranty_commitment_violates():
|
||||||
# 政策变更(2026-09-10 用户定调: 不清楚的留资转经理): AI 输出的保修期限
|
# 政策变更(2026-09-10 用户定调: 不清楚的留资转经理): AI 输出的保修期限
|
||||||
# 是编造的公司政策承诺——从"白名单放行"改为墙级违规
|
# 是编造的公司政策承诺——从"白名单放行"改为墙级违规
|
||||||
assert violates_boundary("we offer 6 months warranty on every car")
|
assert violates_boundary("we offer 6 months warranty on every pipe")
|
||||||
assert violates_boundary("battery has 8 years warranty") # year 靠定向模式
|
assert violates_boundary("battery has 8 years warranty") # year 靠定向模式
|
||||||
assert violates_boundary("warranty of 8 years on battery") # EN 倒装
|
assert violates_boundary("warranty of 8 years on battery") # EN 倒装
|
||||||
assert violates_boundary("电池保修8个月")
|
assert violates_boundary("电池保修8个月")
|
||||||
@@ -214,9 +214,9 @@ def test_chinese_time_commitment_violates():
|
|||||||
|
|
||||||
|
|
||||||
def test_age_and_year_talk_still_clean():
|
def test_age_and_year_talk_still_clean():
|
||||||
# 误杀防护: 二手车龄/车型年款/无数字保修讨论必须放行(实证 13 条全清)
|
# 误杀防护: 库货年限/年款/规格数字/无数字保修讨论必须放行(实证 13 条全清)
|
||||||
assert not violates_boundary("under 5 years old")
|
assert not violates_boundary("under 5 years old")
|
||||||
assert not violates_boundary("5 years old car")
|
assert not violates_boundary("5 years old stock pipe")
|
||||||
assert not violates_boundary("2024年款")
|
assert not violates_boundary("2024年款")
|
||||||
assert not violates_boundary("5座SUV")
|
assert not violates_boundary("5座SUV")
|
||||||
assert not violates_boundary("600km续航")
|
assert not violates_boundary("600km续航")
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def test_scan_wa_me_link():
|
|||||||
|
|
||||||
def test_scan_ignores_prices_years_and_ai_messages():
|
def test_scan_ignores_prices_years_and_ai_messages():
|
||||||
texts = m(
|
texts = m(
|
||||||
"budget $25,000 for 2024 BYD Tang", # 价格+年份
|
"budget $25,000 for 2024 PE100 pipes", # 价格+年份
|
||||||
"50,000 km mileage", # 里程
|
"50,000 km mileage", # 里程
|
||||||
"I want 5 units", # 数量
|
"I want 5 units", # 数量
|
||||||
)
|
)
|
||||||
@@ -221,7 +221,7 @@ def test_recency_kills_extractor_oscillation():
|
|||||||
before = {"phone": "+8613800138004", "whatsapp": "+8613800138004"}
|
before = {"phone": "+8613800138004", "whatsapp": "+8613800138004"}
|
||||||
after = {"phone": "+8613800138000", "whatsapp": "+8613800138004"} # extractor 振荡
|
after = {"phone": "+8613800138000", "whatsapp": "+8613800138004"} # extractor 振荡
|
||||||
msgs = _u("my whatsapp is +8613800138000", "my whatsapp is +8613800138004",
|
msgs = _u("my whatsapp is +8613800138000", "my whatsapp is +8613800138004",
|
||||||
"any car talk")
|
"any pipe talk")
|
||||||
assert resolve_by_recency(before, after, msgs) == before # 无变化→不触发更正
|
assert resolve_by_recency(before, after, msgs) == before # 无变化→不触发更正
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ def test_no_contact_never_emails_and_marks_stuck(async_db, monkeypatch):
|
|||||||
# D8: interested+无联系方式 → 不发邮件,upsert Evaluation 卡单记录
|
# D8: interested+无联系方式 → 不发邮件,upsert Evaluation 卡单记录
|
||||||
(manager, conv), sent = _setup(async_db, monkeypatch,
|
(manager, conv), sent = _setup(async_db, monkeypatch,
|
||||||
ai=_mk_ai({}, evaluate=INTERESTED))
|
ai=_mk_ai({}, evaluate=INTERESTED))
|
||||||
for text in ("hi", "kenya", "20k budget", "byd tang"):
|
for text in ("hi", "kenya", "20k budget", "pe100 pipe"):
|
||||||
asyncio.run(_send(manager, conv.session_id, text))
|
asyncio.run(_send(manager, conv.session_id, text))
|
||||||
assert sent == []
|
assert sent == []
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ def test_oscillation_does_not_burn_correction_quota(async_db, monkeypatch):
|
|||||||
ai = FakeAI(extract=seq)
|
ai = FakeAI(extract=seq)
|
||||||
(manager, conv), sent = _setup(async_db, monkeypatch, ai=ai)
|
(manager, conv), sent = _setup(async_db, monkeypatch, ai=ai)
|
||||||
texts = ["my whatsapp is +8613800138000", "my whatsapp is +8613800138004",
|
texts = ["my whatsapp is +8613800138000", "my whatsapp is +8613800138004",
|
||||||
"什么车好", "再说", "还有吗", "18325698745"]
|
"哪种管好", "再说", "还有吗", "18325698745"]
|
||||||
for t in texts:
|
for t in texts:
|
||||||
asyncio.run(_send(manager, conv.session_id, t))
|
asyncio.run(_send(manager, conv.session_id, t))
|
||||||
kinds = [s[0] for s in sent]
|
kinds = [s[0] for s in sent]
|
||||||
@@ -464,7 +464,7 @@ def test_evaluation_upsert_keeps_flags_and_intent(async_db, monkeypatch):
|
|||||||
|
|
||||||
def test_reminder_boundary_redline_on_price_question():
|
def test_reminder_boundary_redline_on_price_question():
|
||||||
text = ConversationManager._build_context_reminder(
|
text = ConversationManager._build_context_reminder(
|
||||||
{}, "how much is the BYD Tang?", [])
|
{}, "how much is the DN800 pipe?", [])
|
||||||
assert "职能边界" in text and "禁止" in text
|
assert "职能边界" in text and "禁止" in text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user