feat: add simulator page and image quote analyzer
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-03 12:41:28 +08:00
parent 919c70789e
commit b663c7acbf
6 changed files with 605 additions and 7 deletions

View File

@@ -1,8 +1,14 @@
from __future__ import annotations
import asyncio
from collections import defaultdict
from flask import Flask, jsonify, request
from .auto_draw import auto_draw_preview
from .logger import setup_logger
from .orchestrator import Orchestrator
from .rules import extract_image_urls
from .runtime_switch import is_listen_only, set_listen_only
from .task_manager import TaskManager
@@ -11,6 +17,17 @@ def create_http_app(task_manager: TaskManager | None = None) -> Flask:
app = Flask(__name__)
logger = setup_logger()
tm = task_manager or TaskManager()
sim_orch = Orchestrator()
sim_pending_images: dict[str, list[str]] = defaultdict(list)
sim_recent_dialogue: dict[str, list[dict]] = defaultdict(list)
def _sim_append_dialogue(key: str, role: str, text: str) -> None:
t = str(text or "").strip()
if not t:
return
sim_recent_dialogue[key].append({"role": role, "text": t})
if len(sim_recent_dialogue[key]) > 24:
sim_recent_dialogue[key] = sim_recent_dialogue[key][-24:]
@app.get('/api/health')
def health():
@@ -58,6 +75,170 @@ def create_http_app(task_manager: TaskManager | None = None) -> Flask:
limit = int(request.args.get('limit', 100))
return jsonify({'ok': True, 'tasks': tm.list_tasks(limit=limit)})
@app.get('/debug/simulator')
def debug_simulator():
return """
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>链路测试页</title>
<style>
:root { --bg:#f6f8fb; --card:#fff; --ink:#1f2937; --line:#dbe3ef; --brand:#0f766e; }
body { margin:0; background:var(--bg); color:var(--ink); font-family:"Microsoft YaHei UI", "PingFang SC", sans-serif; }
.wrap { max-width:900px; margin:24px auto; padding:0 16px; }
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; }
h2 { margin:0 0 14px; font-size:20px; }
.row { display:grid; grid-template-columns:140px 1fr; gap:8px; margin:8px 0; align-items:center; }
input, textarea { width:100%; box-sizing:border-box; border:1px solid #c7d3e3; border-radius:8px; padding:8px 10px; font-size:14px; }
textarea { min-height:100px; resize:vertical; }
.btns { display:flex; gap:10px; margin-top:12px; }
button { border:0; border-radius:8px; padding:9px 14px; cursor:pointer; color:#fff; background:var(--brand); }
button.secondary { background:#475569; }
pre { margin-top:12px; background:#0f172a; color:#e2e8f0; padding:12px; border-radius:8px; overflow:auto; }
label.chk { display:flex; gap:8px; align-items:center; font-size:14px; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h2>AI链路模拟测试</h2>
<div class="row"><div>店铺acc_id</div><input id="acc_id" value="demo_shop:cs" /></div>
<div class="row"><div>客户customer_id</div><input id="customer_id" value="demo_user_001" /></div>
<div class="row"><div>商品标题</div><input id="goods_name" value="模糊图清晰处理专业代找原图素材淘宝图片找图修复服务" /></div>
<div class="row"><div>订单信息</div><input id="goods_order" value="" placeholder="可空" /></div>
<div class="row"><div>客户消息</div><textarea id="msg" placeholder="可直接粘贴图片URL支持 #*# 拼接"></textarea></div>
<div class="row"><div>选项</div><label class="chk"><input type="checkbox" id="simulate_draw" />quote时顺带模拟作图</label></div>
<div class="btns">
<button onclick="sendMsg()">发送模拟消息</button>
<button class="secondary" onclick="resetCtx()">清空该客户上下文</button>
</div>
<pre id="out">等待发送...</pre>
</div>
</div>
<script>
async function sendMsg() {
const body = {
acc_id: document.getElementById('acc_id').value.trim(),
customer_id: document.getElementById('customer_id').value.trim(),
goods_name: document.getElementById('goods_name').value.trim(),
goods_order: document.getElementById('goods_order').value.trim(),
msg: document.getElementById('msg').value.trim(),
simulate_draw: document.getElementById('simulate_draw').checked
};
const res = await fetch('/api/simulate/message', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
const data = await res.json();
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
}
async function resetCtx() {
const body = {
acc_id: document.getElementById('acc_id').value.trim(),
customer_id: document.getElementById('customer_id').value.trim(),
};
const res = await fetch('/api/simulate/reset', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
const data = await res.json();
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
}
</script>
</body>
</html>
"""
@app.post('/api/simulate/reset')
def sim_reset():
body = request.get_json(silent=True) or {}
acc_id = str(body.get("acc_id", "")).strip()
customer_id = str(body.get("customer_id", "")).strip()
if not acc_id or not customer_id:
return jsonify({'ok': False, 'error': 'acc_id and customer_id required'}), 400
key = f"{acc_id}:{customer_id}"
sim_pending_images.pop(key, None)
sim_recent_dialogue.pop(key, None)
try:
sim_orch.store.upsert_session(key, acc_id, customer_id, "pre_sales", {"after_sales_stage": "new"})
except Exception:
pass
return jsonify({'ok': True, 'customer_key': key, 'message': '上下文已清空'})
@app.post('/api/simulate/message')
def sim_message():
body = request.get_json(silent=True) or {}
acc_id = str(body.get("acc_id", "")).strip()
customer_id = str(body.get("customer_id", "")).strip()
goods_name = str(body.get("goods_name", "")).strip()
goods_order = str(body.get("goods_order", "")).strip()
msg = str(body.get("msg", "")).strip()
simulate_draw = bool(body.get("simulate_draw"))
if not acc_id or not customer_id or not msg:
return jsonify({'ok': False, 'error': 'acc_id, customer_id, msg required'}), 400
key = f"{acc_id}:{customer_id}"
urls = extract_image_urls(msg)
for u in urls:
if u not in sim_pending_images[key]:
sim_pending_images[key].append(u)
_sim_append_dialogue(key, "user", msg)
latest_image_url = urls[-1] if urls else (sim_pending_images[key][-1] if sim_pending_images[key] else "")
context = {
"customer_key": key,
"acc_id": acc_id,
"customer_id": customer_id,
"goods_name": goods_name,
"goods_order": goods_order,
"msg": msg,
"intent": "unknown",
"pending_images": len(sim_pending_images[key]),
"pending_image_urls": sim_pending_images[key][-5:],
"current_image_urls": urls[-3:],
"latest_image_url": latest_image_url,
"auto_quote_trigger": False,
"last_reply": "",
"recent_dialogue": sim_recent_dialogue[key][-12:],
}
route, decision, state = asyncio.run(sim_orch.decide(context))
if decision.reply:
_sim_append_dialogue(key, "assistant", decision.reply)
draw_result: dict = {}
if simulate_draw and decision.action == "quote" and latest_image_url:
draw_result = asyncio.run(
auto_draw_preview(
image_url=latest_image_url,
customer_id=customer_id,
requirement=msg,
)
)
logger.info("[模拟] key=%s route=%s action=%s", key, route, decision.action)
return jsonify(
{
"ok": True,
"customer_key": key,
"route": route,
"decision": {
"action": decision.action,
"reply": decision.reply,
"transfer_msg": decision.transfer_msg,
"quote_mode": decision.quote_mode,
"reason": decision.reason,
},
"state": state,
"pending_images": sim_pending_images[key],
"draw_result": draw_result,
}
)
return app