Fix Zep retries and frontend API proxy
This commit is contained in:
parent
fa0f6519b1
commit
69e7cbacea
|
|
@ -10,10 +10,12 @@ Zep检索工具服务
|
|||
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from zep_cloud.client import Zep
|
||||
from zep_cloud.core.api_error import ApiError
|
||||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
|
|
@ -23,6 +25,55 @@ from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
|
|||
|
||||
logger = get_logger('mirofish.zep_tools')
|
||||
|
||||
_MAX_RATE_LIMIT_DELAY = 60.0
|
||||
|
||||
|
||||
def _is_rate_limit_error(error: Exception) -> bool:
|
||||
status_code = getattr(error, "status_code", None)
|
||||
if status_code == 429:
|
||||
return True
|
||||
|
||||
body = getattr(error, "body", None)
|
||||
if isinstance(body, str) and "rate limit" in body.lower():
|
||||
return True
|
||||
|
||||
text = str(error).lower()
|
||||
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text
|
||||
|
||||
|
||||
def _parse_retry_after(error: Exception) -> float | None:
|
||||
headers = getattr(error, "headers", None)
|
||||
if isinstance(headers, dict):
|
||||
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||
if retry_after:
|
||||
try:
|
||||
return max(float(retry_after), 0.0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
|
||||
if reset:
|
||||
try:
|
||||
reset_seconds = float(reset)
|
||||
if reset_seconds > 1_000_000_000:
|
||||
wait_seconds = reset_seconds - time.time()
|
||||
else:
|
||||
wait_seconds = reset_seconds
|
||||
if wait_seconds > 0:
|
||||
return wait_seconds
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
text = str(error)
|
||||
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
|
||||
if match:
|
||||
try:
|
||||
return max(float(match.group(1)), 0.0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
|
|
@ -448,14 +499,32 @@ class ZepToolsService:
|
|||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func()
|
||||
except Exception as e:
|
||||
except (Exception, ApiError) as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(
|
||||
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
|
||||
)
|
||||
if _is_rate_limit_error(e):
|
||||
retry_after = _parse_retry_after(e)
|
||||
if retry_after is not None:
|
||||
delay = min(max(retry_after, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
|
||||
else:
|
||||
delay = min(max(delay, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
|
||||
else:
|
||||
delay = min(delay, _MAX_RATE_LIMIT_DELAY)
|
||||
|
||||
if _is_rate_limit_error(e):
|
||||
logger.warning(
|
||||
f"Zep {operation_name} rate limit hit (attempt {attempt + 1}/{max_retries}); "
|
||||
f"retrying in {delay:.1f}s..."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
|
||||
)
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
if _is_rate_limit_error(e):
|
||||
delay = min(delay * 1.25, _MAX_RATE_LIMIT_DELAY)
|
||||
else:
|
||||
delay = min(delay * 2, _MAX_RATE_LIMIT_DELAY)
|
||||
else:
|
||||
logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e)))
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ Zep 的 node/edge 列表接口使用 UUID cursor 分页,
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from zep_cloud import InternalServerError
|
||||
from zep_cloud.core.api_error import ApiError
|
||||
from zep_cloud.client import Zep
|
||||
|
||||
from .logger import get_logger
|
||||
|
|
@ -19,8 +21,57 @@ logger = get_logger('mirofish.zep_paging')
|
|||
|
||||
_DEFAULT_PAGE_SIZE = 100
|
||||
_MAX_NODES = 2000
|
||||
_DEFAULT_MAX_RETRIES = 3
|
||||
_DEFAULT_MAX_RETRIES = 5
|
||||
_DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry
|
||||
_MAX_RETRY_DELAY = 60.0
|
||||
|
||||
|
||||
def _is_rate_limit_error(error: Exception) -> bool:
|
||||
status_code = getattr(error, "status_code", None)
|
||||
if status_code == 429:
|
||||
return True
|
||||
|
||||
body = getattr(error, "body", None)
|
||||
if isinstance(body, str) and "rate limit" in body.lower():
|
||||
return True
|
||||
|
||||
text = str(error).lower()
|
||||
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text
|
||||
|
||||
|
||||
def _parse_retry_after(error: Exception) -> float | None:
|
||||
headers = getattr(error, "headers", None)
|
||||
if isinstance(headers, dict):
|
||||
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||
if retry_after:
|
||||
try:
|
||||
return max(float(retry_after), 0.0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
|
||||
if reset:
|
||||
try:
|
||||
reset_seconds = float(reset)
|
||||
if reset_seconds > 1_000_000_000:
|
||||
# Some providers return a unix timestamp instead of a delta.
|
||||
wait_seconds = reset_seconds - time.time()
|
||||
else:
|
||||
wait_seconds = reset_seconds
|
||||
if wait_seconds > 0:
|
||||
return wait_seconds
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
text = str(error)
|
||||
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
|
||||
if match:
|
||||
try:
|
||||
return max(float(match.group(1)), 0.0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_page_with_retry(
|
||||
|
|
@ -41,14 +92,33 @@ def _fetch_page_with_retry(
|
|||
for attempt in range(max_retries):
|
||||
try:
|
||||
return api_call(*args, **kwargs)
|
||||
except (ConnectionError, TimeoutError, OSError, InternalServerError) as e:
|
||||
except (ConnectionError, TimeoutError, OSError, InternalServerError, ApiError) as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(
|
||||
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
|
||||
)
|
||||
if _is_rate_limit_error(e):
|
||||
retry_after = _parse_retry_after(e)
|
||||
if retry_after is not None:
|
||||
delay = min(max(retry_after, retry_delay), _MAX_RETRY_DELAY)
|
||||
else:
|
||||
delay = min(max(delay, retry_delay), _MAX_RETRY_DELAY)
|
||||
else:
|
||||
delay = min(delay, _MAX_RETRY_DELAY)
|
||||
|
||||
if _is_rate_limit_error(e):
|
||||
logger.warning(
|
||||
f"Zep {page_description} rate limit hit (attempt {attempt + 1}/{max_retries}); "
|
||||
f"retrying in {delay:.1f}s..."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
|
||||
)
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
if _is_rate_limit_error(e):
|
||||
# Respect server-advised retry delays; keep the same delay or back off slightly.
|
||||
delay = min(delay * 1.25, _MAX_RETRY_DELAY)
|
||||
else:
|
||||
delay = min(delay * 2, _MAX_RETRY_DELAY)
|
||||
else:
|
||||
logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,9 @@ import service, { requestWithRetry } from './index'
|
|||
export function generateOntology(formData) {
|
||||
return requestWithRetry(() =>
|
||||
service({
|
||||
url: '/api/graph/ontology/generate',
|
||||
url: '/graph/ontology/generate',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
data: formData
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
@ -26,7 +23,7 @@ export function generateOntology(formData) {
|
|||
export function buildGraph(data) {
|
||||
return requestWithRetry(() =>
|
||||
service({
|
||||
url: '/api/graph/build',
|
||||
url: '/graph/build',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
|
@ -40,7 +37,7 @@ export function buildGraph(data) {
|
|||
*/
|
||||
export function getTaskStatus(taskId) {
|
||||
return service({
|
||||
url: `/api/graph/task/${taskId}`,
|
||||
url: `/graph/task/${taskId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -52,7 +49,7 @@ export function getTaskStatus(taskId) {
|
|||
*/
|
||||
export function getGraphData(graphId) {
|
||||
return service({
|
||||
url: `/api/graph/data/${graphId}`,
|
||||
url: `/graph/data/${graphId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -64,7 +61,7 @@ export function getGraphData(graphId) {
|
|||
*/
|
||||
export function getProject(projectId) {
|
||||
return service({
|
||||
url: `/api/graph/project/${projectId}`,
|
||||
url: `/graph/project/${projectId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ import i18n from '../i18n'
|
|||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
|
||||
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
// Default to the Vite proxy so the frontend works both on localhost
|
||||
// and when accessed from another machine via the dev server host.
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 300000 // 5分钟超时(本体生成可能需要较长时间)
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
config.headers['Accept-Language'] = i18n.global.locale.value
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type']
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
|
|||
* @param {Object} data - { simulation_id, force_regenerate? }
|
||||
*/
|
||||
export const generateReport = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/report/generate', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/report/generate', data), 3, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -13,7 +13,7 @@ export const generateReport = (data) => {
|
|||
* @param {string} reportId
|
||||
*/
|
||||
export const getReportStatus = (reportId) => {
|
||||
return service.get(`/api/report/generate/status`, { params: { report_id: reportId } })
|
||||
return service.get(`/report/generate/status`, { params: { report_id: reportId } })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,7 +22,7 @@ export const getReportStatus = (reportId) => {
|
|||
* @param {number} fromLine - 从第几行开始获取
|
||||
*/
|
||||
export const getAgentLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
|
||||
return service.get(`/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +31,7 @@ export const getAgentLog = (reportId, fromLine = 0) => {
|
|||
* @param {number} fromLine - 从第几行开始获取
|
||||
*/
|
||||
export const getConsoleLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } })
|
||||
return service.get(`/report/${reportId}/console-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -39,7 +39,7 @@ export const getConsoleLog = (reportId, fromLine = 0) => {
|
|||
* @param {string} reportId
|
||||
*/
|
||||
export const getReport = (reportId) => {
|
||||
return service.get(`/api/report/${reportId}`)
|
||||
return service.get(`/report/${reportId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,5 +47,5 @@ export const getReport = (reportId) => {
|
|||
* @param {Object} data - { simulation_id, message, chat_history? }
|
||||
*/
|
||||
export const chatWithReport = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/report/chat', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/report/chat', data), 3, 1000)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
|
|||
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
|
||||
*/
|
||||
export const createSimulation = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/simulation/create', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/simulation/create', data), 3, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -13,7 +13,7 @@ export const createSimulation = (data) => {
|
|||
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
|
||||
*/
|
||||
export const prepareSimulation = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/simulation/prepare', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/simulation/prepare', data), 3, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,7 +21,7 @@ export const prepareSimulation = (data) => {
|
|||
* @param {Object} data - { task_id?, simulation_id? }
|
||||
*/
|
||||
export const getPrepareStatus = (data) => {
|
||||
return service.post('/api/simulation/prepare/status', data)
|
||||
return service.post('/simulation/prepare/status', data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +29,7 @@ export const getPrepareStatus = (data) => {
|
|||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulation = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}`)
|
||||
return service.get(`/simulation/${simulationId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,7 +38,7 @@ export const getSimulation = (simulationId) => {
|
|||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
|
||||
return service.get(`/api/simulation/${simulationId}/profiles`, { params: { platform } })
|
||||
return service.get(`/simulation/${simulationId}/profiles`, { params: { platform } })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +47,7 @@ export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
|
|||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => {
|
||||
return service.get(`/api/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
|
||||
return service.get(`/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,7 +55,7 @@ export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit')
|
|||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulationConfig = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/config`)
|
||||
return service.get(`/simulation/${simulationId}/config`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,7 +64,7 @@ export const getSimulationConfig = (simulationId) => {
|
|||
* @returns {Promise} 返回配置信息,包含元数据和配置内容
|
||||
*/
|
||||
export const getSimulationConfigRealtime = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/config/realtime`)
|
||||
return service.get(`/simulation/${simulationId}/config/realtime`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,7 +73,7 @@ export const getSimulationConfigRealtime = (simulationId) => {
|
|||
*/
|
||||
export const listSimulations = (projectId) => {
|
||||
const params = projectId ? { project_id: projectId } : {}
|
||||
return service.get('/api/simulation/list', { params })
|
||||
return service.get('/simulation/list', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,7 +81,7 @@ export const listSimulations = (projectId) => {
|
|||
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
|
||||
*/
|
||||
export const startSimulation = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/simulation/start', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/simulation/start', data), 3, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,7 +89,7 @@ export const startSimulation = (data) => {
|
|||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const stopSimulation = (data) => {
|
||||
return service.post('/api/simulation/stop', data)
|
||||
return service.post('/simulation/stop', data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,7 +97,7 @@ export const stopSimulation = (data) => {
|
|||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatus = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/run-status`)
|
||||
return service.get(`/simulation/${simulationId}/run-status`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,7 +105,7 @@ export const getRunStatus = (simulationId) => {
|
|||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatusDetail = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/run-status/detail`)
|
||||
return service.get(`/simulation/${simulationId}/run-status/detail`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,7 +116,7 @@ export const getRunStatusDetail = (simulationId) => {
|
|||
* @param {number} offset - 偏移量
|
||||
*/
|
||||
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
|
||||
return service.get(`/api/simulation/${simulationId}/posts`, {
|
||||
return service.get(`/simulation/${simulationId}/posts`, {
|
||||
params: { platform, limit, offset }
|
||||
})
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
|
|||
if (endRound !== null) {
|
||||
params.end_round = endRound
|
||||
}
|
||||
return service.get(`/api/simulation/${simulationId}/timeline`, { params })
|
||||
return service.get(`/simulation/${simulationId}/timeline`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,7 +140,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
|
|||
* @param {string} simulationId
|
||||
*/
|
||||
export const getAgentStats = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/agent-stats`)
|
||||
return service.get(`/simulation/${simulationId}/agent-stats`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,7 +149,7 @@ export const getAgentStats = (simulationId) => {
|
|||
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
|
||||
*/
|
||||
export const getSimulationActions = (simulationId, params = {}) => {
|
||||
return service.get(`/api/simulation/${simulationId}/actions`, { params })
|
||||
return service.get(`/simulation/${simulationId}/actions`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,7 +157,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
|
|||
* @param {Object} data - { simulation_id, timeout? }
|
||||
*/
|
||||
export const closeSimulationEnv = (data) => {
|
||||
return service.post('/api/simulation/close-env', data)
|
||||
return service.post('/simulation/close-env', data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -165,7 +165,7 @@ export const closeSimulationEnv = (data) => {
|
|||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const getEnvStatus = (data) => {
|
||||
return service.post('/api/simulation/env-status', data)
|
||||
return service.post('/simulation/env-status', data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +173,7 @@ export const getEnvStatus = (data) => {
|
|||
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
|
||||
*/
|
||||
export const interviewAgents = (data) => {
|
||||
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000)
|
||||
return requestWithRetry(() => service.post('/simulation/interview/batch', data), 3, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,6 +182,5 @@ export const interviewAgents = (data) => {
|
|||
* @param {number} limit - 返回数量限制
|
||||
*/
|
||||
export const getSimulationHistory = (limit = 20) => {
|
||||
return service.get('/api/simulation/history', { params: { limit } })
|
||||
return service.get('/simulation/history', { params: { limit } })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="rateLimitNotice" class="retry-notice">
|
||||
<div class="retry-notice__icon">⟳</div>
|
||||
<div class="retry-notice__body">
|
||||
<div class="retry-notice__title">External API rate limit detected</div>
|
||||
<div class="retry-notice__text">{{ rateLimitNotice.message }}</div>
|
||||
</div>
|
||||
<button class="retry-notice__dismiss" @click="dismissRetryNotice">Dismiss</button>
|
||||
</div>
|
||||
|
||||
<div class="workflow-steps" v-if="workflowSteps.length > 0">
|
||||
<div
|
||||
v-for="(step, sidx) in workflowSteps"
|
||||
|
|
@ -418,6 +427,8 @@ const agentLogs = ref([])
|
|||
const consoleLogs = ref([])
|
||||
const agentLogLine = ref(0)
|
||||
const consoleLogLine = ref(0)
|
||||
const rateLimitNotice = ref(null)
|
||||
let rateLimitNoticeTimer = null
|
||||
const reportOutline = ref(null)
|
||||
const currentSectionIndex = ref(null)
|
||||
const generatedSections = ref({})
|
||||
|
|
@ -2017,6 +2028,52 @@ const getLogLevelClass = (log) => {
|
|||
return ''
|
||||
}
|
||||
|
||||
const clearRetryNoticeTimer = () => {
|
||||
if (rateLimitNoticeTimer) {
|
||||
clearTimeout(rateLimitNoticeTimer)
|
||||
rateLimitNoticeTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const dismissRetryNotice = () => {
|
||||
rateLimitNotice.value = null
|
||||
clearRetryNoticeTimer()
|
||||
}
|
||||
|
||||
const setRetryNotice = (message, retrySeconds = null) => {
|
||||
rateLimitNotice.value = {
|
||||
message,
|
||||
retrySeconds
|
||||
}
|
||||
|
||||
clearRetryNoticeTimer()
|
||||
if (retrySeconds && retrySeconds > 0) {
|
||||
rateLimitNoticeTimer = setTimeout(() => {
|
||||
rateLimitNotice.value = null
|
||||
rateLimitNoticeTimer = null
|
||||
}, Math.min(retrySeconds * 1000 + 1000, 120000))
|
||||
}
|
||||
}
|
||||
|
||||
const maybeHandleRetryNotice = (logLine) => {
|
||||
if (!logLine) return
|
||||
|
||||
const text = String(logLine)
|
||||
const rateLimitMatch = text.match(/rate limit hit.*retrying in ([\d.]+)s/i)
|
||||
if (rateLimitMatch) {
|
||||
const retrySeconds = Number.parseFloat(rateLimitMatch[1])
|
||||
const retryText = Number.isFinite(retrySeconds)
|
||||
? `Zep is rate limited. The system will retry automatically in about ${retrySeconds.toFixed(1)} seconds.`
|
||||
: 'Zep is rate limited. The system will retry automatically.'
|
||||
setRetryNotice(retryText, Number.isFinite(retrySeconds) ? retrySeconds : null)
|
||||
return
|
||||
}
|
||||
|
||||
if (text.includes('Rate limit exceeded for FREE plan') || text.includes('429')) {
|
||||
setRetryNotice('Zep is rate limited. The system will retry automatically.', null)
|
||||
}
|
||||
}
|
||||
|
||||
// Polling
|
||||
let agentLogTimer = null
|
||||
let consoleLogTimer = null
|
||||
|
|
@ -2055,6 +2112,7 @@ const fetchAgentLog = async () => {
|
|||
if (log.action === 'report_complete') {
|
||||
isComplete.value = true
|
||||
currentSectionIndex.value = null // 确保清除 loading 状态
|
||||
dismissRetryNotice()
|
||||
emit('update-status', 'completed')
|
||||
stopPolling()
|
||||
// 滚动逻辑统一在循环结束后的 nextTick 中处理
|
||||
|
|
@ -2141,6 +2199,8 @@ const fetchConsoleLog = async () => {
|
|||
if (newLogs.length > 0) {
|
||||
consoleLogs.value.push(...newLogs)
|
||||
consoleLogLine.value = res.data.from_line + newLogs.length
|
||||
|
||||
newLogs.forEach((log) => maybeHandleRetryNotice(log))
|
||||
|
||||
nextTick(() => {
|
||||
if (logContent.value) {
|
||||
|
|
@ -2185,6 +2245,7 @@ onMounted(() => {
|
|||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
clearRetryNoticeTimer()
|
||||
})
|
||||
|
||||
watch(() => props.reportId, (newId) => {
|
||||
|
|
@ -2193,6 +2254,7 @@ watch(() => props.reportId, (newId) => {
|
|||
consoleLogs.value = []
|
||||
agentLogLine.value = 0
|
||||
consoleLogLine.value = 0
|
||||
dismissRetryNotice()
|
||||
reportOutline.value = null
|
||||
currentSectionIndex.value = null
|
||||
generatedSections.value = {}
|
||||
|
|
@ -2289,6 +2351,64 @@ watch(() => props.reportId, (newId) => {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.retry-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin: 12px 20px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #F59E0B;
|
||||
border-radius: 12px;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: #92400E;
|
||||
}
|
||||
|
||||
.retry-notice__icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.retry-notice__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.retry-notice__title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.retry-notice__text {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #B45309;
|
||||
}
|
||||
|
||||
.retry-notice__dismiss {
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #92400E;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.retry-notice__dismiss:hover {
|
||||
background: rgba(245, 158, 11, 0.24);
|
||||
}
|
||||
|
||||
/* Panel header status variants */
|
||||
.panel-header--active {
|
||||
background: #FAFAFA;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export default defineConfig({
|
|||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5001',
|
||||
|
|
|
|||
Loading…
Reference in New Issue