107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
"""通过 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
|