remove:无用文件
This commit is contained in:
@@ -56,7 +56,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config"
|
|||||||
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
|
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
|
||||||
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
|
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
|
||||||
MMC_VERSION: str = "1.0.0"
|
MMC_VERSION: str = "1.0.0"
|
||||||
CONFIG_VERSION: str = "8.5.0"
|
CONFIG_VERSION: str = "8.5.1"
|
||||||
MODEL_CONFIG_VERSION: str = "1.13.1"
|
MODEL_CONFIG_VERSION: str = "1.13.1"
|
||||||
|
|
||||||
logger = get_logger("config")
|
logger = get_logger("config")
|
||||||
@@ -74,21 +74,18 @@ class Config(ConfigBase):
|
|||||||
personality: PersonalityConfig = Field(default_factory=PersonalityConfig)
|
personality: PersonalityConfig = Field(default_factory=PersonalityConfig)
|
||||||
"""人格配置类"""
|
"""人格配置类"""
|
||||||
|
|
||||||
|
chat: ChatConfig = Field(default_factory=ChatConfig)
|
||||||
|
"""聊天配置类"""
|
||||||
|
|
||||||
visual: VisualConfig = Field(default_factory=VisualConfig)
|
visual: VisualConfig = Field(default_factory=VisualConfig)
|
||||||
"""视觉配置类"""
|
"""视觉配置类"""
|
||||||
|
|
||||||
expression: ExpressionConfig = Field(default_factory=ExpressionConfig)
|
expression: ExpressionConfig = Field(default_factory=ExpressionConfig)
|
||||||
"""表达配置类"""
|
"""表达配置类"""
|
||||||
|
|
||||||
chat: ChatConfig = Field(default_factory=ChatConfig)
|
|
||||||
"""聊天配置类"""
|
|
||||||
|
|
||||||
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
||||||
"""记忆配置类"""
|
"""记忆配置类"""
|
||||||
|
|
||||||
relationship: RelationshipConfig = Field(default_factory=RelationshipConfig)
|
|
||||||
"""关系配置类"""
|
|
||||||
|
|
||||||
message_receive: MessageReceiveConfig = Field(default_factory=MessageReceiveConfig)
|
message_receive: MessageReceiveConfig = Field(default_factory=MessageReceiveConfig)
|
||||||
"""消息接收配置类"""
|
"""消息接收配置类"""
|
||||||
|
|
||||||
|
|||||||
@@ -173,21 +173,6 @@ class VisualConfig(ConfigBase):
|
|||||||
"""_wrap_识图提示词,不建议修改"""
|
"""_wrap_识图提示词,不建议修改"""
|
||||||
|
|
||||||
|
|
||||||
class RelationshipConfig(ConfigBase):
|
|
||||||
"""关系配置类"""
|
|
||||||
|
|
||||||
__ui_parent__ = "debug"
|
|
||||||
|
|
||||||
enable_relationship: bool = Field(
|
|
||||||
default=True,
|
|
||||||
json_schema_extra={
|
|
||||||
"x-widget": "switch",
|
|
||||||
"x-icon": "heart",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
"""是否启用关系系统,关系系统被移除,此部分配置暂时无效"""
|
|
||||||
|
|
||||||
|
|
||||||
class TalkRulesItem(ConfigBase):
|
class TalkRulesItem(ConfigBase):
|
||||||
platform: str = ""
|
platform: str = ""
|
||||||
"""平台,与ID一起留空表示全局"""
|
"""平台,与ID一起留空表示全局"""
|
||||||
@@ -244,6 +229,16 @@ class ChatConfig(ConfigBase):
|
|||||||
)
|
)
|
||||||
"""上下文长度"""
|
"""上下文长度"""
|
||||||
|
|
||||||
|
planner_interrupt_max_consecutive_count: int = Field(
|
||||||
|
default=2,
|
||||||
|
ge=0,
|
||||||
|
json_schema_extra={
|
||||||
|
"x-widget": "input",
|
||||||
|
"x-icon": "pause-circle",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
"""Planner 连续被新消息打断的最大次数,0 表示不启用打断"""
|
||||||
|
|
||||||
plan_reply_log_max_per_chat: int = Field(
|
plan_reply_log_max_per_chat: int = Field(
|
||||||
default=1024,
|
default=1024,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
@@ -1465,16 +1460,6 @@ class MaiSakaConfig(ConfigBase):
|
|||||||
)
|
)
|
||||||
"""MaiSaka 使用的用户名称"""
|
"""MaiSaka 使用的用户名称"""
|
||||||
|
|
||||||
planner_interrupt_max_consecutive_count: int = Field(
|
|
||||||
default=2,
|
|
||||||
ge=0,
|
|
||||||
json_schema_extra={
|
|
||||||
"x-widget": "input",
|
|
||||||
"x-icon": "pause-circle",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
"""Planner 连续被新消息打断的最大次数,0 表示不启用打断"""
|
|
||||||
|
|
||||||
tool_filter_task_name: str = Field(
|
tool_filter_task_name: str = Field(
|
||||||
default="utils",
|
default="utils",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class MaisakaHeartFlowChatting:
|
|||||||
self._planner_interrupt_consecutive_count = 0
|
self._planner_interrupt_consecutive_count = 0
|
||||||
self._planner_interrupt_max_consecutive_count = max(
|
self._planner_interrupt_max_consecutive_count = max(
|
||||||
0,
|
0,
|
||||||
int(global_config.maisaka.planner_interrupt_max_consecutive_count),
|
int(global_config.chat.planner_interrupt_max_consecutive_count),
|
||||||
)
|
)
|
||||||
|
|
||||||
expr_use, jargon_learn, expr_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id)
|
expr_use, jargon_learn, expr_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id)
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
"""
|
|
||||||
MaiSaka 工具处理器。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
|
||||||
|
|
||||||
import json as _json
|
|
||||||
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
from src.cli.console import console
|
|
||||||
from src.cli.input_reader import InputReader
|
|
||||||
from src.llm_models.payload_content.tool_option import ToolCall
|
|
||||||
|
|
||||||
from .context_messages import LLMContextMessage, ToolResultMessage
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from src.mcp_module import MCPManager
|
|
||||||
|
|
||||||
|
|
||||||
class ToolHandlerContext:
|
|
||||||
"""工具处理器共享上下文。"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
reader: InputReader,
|
|
||||||
user_input_times: list[datetime],
|
|
||||||
) -> None:
|
|
||||||
self.reader = reader
|
|
||||||
self.user_input_times = user_input_times
|
|
||||||
self.last_user_input_time: Optional[datetime] = None
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_wait(tc: ToolCall, chat_history: list[LLMContextMessage], ctx: ToolHandlerContext) -> str:
|
|
||||||
"""处理 wait 工具。"""
|
|
||||||
seconds = (tc.args or {}).get("seconds", 30)
|
|
||||||
seconds = max(5, min(seconds, 300))
|
|
||||||
console.print(f"[accent]调用工具: wait({seconds})[/accent]")
|
|
||||||
|
|
||||||
tool_result = await _do_wait(seconds, ctx)
|
|
||||||
chat_history.append(
|
|
||||||
ToolResultMessage(
|
|
||||||
content=tool_result,
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_call_id=tc.call_id,
|
|
||||||
tool_name=tc.func_name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return tool_result
|
|
||||||
|
|
||||||
|
|
||||||
async def _do_wait(seconds: int, ctx: ToolHandlerContext) -> str:
|
|
||||||
"""等待用户输入,支持超时。"""
|
|
||||||
console.print(f"[muted]等待用户输入中(超时: {seconds} 秒)...[/muted]")
|
|
||||||
console.print("[bold magenta]> [/bold magenta]", end="")
|
|
||||||
|
|
||||||
user_input = await ctx.reader.get_line(timeout=seconds)
|
|
||||||
|
|
||||||
if user_input is None:
|
|
||||||
console.print()
|
|
||||||
console.print("[muted]等待超时[/muted]")
|
|
||||||
return "等待超时,未收到用户输入。"
|
|
||||||
|
|
||||||
user_input = user_input.strip()
|
|
||||||
if not user_input:
|
|
||||||
return "用户提交了空输入。"
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
ctx.last_user_input_time = now
|
|
||||||
ctx.user_input_times.append(now)
|
|
||||||
|
|
||||||
if user_input.lower() in ("/quit", "/exit", "/q"):
|
|
||||||
return "[[QUIT]] 用户请求退出。"
|
|
||||||
|
|
||||||
return f"已收到用户输入: {user_input}"
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_mcp_tool(tc: ToolCall, chat_history: list[LLMContextMessage], mcp_manager: "MCPManager") -> None:
|
|
||||||
"""处理 MCP 工具调用。"""
|
|
||||||
args_str = _json.dumps(tc.args or {}, ensure_ascii=False)
|
|
||||||
args_preview = args_str if len(args_str) <= 120 else args_str[:120] + "..."
|
|
||||||
console.print(f"[accent]调用 MCP 工具: {tc.func_name}({args_preview})[/accent]")
|
|
||||||
|
|
||||||
with console.status(f"[info]正在执行 MCP 工具 {tc.func_name}...[/info]", spinner="dots"):
|
|
||||||
result = await mcp_manager.call_tool(tc.func_name, tc.args or {})
|
|
||||||
|
|
||||||
display_text = result if len(result) <= 800 else result[:800] + "\n...(已截断)"
|
|
||||||
console.print(
|
|
||||||
Panel(
|
|
||||||
display_text,
|
|
||||||
title=f"MCP 工具:{tc.func_name}",
|
|
||||||
border_style="bright_green",
|
|
||||||
padding=(0, 1),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
chat_history.append(
|
|
||||||
ToolResultMessage(
|
|
||||||
content=result,
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_call_id=tc.call_id,
|
|
||||||
tool_name=tc.func_name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_unknown_tool(tc: ToolCall, chat_history: list[LLMContextMessage]) -> None:
|
|
||||||
"""处理未知工具调用。"""
|
|
||||||
console.print(f"[accent]调用未知工具: {tc.func_name}({tc.args})[/accent]")
|
|
||||||
chat_history.append(
|
|
||||||
ToolResultMessage(
|
|
||||||
content=f"未知工具: {tc.func_name}",
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_call_id=tc.call_id,
|
|
||||||
tool_name=tc.func_name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
"""
|
|
||||||
规划器监控API
|
|
||||||
提供规划器日志数据的查询接口
|
|
||||||
|
|
||||||
性能优化:
|
|
||||||
1. 聊天摘要只统计文件数量和最新时间戳,不读取文件内容
|
|
||||||
2. 日志列表使用文件名解析时间戳,只在需要时读取完整内容
|
|
||||||
3. 详情按需加载
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from src.webui.dependencies import require_auth
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/planner", tags=["planner"], dependencies=[Depends(require_auth)])
|
|
||||||
|
|
||||||
# 规划器日志目录
|
|
||||||
PLAN_LOG_DIR = Path("logs/plan")
|
|
||||||
|
|
||||||
|
|
||||||
class ChatSummary(BaseModel):
|
|
||||||
"""聊天摘要 - 轻量级,不读取文件内容"""
|
|
||||||
|
|
||||||
chat_id: str
|
|
||||||
plan_count: int
|
|
||||||
latest_timestamp: float
|
|
||||||
latest_filename: str
|
|
||||||
|
|
||||||
|
|
||||||
class PlanLogSummary(BaseModel):
|
|
||||||
"""规划日志摘要"""
|
|
||||||
|
|
||||||
chat_id: str
|
|
||||||
timestamp: float
|
|
||||||
filename: str
|
|
||||||
action_count: int
|
|
||||||
action_types: List[str] # 动作类型列表
|
|
||||||
total_plan_ms: float
|
|
||||||
llm_duration_ms: float
|
|
||||||
reasoning_preview: str
|
|
||||||
|
|
||||||
|
|
||||||
class PlanLogDetail(BaseModel):
|
|
||||||
"""规划日志详情"""
|
|
||||||
|
|
||||||
type: str
|
|
||||||
chat_id: str
|
|
||||||
timestamp: float
|
|
||||||
prompt: str
|
|
||||||
reasoning: str
|
|
||||||
raw_output: str
|
|
||||||
actions: List[Dict]
|
|
||||||
timing: Dict
|
|
||||||
extra: Optional[Dict] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PlannerOverview(BaseModel):
|
|
||||||
"""规划器总览 - 轻量级统计"""
|
|
||||||
|
|
||||||
total_chats: int
|
|
||||||
total_plans: int
|
|
||||||
chats: List[ChatSummary]
|
|
||||||
|
|
||||||
|
|
||||||
class PaginatedChatLogs(BaseModel):
|
|
||||||
"""分页的聊天日志列表"""
|
|
||||||
|
|
||||||
data: List[PlanLogSummary]
|
|
||||||
total: int
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
chat_id: str
|
|
||||||
|
|
||||||
|
|
||||||
def parse_timestamp_from_filename(filename: str) -> float:
|
|
||||||
"""从文件名解析时间戳: 1766497488220_af92bdb1.json -> 1766497488.220"""
|
|
||||||
try:
|
|
||||||
timestamp_str = filename.split("_")[0]
|
|
||||||
# 时间戳是毫秒级,需要转换为秒
|
|
||||||
return float(timestamp_str) / 1000
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/overview", response_model=PlannerOverview)
|
|
||||||
async def get_planner_overview():
|
|
||||||
"""
|
|
||||||
获取规划器总览 - 轻量级接口
|
|
||||||
只统计文件数量,不读取文件内容
|
|
||||||
"""
|
|
||||||
if not PLAN_LOG_DIR.exists():
|
|
||||||
return PlannerOverview(total_chats=0, total_plans=0, chats=[])
|
|
||||||
|
|
||||||
chats = []
|
|
||||||
total_plans = 0
|
|
||||||
|
|
||||||
for chat_dir in PLAN_LOG_DIR.iterdir():
|
|
||||||
if not chat_dir.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 只统计json文件数量
|
|
||||||
json_files = list(chat_dir.glob("*.json"))
|
|
||||||
plan_count = len(json_files)
|
|
||||||
total_plans += plan_count
|
|
||||||
|
|
||||||
if plan_count == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 从文件名获取最新时间戳
|
|
||||||
latest_file = max(json_files, key=lambda f: parse_timestamp_from_filename(f.name))
|
|
||||||
latest_timestamp = parse_timestamp_from_filename(latest_file.name)
|
|
||||||
|
|
||||||
chats.append(
|
|
||||||
ChatSummary(
|
|
||||||
chat_id=chat_dir.name,
|
|
||||||
plan_count=plan_count,
|
|
||||||
latest_timestamp=latest_timestamp,
|
|
||||||
latest_filename=latest_file.name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 按最新时间戳排序
|
|
||||||
chats.sort(key=lambda x: x.latest_timestamp, reverse=True)
|
|
||||||
|
|
||||||
return PlannerOverview(total_chats=len(chats), total_plans=total_plans, chats=chats)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chat/{chat_id}/logs", response_model=PaginatedChatLogs)
|
|
||||||
async def get_chat_plan_logs(
|
|
||||||
chat_id: str,
|
|
||||||
page: int = Query(1, ge=1),
|
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
|
||||||
search: Optional[str] = Query(None, description="搜索关键词,匹配提示词内容"),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取指定聊天的规划日志列表(分页)
|
|
||||||
需要读取文件内容获取摘要信息
|
|
||||||
支持搜索提示词内容
|
|
||||||
"""
|
|
||||||
chat_dir = PLAN_LOG_DIR / chat_id
|
|
||||||
if not chat_dir.exists():
|
|
||||||
return PaginatedChatLogs(data=[], total=0, page=page, page_size=page_size, chat_id=chat_id)
|
|
||||||
|
|
||||||
# 先获取所有文件并按时间戳排序
|
|
||||||
json_files = list(chat_dir.glob("*.json"))
|
|
||||||
json_files.sort(key=lambda f: parse_timestamp_from_filename(f.name), reverse=True)
|
|
||||||
|
|
||||||
# 如果有搜索关键词,需要过滤文件
|
|
||||||
if search:
|
|
||||||
search_lower = search.lower()
|
|
||||||
filtered_files = []
|
|
||||||
for log_file in json_files:
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
prompt = data.get("prompt", "")
|
|
||||||
if search_lower in prompt.lower():
|
|
||||||
filtered_files.append(log_file)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
json_files = filtered_files
|
|
||||||
|
|
||||||
total = len(json_files)
|
|
||||||
|
|
||||||
# 分页 - 只读取当前页的文件
|
|
||||||
offset = (page - 1) * page_size
|
|
||||||
page_files = json_files[offset : offset + page_size]
|
|
||||||
|
|
||||||
logs = []
|
|
||||||
for log_file in page_files:
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
reasoning = data.get("reasoning", "")
|
|
||||||
actions = data.get("actions", [])
|
|
||||||
action_types = [a.get("action_type", "") for a in actions if a.get("action_type")]
|
|
||||||
logs.append(
|
|
||||||
PlanLogSummary(
|
|
||||||
chat_id=data.get("chat_id", chat_id),
|
|
||||||
timestamp=data.get("timestamp", parse_timestamp_from_filename(log_file.name)),
|
|
||||||
filename=log_file.name,
|
|
||||||
action_count=len(actions),
|
|
||||||
action_types=action_types,
|
|
||||||
total_plan_ms=data.get("timing", {}).get("total_plan_ms", 0),
|
|
||||||
llm_duration_ms=data.get("timing", {}).get("llm_duration_ms", 0),
|
|
||||||
reasoning_preview=reasoning[:100] if reasoning else "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# 文件读取失败时使用文件名信息
|
|
||||||
logs.append(
|
|
||||||
PlanLogSummary(
|
|
||||||
chat_id=chat_id,
|
|
||||||
timestamp=parse_timestamp_from_filename(log_file.name),
|
|
||||||
filename=log_file.name,
|
|
||||||
action_count=0,
|
|
||||||
action_types=[],
|
|
||||||
total_plan_ms=0,
|
|
||||||
llm_duration_ms=0,
|
|
||||||
reasoning_preview="[读取失败]",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return PaginatedChatLogs(data=logs, total=total, page=page, page_size=page_size, chat_id=chat_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/log/{chat_id}/{filename}", response_model=PlanLogDetail)
|
|
||||||
async def get_log_detail(chat_id: str, filename: str):
|
|
||||||
"""获取规划日志详情 - 按需加载完整内容"""
|
|
||||||
log_file = PLAN_LOG_DIR / chat_id / filename
|
|
||||||
if not log_file.exists():
|
|
||||||
raise HTTPException(status_code=404, detail="日志文件不存在")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return PlanLogDetail(**data)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"读取日志失败: {str(e)}") from e
|
|
||||||
|
|
||||||
|
|
||||||
# ========== 兼容旧接口 ==========
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
|
||||||
async def get_planner_stats():
|
|
||||||
"""获取规划器统计信息 - 兼容旧接口"""
|
|
||||||
overview = await get_planner_overview()
|
|
||||||
|
|
||||||
# 获取最近10条计划的摘要
|
|
||||||
recent_plans = []
|
|
||||||
for chat in overview.chats[:5]: # 从最近5个聊天中获取
|
|
||||||
try:
|
|
||||||
chat_logs = await get_chat_plan_logs(chat.chat_id, page=1, page_size=2)
|
|
||||||
recent_plans.extend(chat_logs.data)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 按时间排序取前10
|
|
||||||
recent_plans.sort(key=lambda x: x.timestamp, reverse=True)
|
|
||||||
recent_plans = recent_plans[:10]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_chats": overview.total_chats,
|
|
||||||
"total_plans": overview.total_plans,
|
|
||||||
"avg_plan_time_ms": 0,
|
|
||||||
"avg_llm_time_ms": 0,
|
|
||||||
"recent_plans": recent_plans,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chats")
|
|
||||||
async def get_chat_list():
|
|
||||||
"""获取所有聊天ID列表 - 兼容旧接口"""
|
|
||||||
overview = await get_planner_overview()
|
|
||||||
return [chat.chat_id for chat in overview.chats]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all-logs")
|
|
||||||
async def get_all_logs(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100)):
|
|
||||||
"""获取所有规划日志 - 兼容旧接口"""
|
|
||||||
if not PLAN_LOG_DIR.exists():
|
|
||||||
return {"data": [], "total": 0, "page": page, "page_size": page_size}
|
|
||||||
|
|
||||||
# 收集所有文件
|
|
||||||
all_files = []
|
|
||||||
for chat_dir in PLAN_LOG_DIR.iterdir():
|
|
||||||
if chat_dir.is_dir():
|
|
||||||
all_files.extend((chat_dir.name, log_file) for log_file in chat_dir.glob("*.json"))
|
|
||||||
|
|
||||||
# 按时间戳排序
|
|
||||||
all_files.sort(key=lambda x: parse_timestamp_from_filename(x[1].name), reverse=True)
|
|
||||||
|
|
||||||
total = len(all_files)
|
|
||||||
offset = (page - 1) * page_size
|
|
||||||
page_files = all_files[offset : offset + page_size]
|
|
||||||
|
|
||||||
logs = []
|
|
||||||
for chat_id, log_file in page_files:
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
reasoning = data.get("reasoning", "")
|
|
||||||
logs.append(
|
|
||||||
{
|
|
||||||
"chat_id": data.get("chat_id", chat_id),
|
|
||||||
"timestamp": data.get("timestamp", parse_timestamp_from_filename(log_file.name)),
|
|
||||||
"filename": log_file.name,
|
|
||||||
"action_count": len(data.get("actions", [])),
|
|
||||||
"total_plan_ms": data.get("timing", {}).get("total_plan_ms", 0),
|
|
||||||
"llm_duration_ms": data.get("timing", {}).get("llm_duration_ms", 0),
|
|
||||||
"reasoning_preview": reasoning[:100] if reasoning else "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {"data": logs, "total": total, "page": page, "page_size": page_size}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
"""
|
|
||||||
回复器监控API
|
|
||||||
提供回复器日志数据的查询接口
|
|
||||||
|
|
||||||
性能优化:
|
|
||||||
1. 聊天摘要只统计文件数量和最新时间戳,不读取文件内容
|
|
||||||
2. 日志列表使用文件名解析时间戳,只在需要时读取完整内容
|
|
||||||
3. 详情按需加载
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from src.webui.dependencies import require_auth
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/replier", tags=["replier"], dependencies=[Depends(require_auth)])
|
|
||||||
|
|
||||||
# 回复器日志目录
|
|
||||||
REPLY_LOG_DIR = Path("logs/reply")
|
|
||||||
|
|
||||||
|
|
||||||
class ReplierChatSummary(BaseModel):
|
|
||||||
"""聊天摘要 - 轻量级,不读取文件内容"""
|
|
||||||
|
|
||||||
chat_id: str
|
|
||||||
reply_count: int
|
|
||||||
latest_timestamp: float
|
|
||||||
latest_filename: str
|
|
||||||
|
|
||||||
|
|
||||||
class ReplyLogSummary(BaseModel):
|
|
||||||
"""回复日志摘要"""
|
|
||||||
|
|
||||||
chat_id: str
|
|
||||||
timestamp: float
|
|
||||||
filename: str
|
|
||||||
model: str
|
|
||||||
success: bool
|
|
||||||
llm_ms: float
|
|
||||||
overall_ms: float
|
|
||||||
output_preview: str
|
|
||||||
|
|
||||||
|
|
||||||
class ReplyLogDetail(BaseModel):
|
|
||||||
"""回复日志详情"""
|
|
||||||
|
|
||||||
type: str
|
|
||||||
chat_id: str
|
|
||||||
timestamp: float
|
|
||||||
prompt: str
|
|
||||||
output: str
|
|
||||||
processed_output: List[str]
|
|
||||||
model: str
|
|
||||||
reasoning: str
|
|
||||||
think_level: int
|
|
||||||
timing: Dict
|
|
||||||
error: Optional[str] = None
|
|
||||||
success: bool
|
|
||||||
|
|
||||||
|
|
||||||
class ReplierOverview(BaseModel):
|
|
||||||
"""回复器总览 - 轻量级统计"""
|
|
||||||
|
|
||||||
total_chats: int
|
|
||||||
total_replies: int
|
|
||||||
chats: List[ReplierChatSummary]
|
|
||||||
|
|
||||||
|
|
||||||
class PaginatedReplyLogs(BaseModel):
|
|
||||||
"""分页的回复日志列表"""
|
|
||||||
|
|
||||||
data: List[ReplyLogSummary]
|
|
||||||
total: int
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
chat_id: str
|
|
||||||
|
|
||||||
|
|
||||||
def parse_timestamp_from_filename(filename: str) -> float:
|
|
||||||
"""从文件名解析时间戳: 1766497488220_af92bdb1.json -> 1766497488.220"""
|
|
||||||
try:
|
|
||||||
timestamp_str = filename.split("_")[0]
|
|
||||||
# 时间戳是毫秒级,需要转换为秒
|
|
||||||
return float(timestamp_str) / 1000
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/overview", response_model=ReplierOverview)
|
|
||||||
async def get_replier_overview():
|
|
||||||
"""
|
|
||||||
获取回复器总览 - 轻量级接口
|
|
||||||
只统计文件数量,不读取文件内容
|
|
||||||
"""
|
|
||||||
if not REPLY_LOG_DIR.exists():
|
|
||||||
return ReplierOverview(total_chats=0, total_replies=0, chats=[])
|
|
||||||
|
|
||||||
chats = []
|
|
||||||
total_replies = 0
|
|
||||||
|
|
||||||
for chat_dir in REPLY_LOG_DIR.iterdir():
|
|
||||||
if not chat_dir.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 只统计json文件数量
|
|
||||||
json_files = list(chat_dir.glob("*.json"))
|
|
||||||
reply_count = len(json_files)
|
|
||||||
total_replies += reply_count
|
|
||||||
|
|
||||||
if reply_count == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 从文件名获取最新时间戳
|
|
||||||
latest_file = max(json_files, key=lambda f: parse_timestamp_from_filename(f.name))
|
|
||||||
latest_timestamp = parse_timestamp_from_filename(latest_file.name)
|
|
||||||
|
|
||||||
chats.append(
|
|
||||||
ReplierChatSummary(
|
|
||||||
chat_id=chat_dir.name,
|
|
||||||
reply_count=reply_count,
|
|
||||||
latest_timestamp=latest_timestamp,
|
|
||||||
latest_filename=latest_file.name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 按最新时间戳排序
|
|
||||||
chats.sort(key=lambda x: x.latest_timestamp, reverse=True)
|
|
||||||
|
|
||||||
return ReplierOverview(total_chats=len(chats), total_replies=total_replies, chats=chats)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chat/{chat_id}/logs", response_model=PaginatedReplyLogs)
|
|
||||||
async def get_chat_reply_logs(
|
|
||||||
chat_id: str,
|
|
||||||
page: int = Query(1, ge=1),
|
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
|
||||||
search: Optional[str] = Query(None, description="搜索关键词,匹配提示词内容"),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取指定聊天的回复日志列表(分页)
|
|
||||||
需要读取文件内容获取摘要信息
|
|
||||||
支持搜索提示词内容
|
|
||||||
"""
|
|
||||||
chat_dir = REPLY_LOG_DIR / chat_id
|
|
||||||
if not chat_dir.exists():
|
|
||||||
return PaginatedReplyLogs(data=[], total=0, page=page, page_size=page_size, chat_id=chat_id)
|
|
||||||
|
|
||||||
# 先获取所有文件并按时间戳排序
|
|
||||||
json_files = list(chat_dir.glob("*.json"))
|
|
||||||
json_files.sort(key=lambda f: parse_timestamp_from_filename(f.name), reverse=True)
|
|
||||||
|
|
||||||
# 如果有搜索关键词,需要过滤文件
|
|
||||||
if search:
|
|
||||||
search_lower = search.lower()
|
|
||||||
filtered_files = []
|
|
||||||
for log_file in json_files:
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
prompt = data.get("prompt", "")
|
|
||||||
if search_lower in prompt.lower():
|
|
||||||
filtered_files.append(log_file)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
json_files = filtered_files
|
|
||||||
|
|
||||||
total = len(json_files)
|
|
||||||
|
|
||||||
# 分页 - 只读取当前页的文件
|
|
||||||
offset = (page - 1) * page_size
|
|
||||||
page_files = json_files[offset : offset + page_size]
|
|
||||||
|
|
||||||
logs = []
|
|
||||||
for log_file in page_files:
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
output = data.get("output", "")
|
|
||||||
logs.append(
|
|
||||||
ReplyLogSummary(
|
|
||||||
chat_id=data.get("chat_id", chat_id),
|
|
||||||
timestamp=data.get("timestamp", parse_timestamp_from_filename(log_file.name)),
|
|
||||||
filename=log_file.name,
|
|
||||||
model=data.get("model", ""),
|
|
||||||
success=data.get("success", True),
|
|
||||||
llm_ms=data.get("timing", {}).get("llm_ms", 0),
|
|
||||||
overall_ms=data.get("timing", {}).get("overall_ms", 0),
|
|
||||||
output_preview=output[:100] if output else "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# 文件读取失败时使用文件名信息
|
|
||||||
logs.append(
|
|
||||||
ReplyLogSummary(
|
|
||||||
chat_id=chat_id,
|
|
||||||
timestamp=parse_timestamp_from_filename(log_file.name),
|
|
||||||
filename=log_file.name,
|
|
||||||
model="",
|
|
||||||
success=False,
|
|
||||||
llm_ms=0,
|
|
||||||
overall_ms=0,
|
|
||||||
output_preview="[读取失败]",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return PaginatedReplyLogs(data=logs, total=total, page=page, page_size=page_size, chat_id=chat_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/log/{chat_id}/{filename}", response_model=ReplyLogDetail)
|
|
||||||
async def get_reply_log_detail(chat_id: str, filename: str):
|
|
||||||
"""获取回复日志详情 - 按需加载完整内容"""
|
|
||||||
log_file = REPLY_LOG_DIR / chat_id / filename
|
|
||||||
if not log_file.exists():
|
|
||||||
raise HTTPException(status_code=404, detail="日志文件不存在")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(log_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return ReplyLogDetail(
|
|
||||||
type=data.get("type", "reply"),
|
|
||||||
chat_id=data.get("chat_id", chat_id),
|
|
||||||
timestamp=data.get("timestamp", 0),
|
|
||||||
prompt=data.get("prompt", ""),
|
|
||||||
output=data.get("output", ""),
|
|
||||||
processed_output=data.get("processed_output", []),
|
|
||||||
model=data.get("model", ""),
|
|
||||||
reasoning=data.get("reasoning", ""),
|
|
||||||
think_level=data.get("think_level", 0),
|
|
||||||
timing=data.get("timing", {}),
|
|
||||||
error=data.get("error"),
|
|
||||||
success=data.get("success", True),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"读取日志失败: {str(e)}") from e
|
|
||||||
|
|
||||||
|
|
||||||
# ========== 兼容接口 ==========
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
|
||||||
async def get_replier_stats():
|
|
||||||
"""获取回复器统计信息"""
|
|
||||||
overview = await get_replier_overview()
|
|
||||||
|
|
||||||
# 获取最近10条回复的摘要
|
|
||||||
recent_replies = []
|
|
||||||
for chat in overview.chats[:5]: # 从最近5个聊天中获取
|
|
||||||
try:
|
|
||||||
chat_logs = await get_chat_reply_logs(chat.chat_id, page=1, page_size=2)
|
|
||||||
recent_replies.extend(chat_logs.data)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 按时间排序取前10
|
|
||||||
recent_replies.sort(key=lambda x: x.timestamp, reverse=True)
|
|
||||||
recent_replies = recent_replies[:10]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_chats": overview.total_chats,
|
|
||||||
"total_replies": overview.total_replies,
|
|
||||||
"recent_replies": recent_replies,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chats")
|
|
||||||
async def get_replier_chat_list():
|
|
||||||
"""获取所有聊天ID列表"""
|
|
||||||
overview = await get_replier_overview()
|
|
||||||
return [chat.chat_id for chat in overview.chats]
|
|
||||||
Reference in New Issue
Block a user