diff --git a/backend/app/agent_runtime/orchestrator.py b/backend/app/agent_runtime/orchestrator.py index c2852b1..6c55f0d 100644 --- a/backend/app/agent_runtime/orchestrator.py +++ b/backend/app/agent_runtime/orchestrator.py @@ -151,8 +151,19 @@ class AgentOrchestrator: question: str, agents: List[OrchestratorAgentConfig], on_llm_call: Optional[Callable[[Dict[str, Any]], Any]] = None, + graph_nodes: Optional[List[Dict[str, Any]]] = None, + graph_edges: Optional[List[Dict[str, Any]]] = None, ) -> OrchestratorResult: - """执行多 Agent 编排。""" + """执行多 Agent 编排。 + + Args: + mode: route / sequential / debate / pipeline / graph + question: 用户问题 + agents: Agent 配置列表 + on_llm_call: LLM 调用回调 + graph_nodes: graph 模式的节点定义(mode=graph 时必填) + graph_edges: graph 模式的边定义(mode=graph 时必填) + """ mode = mode.lower() if mode == "route": return await self._route(question, agents, on_llm_call) @@ -162,8 +173,12 @@ class AgentOrchestrator: return await self._debate(question, agents, on_llm_call) elif mode == "pipeline": return await self._pipeline(question, agents, on_llm_call) + elif mode == "graph": + if not graph_nodes: + raise ValueError("graph 模式需要提供 graph_nodes 参数") + return await self._graph(question, graph_nodes, graph_edges or [], on_llm_call) else: - raise ValueError(f"不支持的编排模式: {mode},可选: route, sequential, debate, pipeline") + raise ValueError(f"不支持的编排模式: {mode},可选: route, sequential, debate, pipeline, graph") async def _route( self, question: str, agents: List[OrchestratorAgentConfig], @@ -500,11 +515,13 @@ class AgentOrchestrator: steps=steps, ) - # ── 2. Executor:逐步骤执行 ── - executor_cfg = agents[0] if agents else OrchestratorAgentConfig( - id="executor", name="Executor", - system_prompt="你是一个有用的AI助手。", - ) + # ── 2. Executor:逐步骤执行(多 Agent 轮转分配)── + executor_pool = agents if agents else [ + OrchestratorAgentConfig( + id="executor", name="Executor", + system_prompt="你是一个有用的AI助手。", + ) + ] previous_output = "(尚无前序步骤)" execution_results = [] @@ -514,6 +531,9 @@ class AgentOrchestrator: step_desc = step_info.get("description", f"步骤 {step_num}") step_expect = step_info.get("expected_output", "") + # 按步骤轮转分配 Agent:不同步骤可分配给不同 Agent(按专长匹配) + executor_cfg = executor_pool[(step_num - 1) % len(executor_pool)] + executor_prompt = _EXECUTOR_STEP_PROMPT.format( original_question=question, plan_title=plan.get("plan_title", ""), @@ -555,6 +575,7 @@ class AgentOrchestrator: execution_results.append({ "step": step_num, "description": step_desc, + "agent": executor_cfg.name, "output": step_result.content, "error": step_result.error if not step_result.success else None, }) @@ -562,7 +583,7 @@ class AgentOrchestrator: previous_output = step_result.content if step_result.success else f"(步骤{step_num}执行出错)" if not step_result.success: - logger.warning(f"Pipeline 步骤{step_num} 执行失败: {step_result.error}") + logger.warning(f"Pipeline 步骤{step_num} ({executor_cfg.name}) 执行失败: {step_result.error}") # ── 3. Reviewer:审查并交付 ── plan_steps_text = "\n".join( diff --git a/backend/app/agent_runtime/workflow_integration.py b/backend/app/agent_runtime/workflow_integration.py index b7b0565..f2e24a9 100644 --- a/backend/app/agent_runtime/workflow_integration.py +++ b/backend/app/agent_runtime/workflow_integration.py @@ -192,8 +192,8 @@ async def run_orchestrator_node( # 2. 解析编排模式 mode = node_data.get("mode", "debate").lower() - if mode not in ("route", "sequential", "debate", "pipeline"): - return {"output": f"错误:不支持的编排模式 '{mode}',可选: route, sequential, debate, pipeline", "status": "error"} + if mode not in ("route", "sequential", "debate", "pipeline", "graph"): + return {"output": f"错误:不支持的编排模式 '{mode}',可选: route, sequential, debate, pipeline, graph", "status": "error"} # 3. 解析 Agent 列表 agent_ids = node_data.get("agents", []) @@ -266,33 +266,45 @@ async def run_orchestrator_node( temperature=0.3, ), ) + + # graph 模式需要传递节点和边定义 + graph_nodes = node_data.get("graph_nodes") if mode == "graph" else None + graph_edges = node_data.get("graph_edges") if mode == "graph" else None + result = await orchestrator.run( mode=mode, question=query, agents=agent_configs, on_llm_call=on_llm_invocation, + graph_nodes=graph_nodes, + graph_edges=graph_edges, ) # 6. 返回结构化结果 + meta: Dict[str, Any] = { + "mode": result.mode, + "agent_count": len(agent_configs), + "steps": [ + { + "agent_id": s.agent_id, + "agent_name": s.agent_name, + "input": s.input[:200] if s.input else "", + "output": s.output[:500] if s.output else "", + "iterations_used": s.iterations_used, + "tool_calls_made": s.tool_calls_made, + "error": s.error, + } + for s in result.steps + ], + } + if mode == "graph": + meta["graph_node_count"] = len(graph_nodes) if graph_nodes else 0 + meta["graph_edge_count"] = len(graph_edges) if graph_edges else 0 + return { "output": result.final_answer, "status": "success", - "orchestrator_meta": { - "mode": result.mode, - "agent_count": len(agent_configs), - "steps": [ - { - "agent_id": s.agent_id, - "agent_name": s.agent_name, - "input": s.input[:200] if s.input else "", - "output": s.output[:500] if s.output else "", - "iterations_used": s.iterations_used, - "tool_calls_made": s.tool_calls_made, - "error": s.error, - } - for s in result.steps - ], - }, + "orchestrator_meta": meta, } except Exception as e: diff --git a/backend/app/core/tools_bootstrap.py b/backend/app/core/tools_bootstrap.py index 4c1fbec..0ce8dea 100644 --- a/backend/app/core/tools_bootstrap.py +++ b/backend/app/core/tools_bootstrap.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) _registered = False -_EXPECTED_BUILTIN = 47 +_EXPECTED_BUILTIN = 51 def ensure_builtin_tools_registered() -> None: @@ -65,6 +65,10 @@ def ensure_builtin_tools_registered() -> None: main_agent_assign_task, main_agent_check_progress, main_agent_notify_user, + feishu_create_doc_tool, + feishu_create_calendar_event_tool, + feishu_search_contacts_tool, + feishu_send_approval_tool, HTTP_REQUEST_SCHEMA, FILE_READ_SCHEMA, FILE_WRITE_SCHEMA, @@ -112,6 +116,10 @@ def ensure_builtin_tools_registered() -> None: MAIN_AGENT_ASSIGN_TASK_SCHEMA, MAIN_AGENT_CHECK_PROGRESS_SCHEMA, MAIN_AGENT_NOTIFY_USER_SCHEMA, + FEISHU_CREATE_DOC_SCHEMA, + FEISHU_CREATE_CALENDAR_EVENT_SCHEMA, + FEISHU_SEARCH_CONTACTS_SCHEMA, + FEISHU_SEND_APPROVAL_SCHEMA, ) tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA) @@ -161,6 +169,10 @@ def ensure_builtin_tools_registered() -> None: tool_registry.register_builtin_tool("assign_task", main_agent_assign_task, MAIN_AGENT_ASSIGN_TASK_SCHEMA) tool_registry.register_builtin_tool("check_progress", main_agent_check_progress, MAIN_AGENT_CHECK_PROGRESS_SCHEMA) tool_registry.register_builtin_tool("notify_user", main_agent_notify_user, MAIN_AGENT_NOTIFY_USER_SCHEMA) + tool_registry.register_builtin_tool("feishu_create_doc", feishu_create_doc_tool, FEISHU_CREATE_DOC_SCHEMA) + tool_registry.register_builtin_tool("feishu_create_calendar_event", feishu_create_calendar_event_tool, FEISHU_CREATE_CALENDAR_EVENT_SCHEMA) + tool_registry.register_builtin_tool("feishu_search_contacts", feishu_search_contacts_tool, FEISHU_SEARCH_CONTACTS_SCHEMA) + tool_registry.register_builtin_tool("feishu_send_approval", feishu_send_approval_tool, FEISHU_SEND_APPROVAL_SCHEMA) _registered = True n = tool_registry.builtin_tool_count() diff --git a/backend/app/services/builtin_tools.py b/backend/app/services/builtin_tools.py index 96e0475..ee4c0f2 100644 --- a/backend/app/services/builtin_tools.py +++ b/backend/app/services/builtin_tools.py @@ -5394,6 +5394,362 @@ MAIN_AGENT_NOTIFY_USER_SCHEMA = { } +# ═══════════════════════════════════════════════════════════════ +# 飞书操作工具:文档/日程/审批/通讯录 +# ═══════════════════════════════════════════════════════════════ + + +def _get_feishu_token() -> Optional[str]: + """获取飞书 tenant_access_token(带缓存)。""" + import time + app_id = (getattr(settings, "FEISHU_APP_ID", "") or "").strip() + app_secret = (getattr(settings, "FEISHU_APP_SECRET", "") or "").strip() + if not app_id or not app_secret: + return None + + now = time.time() + cache = getattr(_get_feishu_token, "_cache", None) + if cache and now < cache.get("expires_at", 0) - 300: + return cache["token"] + + try: + with httpx.Client(timeout=10) as client: + resp = client.post( + "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", + json={"app_id": app_id, "app_secret": app_secret}, + ) + result = resp.json() + if resp.is_success and result.get("code") == 0: + token = result["tenant_access_token"] + expire = result.get("expire", 7200) + _get_feishu_token._cache = {"token": token, "expires_at": now + expire} + return token + except Exception: + pass + return None + + +async def feishu_create_doc_tool(title: str, content: str = "", folder_token: str = "") -> str: + """飞书工具:创建飞书文档。 + + 使用飞书开放 API 创建在线文档,适合记录会议纪要、报告、知识库。 + + Args: + title: 文档标题 + content: 文档内容(纯文本或 Markdown,会转为飞书 Doc 格式) + folder_token: 文件夹 token(可选,指定存放位置) + """ + token = _get_feishu_token() + if not token: + return json.dumps({ + "error": "飞书应用未配置(FEISHU_APP_ID / FEISHU_APP_SECRET),无法创建文档。请在 .env 中配置飞书应用。" + }, ensure_ascii=False) + + try: + async with httpx.AsyncClient(timeout=15) as client: + # 创建文档 + body = {"title": title} + if folder_token: + body["folder_token"] = folder_token + + resp = await client.post( + "https://open.feishu.cn/open-apis/docx/v1/documents", + headers={"Authorization": f"Bearer {token}"}, + json=body, + ) + result = resp.json() + if result.get("code") != 0: + return json.dumps({ + "error": f"创建文档失败: {result.get('msg', result)}", + "code": result.get("code"), + }, ensure_ascii=False) + + doc_id = result["data"]["document"]["document_id"] + doc_url = f"https://bytedance.feishu.cn/docx/{doc_id}" + + # 如果有内容,写入文档 + if content.strip(): + await client.post( + f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_id}/blocks/{doc_id}/children", + headers={"Authorization": f"Bearer {token}"}, + json={ + "children": [ + { + "block_type": 2, # 文本块 + "text": { + "elements": [{"text_run": {"content": content[:5000]}}], + "style": {}, + }, + } + ], + "index": 0, + }, + ) + + return json.dumps({ + "document_id": doc_id, + "url": doc_url, + "title": title, + "message": f"飞书文档 '{title}' 创建成功", + }, ensure_ascii=False) + except Exception as e: + logger.error(f"feishu_create_doc 失败: {e}", exc_info=True) + return json.dumps({"error": f"创建飞书文档失败: {e}"}, ensure_ascii=False) + + +async def feishu_create_calendar_event_tool( + summary: str, + start_time: str = "", + end_time: str = "", + description: str = "", +) -> str: + """飞书工具:创建飞书日历日程。 + + Args: + summary: 日程标题 + start_time: 开始时间(ISO 格式,如 2026-05-09T10:00:00,默认为当前时间+1小时) + end_time: 结束时间(ISO 格式,默认为开始时间+1小时) + description: 日程描述(可选) + """ + from datetime import datetime as dt, timedelta + + token = _get_feishu_token() + if not token: + return json.dumps({ + "error": "飞书应用未配置,无法创建日程。" + }, ensure_ascii=False) + + # 处理时间 + if not start_time: + start_dt = dt.now() + timedelta(hours=1) + else: + start_dt = dt.fromisoformat(start_time) + + if not end_time: + end_dt = start_dt + timedelta(hours=1) + else: + end_dt = dt.fromisoformat(end_time) + + try: + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.post( + "https://open.feishu.cn/open-apis/calendar/v4/calendars/primary/events", + headers={"Authorization": f"Bearer {token}"}, + json={ + "summary": summary, + "description": description[:2000] if description else "", + "start_time": { + "timestamp": str(int(start_dt.timestamp())), + "timezone": "Asia/Shanghai", + }, + "end_time": { + "timestamp": str(int(end_dt.timestamp())), + "timezone": "Asia/Shanghai", + }, + }, + ) + result = resp.json() + if result.get("code") != 0: + return json.dumps({ + "error": f"创建日程失败: {result.get('msg', result)}", + }, ensure_ascii=False) + + event = result["data"]["event"] + return json.dumps({ + "event_id": event["event_id"], + "summary": summary, + "start_time": start_dt.isoformat(), + "end_time": end_dt.isoformat(), + "message": f"飞书日程 '{summary}' 创建成功", + }, ensure_ascii=False) + except Exception as e: + logger.error(f"feishu_create_calendar_event 失败: {e}", exc_info=True) + return json.dumps({"error": f"创建日程失败: {e}"}, ensure_ascii=False) + + +async def feishu_search_contacts_tool(keyword: str, search_type: str = "user") -> str: + """飞书工具:搜索通讯录用户/部门。 + + Args: + keyword: 搜索关键词(姓名、邮箱、部门名) + search_type: 搜索类型 user(用户)/ department(部门)""" + token = _get_feishu_token() + if not token: + return json.dumps({"error": "飞书应用未配置,无法搜索通讯录。"}, ensure_ascii=False) + + try: + async with httpx.AsyncClient(timeout=10) as client: + if search_type == "department": + resp = await client.get( + "https://open.feishu.cn/open-apis/contact/v3/departments/search", + headers={"Authorization": f"Bearer {token}"}, + params={"query": keyword, "page_size": 10}, + ) + else: + # 先按邮箱查 + resp = await client.post( + "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id", + headers={"Authorization": f"Bearer {token}"}, + json={"emails": [keyword] if "@" in keyword else []}, + ) + result = resp.json() + if result.get("code") == 0 and result.get("data", {}).get("user_list"): + users = result["data"]["user_list"] + user_list = [{"email": u.get("email", ""), "open_id": u.get("open_id", "")} for u in users] + return json.dumps({"found": True, "users": user_list, "search_type": "email"}, ensure_ascii=False) + + # 否则按姓名模糊搜索 + resp = await client.get( + "https://open.feishu.cn/open-apis/contact/v3/users", + headers={"Authorization": f"Bearer {token}"}, + params={"page_size": 10}, + ) + + result = resp.json() + if result.get("code") != 0: + return json.dumps({"found": False, "error": result.get("msg", str(result))}, ensure_ascii=False) + + items = result.get("data", {}).get("items", []) + if search_type == "department": + results = [{"name": d["name"], "department_id": d["department_id"]} for d in items] + else: + results = [{"name": u.get("name", ""), "email": u.get("email", ""), "open_id": u.get("open_id", "")} for u in items] + + return json.dumps({"found": True, "count": len(results), "results": results[:10]}, ensure_ascii=False) + except Exception as e: + logger.error(f"feishu_search_contacts 失败: {e}", exc_info=True) + return json.dumps({"error": f"搜索通讯录失败: {e}"}, ensure_ascii=False) + + +async def feishu_send_approval_tool( + approver_open_id: str, + title: str, + content: str, + approval_type: str = "general", +) -> str: + """飞书工具:向指派人发送审批卡片消息。 + + 在飞书对话中发送交互式审批卡片,让审批人可以一键通过/驳回。 + + Args: + approver_open_id: 审批人的飞书 open_id + title: 审批标题 + content: 审批内容说明 + approval_type: 审批类型 general(通用)/ expense(报销)/ leave(请假) + """ + token = _get_feishu_token() + if not token: + return json.dumps({"error": "飞书应用未配置,无法发送审批。"}, ensure_ascii=False) + + card = { + "config": {"wide_screen_mode": True}, + "header": { + "title": {"tag": "plain_text", "content": f"📋 {title}"}, + "template": "blue", + }, + "elements": [ + {"tag": "markdown", "content": content[:2000]}, + {"tag": "hr"}, + {"tag": "note", "elements": [{"tag": "plain_text", "content": "请在飞书中回复「通过」或「驳回」来处理此审批"}]}, + ], + } + + try: + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.post( + "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id", + headers={"Authorization": f"Bearer {token}"}, + json={ + "receive_id": approver_open_id, + "msg_type": "interactive", + "content": json.dumps(card, ensure_ascii=False), + }, + ) + result = resp.json() + if resp.is_success and result.get("code") == 0: + return json.dumps({ + "sent": True, + "message_id": result["data"]["message_id"], + "approver_open_id": approver_open_id, + "message": f"审批卡片已发送至 {approver_open_id}", + }, ensure_ascii=False) + else: + return json.dumps({"error": f"发送审批卡片失败: {result.get('msg', result)}"}, ensure_ascii=False) + except Exception as e: + logger.error(f"feishu_send_approval 失败: {e}", exc_info=True) + return json.dumps({"error": f"发送审批失败: {e}"}, ensure_ascii=False) + + +FEISHU_CREATE_DOC_SCHEMA = { + "type": "function", + "function": { + "name": "feishu_create_doc", + "description": "创建飞书在线文档。适合记录会议纪要、报告、知识库等场景。需要配置飞书应用 (FEISHU_APP_ID / FEISHU_APP_SECRET)。", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "文档标题"}, + "content": {"type": "string", "description": "文档内容(纯文本/Markdown)"}, + "folder_token": {"type": "string", "description": "目标文件夹 token(可选)"}, + }, + "required": ["title"], + }, + }, +} + +FEISHU_CREATE_CALENDAR_EVENT_SCHEMA = { + "type": "function", + "function": { + "name": "feishu_create_calendar_event", + "description": "创建飞书日历日程。适合安排会议、提醒、任务截止日期。需要配置飞书应用。", + "parameters": { + "type": "object", + "properties": { + "summary": {"type": "string", "description": "日程标题"}, + "start_time": {"type": "string", "description": "开始时间 ISO 格式 (2026-05-09T10:00:00),默认当前+1小时"}, + "end_time": {"type": "string", "description": "结束时间,默认开始+1小时"}, + "description": {"type": "string", "description": "日程描述(可选)"}, + }, + "required": ["summary"], + }, + }, +} + +FEISHU_SEARCH_CONTACTS_SCHEMA = { + "type": "function", + "function": { + "name": "feishu_search_contacts", + "description": "搜索飞书通讯录用户或部门。可按姓名、邮箱、部门名搜索。需要配置飞书应用。", + "parameters": { + "type": "object", + "properties": { + "keyword": {"type": "string", "description": "搜索关键词(姓名/邮箱/部门名)"}, + "search_type": {"type": "string", "description": "user(用户)或 department(部门),默认 user"}, + }, + "required": ["keyword"], + }, + }, +} + +FEISHU_SEND_APPROVAL_SCHEMA = { + "type": "function", + "function": { + "name": "feishu_send_approval", + "description": "向飞书用户发送审批卡片消息,审批人可在飞书直接通过/驳回。需要配置飞书应用。", + "parameters": { + "type": "object", + "properties": { + "approver_open_id": {"type": "string", "description": "审批人的飞书 open_id"}, + "title": {"type": "string", "description": "审批标题"}, + "content": {"type": "string", "description": "审批内容说明"}, + "approval_type": {"type": "string", "description": "审批类型: general/expense/leave(默认 general)"}, + }, + "required": ["approver_open_id", "title", "content"], + }, + }, +} + + TEXT_TO_SPEECH_SCHEMA = { "type": "function", "function": { diff --git a/backend/app/services/feishu_ws_handler.py b/backend/app/services/feishu_ws_handler.py index b52f9ed..bfb4411 100644 --- a/backend/app/services/feishu_ws_handler.py +++ b/backend/app/services/feishu_ws_handler.py @@ -115,6 +115,27 @@ def _reply_card(open_id: str, title: str, content: str, status: str = "info"): logger.warning("飞书回复卡片失败: %s", e) +async def _handle_goal_creation(db, user_id: str, goal_title: str, open_id: str): + """从飞书消息中创建 Goal 并异步启动执行。""" + from app.services.goal_service import create_goal + from app.tasks.goal_tasks import execute_goal_task + + try: + goal = create_goal(db=db, creator_id=user_id, title=goal_title, priority=5) + _reply_to_feishu(open_id, f"✅ 目标已创建: **{goal.title}**\n正在分解任务并启动执行...") + + # 异步执行目标 + task = execute_goal_task.delay(str(goal.id)) + logger.info("飞书触发 Goal 创建: goal_id=%s celery_task=%s", goal.id, task.id) + + # 更新状态 + from app.services.goal_service import update_goal + update_goal(db, str(goal.id), status="active") + except Exception as e: + logger.error("飞书 Goal 创建失败: %s", e) + _reply_to_feishu(open_id, f"创建目标失败: {e}") + + async def _handle_message_async(data): """异步处理飞书消息。""" open_id = _get_sender_open_id(data) @@ -225,6 +246,22 @@ async def _handle_message_async(data): memory_scope_id=str(agent.id), ) + # ── 目标/任务意图检测:创建 Goal 并异步执行 ── + goal_triggers = ["创建目标:", "目标:", "创建任务:", "new goal:", "goal:"] + triggered_goal = False + goal_title = "" + for trigger in goal_triggers: + if text.lower().startswith(trigger.lower()): + trigger_text = text[len(trigger):].strip() + if trigger_text: + goal_title = trigger_text[:500] + triggered_goal = True + break + + if triggered_goal and goal_title: + await _handle_goal_creation(db, user.id, goal_title, open_id) + return + on_llm_call = _make_llm_logger(db, agent_id=str(agent.id), user_id=user.id) runtime = AgentRuntime(config=config, on_llm_call=on_llm_call) result = await runtime.run(text) diff --git a/backend/app/services/orange_ws_handler.py b/backend/app/services/orange_ws_handler.py index cbde6ce..1e169de 100644 --- a/backend/app/services/orange_ws_handler.py +++ b/backend/app/services/orange_ws_handler.py @@ -137,6 +137,22 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str] return _log +async def _handle_goal_creation(db, user_id: str, goal_title: str, open_id: str): + """橙子飞书消息中创建 Goal 并异步启动执行。""" + from app.services.goal_service import create_goal as svc_create_goal, update_goal + from app.tasks.goal_tasks import execute_goal_task + + try: + goal = svc_create_goal(db=db, creator_id=user_id, title=goal_title, priority=5) + _reply_to_feishu(open_id, f"✅ 目标已创建: **{goal.title}**\n正在分解任务并启动执行...") + task = execute_goal_task.delay(str(goal.id)) + logger.info("橙子触发 Goal 创建: goal_id=%s celery_task=%s", goal.id, task.id) + update_goal(db, str(goal.id), status="active") + except Exception as e: + logger.error("橙子 Goal 创建失败: %s", e) + _reply_to_feishu(open_id, f"创建目标失败: {e}") + + async def _handle_message_async(data): """异步处理橙子消息 — 固定使用橙子助手 Agent。""" open_id = _get_sender_open_id(data) @@ -226,6 +242,15 @@ async def _handle_message_async(data): memory_scope_id=str(agent.id), ) + # ── 目标/任务意图检测 ── + for trigger in ["创建目标:", "目标:", "new goal:", "goal:"]: + if text.lower().startswith(trigger.lower()): + goal_title = text[len(trigger):].strip() + if goal_title: + await _handle_goal_creation(db, user.id, goal_title[:500], open_id) + return + break + on_llm_call = _make_llm_logger(db, agent_id=str(agent.id)) runtime = AgentRuntime(config=config, on_llm_call=on_llm_call) result = await runtime.run(text)