"""Compose knowledge, freight and model output into the existing SSE protocol.""" import json import re from . import ai_client from .ai_client import compress_answer, gen_followups, has_chinese from .config import get_setting from .db import connect, last_user_question, load_context, save_message from .knowledge import search_knowledge from .pricing import (parse_shipping_intent, detect_dim_unit, freight_reply, calc_warehouse_card, detect_surcharge, surcharge_reply, check_island) from .tracking import CARD_PREFIX, extract_tracking_no, build_track def sse(payload): """统一的事件格式:data: {"t":正文} / {"e":错误} / {"card":卡片} / {"d":1结束} JSON 编码可以避免正文里的换行把 SSE 帧撑断。""" return 'data: ' + json.dumps(payload, ensure_ascii=False) + '\n\n' def create_chat_stream(msg, conv_id): if not msg: return (sse({'e': '问题不能为空'}), {}) if '[图片]' in msg and len(msg) < 300: return (sse({'t': '收到您的图片,请描述一下您的问题。'}) + sse({'d': 1}), {}) track_no = extract_tracking_no(re.sub(r'\[(?:图片|文件)\]/uploads/\S+', '', msg)) if track_no: card = build_track(track_no) def track_stream(): save_message(conv_id, 'user', msg) yield sse({'card': card}) save_message(conv_id, 'assistant', CARD_PREFIX + json.dumps(card, ensure_ascii=False)) yield sse({'d': 1}) return (track_stream(), {'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) if not get_setting('DEEPSEEK_API_KEY'): return (sse({'e': '服务未配置 API Key,请在 .env 或环境变量中设置 DEEPSEEK_API_KEY 后重启服务'}), {}) know_text = search_knowledge(msg) ship = parse_shipping_intent(msg) ship['dim_unit'] = detect_dim_unit(msg) freight_text = None if not ship['weight'] and (not ship['dims']) and ship['channel']: prev_q = last_user_question(conv_id) if prev_q: prev_ship = parse_shipping_intent(prev_q) if prev_ship['weight'] or prev_ship['dims']: ship['weight'] = prev_ship['weight'] ship['dims'] = prev_ship['dims'] ship['dim_unit'] = detect_dim_unit(prev_q) if ship['weight'] or ship['dims']: if re.search('英国|UK|伦敦|英格兰', msg, re.I): uk_map = {'sea': 'sea_uk', 'rail': 'rail_uk', 'truck': 'truck_uk', 'air': 'air_uk'} ship['channel'] = uk_map.get(ship['channel']) freight_text = freight_reply(ship) if not ship['channel']: freight_text += '\n\n(客户没有指定渠道,上面已同时给出海运和空运,请在回答末尾询问更看重时效还是成本)' extra_parts = [] cbm_m = re.search('(\\d+(?:\\.\\d+)?)\\s*(?:cbm|方|立方)', msg, re.I) if cbm_m: cbm = float(cbm_m.group(1)) ch = '海运' if re.search('卡航|卡车', msg): ch = '卡航' elif re.search('铁路|班列', msg): ch = '铁路' elif re.search('普卡', msg): ch = '普卡' want = None for kw, ctry in (('德国', 'DE'), ('法国', 'FR'), ('波兰', 'PL'), ('捷克', 'CZ'), ('西班牙', 'ES'), ('马德里', 'ES'), ('中国', '')): if kw in msg: want = ctry break card = calc_warehouse_card(cbm, ch, country=want) if card: store = card['warehouse'].splitlines()[0] extra_parts.append(f"【海外仓卡派 · 按方计费】\n 仓点:{store}\n 计费:{card['note']}\n 渠道:{card['channel']},适用档位 {card['tier']}\n 运费:{card['formula']}\n 说明:单箱实重超 35KG 加收 30 元/箱(封顶 3600 元);不接受仓储,特殊情况 85 元/箱/周") sur = detect_surcharge(msg) if sur: extra_parts.append(surcharge_reply(sur[0], sur[1], ship.get('weight'))) pc_m = re.search('(? limit: short = compress_answer(full_text, limit) if len(short) > limit: short = compress_answer(short, limit - 60) if short != full_text: full_text = short yield sse({'replace': short}) save_message(conv_id, 'assistant', full_text) if is_first_turn: try: fu = gen_followups(msg, full_text) if fu: yield sse({'suggest': fu}) except Exception: pass yield sse({'d': 1}) except GeneratorExit: interrupted = True raise except Exception: interrupted = True yield sse({'e': '传输中断,请点击重试'}) finally: text = ''.join(parts) if interrupted and len(text) >= 20: save_message(conv_id, 'assistant', text + '\n\n(回答中断,以上内容未生成完整)') resp.close() return (generate(), {'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})