@@ -48,8 +48,8 @@ class AiProvider(models.Model):
|
||||
temperature = fields.Float(string='温度', default=0.7)
|
||||
timeout = fields.Integer(
|
||||
string='超时(秒)',
|
||||
default=20,
|
||||
help='AI 单次请求的最大等待秒数,超过后返回超时错误。默认 20 秒。'
|
||||
default=60,
|
||||
help='AI 单次请求的最大等待秒数(读取超时),超过后返回超时错误。连接超时固定 10 秒。默认 60 秒。'
|
||||
)
|
||||
active = fields.Boolean(string='启用', default=True)
|
||||
enable_tools = fields.Boolean(
|
||||
@@ -521,9 +521,9 @@ class AiConversation(models.Model):
|
||||
'message': f'报表生成失败,请稍后重试或联系管理员检查服务端日志。'
|
||||
}
|
||||
|
||||
def _call_chat_completion(self, provider, messages, tools=None):
|
||||
def _call_chat_completion(self, provider, messages, tools=None, retry=2):
|
||||
"""封装一次 HTTP 调用,返回 (assistant_message_dict, error_str)。
|
||||
同时记录 Token 使用量。
|
||||
同时记录 Token 使用量。支持自动重试瞬态网络错误。
|
||||
"""
|
||||
payload = {
|
||||
'model': provider.model_name,
|
||||
@@ -534,41 +534,66 @@ class AiConversation(models.Model):
|
||||
if tools:
|
||||
payload['tools'] = tools
|
||||
payload['tool_choice'] = 'auto'
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{provider.api_base}/chat/completions",
|
||||
headers={
|
||||
'Authorization': f'Bearer {provider.api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json=payload,
|
||||
timeout=max(provider.timeout or 20, 1),
|
||||
)
|
||||
if not resp.ok:
|
||||
_logger.error(
|
||||
f"DeepSeek API 返回 {resp.status_code}: {resp.text[:1000]}"
|
||||
|
||||
read_timeout = max(provider.timeout or 60, 10)
|
||||
last_error = None
|
||||
|
||||
for attempt in range(retry + 1):
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{provider.api_base}/chat/completions",
|
||||
headers={
|
||||
'Authorization': f'Bearer {provider.api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json=payload,
|
||||
timeout=(10, read_timeout), # (connect_timeout, read_timeout)
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
# 记录 Token 使用量
|
||||
usage = data.get('usage') or {}
|
||||
if usage:
|
||||
self.env['ai.token.usage'].sudo().create({
|
||||
'user_id': self.env.uid,
|
||||
'conversation_id': self.id,
|
||||
'provider_id': provider.id,
|
||||
'model_name': provider.model_name,
|
||||
'prompt_tokens': usage.get('prompt_tokens', 0),
|
||||
'completion_tokens': usage.get('completion_tokens', 0),
|
||||
'total_tokens': usage.get('total_tokens', 0),
|
||||
})
|
||||
return data['choices'][0]['message'], None
|
||||
except requests.exceptions.Timeout:
|
||||
return None, f'错误:AI 响应超时(超过 {provider.timeout or 20} 秒),请重试或在服务商配置中调高超时。'
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f'错误:API 调用失败 — {e}'
|
||||
except (KeyError, IndexError, ValueError) as e:
|
||||
return None, f'错误:解析 AI 响应失败 — {e}'
|
||||
if not resp.ok:
|
||||
_logger.error(
|
||||
"AI API 返回 %s: %s", resp.status_code, resp.text[:500]
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
# 记录 Token 使用量
|
||||
usage = data.get('usage') or {}
|
||||
if usage:
|
||||
self.env['ai.token.usage'].sudo().create({
|
||||
'user_id': self.env.uid,
|
||||
'conversation_id': self.id,
|
||||
'provider_id': provider.id,
|
||||
'model_name': provider.model_name,
|
||||
'prompt_tokens': usage.get('prompt_tokens', 0),
|
||||
'completion_tokens': usage.get('completion_tokens', 0),
|
||||
'total_tokens': usage.get('total_tokens', 0),
|
||||
})
|
||||
return data['choices'][0]['message'], None
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
last_error = f'AI 响应超时(超过 {read_timeout} 秒)'
|
||||
if attempt < retry:
|
||||
_logger.warning(
|
||||
'%s,第 %s/%s 次重试...', last_error, attempt + 1, retry
|
||||
)
|
||||
continue
|
||||
return None, f'错误:{last_error},已重试 {retry} 次仍失败。请在服务商配置中调高超时时间或检查网络。'
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
last_error = f'无法连接到 AI 服务 ({e})'
|
||||
if attempt < retry:
|
||||
_logger.warning(
|
||||
'%s,第 %s/%s 次重试...', last_error, attempt + 1, retry
|
||||
)
|
||||
continue
|
||||
return None, f'错误:{last_error},已重试 {retry} 次仍失败。请检查服务器能否访问外网 API。'
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f'错误:API 调用失败 — {e}'
|
||||
|
||||
except (KeyError, IndexError, ValueError) as e:
|
||||
return None, f'错误:解析 AI 响应失败 — {e}'
|
||||
|
||||
return None, f'错误:{last_error}'
|
||||
|
||||
def _convert_records_timezone(self, records, model_name, fields_list):
|
||||
"""将查询结果中的 datetime 字段从 UTC 转换为用户时区"""
|
||||
|
||||
Reference in New Issue
Block a user