Files
T
2025-10-14 14:54:05 +08:00

644 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from aiohttp import web
from server import PromptServer
from .config_manager import config_manager
from .services.baidu import BaiduTranslateService
from .services.llm import LLMService
from .services.vlm import VisionService
from .services.model_list import ModelListService
import base64
import json
import traceback
import asyncio
# 定义颜色常量
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
RESET = "\033[0m"
# 统一日志前缀(按阶段上色)
# 绿色:结果阶段
PREFIX = f"{GREEN}[✨PromptAssistant]{RESET}"
AUTO_TRANSLATE_PREFIX = f"{GREEN}[✨PromptAssistant-自动翻译]{RESET}"
# 红色:错误
ERROR_PREFIX = f"{RED}[✨PromptAssistant-错误]{RESET}"
# 黄色:过程阶段(创建客户端/调用API等)
PROCESS_PREFIX = f"{YELLOW}[✨PromptAssistant]{RESET}"
# 蓝色:发起请求阶段
REQUEST_PREFIX = f"{BLUE}[✨PromptAssistant]{RESET}"
# 黄色:告警保持不变
WARN_PREFIX = f"{YELLOW}[✨PromptAssistant-警告]{RESET}"
# 蓝色:自动翻译的“请求阶段”前缀(仅用于请求日志着色)
AUTO_TRANSLATE_REQUEST_PREFIX = f"{BLUE}[✨PromptAssistant-自动翻译]{RESET}"
# 定义路由前缀,确保与前端请求匹配
API_PREFIX = '/prompt_assistant/api'
# 在服务器初始化时验证激活提示词
# print(f"{PREFIX} 正在验证激活提示词配置...")
config_manager.validate_and_fix_active_prompts()
# 新增:在启动时补全模型提供商及参数
config_manager.validate_and_fix_model_params()
# 用于跟踪正在进行的异步任务
ACTIVE_TASKS = {}
# 不再使用RouteTableDef
# routes = web.RouteTableDef()
def get_result_text(result):
"""
智能提取接口返回中的文本内容,用于字符统计。
优先级:data.translated > data.expanded > data.description > data.result > data.text > result > data(str)
"""
if not isinstance(result, dict):
return str(result) if result is not None else ''
data = result.get('data')
if isinstance(data, dict):
for key in ['translated', 'expanded', 'description', 'result', 'text']:
if key in data and isinstance(data[key], str):
return data[key]
if isinstance(data, str):
return data
if 'result' in result and isinstance(result['result'], str):
return result['result']
if 'text' in result and isinstance(result['text'], str):
return result['text']
return ''
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/baidu_translate')
async def get_baidu_translate_config(request):
"""获取百度翻译配置"""
config = config_manager.get_baidu_translate_config()
return web.json_response(config)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/llm')
async def get_llm_config(request):
"""获取LLM配置"""
config = config_manager.get_llm_config()
return web.json_response(config)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/vision')
async def get_vision_config(request):
"""获取视觉模型配置"""
config = config_manager.get_vision_config()
return web.json_response(config)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/system_prompts')
async def get_system_prompts_config(request):
"""获取系统提示词配置"""
config = config_manager.get_system_prompts()
return web.json_response(config)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/default_system_prompts')
async def get_default_system_prompts_config(request):
"""获取默认系统提示词配置"""
default_prompts = config_manager.default_system_prompts
return web.json_response(default_prompts)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/tags')
async def get_tags_config(request):
"""获取标签配置"""
try:
import os
import json
# 获取标签配置文件路径
tags_file_path = os.path.join(os.path.dirname(__file__), "config", "tags.json")
# 检查文件是否存在
if not os.path.exists(tags_file_path):
print(f"{ERROR_PREFIX} 标签配置文件不存在 | 路径:{tags_file_path}")
return web.json_response({"error": "标签配置文件不存在"}, status=404)
# 读取文件内容
with open(tags_file_path, "r", encoding="utf-8") as f:
tags_data = json.load(f)
# print(f"{PREFIX} 标签配置文件加载成功")
return web.json_response(tags_data)
except Exception as e:
print(f"{ERROR_PREFIX} 标签配置加载失败 | 错误:{str(e)}")
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.get(f'{API_PREFIX}/config/tags_user')
async def get_user_tags_config(request):
"""获取用户自定义标签配置"""
try:
# 使用配置管理器加载用户标签
user_tags = config_manager.load_user_tags()
# print(f"{PREFIX} 用户标签配置文件加载成功")
return web.json_response(user_tags)
except Exception as e:
print(f"{ERROR_PREFIX} 用户标签配置加载失败 | 错误:{str(e)}")
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/tags_user')
async def update_user_tags_config(request):
"""更新用户自定义标签配置"""
try:
data = await request.json()
# 检查数据结构是否正确
if not isinstance(data, dict):
print(f"{ERROR_PREFIX} 用户标签配置更新失败 | 错误:参数格式错误")
return web.json_response({"error": "参数格式错误"}, status=400)
# 保存用户标签数据
import os
import json
# 获取用户标签配置文件路径
tags_user_file_path = os.path.join(os.path.dirname(__file__), "config", "tags_user.json")
# 保存数据到文件
with open(tags_user_file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# print(f"{PREFIX} 用户标签配置文件更新成功")
return web.json_response({"success": True})
except Exception as e:
print(f"{ERROR_PREFIX} 用户标签配置更新异常 | 错误:{str(e)}")
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/baidu_translate')
async def update_baidu_translate_config(request):
"""更新百度翻译配置"""
try:
data = await request.json()
app_id = data.get('app_id')
secret_key = data.get('secret_key')
# 获取当前配置
current_config = config_manager.get_baidu_translate_config()
# 如果提供了 app_id,则更新 app_id
if app_id is not None:
current_config['app_id'] = app_id
# 如果提供了 secret_key,则更新 secret_key
if secret_key is not None:
current_config['secret_key'] = secret_key
# 更新配置
success = config_manager.update_baidu_translate_config(
current_config['app_id'],
current_config['secret_key']
)
if success:
return web.json_response({'message': '配置已更新'})
else:
print(f"{ERROR_PREFIX} 百度翻译配置更新失败")
return web.json_response({'error': '配置更新失败'}, status=500)
except Exception as e:
print(f"{ERROR_PREFIX} 百度翻译配置更新异常 | 错误:{str(e)}")
return web.json_response({'error': str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/system_prompts')
async def update_system_prompts_config(request):
"""更新系统提示词配置"""
try:
data = await request.json()
# 检查数据结构是否正确
if not isinstance(data, dict):
print(f"{ERROR_PREFIX} 系统提示词配置更新失败 | 错误:参数格式错误")
return web.json_response({'error': '参数格式错误'}, status=400)
# 前端会直接发送包含 active_prompts 的完整配置,后端只需验证并保存
if 'active_prompts' not in data:
print(f"{WARN_PREFIX} 系统提示词配置更新警告 | 错误:缺少 active_prompts,尝试从现有配置恢复")
# 即使缺少,也尝试从当前配置中恢复,以增强健壮性
current_config = config_manager.get_system_prompts()
data['active_prompts'] = current_config.get('active_prompts', {
"expand": None, "vision_zh": None, "vision_en": None
})
# 将 active_prompts 分离出来单独处理
active_prompts_to_update = data.pop('active_prompts', None)
# 清理提示词数据中可能存在的isActive标志(以防万一)
for prompt_type in ['expand_prompts', 'vision_prompts']:
if prompt_type in data:
for prompt_id, prompt_data in data[prompt_type].items():
if isinstance(prompt_data, dict) and 'isActive' in prompt_data:
del prompt_data['isActive']
# 更新提示词定义
success = config_manager.update_system_prompts(data)
# 如果有 active_prompts,则更新它们
if active_prompts_to_update:
success = success and config_manager.update_active_prompts(active_prompts_to_update)
if success:
active_prompts = active_prompts_to_update or {}
expand_id = active_prompts.get('expand', '无')
vision_zh_id = active_prompts.get('vision_zh', '无')
vision_en_id = active_prompts.get('vision_en', '无')
print(f"{PREFIX} 系统提示词配置更新成功 | 激活提示词: 扩写={expand_id}, 中文反推={vision_zh_id}, 英文反推={vision_en_id}")
# 在更新配置后验证激活提示词
print(f"{PREFIX} 正在验证更新后的激活提示词配置...")
config_manager.validate_and_fix_active_prompts()
return web.json_response({'success': True})
else:
print(f"{ERROR_PREFIX} 系统提示词配置更新失败")
return web.json_response({'error': '配置更新失败'}, status=500)
except Exception as e:
print(f"{ERROR_PREFIX} 系统提示词配置更新异常 | 错误:{str(e)}")
return web.json_response({'error': str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/active_prompt')
async def update_active_prompt_config(request):
"""更新单个激活的提示词配置"""
try:
data = await request.json()
prompt_type = data.get('type')
prompt_id = data.get('prompt_id')
if not prompt_type or not prompt_id:
return web.json_response({'error': '缺少参数 type 或 prompt_id'}, status=400)
success = config_manager.update_active_prompt(prompt_type, prompt_id)
if success:
print(f"{PREFIX} 激活的提示词已更新 | 类型:{prompt_type} | ID:{prompt_id}")
# 在更新单个激活提示词后验证配置
print(f"{PREFIX} 正在验证更新后的激活提示词配置...")
config_manager.validate_and_fix_active_prompts()
return web.json_response({'success': True})
else:
print(f"{ERROR_PREFIX} 激活的提示词更新失败")
return web.json_response({'error': '配置更新失败'}, status=500)
except Exception as e:
print(f"{ERROR_PREFIX} 激活的提示词更新异常 | 错误:{str(e)}")
return web.json_response({'error': str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/llm')
async def update_llm_config(request):
"""更新LLM配置"""
try:
data = await request.json()
current_provider = data.get('current_provider')
providers = data.get('providers', {})
# 确保至少有一个参数提供
if not current_provider and not providers:
print(f"{ERROR_PREFIX} LLM配置更新失败 | 错误:参数不能全为空")
return web.json_response({'error': '参数不能全为空'}, status=400)
success = True
# 先更新各提供商的配置
if providers:
# 逐个更新各提供商的配置
for provider, provider_config in providers.items():
if provider not in ['zhipu', 'siliconflow', '302ai', 'ollama', 'custom']:
continue
model = provider_config.get('model')
api_key = provider_config.get('api_key')
base_url = provider_config.get('base_url')
temperature = provider_config.get('temperature')
max_tokens = provider_config.get('max_tokens')
top_p = provider_config.get('top_p')
auto_unload = provider_config.get('auto_unload')
# 更新配置,但不更新current_provider
success = success and config_manager.update_llm_config(
provider=provider,
model=model,
base_url=base_url,
api_key=api_key,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
auto_unload=auto_unload,
update_current=False
)
# 最后更新当前提供商
if current_provider:
success = success and config_manager.update_llm_config(
provider=current_provider,
update_current=True
)
if success:
print(f"{PREFIX} LLM配置更新成功")
return web.json_response({'success': True})
else:
print(f"{ERROR_PREFIX} LLM配置更新失败")
return web.json_response({'error': '配置更新失败'}, status=500)
except Exception as e:
print(f"{ERROR_PREFIX} LLM配置更新异常 | 错误:{str(e)}")
return web.json_response({'error': str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/config/vision')
async def update_vision_config(request):
"""更新视觉模型配置"""
try:
data = await request.json()
current_provider = data.get('current_provider')
providers = data.get('providers', {})
# 确保至少有一个参数提供
if not current_provider and not providers:
print(f"{ERROR_PREFIX} 视觉模型配置更新失败 | 错误:参数不能全为空")
return web.json_response({'error': '参数不能全为空'}, status=400)
success = True
# 先更新各提供商的配置
if providers:
# 逐个更新各提供商的配置
for provider, provider_config in providers.items():
if provider not in ['zhipu', 'siliconflow', '302ai', 'ollama', 'custom']:
continue
model = provider_config.get('model')
api_key = provider_config.get('api_key')
base_url = provider_config.get('base_url')
temperature = provider_config.get('temperature')
max_tokens = provider_config.get('max_tokens')
top_p = provider_config.get('top_p')
auto_unload = provider_config.get('auto_unload')
# 更新配置,但不更新current_provider
success = success and config_manager.update_vision_config(
provider=provider,
model=model,
base_url=base_url,
api_key=api_key,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
auto_unload=auto_unload,
update_current=False
)
# 最后更新当前提供商
if current_provider:
success = success and config_manager.update_vision_config(
provider=current_provider,
update_current=True
)
if success:
print(f"{PREFIX} 视觉模型配置更新成功")
return web.json_response({'success': True})
else:
print(f"{ERROR_PREFIX} 视觉模型配置更新失败")
return web.json_response({'error': '配置更新失败'}, status=500)
except Exception as e:
print(f"{ERROR_PREFIX} 视觉模型配置更新异常 | 错误:{str(e)}")
return web.json_response({'error': str(e)}, status=500)
@PromptServer.instance.routes.post(f'{API_PREFIX}/request/cancel')
async def cancel_request(request):
"""取消一个正在进行的异步请求"""
try:
data = await request.json()
request_id = data.get("request_id")
if not request_id:
return web.json_response({"success": False, "error": "缺少request_id"}, status=400)
task = ACTIVE_TASKS.get(request_id)
if task and not task.done():
task.cancel()
# 从字典中移除已取消的任务
del ACTIVE_TASKS[request_id]
print(f"{PREFIX} 请求已取消 | ID:{request_id}")
return web.json_response({"success": True, "message": "请求已取消"})
else:
return web.json_response({"success": False, "error": "未找到活动任务或任务已完成"}, status=404)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} 取消请求时发生异常 | 错误:{error_msg}")
return web.json_response({"success": False, "error": error_msg}, status=500)
# 新增API路由
@PromptServer.instance.routes.post(f'{API_PREFIX}/baidu/translate')
async def baidu_translate(request):
"""
百度翻译API
注意:所有的格式处理(包括序号"1."格式保护等)均由前端的promptFormatter.js负责,
后端只负责原始文本的翻译,不进行格式预处理或后处理。
"""
request_id = None
try:
data = await request.json()
text = data.get("text")
from_lang = data.get("from", "auto")
to_lang = data.get("to", "zh")
request_id = data.get("request_id")
is_auto = data.get("is_auto", False)
if not request_id:
return web.json_response({"success": False, "error": "缺少request_id"}, status=400)
# 创建并注册任务
task = asyncio.create_task(BaiduTranslateService.translate(text, from_lang, to_lang, request_id, is_auto))
ACTIVE_TASKS[request_id] = task
result = await task
# 如果发生错误,输出详细错误信息
if not result.get('success'):
if not result.get('cancelled', False):
error_msg = result.get('error', '未知错误')
print(f"{ERROR_PREFIX} 百度翻译请求失败 | 请求ID:{request_id} | 错误:{error_msg}")
return web.json_response(result)
except asyncio.CancelledError:
print(f"{PREFIX} 百度翻译任务被取消 | ID:{request_id}")
return web.json_response({"success": False, "error": "请求已取消", "cancelled": True}, status=400)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} 百度翻译请求异常 | 错误:{error_msg}")
return web.json_response({"success": False, "error": error_msg})
finally:
if request_id and request_id in ACTIVE_TASKS:
del ACTIVE_TASKS[request_id]
@PromptServer.instance.routes.post(f'{API_PREFIX}/llm/expand')
async def llm_expand(request):
"""LLM扩写API"""
request_id = None
try:
data = await request.json()
prompt = data.get("prompt")
request_id = data.get("request_id")
if not request_id:
return web.json_response({"success": False, "error": "缺少request_id"}, status=400)
# 创建并注册任务
task = asyncio.create_task(LLMService.expand_prompt(prompt, request_id))
ACTIVE_TASKS[request_id] = task
result = await task
# 如果发生错误,输出详细错误信息
if not result.get('success'):
# 检查是否是用户取消
if not result.get('cancelled', False):
error_msg = result.get('error', '未知错误')
print(f"{ERROR_PREFIX} LLM扩写请求失败 | 请求ID:{request_id} | 错误:{error_msg}")
return web.json_response(result)
except asyncio.CancelledError:
print(f"{PREFIX} LLM扩写任务被取消 | ID:{request_id}")
return web.json_response({"success": False, "error": "请求已取消", "cancelled": True}, status=400)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} LLM扩写请求异常 | 请求ID:{request_id if request_id else '未知'} | 错误:{error_msg}")
return web.json_response({"success": False, "error": error_msg})
finally:
if request_id and request_id in ACTIVE_TASKS:
del ACTIVE_TASKS[request_id]
@PromptServer.instance.routes.post(f'{API_PREFIX}/llm/translate')
async def llm_translate(request):
"""LLM翻译API"""
request_id = None
try:
data = await request.json()
text = data.get("text")
from_lang = data.get("from", "auto")
to_lang = data.get("to", "zh")
request_id = data.get("request_id")
is_auto = data.get("is_auto", False)
if not request_id:
return web.json_response({"success": False, "error": "缺少request_id"}, status=400)
# 创建并注册任务
task = asyncio.create_task(LLMService.translate(text, from_lang, to_lang, request_id, is_auto))
ACTIVE_TASKS[request_id] = task
result = await task
# 如果发生错误,输出详细错误信息
if not result.get('success') and not result.get('cancelled', False):
error_msg = result.get('error', '未知错误')
print(f"{ERROR_PREFIX} LLM翻译请求失败 | 请求ID:{request_id} | 错误:{error_msg}")
return web.json_response(result)
except asyncio.CancelledError:
print(f"{PREFIX} LLM翻译任务被取消 | ID:{request_id}")
return web.json_response({"success": False, "error": "请求已取消", "cancelled": True}, status=400)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} LLM翻译请求异常 | 请求ID:{request_id if request_id else '未知'} | 错误:{error_msg}")
return web.json_response({"success": False, "error": error_msg})
finally:
if request_id and request_id in ACTIVE_TASKS:
del ACTIVE_TASKS[request_id]
@PromptServer.instance.routes.post(f'{API_PREFIX}/vlm/analyze')
async def vlm_analyze(request):
"""视觉分析API"""
request_id = None
try:
data = await request.json()
image_data = data.get("image")
request_id = data.get("request_id")
# 从请求中获取提示词内容,这是关键的修复
prompt_content = data.get("prompt")
if not request_id:
return web.json_response({"success": False, "error": "缺少request_id"}, status=400)
# 创建并注册任务,使用新的接口签名
task = asyncio.create_task(VisionService.analyze_image(
image_data=image_data,
request_id=request_id,
prompt_content=prompt_content
))
ACTIVE_TASKS[request_id] = task
result = await task
# 如果发生错误,输出详细错误信息
if not result.get('success'):
if not result.get('cancelled', False):
error_msg = result.get('error', '未知错误')
print(f"{ERROR_PREFIX} 视觉分析请求失败 | 请求ID:{request_id} | 错误:{error_msg}")
return web.json_response(result)
except asyncio.CancelledError:
print(f"{PREFIX} 视觉分析任务被取消 | ID:{request_id}")
return web.json_response({"success": False, "error": "请求已取消", "cancelled": True}, status=400)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} 视觉分析请求异常 | 请求ID:{request_id if request_id else '未知'} | 错误:{error_msg}")
return web.json_response({"success": False, "error": error_msg})
finally:
if request_id and request_id in ACTIVE_TASKS:
del ACTIVE_TASKS[request_id]
@PromptServer.instance.routes.post(f'{API_PREFIX}/models/list')
async def get_models_list(request):
"""
获取模型列表API
请求参数:
- provider: 服务提供商 (zhipu, siliconflow, 302ai, ollama, custom)
- model_type: 模型类型 ('llm' 或 'vision')
- recommended: 是否获取推荐列表 (可选,默认False)
"""
try:
data = await request.json()
provider = data.get("provider")
model_type = data.get("model_type", "llm")
recommended = data.get("recommended", False)
if not provider:
return web.json_response({
"success": False,
"error": "缺少provider参数"
}, status=400)
# 如果请求推荐模型列表,直接返回静态列表
if recommended:
result = ModelListService.get_recommended_models(provider, model_type)
return web.json_response(result)
# 从配置中获取对应provider的API Key
if model_type == 'llm':
config = config_manager.get_llm_config()
else:
config = config_manager.get_vision_config()
# 获取对应provider的配置
providers_config = config.get('providers', {})
provider_config = providers_config.get(provider, {})
api_key = provider_config.get('api_key', '')
base_url = provider_config.get('base_url', '')
# 调用模型列表服务
result = ModelListService.get_models(provider, api_key, model_type, base_url)
return web.json_response(result)
except Exception as e:
error_msg = str(e)
print(f"{ERROR_PREFIX} 获取模型列表异常 | 错误:{error_msg}")
return web.json_response({
"success": False,
"error": error_msg
}, status=500)