fix(chatai): 安全修复+代码质量改进
- _force_kill_chrome: 仅kill带--remote-debugging-port的headless chrome - AI API: 添加60s timeout + run_in_executor避免阻塞事件循环 - AI系统提示抽取为常量 - markdown转图片: 移除错误的html.escape前置 - screenshot: 等待渲染完成替代固定sleep - 错误信息不再暴露异常详情给用户
This commit is contained in:
@@ -44,16 +44,17 @@ driver = get_driver()
|
|||||||
|
|
||||||
|
|
||||||
def _force_kill_chrome():
|
def _force_kill_chrome():
|
||||||
"""强制终止残留 Chrome 进程"""
|
"""强制终止残留的 headless Chrome 进程(仅 pyppeteer 创建的)"""
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
# 只杀带 --headless 参数的 chrome(避免误杀用户浏览器)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["taskkill", "/F", "/IM", "chrome.exe"],
|
["taskkill", "/F", "/IM", "chrome.exe", "/FI", "MODULES eq *pyppeteer*"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["pkill", "-9", "-f", "chrome"],
|
["pkill", "-9", "-f", "chrome.*--remote-debugging-port"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -97,6 +98,17 @@ async def init_browser() -> "pyppeteer.browser.Browser":
|
|||||||
return _browser
|
return _browser
|
||||||
|
|
||||||
|
|
||||||
|
_AI_API_TIMEOUT = 60 # seconds
|
||||||
|
_AI_SYSTEM_PROMPT = (
|
||||||
|
"你是一个活泼可爱的群助手。请在回复中使用丰富的 Emoji 表情"
|
||||||
|
"(如 😊 😄 🎉 ✨ 💖 等)。你的语气要俏皮活泼,像在和朋友聊天一样自然。"
|
||||||
|
"在回答问题时要保持专业性的同时,也要让回复显得生动有趣。"
|
||||||
|
"每条回复都必须包含至少2-3个 Emoji 表情。"
|
||||||
|
"如果你需要展示代码片段,请确保代码中不包含任何颜文字或表情符号,"
|
||||||
|
"保持代码的专业性和可读性。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_ai_client() -> OpenAI:
|
def _get_ai_client() -> OpenAI:
|
||||||
"""获取或创建 OpenAI 客户端(单例)"""
|
"""获取或创建 OpenAI 客户端(单例)"""
|
||||||
global _ai_client
|
global _ai_client
|
||||||
@@ -104,6 +116,7 @@ def _get_ai_client() -> OpenAI:
|
|||||||
_ai_client = OpenAI(
|
_ai_client = OpenAI(
|
||||||
api_key=plugin_config.deepseek_token,
|
api_key=plugin_config.deepseek_token,
|
||||||
base_url="https://api.siliconflow.cn/v1",
|
base_url="https://api.siliconflow.cn/v1",
|
||||||
|
timeout=_AI_API_TIMEOUT,
|
||||||
)
|
)
|
||||||
return _ai_client
|
return _ai_client
|
||||||
|
|
||||||
@@ -111,20 +124,16 @@ def _get_ai_client() -> OpenAI:
|
|||||||
async def call_ai_api(message: str) -> str:
|
async def call_ai_api(message: str) -> str:
|
||||||
"""调用 AI 接口"""
|
"""调用 AI 接口"""
|
||||||
client = _get_ai_client()
|
client = _get_ai_client()
|
||||||
response = client.chat.completions.create(
|
response = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: client.chat.completions.create(
|
||||||
model="deepseek-ai/DeepSeek-V3",
|
model="deepseek-ai/DeepSeek-V3",
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": (
|
{"role": "system", "content": _AI_SYSTEM_PROMPT},
|
||||||
"你是一个活泼可爱的群助手。请在回复中使用丰富的 Emoji 表情"
|
|
||||||
"(如 😊 😄 🎉 ✨ 💖 等)。你的语气要俏皮活泼,像在和朋友聊天一样自然。"
|
|
||||||
"在回答问题时要保持专业性的同时,也要让回复显得生动有趣。"
|
|
||||||
"每条回复都必须包含至少2-3个 Emoji 表情。"
|
|
||||||
"如果你需要展示代码片段,请确保代码中不包含任何颜文字或表情符号,"
|
|
||||||
"保持代码的专业性和可读性。"
|
|
||||||
)},
|
|
||||||
{"role": "user", "content": message},
|
{"role": "user", "content": message},
|
||||||
],
|
],
|
||||||
stream=False,
|
stream=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return response.choices[0].message.content or ""
|
return response.choices[0].message.content or ""
|
||||||
|
|
||||||
@@ -174,7 +183,8 @@ async def handle_message(event: MessageEvent, bot: Bot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"chatai处理失败: user_id={event.user_id} error={e}")
|
logger.error(f"chatai处理失败: user_id={event.user_id} error={e}")
|
||||||
await asyncio.sleep(random.uniform(2, 3))
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
await message_handler.finish(f"出错了: {e}")
|
logger.error(f"chatai详细错误: {e}")
|
||||||
|
await message_handler.finish("出错了,请稍后再试~")
|
||||||
|
|
||||||
|
|
||||||
async def _delete_message_after_delay(bot: Bot, message_id: int):
|
async def _delete_message_after_delay(bot: Bot, message_id: int):
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ async def markdown_to_image(markdown_text: str, output_path: str, browser=None):
|
|||||||
page = None
|
page = None
|
||||||
should_close_browser = False
|
should_close_browser = False
|
||||||
try:
|
try:
|
||||||
# 转义用户输入中的HTML特殊字符,防止XSS
|
# Convert markdown to HTML. The markdown library handles special chars safely.
|
||||||
safe_text = html_module.escape(markdown_text)
|
# Note: do NOT html.escape() before markdown.markdown() - it breaks markdown syntax.
|
||||||
html_content = markdown.markdown(safe_text)
|
html_content = markdown.markdown(markdown_text, extensions=["fenced_code", "tables"])
|
||||||
|
|
||||||
# 使用传入的浏览器实例或创建新的
|
# 使用传入的浏览器实例或创建新的
|
||||||
if browser is None:
|
if browser is None:
|
||||||
@@ -19,6 +19,7 @@ async def markdown_to_image(markdown_text: str, output_path: str, browser=None):
|
|||||||
should_close_browser = True
|
should_close_browser = True
|
||||||
|
|
||||||
page = await browser.newPage()
|
page = await browser.newPage()
|
||||||
|
page.setDefaultNavigationTimeout(15000)
|
||||||
|
|
||||||
# 设置页面样式,使内容更美观
|
# 设置页面样式,使内容更美观
|
||||||
await page.setContent(f"""
|
await page.setContent(f"""
|
||||||
@@ -122,7 +123,10 @@ async def markdown_to_image(markdown_text: str, output_path: str, browser=None):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# 等待内容渲染完成
|
# 等待内容渲染完成
|
||||||
await asyncio.sleep(0.5)
|
try:
|
||||||
|
await asyncio.wait_for(page.waitForNavigation({'waitUntil': 'networkidle0'}), timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass # rendering may already be complete
|
||||||
|
|
||||||
# 获取内容尺寸并设置视口
|
# 获取内容尺寸并设置视口
|
||||||
dimensions = await page.evaluate('''() => {
|
dimensions = await page.evaluate('''() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user