Files
tw2/qingjian_cs/app/http_api.py
jimi b663c7acbf
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
feat: add simulator page and image quote analyzer
2026-03-03 12:41:28 +08:00

248 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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():
return jsonify({'ok': True})
@app.get('/api/runtime/listen_only')
def get_listen_only():
return jsonify({'ok': True, 'listen_only': is_listen_only()})
@app.post('/api/runtime/listen_only')
def set_listen_only_mode():
body = request.get_json(silent=True) or {}
if "enabled" not in body:
return jsonify({'ok': False, 'error': 'enabled required'}), 400
enabled = bool(body.get("enabled"))
current = set_listen_only(enabled)
logger.info('[运行时] listen_only=%s', current)
return jsonify({'ok': True, 'listen_only': current})
@app.post('/api/task/receive')
def receive_task():
payload = request.get_json(silent=True) or {}
task_id = tm.create_task(payload)
logger.info('[任务] receive task_id=%s', task_id)
return jsonify({'ok': True, 'task_id': task_id})
@app.post('/api/task/cancel')
def cancel_task():
body = request.get_json(silent=True) or {}
task_id = str(body.get('task_id', '')).strip()
if not task_id:
return jsonify({'ok': False, 'error': 'task_id required'}), 400
ok = tm.cancel_task(task_id)
return jsonify({'ok': ok, 'task_id': task_id})
@app.get('/api/task/status/<task_id>')
def task_status(task_id: str):
task = tm.get_task(task_id)
if not task:
return jsonify({'ok': False, 'error': 'not found'}), 404
return jsonify({'ok': True, 'task': task})
@app.get('/api/task/list')
def task_list():
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
def run_http_server(host: str, port: int, task_manager: TaskManager | None = None) -> None:
app = create_http_app(task_manager=task_manager)
app.run(host=host, port=port, debug=False, use_reloader=False)