fix: improve api error diagnostics and raise model quotas

This commit is contained in:
JOJO 2026-03-06 17:02:19 +08:00
parent 868640b479
commit 877bcc2fad
6 changed files with 133 additions and 36 deletions

View File

@ -13,12 +13,12 @@ CODE_EXECUTION_TIMEOUT = 60
TERMINAL_COMMAND_TIMEOUT = 30 TERMINAL_COMMAND_TIMEOUT = 30
SEARCH_MAX_RESULTS = 10 SEARCH_MAX_RESULTS = 10
# 自动修复与工具调用限制 # 自动修复与工具调用限制None 表示不限制)
AUTO_FIX_TOOL_CALL = False AUTO_FIX_TOOL_CALL = False
AUTO_FIX_MAX_ATTEMPTS = 3 AUTO_FIX_MAX_ATTEMPTS = 3
MAX_ITERATIONS_PER_TASK = 100 MAX_ITERATIONS_PER_TASK = None
MAX_CONSECUTIVE_SAME_TOOL = 50 MAX_CONSECUTIVE_SAME_TOOL = None
MAX_TOTAL_TOOL_CALLS = 100 MAX_TOTAL_TOOL_CALLS = None
TOOL_CALL_COOLDOWN = 0.5 TOOL_CALL_COOLDOWN = 0.5
THINKING_FAST_INTERVAL = 10 THINKING_FAST_INTERVAL = 10

View File

