feat: low-latency debounce, context logs, and stable draw/upload config
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
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:
@@ -26,7 +26,7 @@ async def auto_draw_preview(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("[作图] 开始 customer=%s image=%s", customer_id, image_url)
|
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
|
from services.service_tuhui_upload import upload_to_tuhui # type: ignore
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("[作图] 依赖加载失败: %s", e)
|
logger.error("[作图] 依赖加载失败: %s", e)
|
||||||
@@ -56,7 +56,7 @@ async def auto_draw_preview(
|
|||||||
logger.info("[作图] 原图下载完成 size=%s", len(resp.content))
|
logger.info("[作图] 原图下载完成 size=%s", len(resp.content))
|
||||||
|
|
||||||
logger.info("[作图] Gemini 生成中")
|
logger.info("[作图] Gemini 生成中")
|
||||||
service = GeminiExtractV2Service()
|
service = GeminiExtractStableService()
|
||||||
ok_extract, msg_extract, _ = await service.extract_pattern(
|
ok_extract, msg_extract, _ = await service.extract_pattern(
|
||||||
input_path=input_path,
|
input_path=input_path,
|
||||||
output_path=output_path,
|
output_path=output_path,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .config import (
|
|||||||
AUTO_DRAW_ENABLED,
|
AUTO_DRAW_ENABLED,
|
||||||
AUTO_QUOTE_WAIT_SECONDS,
|
AUTO_QUOTE_WAIT_SECONDS,
|
||||||
DECISION_TIMEOUT_SECONDS,
|
DECISION_TIMEOUT_SECONDS,
|
||||||
|
IMAGE_MESSAGE_DEBOUNCE_SECONDS,
|
||||||
MAX_CONCURRENT_TURNS,
|
MAX_CONCURRENT_TURNS,
|
||||||
MESSAGE_DEBOUNCE_SECONDS,
|
MESSAGE_DEBOUNCE_SECONDS,
|
||||||
QINGJIAN_WS_URI,
|
QINGJIAN_WS_URI,
|
||||||
@@ -133,7 +134,7 @@ class QingjianClient:
|
|||||||
|
|
||||||
def _debounce_seconds(self, msg: str) -> float:
|
def _debounce_seconds(self, msg: str) -> float:
|
||||||
if extract_image_urls(msg):
|
if extract_image_urls(msg):
|
||||||
return 2.5
|
return float(IMAGE_MESSAGE_DEBOUNCE_SECONDS)
|
||||||
return float(MESSAGE_DEBOUNCE_SECONDS)
|
return float(MESSAGE_DEBOUNCE_SECONDS)
|
||||||
|
|
||||||
async def send_message(self, message: dict) -> None:
|
async def send_message(self, message: dict) -> None:
|
||||||
@@ -271,7 +272,7 @@ class QingjianClient:
|
|||||||
def _fallback_reply(self, action: str) -> str:
|
def _fallback_reply(self, action: str) -> str:
|
||||||
if action == "transfer":
|
if action == "transfer":
|
||||||
return "我先给你转人工处理。"
|
return "我先给你转人工处理。"
|
||||||
return "收到,我先处理一下。"
|
return "我先看看"
|
||||||
|
|
||||||
def _is_outbound_echo(self, data: dict, msg: str) -> bool:
|
def _is_outbound_echo(self, data: dict, msg: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -338,6 +339,20 @@ class QingjianClient:
|
|||||||
"last_reply": self.last_reply_key.get(key, ""),
|
"last_reply": self.last_reply_key.get(key, ""),
|
||||||
"recent_dialogue": recent_dialogue[-12:],
|
"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"])
|
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)
|
route, decision, state = await self.orchestrator.decide(context)
|
||||||
|
|||||||
@@ -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_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()
|
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"))
|
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"}
|
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"))
|
SHORT_REPLY_MAX_CHARS = int(os.getenv("SHORT_REPLY_MAX_CHARS", "18"))
|
||||||
|
|
||||||
|
|||||||
122
qingjian_cs/services/service_gemini_stable.py
Normal file
122
qingjian_cs/services/service_gemini_stable.py
Normal file
@@ -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
|
||||||
@@ -7,7 +7,7 @@ from urllib.parse import urljoin
|
|||||||
import requests
|
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_PHONE = os.getenv("TUHUI_PHONE", "17520145271").strip()
|
||||||
TUHUI_PASSWORD = os.getenv("TUHUI_PASSWORD", "zuowei1216").strip()
|
TUHUI_PASSWORD = os.getenv("TUHUI_PASSWORD", "zuowei1216").strip()
|
||||||
TUHUI_DEFAULT_PRICE = int(os.getenv("TUHUI_DEFAULT_PRICE", "20"))
|
TUHUI_DEFAULT_PRICE = int(os.getenv("TUHUI_DEFAULT_PRICE", "20"))
|
||||||
@@ -104,4 +104,3 @@ async def upload_to_tuhui(
|
|||||||
price,
|
price,
|
||||||
category,
|
category,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user