From 01c32be6ea76da3d1f725026b88e69de1c23eb55 Mon Sep 17 00:00:00 2001 From: jimi <1847930177@qq.com> Date: Tue, 3 Mar 2026 13:38:18 +0800 Subject: [PATCH] feat: low-latency debounce, context logs, and stable draw/upload config --- qingjian_cs/app/auto_draw.py | 4 +- qingjian_cs/app/client.py | 19 ++- qingjian_cs/app/config.py | 5 +- qingjian_cs/services/service_gemini_stable.py | 122 ++++++++++++++++++ qingjian_cs/services/service_tuhui_upload.py | 3 +- 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 qingjian_cs/services/service_gemini_stable.py diff --git a/qingjian_cs/app/auto_draw.py b/qingjian_cs/app/auto_draw.py index 0584f09..fca0ab8 100644 --- a/qingjian_cs/app/auto_draw.py +++ b/qingjian_cs/app/auto_draw.py @@ -26,7 +26,7 @@ async def auto_draw_preview( """ try: logger.info("[作图] 开始 customer=%s image=%s", customer_id, image_url) - from services.service_gemini import GeminiExtractV2Service # type: ignore + from services.service_gemini_stable import GeminiExtractStableService # type: ignore from services.service_tuhui_upload import upload_to_tuhui # type: ignore except Exception as e: logger.error("[作图] 依赖加载失败: %s", e) @@ -56,7 +56,7 @@ async def auto_draw_preview( logger.info("[作图] 原图下载完成 size=%s", len(resp.content)) logger.info("[作图] Gemini 生成中") - service = GeminiExtractV2Service() + service = GeminiExtractStableService() ok_extract, msg_extract, _ = await service.extract_pattern( input_path=input_path, output_path=output_path, diff --git a/qingjian_cs/app/client.py b/qingjian_cs/app/client.py index 1f8aae9..8e477e3 100644 --- a/qingjian_cs/app/client.py +++ b/qingjian_cs/app/client.py @@ -14,6 +14,7 @@ from .config import ( AUTO_DRAW_ENABLED, AUTO_QUOTE_WAIT_SECONDS, DECISION_TIMEOUT_SECONDS, + IMAGE_MESSAGE_DEBOUNCE_SECONDS, MAX_CONCURRENT_TURNS, MESSAGE_DEBOUNCE_SECONDS, QINGJIAN_WS_URI, @@ -133,7 +134,7 @@ class QingjianClient: def _debounce_seconds(self, msg: str) -> float: if extract_image_urls(msg): - return 2.5 + return float(IMAGE_MESSAGE_DEBOUNCE_SECONDS) return float(MESSAGE_DEBOUNCE_SECONDS) async def send_message(self, message: dict) -> None: @@ -271,7 +272,7 @@ class QingjianClient: def _fallback_reply(self, action: str) -> str: if action == "transfer": return "我先给你转人工处理。" - return "收到,我先处理一下。" + return "我先看看" def _is_outbound_echo(self, data: dict, msg: str) -> bool: """ @@ -338,6 +339,20 @@ class QingjianClient: "last_reply": self.last_reply_key.get(key, ""), "recent_dialogue": recent_dialogue[-12:], } + try: + rd = context.get("recent_dialogue", []) or [] + rd_preview = " | ".join( + [f"{str(x.get('role',''))}:{str(x.get('text',''))[:32]}" for x in rd[-8:] if isinstance(x, dict)] + ) + self.logger.info( + "[AI上下文] customer=%s msg=%s pending_images=%s recent=%s", + context["customer_id"], + str(merged_msg or "")[:120], + context["pending_images"], + rd_preview, + ) + except Exception: + pass activity_event(self.logger, "agent_process_start", trace_id=trace_id, customer_id=context["customer_id"], acc_id=context["acc_id"], intent=context["intent"]) route, decision, state = await self.orchestrator.decide(context) diff --git a/qingjian_cs/app/config.py b/qingjian_cs/app/config.py index 29e8d47..bc1bee1 100644 --- a/qingjian_cs/app/config.py +++ b/qingjian_cs/app/config.py @@ -12,9 +12,10 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3").strip() OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "doubao-seed-2-0-pro-260215").strip() -MESSAGE_DEBOUNCE_SECONDS = int(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "6")) +MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "0.8")) +IMAGE_MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("IMAGE_MESSAGE_DEBOUNCE_SECONDS", "1.2")) AUTO_QUOTE_WAIT_SECONDS = int(os.getenv("AUTO_QUOTE_WAIT_SECONDS", "18")) -AGENT_MAX_ITERS = int(os.getenv("AGENT_MAX_ITERS", "3")) +AGENT_MAX_ITERS = int(os.getenv("AGENT_MAX_ITERS", "1")) FAST_ROUTE_ENABLED = os.getenv("FAST_ROUTE_ENABLED", "1").strip() in {"1", "true", "True", "yes", "on"} SHORT_REPLY_MAX_CHARS = int(os.getenv("SHORT_REPLY_MAX_CHARS", "18")) diff --git a/qingjian_cs/services/service_gemini_stable.py b/qingjian_cs/services/service_gemini_stable.py new file mode 100644 index 0000000..d6b3aab --- /dev/null +++ b/qingjian_cs/services/service_gemini_stable.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import asyncio +import base64 +import logging +import os +from typing import Any + +import aiohttp + +logger = logging.getLogger(__name__) + +GEMINI_API_KEY = "sk-YOUR_NEW_KEY" +GEMINI_API_URL = "https://api.laozhang.ai/v1beta/models" +GEMINI_MODEL = "gemini-3.1-flash-image-preview" +GEMINI_IMAGE_SIZE = "4K" +GEMINI_MAX_RETRIES = 3 + +DEFAULT_PROMPT = ( + "提取印花图案,去褶皱并补齐缺失区域,生成完整清晰的平面图。" + "严格保持原图元素位置、颜色和细节,不要改风格。" +) + + +class GeminiExtractStableService: + def image_to_base64(self, image_path: str) -> str | None: + try: + if not os.path.exists(image_path): + logger.error("文件不存在: %s", image_path) + return None + with open(image_path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + except Exception as e: + logger.error("Base64转换失败: %s", e) + return None + + async def extract_pattern( + self, + input_path: str, + output_path: str, + custom_prompt: str | None = None, + aspect_ratio: str = "1:1", + ) -> tuple[bool, str, dict[str, Any]]: + img64 = self.image_to_base64(input_path) + if not img64: + return False, "图片编码失败", {} + + prompt = custom_prompt or DEFAULT_PROMPT + api_url = f"{GEMINI_API_URL}/{GEMINI_MODEL}:generateContent" + valid_ratios = {"1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5"} + image_config: dict[str, Any] = {"imageSize": GEMINI_IMAGE_SIZE} + if aspect_ratio in valid_ratios: + image_config["aspectRatio"] = aspect_ratio + + headers = { + "Authorization": f"Bearer {GEMINI_API_KEY}", + "Content-Type": "application/json", + } + payload = { + "contents": [ + { + "parts": [ + {"text": prompt}, + ] + } + ], + "generationConfig": { + "responseModalities": ["IMAGE"], + "imageConfig": image_config, + }, + } + + logger.info("[稳定作图] model=%s ratio=%s size=%s", GEMINI_MODEL, image_config.get("aspectRatio", "1:1"), GEMINI_IMAGE_SIZE) + timeout = aiohttp.ClientTimeout(total=180, connect=30) + for i in range(1, GEMINI_MAX_RETRIES + 1): + try: + logger.info("[稳定作图] 请求中 第%s/%s次", i, GEMINI_MAX_RETRIES) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post(api_url, headers=headers, json=payload) as resp: + txt = await resp.text() + if resp.status != 200: + logger.error("[稳定作图] 请求失败 status=%s body=%s", resp.status, txt[:240]) + if i < GEMINI_MAX_RETRIES: + await asyncio.sleep(i) + continue + return False, f"API失败:{resp.status}", {} + result = await resp.json() + + try: + parts = result["candidates"][0]["content"]["parts"] + b64 = "" + for p in parts: + if "inlineData" in p and isinstance(p["inlineData"], dict): + b64 = str(p["inlineData"].get("data", "") or "") + if b64: + break + if not b64: + logger.error("[稳定作图] 未找到 inlineData") + return False, "未找到图片数据", {} + image_data = base64.b64decode(b64) + except Exception as e: + logger.error("[稳定作图] 解析返回失败: %s", e) + return False, f"解析失败:{e}", {} + + out_dir = os.path.dirname(output_path) + if out_dir: + os.makedirs(out_dir, exist_ok=True) + with open(output_path, "wb") as f: + f.write(image_data) + logger.info("[稳定作图] 输出成功: %s", output_path) + return True, "ok", {"output_path": output_path, "file_size": len(image_data), "api_used": "gemini_stable"} + except Exception as e: + logger.error("[稳定作图] 异常: %s", e) + if i < GEMINI_MAX_RETRIES: + await asyncio.sleep(i) + continue + return False, f"异常:{e}", {} + return False, "未知失败", {} + + async def cleanup(self) -> None: + return None diff --git a/qingjian_cs/services/service_tuhui_upload.py b/qingjian_cs/services/service_tuhui_upload.py index ae50e05..a802327 100644 --- a/qingjian_cs/services/service_tuhui_upload.py +++ b/qingjian_cs/services/service_tuhui_upload.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin import requests -TUHUI_BASE_URL = os.getenv("TUHUI_BASE_URL", "http://127.0.0.1:8002").strip() +TUHUI_BASE_URL = os.getenv("TUHUI_BASE_URL", "https://tuhui.cloud").strip() TUHUI_PHONE = os.getenv("TUHUI_PHONE", "17520145271").strip() TUHUI_PASSWORD = os.getenv("TUHUI_PASSWORD", "zuowei1216").strip() TUHUI_DEFAULT_PRICE = int(os.getenv("TUHUI_DEFAULT_PRICE", "20")) @@ -104,4 +104,3 @@ async def upload_to_tuhui( price, category, ) -