@ -12,8 +12,8 @@ QuotaKey = Literal["fast", "thinking", "search"]
QUOTA_DEFAULTS = { QUOTA_DEFAULTS = {
"default": { "default": {
"fast": {"limit": 50, "window_hours": 5}, "fast": {"limit": 200, "window_hours": 5},
"thinking": {"limit": 20, "window_hours": 5}, "thinking": {"limit": 200, "window_hours": 5},
"search": {"limit": 20, "window_hours": 24}, "search": {"limit": 20, "window_hours": 24},
}, },
"search_daily": {"limit": 20, "window_hours": 24}, "search_daily": {"limit": 20, "window_hours": 24},

View File

@ -695,7 +695,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
last_tool_call_time = 0 last_tool_call_time = 0
detected_tool_intent: Dict[str, str] = {} detected_tool_intent: Dict[str, str] = {}
# 设置最大迭代次数 # 设置最大迭代次数None 表示不限制
max_iterations = MAX_ITERATIONS_PER_TASK max_iterations = MAX_ITERATIONS_PER_TASK
pending_append = None # {"path": str, "tool_call_id": str, "buffer": str, ...} pending_append = None # {"path": str, "tool_call_id": str, "buffer": str, ...}
@ -1337,12 +1337,16 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
}) })
maybe_mark_failure_from_message(web_terminal, message) maybe_mark_failure_from_message(web_terminal, message)
for iteration in range(max_iterations): iteration = 0
while max_iterations is None or iteration < max_iterations:
current_iteration = iteration + 1
iteration += 1
total_iterations += 1 total_iterations += 1
debug_log(f"\n--- 迭代 {iteration + 1}/{max_iterations} 开始 ---") iteration_limit_label = max_iterations if max_iterations is not None else ""
debug_log(f"\n--- 迭代 {current_iteration}/{iteration_limit_label} 开始 ---")
# 检查是否超过总工具调用限制 # 检查是否超过总工具调用限制
if total_tool_calls >= MAX_TOTAL_TOOL_CALLS: if MAX_TOTAL_TOOL_CALLS is not None and total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})") debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})")
sender('system_message', { sender('system_message', {
'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。' 'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。'
@ -1403,7 +1407,8 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
}) })
return return
print(f"[API] 第{iteration + 1}次调用 (总工具调用: {total_tool_calls}/{MAX_TOTAL_TOOL_CALLS})") tool_call_limit_label = MAX_TOTAL_TOOL_CALLS if MAX_TOTAL_TOOL_CALLS is not None else ""
print(f"[API] 第{current_iteration}次调用 (总工具调用: {total_tool_calls}/{tool_call_limit_label})")
# 收集流式响应 # 收集流式响应
async for chunk in web_terminal.api_client.chat(messages, tools, stream=True): async for chunk in web_terminal.api_client.chat(messages, tools, stream=True):
@ -1677,7 +1682,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
text_chunk_index += 1 text_chunk_index += 1
log_backend_chunk( log_backend_chunk(
conversation_id, conversation_id,
iteration + 1, current_iteration,
text_chunk_index, text_chunk_index,
elapsed, elapsed,
len(content), len(content),
@ -2099,7 +2104,10 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
if tool_name == last_tool_name: if tool_name == last_tool_name:
consecutive_same_tool[tool_name] += 1 consecutive_same_tool[tool_name] += 1
if consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL: if (
MAX_CONSECUTIVE_SAME_TOOL is not None
and consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL
):
debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL}") debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL}")
sender('system_message', { sender('system_message', {
'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。' 'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。'

View File

@ -48,7 +48,7 @@ from core.web_terminal import WebTerminal
from utils.tool_result_formatter import format_tool_result_for_context from utils.tool_result_formatter import format_tool_result_for_context
from utils.conversation_manager import ConversationManager from utils.conversation_manager import ConversationManager
from utils.api_client import DeepSeekClient from utils.api_client import DeepSeekClient
from config.model_profiles import get_model_context_window from config.model_profiles import get_model_context_window, get_model_profile
from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker
@ -425,7 +425,12 @@ def process_message_task(terminal: WebTerminal, message: str, images, sender, cl
debug_log(f"任务处理错误: {e}") debug_log(f"任务处理错误: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
sender('error', {'message': str(e)}) sender('error', {
'message': str(e),
'conversation_id': getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None),
'task_id': getattr(terminal, "task_id", None) or client_sid,
'client_sid': client_sid
})
sender('task_complete', { sender('task_complete', {
'total_iterations': 0, 'total_iterations': 0,
'total_tool_calls': 0, 'total_tool_calls': 0,
@ -469,6 +474,25 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
web_terminal = terminal web_terminal = terminal
conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None) conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
videos = videos or [] videos = videos or []
raw_sender = sender
def sender(event_type, data):
"""为关键事件补充会话标识,便于前端定位报错归属。"""
if not isinstance(data, dict):
raw_sender(event_type, data)
return
payload = data
if event_type in {"error", "quota_exceeded", "task_stopped", "task_complete"}:
payload = dict(data)
current_conv = conversation_id or getattr(web_terminal.context_manager, "current_conversation_id", None)
if current_conv:
payload.setdefault("conversation_id", current_conv)
task_id = getattr(web_terminal, "task_id", None) or client_sid
if task_id:
payload.setdefault("task_id", task_id)
if client_sid:
payload.setdefault("client_sid", client_sid)
raw_sender(event_type, payload)
# 如果是思考模式,重置状态 # 如果是思考模式,重置状态
if web_terminal.thinking_mode: if web_terminal.thinking_mode:
@ -562,8 +586,9 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
last_tool_call_time = 0 last_tool_call_time = 0
detected_tool_intent: Dict[str, str] = {} detected_tool_intent: Dict[str, str] = {}
# 设置最大迭代次数API 可覆盖) # 设置最大迭代次数API 可覆盖None 表示不限制
max_iterations = getattr(web_terminal, "max_iterations_override", None) or MAX_ITERATIONS_PER_TASK max_iterations_override = getattr(web_terminal, "max_iterations_override", None)
max_iterations = max_iterations_override if max_iterations_override is not None else MAX_ITERATIONS_PER_TASK
max_api_retries = 4 max_api_retries = 4
retry_delay_seconds = 10 retry_delay_seconds = 10
@ -1225,12 +1250,16 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
return False return False
for iteration in range(max_iterations): iteration = 0
while max_iterations is None or iteration < max_iterations:
current_iteration = iteration + 1
iteration += 1
total_iterations += 1 total_iterations += 1
debug_log(f"\n--- 迭代 {iteration + 1}/{max_iterations} 开始 ---") iteration_limit_label = max_iterations if max_iterations is not None else ""
debug_log(f"\n--- 迭代 {current_iteration}/{iteration_limit_label} 开始 ---")
# 检查是否超过总工具调用限制 # 检查是否超过总工具调用限制
if total_tool_calls >= MAX_TOTAL_TOOL_CALLS: if MAX_TOTAL_TOOL_CALLS is not None and total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})") debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})")
sender('system_message', { sender('system_message', {
'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。' 'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS}),任务结束。'
@ -1317,7 +1346,8 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
}) })
return return
print(f"[API] 第{iteration + 1}次调用 (总工具调用: {total_tool_calls}/{MAX_TOTAL_TOOL_CALLS})") tool_call_limit_label = MAX_TOTAL_TOOL_CALLS if MAX_TOTAL_TOOL_CALLS is not None else ""
print(f"[API] 第{current_iteration}次调用 (总工具调用: {total_tool_calls}/{tool_call_limit_label})")
api_error = None api_error = None
for api_attempt in range(max_api_retries + 1): for api_attempt in range(max_api_retries + 1):
@ -1612,7 +1642,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
text_chunk_index += 1 text_chunk_index += 1
log_backend_chunk( log_backend_chunk(
conversation_id, conversation_id,
iteration + 1, current_iteration,
text_chunk_index, text_chunk_index,
elapsed, elapsed,
len(content), len(content),
@ -1746,14 +1776,38 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
if api_error: if api_error:
try:
debug_log(f"API错误原始数据: {json.dumps(api_error, ensure_ascii=False)}")
except Exception:
debug_log(f"API错误原始数据(不可序列化): {repr(api_error)}")
error_message = "" error_message = ""
error_status = None error_status = None
error_type = None error_type = None
error_code = None
error_text = ""
request_dump = None
error_base_url = None
error_model_id = None
if isinstance(api_error, dict): if isinstance(api_error, dict):
error_status = api_error.get("status_code") error_status = api_error.get("status_code")
error_type = api_error.get("error_type") error_type = api_error.get("error_type") or api_error.get("type")
error_message = api_error.get("error_message") or api_error.get("error_text") or "" error_code = api_error.get("error_code") or api_error.get("code")
error_text = api_error.get("error_text") or ""
error_message = (
api_error.get("error_message")
or api_error.get("message")
or error_text
or ""
)
request_dump = api_error.get("request_dump")
error_base_url = api_error.get("base_url")
error_model_id = api_error.get("model_id")
elif isinstance(api_error, str):
error_message = api_error
if not error_message: if not error_message:
if error_status:
error_message = f"API 请求失败HTTP {error_status}"
else:
error_message = "API 请求失败" error_message = "API 请求失败"
# 若命中阿里云配额错误,立即写入状态并切换到官方 API # 若命中阿里云配额错误,立即写入状态并切换到官方 API
try: try:
@ -1777,6 +1831,11 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
'message': error_message, 'message': error_message,
'status_code': error_status, 'status_code': error_status,
'error_type': error_type, 'error_type': error_type,
'error_code': error_code,
'error_text': error_text,
'request_dump': request_dump,
'base_url': error_base_url,
'model_id': error_model_id,
'retry': bool(can_retry), 'retry': bool(can_retry),
'retry_in': retry_delay_seconds if can_retry else None, 'retry_in': retry_delay_seconds if can_retry else None,
'attempt': api_attempt + 1, 'attempt': api_attempt + 1,
@ -2092,7 +2151,10 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
if tool_name == last_tool_name: if tool_name == last_tool_name:
consecutive_same_tool[tool_name] += 1 consecutive_same_tool[tool_name] += 1
if consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL: if (
MAX_CONSECUTIVE_SAME_TOOL is not None
and consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL
):
debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL}") debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL}")
sender('system_message', { sender('system_message', {
'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。' 'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次,可能存在循环。'

View File

@ -1409,14 +1409,28 @@ export async function initializeLegacySocket(ctx: any) {
const msg = data?.message || '发生未知错误'; const msg = data?.message || '发生未知错误';
const code = data?.status_code; const code = data?.status_code;
const errType = data?.error_type; const errType = data?.error_type;
const errCode = data?.error_code;
const dumpPath = data?.request_dump;
const baseUrl = data?.base_url;
const modelId = data?.model_id;
const conversationId = data?.conversation_id;
const taskId = data?.task_id;
const shouldRetry = Boolean(data?.retry); const shouldRetry = Boolean(data?.retry);
const retryIn = Number(data?.retry_in) || 5; const retryIn = Number(data?.retry_in) || 5;
const retryAttempt = Number(data?.attempt) || 1; const retryAttempt = Number(data?.attempt) || 1;
const retryMax = Number(data?.max_attempts) || retryAttempt; const retryMax = Number(data?.max_attempts) || retryAttempt;
const detailParts = [
dumpPath ? `请求记录: ${dumpPath}` : '',
baseUrl ? `接口: ${baseUrl}` : '',
modelId ? `模型: ${modelId}` : '',
conversationId ? `对话ID: ${conversationId}` : '',
taskId ? `任务ID: ${taskId}` : ''
].filter(Boolean);
const detailText = detailParts.length ? `\n${detailParts.join('\n')}` : '';
if (typeof ctx.uiPushToast === 'function') { if (typeof ctx.uiPushToast === 'function') {
ctx.uiPushToast({ ctx.uiPushToast({
title: code ? `API错误 ${code}` : 'API错误', title: code ? `API错误 ${code}` : 'API错误',
message: errType ? `${errType}: ${msg}` : msg, message: `${errType ? `${errType}${errCode ? `(${errCode})` : ''}: ${msg}` : msg}${detailText}`,
type: 'error', type: 'error',
duration: 6000 duration: 6000
}); });
@ -1437,6 +1451,12 @@ export async function initializeLegacySocket(ctx: any) {
return; return;
} }
if (typeof ctx.appendSystemAction === 'function') {
ctx.appendSystemAction(
`${code ? `[API ${code}] ` : '[API] '}${errType ? `${errType}${errCode ? `(${errCode})` : ''}: ` : ''}${msg}${detailText}`
);
}
// 最后一次报错:恢复输入状态并清理提示动画 // 最后一次报错:恢复输入状态并清理提示动画
const msgIndex = typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : -1; const msgIndex = typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : -1;
if (msgIndex >= 0 && Array.isArray(ctx.messages)) { if (msgIndex >= 0 && Array.isArray(ctx.messages)) {

View File

@ -727,13 +727,18 @@ class DeepSeekClient:
self.last_error_info = None self.last_error_info = None
yield response.json() yield response.json()
except httpx.ConnectError: except httpx.ConnectError as e:
self._print(f"{OUTPUT_FORMATS['error']} 无法连接到API服务器请检查网络连接") connect_detail = str(e).strip() or repr(e)
self._print(
f"{OUTPUT_FORMATS['error']} 无法连接到API服务器请检查网络连接"
f"{connect_detail}"
)
self.last_error_info = { self.last_error_info = {
"status_code": None, "status_code": None,
"error_text": "connect_error", "error_text": "connect_error",
"error_type": "connection_error", "error_type": "connection_error",
"error_message": "无法连接到API服务器", "error_message": f"无法连接到API服务器: {connect_detail}",
"error_detail": connect_detail,
"request_dump": str(dump_path), "request_dump": str(dump_path),
"base_url": api_config.get("base_url"), "base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"), "model_id": api_config.get("model_id"),
@ -744,12 +749,13 @@ class DeepSeekClient:
"event": "connect_error", "event": "connect_error",
"status_code": None, "status_code": None,
"error_text": "connect_error", "error_text": "connect_error",
"error_detail": connect_detail,
"base_url": api_config.get("base_url"), "base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"), "model_id": api_config.get("model_id"),
"model_key": self.model_key, "model_key": self.model_key,
"request_dump": str(dump_path) "request_dump": str(dump_path)
}) })
self._mark_request_error(dump_path, error_text="connect_error") self._mark_request_error(dump_path, error_text=f"connect_error: {connect_detail}")
yield {"error": self.last_error_info} yield {"error": self.last_error_info}
except httpx.TimeoutException: except httpx.TimeoutException:
self._print(f"{OUTPUT_FORMATS['error']} API请求超时") self._print(f"{OUTPUT_FORMATS['error']} API请求超时")
@ -776,12 +782,13 @@ class DeepSeekClient:
self._mark_request_error(dump_path, error_text="timeout") self._mark_request_error(dump_path, error_text="timeout")
yield {"error": self.last_error_info} yield {"error": self.last_error_info}
except Exception as e: except Exception as e:
self._print(f"{OUTPUT_FORMATS['error']} API调用异常: {e}") error_text = str(e).strip() or repr(e)
self._print(f"{OUTPUT_FORMATS['error']} API调用异常: {error_text}")
self.last_error_info = { self.last_error_info = {
"status_code": None, "status_code": None,
"error_text": str(e), "error_text": error_text,
"error_type": "exception", "error_type": "exception",
"error_message": str(e), "error_message": error_text,
"request_dump": str(dump_path), "request_dump": str(dump_path),
"base_url": api_config.get("base_url"), "base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"), "model_id": api_config.get("model_id"),
@ -791,13 +798,13 @@ class DeepSeekClient:
self._debug_log({ self._debug_log({
"event": "exception", "event": "exception",
"status_code": None, "status_code": None,
"error_text": str(e), "error_text": error_text,
"base_url": api_config.get("base_url"), "base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"), "model_id": api_config.get("model_id"),
"model_key": self.model_key, "model_key": self.model_key,
"request_dump": str(dump_path) "request_dump": str(dump_path)
}) })
self._mark_request_error(dump_path, error_text=str(e)) self._mark_request_error(dump_path, error_text=error_text)
yield {"error": self.last_error_info} yield {"error": self.last_error_info}
async def chat_with_tools( async def chat_with_tools(