Files
daily-robots/bot/cursor_runner.py
2026-07-02 11:31:16 +08:00

107 lines
3.3 KiB
Python
Raw 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.

"""通过 Cursor SDK 执行用户任务。"""
from __future__ import annotations
import asyncio
import logging
import re
from typing import Awaitable, Callable
import env_config
from bridge_manager import warm_cursor_bridge
logger = logging.getLogger(__name__)
_cursor_lock = asyncio.Lock()
WECHAT_SYSTEM_PREFIX = """你是企业微信群里的 Skills 助手,正在回复群成员的消息。
要求:
- 用简洁的中文回答(除非用户用其他语言提问)
- 使用企业微信支持的 Markdown 子集(加粗、链接、列表;避免复杂表格)
- 直接给出结论,不要冗长铺垫
- 若任务涉及 skills.sh可说明安装命令 `npx skills add owner/repo/skill-name`
- **不要**在回复里写 `[图片]` 占位符;企微无法通过 Markdown 显示图片
- 若用户要页面截图,请明确告知其发送:`截图` 或 `preview`(由 bot 自动发图)
用户任务:
"""
def _cursor_settings() -> dict[str, str | int]:
timeout_raw = env_config.env("CURSOR_TIMEOUT", "600") or "600"
return {
"api_key": env_config.env("CURSOR_API_KEY"),
"cwd": env_config.env("CURSOR_CWD", r"d:\LY\test\tech") or r"d:\LY\test\tech",
"model": env_config.env("CURSOR_MODEL", "composer-2.5") or "composer-2.5",
"timeout": int(timeout_raw),
}
def strip_mention(text: str) -> str:
return re.sub(r"@\S+\s*", "", text).strip()
def _build_prompt(task: str) -> str:
return WECHAT_SYSTEM_PREFIX + task.strip()
def execute_cursor_task_sync(task: str) -> str:
from cursor_sdk import Agent, AgentOptions, CursorAgentError, LocalAgentOptions
settings = _cursor_settings()
api_key = settings["api_key"]
if not api_key:
raise RuntimeError(
"未配置 CURSOR_API_KEY。请在 bot/.env 中设置,"
"密钥见 https://cursor.com/dashboard/integrations"
)
warm_cursor_bridge()
cwd = str(settings["cwd"])
prompt = _build_prompt(task)
logger.info("Cursor 执行任务 cwd=%s model=%s", cwd, settings["model"])
try:
result = Agent.prompt(
prompt,
AgentOptions(
api_key=api_key,
model=settings["model"],
local=LocalAgentOptions(cwd=cwd),
),
)
except CursorAgentError as exc:
raise RuntimeError(
f"Cursor 启动失败:{exc.message}"
+ ("(可重试)" if exc.is_retryable else "")
) from exc
if result.status == "error":
detail = result.result or "运行失败,无详细错误"
raise RuntimeError(f"Cursor 执行失败:{detail}")
text = (result.result or "").strip()
if not text:
return "Cursor 已完成任务,但没有返回文本内容。"
return text
async def run_cursor_task(
task: str,
on_progress: Callable[[str], Awaitable[None]] | None = None,
) -> str:
timeout = int(_cursor_settings()["timeout"])
if on_progress:
await on_progress("Cursor 正在执行任务,请稍候…")
async with _cursor_lock:
try:
return await asyncio.wait_for(
asyncio.to_thread(execute_cursor_task_sync, task),
timeout=timeout,
)
except asyncio.TimeoutError as exc:
raise RuntimeError(f"Cursor 执行超时(>{timeout}s") from exc