@@ -283,6 +283,121 @@ def _default_openclaw_task_description() -> str:
283283 return _rp_phrase ('openclaw_processing' , _rp_lang (None ))
284284
285285
286+ def _resolve_openclaw_sender_id (messages : list [dict [str , Any ]] | None ) -> str :
287+ if not isinstance (messages , list ):
288+ return ""
289+
290+ for message in reversed (messages [- 10 :]):
291+ if not isinstance (message , dict ) or message .get ("role" ) != "user" :
292+ continue
293+
294+ candidates : list [Any ] = [
295+ message .get ("sender_id" ),
296+ message .get ("user_id" ),
297+ ]
298+ for container_key in ("meta" , "metadata" , "_ctx" ):
299+ container = message .get (container_key )
300+ if isinstance (container , dict ):
301+ candidates .extend ([
302+ container .get ("sender_id" ),
303+ container .get ("user_id" ),
304+ ])
305+
306+ for candidate in candidates :
307+ resolved = str (candidate or "" ).strip ()
308+ if resolved :
309+ return resolved
310+ return ""
311+
312+
313+ def _collect_active_openclaw_task_ids (
314+ * ,
315+ sender_id : Optional [str ] = None ,
316+ lanlan_name : Optional [str ] = None ,
317+ exclude_task_id : Optional [str ] = None ,
318+ ) -> list [str ]:
319+ task_ids : list [str ] = []
320+ for task_id , info in Modules .task_registry .items ():
321+ if task_id == exclude_task_id or not isinstance (info , dict ):
322+ continue
323+ if info .get ("type" ) != "openclaw" :
324+ continue
325+ if info .get ("status" ) not in {"queued" , "running" }:
326+ continue
327+ if sender_id and str (info .get ("sender_id" ) or "" ).strip () != str (sender_id ).strip ():
328+ continue
329+ if lanlan_name and str (info .get ("lanlan_name" ) or "" ).strip () != str (lanlan_name ).strip ():
330+ continue
331+ task_ids .append (task_id )
332+ return task_ids
333+
334+
335+ async def _cancel_openclaw_tasks_for_stop (
336+ * ,
337+ sender_id : Optional [str ],
338+ lanlan_name : Optional [str ],
339+ exclude_task_id : Optional [str ] = None ,
340+ ) -> list [str ]:
341+ cancelled_task_ids : list [str ] = []
342+ for task_id in _collect_active_openclaw_task_ids (
343+ sender_id = sender_id ,
344+ lanlan_name = lanlan_name ,
345+ exclude_task_id = exclude_task_id ,
346+ ):
347+ info = Modules .task_registry .get (task_id )
348+ if not isinstance (info , dict ):
349+ continue
350+
351+ bg = Modules .task_async_handles .get (task_id )
352+ if bg and not bg .done ():
353+ bg .cancel ()
354+
355+ if Modules .openclaw :
356+ try :
357+ stop_result = await Modules .openclaw .stop_running (
358+ sender_id = info .get ("sender_id" ),
359+ session_id = info .get ("session_id" ),
360+ conversation_id = info .get ("session_id" ),
361+ role_name = info .get ("lanlan_name" ),
362+ task_id = task_id ,
363+ )
364+ if not stop_result .get ("success" ):
365+ logger .warning (
366+ "[OpenClaw] stop_running failed during /stop for %s: %s" ,
367+ task_id ,
368+ stop_result .get ("error" ),
369+ )
370+ except Exception as exc :
371+ logger .warning ("[OpenClaw] stop_running failed during /stop for %s: %s" , task_id , exc )
372+
373+ info ["status" ] = "cancelled"
374+ info ["error" ] = "Cancelled by user"
375+ info ["end_time" ] = _now_iso ()
376+ cancelled_task_ids .append (task_id )
377+
378+ # Let the task coroutine emit the cancelled update when it is still
379+ # alive; only emit here when there is no active background handle.
380+ if not (bg and not bg .done ()):
381+ try :
382+ await _emit_main_event (
383+ "task_update" ,
384+ info .get ("lanlan_name" ),
385+ task = {
386+ "id" : task_id ,
387+ "status" : "cancelled" ,
388+ "type" : "openclaw" ,
389+ "start_time" : info .get ("start_time" ),
390+ "end_time" : info .get ("end_time" ),
391+ "params" : info .get ("params" , {}),
392+ "error" : "Cancelled by user" ,
393+ },
394+ )
395+ except Exception :
396+ logger .debug ("[OpenClaw] emit task_update(cancelled by /stop) failed: task_id=%s" , task_id , exc_info = True )
397+
398+ return cancelled_task_ids
399+
400+
286401def _cleanup_task_registry () -> List [Dict [str , Any ]]:
287402 """清理 task_registry 中超过 5 分钟的已完成/失败/取消任务,防止内存泄漏;同时检查 deferred 任务超时
288403
@@ -939,8 +1054,23 @@ def _build_user_turn_fingerprint(messages: Any) -> Optional[str]:
9391054 if m .get ("role" ) != "user" :
9401055 continue
9411056 text = str (m .get ("text" ) or m .get ("content" ) or "" ).strip ()
942- if text :
943- user_parts .append (text )
1057+ attachments = m .get ("attachments" ) or []
1058+ attachment_urls : list [str ] = []
1059+ if isinstance (attachments , list ):
1060+ for item in attachments :
1061+ if isinstance (item , str ):
1062+ url = item .strip ()
1063+ elif isinstance (item , dict ):
1064+ url = str (item .get ("url" ) or item .get ("image_url" ) or "" ).strip ()
1065+ else :
1066+ url = ""
1067+ if url :
1068+ attachment_urls .append (url )
1069+ if text or attachment_urls :
1070+ part = text
1071+ if attachment_urls :
1072+ part = f"{ part } \n [attachments]\n " + "\n " .join (attachment_urls )
1073+ user_parts .append (part .strip ())
9441074 if not user_parts :
9451075 return None
9461076 payload = "\n " .join (user_parts ).encode ("utf-8" , errors = "ignore" )
@@ -1632,10 +1762,71 @@ def _cleanup_up_task(_t, _tid=result.task_id):
16321762 if Modules .agent_flags .get ("openclaw_enabled" , False ) and Modules .openclaw :
16331763 nk_start = _now_iso ()
16341764 instruction = ""
1765+ attachments = []
1766+ magic_command = None
1767+ direct_reply = False
16351768 if isinstance (result .tool_args , dict ):
16361769 instruction = str (result .tool_args .get ("instruction" ) or "" )
1637- task_params = {"description" : result .task_description or _default_openclaw_task_description ()}
1638- nk_sender_id = Modules .openclaw .default_sender_id
1770+ attachments = result .tool_args .get ("attachments" ) or []
1771+ magic_command = Modules .openclaw .normalize_magic_command (result .tool_args .get ("magic_command" ))
1772+ direct_reply = bool (result .tool_args .get ("direct_reply" ))
1773+ task_params = {
1774+ "description" : result .task_description or _default_openclaw_task_description (),
1775+ "attachment_count" : len (attachments ) if isinstance (attachments , list ) else 0 ,
1776+ }
1777+ if magic_command :
1778+ task_params ["magic_command" ] = magic_command
1779+ nk_sender_id = _resolve_openclaw_sender_id (messages ) or Modules .openclaw .default_sender_id
1780+ if magic_command :
1781+ if magic_command == "/stop" :
1782+ cancelled_task_ids = await _cancel_openclaw_tasks_for_stop (
1783+ sender_id = nk_sender_id ,
1784+ lanlan_name = lanlan_name ,
1785+ exclude_task_id = result .task_id ,
1786+ )
1787+ if cancelled_task_ids :
1788+ task_params ["cancelled_task_ids" ] = cancelled_task_ids
1789+ try :
1790+ nk_result = await Modules .openclaw .run_magic_command (
1791+ magic_command ,
1792+ sender_id = nk_sender_id ,
1793+ role_name = lanlan_name ,
1794+ )
1795+ success = bool (nk_result .get ("success" ))
1796+ reply = str (nk_result .get ("reply" ) or "" )
1797+ if success :
1798+ await _emit_task_result (
1799+ lanlan_name ,
1800+ channel = "openclaw" ,
1801+ task_id = str (result .task_id or "" ),
1802+ success = True ,
1803+ summary = reply [:500 ] if reply else _rp_phrase ('openclaw_done' , _rp_lang (None )),
1804+ detail = reply ,
1805+ direct_reply = direct_reply ,
1806+ )
1807+ else :
1808+ await _emit_task_result (
1809+ lanlan_name ,
1810+ channel = "openclaw" ,
1811+ task_id = str (result .task_id or "" ),
1812+ success = False ,
1813+ summary = _rp_phrase ('openclaw_failed' , _rp_lang (None )),
1814+ error_message = str (nk_result .get ("error" ) or "" )[:500 ],
1815+ )
1816+ except Exception as e :
1817+ logger .exception ("[OpenClaw] magic command dispatch failed: %s" , e )
1818+ try :
1819+ await _emit_task_result (
1820+ lanlan_name ,
1821+ channel = "openclaw" ,
1822+ task_id = str (result .task_id or "" ),
1823+ success = False ,
1824+ summary = _rp_phrase ('openclaw_dispatch_failed' , _rp_lang (None )),
1825+ error_message = str (e )[:500 ],
1826+ )
1827+ except Exception :
1828+ pass
1829+ return
16391830 nk_session_id = Modules .openclaw .get_or_create_persistent_session_id (
16401831 role_name = lanlan_name ,
16411832 sender_id = nk_sender_id ,
@@ -1672,11 +1863,12 @@ def _cleanup_up_task(_t, _tid=result.task_id):
16721863 except Exception as emit_err :
16731864 logger .debug ("[OpenClaw] emit task_update(running) failed: task_id=%s error=%s" , result .task_id , emit_err )
16741865 try :
1866+ ack_text = _rp_phrase ("openclaw_try" , _rp_lang (None ))
16751867 await _emit_main_event (
16761868 "proactive_message" ,
16771869 lanlan_name ,
1678- text = "我试试" ,
1679- detail = "我试试" ,
1870+ text = ack_text ,
1871+ detail = ack_text ,
16801872 direct_reply = True ,
16811873 timestamp = _now_iso (),
16821874 )
@@ -1686,6 +1878,7 @@ async def _run_openclaw_dispatch():
16861878 try :
16871879 nk_result = await Modules .openclaw .run_instruction (
16881880 instruction ,
1881+ attachments = attachments ,
16891882 sender_id = nk_sender_id ,
16901883 session_id = nk_session_id ,
16911884 conversation_id = conversation_id ,
@@ -1701,6 +1894,7 @@ async def _run_openclaw_dispatch():
17011894 _reg ["status" ] = "completed" if success else "failed"
17021895 _reg ["end_time" ] = _now_iso ()
17031896 _reg ["result" ] = nk_result
1897+ _reg ["session_id" ] = str (nk_result .get ("session_id" ) or _reg .get ("session_id" ) or "" )
17041898 if not success :
17051899 _reg ["error" ] = str (nk_result .get ("error" ) or "" )[:500 ]
17061900 _task_tracker .record_completed (
@@ -1716,7 +1910,7 @@ async def _run_openclaw_dispatch():
17161910 success = True ,
17171911 summary = reply [:500 ] if reply else _rp_phrase ('openclaw_done' , _rp_lang (None )),
17181912 detail = reply ,
1719- direct_reply = False ,
1913+ direct_reply = direct_reply ,
17201914 )
17211915 else :
17221916 await _emit_task_result (
@@ -2505,6 +2699,68 @@ async def health():
25052699 )
25062700
25072701
2702+ @app .post ("/openclaw/preflight" )
2703+ async def openclaw_preflight (payload : Dict [str , Any ]):
2704+ """快速判断当前输入是否应由 OpenClaw(QwenPaw) 接管。"""
2705+ if not Modules .task_executor :
2706+ raise HTTPException (503 , "Task executor not ready" )
2707+
2708+ if not Modules .analyzer_enabled :
2709+ return {
2710+ "success" : True ,
2711+ "should_handoff" : False ,
2712+ "reason" : "analyzer_disabled" ,
2713+ }
2714+
2715+ if not Modules .agent_flags .get ("openclaw_enabled" , False ):
2716+ return {
2717+ "success" : True ,
2718+ "should_handoff" : False ,
2719+ "reason" : "openclaw_disabled" ,
2720+ }
2721+
2722+ messages = (payload or {}).get ("messages" ) or []
2723+ if not isinstance (messages , list ) or not messages :
2724+ raise HTTPException (400 , "messages required" )
2725+
2726+ lanlan_name = (payload or {}).get ("lanlan_name" )
2727+ conversation_id = (payload or {}).get ("conversation_id" )
2728+ lang = str ((payload or {}).get ("lang" ) or "en" )
2729+
2730+ flags = {
2731+ "computer_use_enabled" : False ,
2732+ "browser_use_enabled" : False ,
2733+ "user_plugin_enabled" : False ,
2734+ "openclaw_enabled" : True ,
2735+ "openfang_enabled" : False ,
2736+ }
2737+
2738+ result = await Modules .task_executor .analyze_and_execute (
2739+ messages = messages ,
2740+ lanlan_name = lanlan_name ,
2741+ agent_flags = flags ,
2742+ conversation_id = conversation_id ,
2743+ lang = lang ,
2744+ )
2745+
2746+ should_handoff = bool (
2747+ result
2748+ and getattr (result , "has_task" , False )
2749+ and getattr (result , "execution_method" , "" ) == "openclaw"
2750+ )
2751+ tool_args = result .tool_args if isinstance (getattr (result , "tool_args" , None ), dict ) else {}
2752+
2753+ return {
2754+ "success" : True ,
2755+ "should_handoff" : should_handoff ,
2756+ "execution_method" : getattr (result , "execution_method" , None ) if result else None ,
2757+ "task_description" : getattr (result , "task_description" , "" ) if result else "" ,
2758+ "reason" : getattr (result , "reason" , "" ) if result else "" ,
2759+ "magic_command" : tool_args .get ("magic_command" ),
2760+ "direct_reply" : bool (tool_args .get ("direct_reply" )) if tool_args else False ,
2761+ }
2762+
2763+
25082764# 插件直接触发路由(放在顶层,确保不在其它函数体内)
25092765@app .post ("/plugin/execute" )
25102766async def plugin_execute_direct (payload : Dict [str , Any ]):
0 commit comments