@researai/deepscientist 1.5.8 → 1.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +186 -21
  2. package/README.md +108 -95
  3. package/assets/branding/connector-qq.png +0 -0
  4. package/assets/branding/connector-rokid.png +0 -0
  5. package/assets/branding/connector-weixin.png +0 -0
  6. package/assets/branding/projects.png +0 -0
  7. package/bin/ds.js +172 -13
  8. package/docs/assets/branding/projects.png +0 -0
  9. package/docs/en/00_QUICK_START.md +308 -70
  10. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  11. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  12. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  13. package/docs/en/09_DOCTOR.md +41 -5
  14. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  15. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  16. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
  17. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  18. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  19. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  20. package/docs/en/README.md +79 -0
  21. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  23. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  24. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  25. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  26. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  27. package/docs/zh/00_QUICK_START.md +315 -74
  28. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  29. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  30. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  31. package/docs/zh/09_DOCTOR.md +41 -5
  32. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  33. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  34. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
  35. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  36. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  37. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  38. package/docs/zh/README.md +126 -0
  39. package/install.sh +0 -34
  40. package/package.json +3 -3
  41. package/pyproject.toml +2 -2
  42. package/src/deepscientist/__init__.py +1 -1
  43. package/src/deepscientist/annotations.py +343 -0
  44. package/src/deepscientist/artifact/arxiv.py +484 -37
  45. package/src/deepscientist/artifact/metrics.py +1 -3
  46. package/src/deepscientist/artifact/service.py +1347 -111
  47. package/src/deepscientist/arxiv_library.py +275 -0
  48. package/src/deepscientist/bash_exec/service.py +9 -0
  49. package/src/deepscientist/bridges/builtins.py +2 -0
  50. package/src/deepscientist/bridges/connectors.py +447 -0
  51. package/src/deepscientist/channels/__init__.py +2 -0
  52. package/src/deepscientist/channels/builtins.py +3 -1
  53. package/src/deepscientist/channels/qq.py +1 -1
  54. package/src/deepscientist/channels/qq_gateway.py +1 -1
  55. package/src/deepscientist/channels/relay.py +7 -1
  56. package/src/deepscientist/channels/weixin.py +59 -0
  57. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  58. package/src/deepscientist/config/models.py +22 -2
  59. package/src/deepscientist/config/service.py +431 -60
  60. package/src/deepscientist/connector/__init__.py +4 -0
  61. package/src/deepscientist/connector/connector_profiles.py +481 -0
  62. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  63. package/src/deepscientist/connector/qq_profiles.py +206 -0
  64. package/src/deepscientist/connector/weixin_support.py +663 -0
  65. package/src/deepscientist/connector_profiles.py +1 -374
  66. package/src/deepscientist/connector_runtime.py +2 -0
  67. package/src/deepscientist/daemon/api/handlers.py +295 -5
  68. package/src/deepscientist/daemon/api/router.py +16 -1
  69. package/src/deepscientist/daemon/app.py +1130 -61
  70. package/src/deepscientist/doctor.py +5 -2
  71. package/src/deepscientist/gitops/diff.py +120 -29
  72. package/src/deepscientist/lingzhu_support.py +1 -182
  73. package/src/deepscientist/mcp/server.py +14 -5
  74. package/src/deepscientist/prompts/builder.py +29 -1
  75. package/src/deepscientist/qq_profiles.py +1 -196
  76. package/src/deepscientist/quest/node_traces.py +152 -2
  77. package/src/deepscientist/quest/service.py +169 -43
  78. package/src/deepscientist/quest/stage_views.py +172 -9
  79. package/src/deepscientist/registries/baseline.py +56 -4
  80. package/src/deepscientist/runners/codex.py +55 -3
  81. package/src/deepscientist/weixin_support.py +1 -0
  82. package/src/prompts/connectors/lingzhu.md +3 -1
  83. package/src/prompts/connectors/weixin.md +230 -0
  84. package/src/prompts/system.md +9 -0
  85. package/src/skills/idea/SKILL.md +16 -0
  86. package/src/skills/idea/references/literature-survey-template.md +24 -0
  87. package/src/skills/idea/references/related-work-playbook.md +4 -0
  88. package/src/skills/idea/references/selection-gate.md +9 -0
  89. package/src/skills/write/SKILL.md +1 -1
  90. package/src/tui/package.json +1 -1
  91. package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  92. package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  93. package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
  94. package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  95. package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  96. package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  97. package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  98. package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  99. package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
  100. package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
  101. package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
  102. package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  103. package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
  104. package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
  105. package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  106. package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
  107. package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  108. package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  109. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  110. package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
  111. package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  112. package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
  113. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  114. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  115. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  116. package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
  117. package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
  118. package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
  119. package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
  120. package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
  121. package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
  122. package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
  123. package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
  124. package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
  125. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  126. package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
  127. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  128. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  129. package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
  130. package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
  131. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  132. package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
  133. package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
  134. package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
  135. package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
  136. package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  137. package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
  138. package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
  139. package/src/ui/dist/index.html +2 -2
  140. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  141. package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
  142. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  143. package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
  144. package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
  145. package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
  146. package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
  147. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  148. package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
@@ -0,0 +1,668 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import ipaddress
5
+ import json
6
+ import re
7
+ import secrets
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+
12
+ DEFAULT_LINGZHU_GATEWAY_PORT = 18789
13
+ DEFAULT_LINGZHU_LOCAL_HOST = "127.0.0.1"
14
+ DEFAULT_LINGZHU_AGENT_ID = "main"
15
+ DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
16
+ DEFAULT_LINGZHU_TASK_PREFIX = "我现在的任务是"
17
+ DEFAULT_LINGZHU_PASSIVE_CHAT_TYPE = "passive"
18
+
19
+ _AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
20
+ _AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
21
+ _EXAMPLE_AUTH_AKS = frozenset(
22
+ {
23
+ "abcd1234-abcd-abcd-abcd-abcdefghijkl",
24
+ }
25
+ )
26
+ _PRIVATE_IPV4_NETWORKS = tuple(
27
+ ipaddress.ip_network(item)
28
+ for item in (
29
+ "0.0.0.0/8",
30
+ "10.0.0.0/8",
31
+ "100.64.0.0/10",
32
+ "127.0.0.0/8",
33
+ "169.254.0.0/16",
34
+ "172.16.0.0/12",
35
+ "192.168.0.0/16",
36
+ "198.18.0.0/15",
37
+ )
38
+ )
39
+ _PRIVATE_IPV6_NETWORKS = tuple(
40
+ ipaddress.ip_network(item)
41
+ for item in (
42
+ "::/128",
43
+ "::1/128",
44
+ "fc00::/7",
45
+ "fe80::/10",
46
+ )
47
+ )
48
+ _LINGZHU_EXPERIMENTAL_COMMANDS = {
49
+ "send_notification",
50
+ "send_toast",
51
+ "speak_tts",
52
+ "start_video_record",
53
+ "stop_video_record",
54
+ "open_custom_view",
55
+ }
56
+ _LINGZHU_TOOL_COMMAND_ALIASES = {
57
+ "take_photo": "take_photo",
58
+ "camera": "take_photo",
59
+ "photo": "take_photo",
60
+ "takepicture": "take_photo",
61
+ "take_picture": "take_photo",
62
+ "snapshot": "take_photo",
63
+ "take_navigation": "take_navigation",
64
+ "navigate": "take_navigation",
65
+ "navigation": "take_navigation",
66
+ "maps": "take_navigation",
67
+ "route": "take_navigation",
68
+ "directions": "take_navigation",
69
+ "control_calendar": "control_calendar",
70
+ "calendar": "control_calendar",
71
+ "add_calendar": "control_calendar",
72
+ "schedule": "control_calendar",
73
+ "reminder": "control_calendar",
74
+ "add_reminder": "control_calendar",
75
+ "create_event": "control_calendar",
76
+ "set_schedule": "control_calendar",
77
+ "notify_agent_off": "notify_agent_off",
78
+ "exit_agent": "notify_agent_off",
79
+ "exit": "notify_agent_off",
80
+ "quit": "notify_agent_off",
81
+ "close_agent": "notify_agent_off",
82
+ "leave_agent": "notify_agent_off",
83
+ "send_notification": "send_notification",
84
+ "notification": "send_notification",
85
+ "notify": "send_notification",
86
+ "send_toast": "send_toast",
87
+ "toast": "send_toast",
88
+ "speak_tts": "speak_tts",
89
+ "tts": "speak_tts",
90
+ "speak": "speak_tts",
91
+ "start_video_record": "start_video_record",
92
+ "start_recording": "start_video_record",
93
+ "record_video": "start_video_record",
94
+ "stop_video_record": "stop_video_record",
95
+ "stop_recording": "stop_video_record",
96
+ "open_custom_view": "open_custom_view",
97
+ "custom_view": "open_custom_view",
98
+ "show_view": "open_custom_view",
99
+ }
100
+ _LINGZHU_TOOL_MARKER_RE = re.compile(r"<LINGZHU_TOOL_CALL:([^:>]+):([^>]*)>")
101
+ _LINGZHU_PHOTO_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:拍照|拍张照|照相|拍一张|帮我拍)(?:[^\n]*)$", re.IGNORECASE)
102
+ _LINGZHU_EXIT_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:退出智能体|退出当前会话|结束对话|关闭智能体)(?:[^\n]*)$", re.IGNORECASE)
103
+ _LINGZHU_NOTIFICATION_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:发(?:一条|个)?通知|发送通知)(?:[^\n]*)$", re.IGNORECASE)
104
+ _LINGZHU_TOAST_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:toast|轻提示|弹出提示)(?:[^\n]*)$", re.IGNORECASE)
105
+ _LINGZHU_TTS_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:播报|朗读|语音提示|念一段)(?:[^\n]*)$", re.IGNORECASE)
106
+ _LINGZHU_START_RECORD_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:开始录像|录一段视频|开始录制)(?:[^\n]*)$", re.IGNORECASE)
107
+ _LINGZHU_STOP_RECORD_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:停止录像|结束录像|停止录制)(?:[^\n]*)$", re.IGNORECASE)
108
+ _LINGZHU_OPEN_VIEW_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:打开.*页面|显示.*页面|展示.*页面)(?:[^\n]*)$", re.IGNORECASE)
109
+ _LINGZHU_NAVIGATION_REQUEST_RE = re.compile(r"^(?:请|先|帮我|麻烦|现在|立即)?(?:导航(?:到|去)?|前往|带我去|带路去)\s*[::]?\s*([^\n,。!?]+)", re.IGNORECASE)
110
+
111
+
112
+ def generate_lingzhu_auth_ak() -> str:
113
+ parts: list[str] = []
114
+ for segment_length in _AUTH_AK_SEGMENTS:
115
+ parts.append("".join(secrets.choice(_AUTH_AK_CHARS) for _ in range(segment_length)))
116
+ return "-".join(parts)
117
+
118
+
119
+ def lingzhu_auth_ak_needs_rotation(value: Any) -> bool:
120
+ return str(value or "").strip() in _EXAMPLE_AUTH_AKS
121
+
122
+
123
+ def lingzhu_local_host(config: dict[str, Any] | None) -> str:
124
+ value = str((config or {}).get("local_host") or DEFAULT_LINGZHU_LOCAL_HOST).strip()
125
+ return value or DEFAULT_LINGZHU_LOCAL_HOST
126
+
127
+
128
+ def lingzhu_gateway_port(config: dict[str, Any] | None) -> int:
129
+ raw = (config or {}).get("gateway_port")
130
+ try:
131
+ value = int(raw)
132
+ except (TypeError, ValueError):
133
+ return DEFAULT_LINGZHU_GATEWAY_PORT
134
+ if value < 1 or value > 65535:
135
+ return DEFAULT_LINGZHU_GATEWAY_PORT
136
+ return value
137
+
138
+
139
+ def normalize_public_base_url(value: Any) -> str | None:
140
+ text = str(value or "").strip()
141
+ if not text:
142
+ return None
143
+ parsed = urlparse(text)
144
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
145
+ return None
146
+ return text.rstrip("/")
147
+
148
+
149
+ def public_base_url_looks_public(value: Any) -> bool:
150
+ normalized = normalize_public_base_url(value)
151
+ if normalized is None:
152
+ return False
153
+ parsed = urlparse(normalized)
154
+ hostname = str(parsed.hostname or "").strip().lower()
155
+ if not hostname:
156
+ return False
157
+ if hostname in {"localhost", "0.0.0.0", "::", "::1"}:
158
+ return False
159
+ if hostname.endswith(".local"):
160
+ return False
161
+ try:
162
+ ip = ipaddress.ip_address(hostname)
163
+ except ValueError:
164
+ return True
165
+ if ip.is_multicast:
166
+ return False
167
+ networks = _PRIVATE_IPV4_NETWORKS if ip.version == 4 else _PRIVATE_IPV6_NETWORKS
168
+ return not any(ip in network for network in networks)
169
+
170
+
171
+ def lingzhu_local_base_url(config: dict[str, Any] | None) -> str:
172
+ return f"http://{lingzhu_local_host(config)}:{lingzhu_gateway_port(config)}"
173
+
174
+
175
+ def lingzhu_public_base_url(config: dict[str, Any] | None) -> str | None:
176
+ return normalize_public_base_url((config or {}).get("public_base_url"))
177
+
178
+
179
+ def lingzhu_health_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
180
+ base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
181
+ if not base:
182
+ return None
183
+ return f"{base}/metis/agent/api/health"
184
+
185
+
186
+ def lingzhu_sse_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
187
+ base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
188
+ if not base:
189
+ return None
190
+ return f"{base}/metis/agent/api/sse"
191
+
192
+
193
+ def lingzhu_agent_id(config: dict[str, Any] | None) -> str:
194
+ value = str((config or {}).get("agent_id") or DEFAULT_LINGZHU_AGENT_ID).strip()
195
+ return value or DEFAULT_LINGZHU_AGENT_ID
196
+
197
+
198
+ def lingzhu_passive_conversation_id(config: dict[str, Any] | None) -> str:
199
+ return f"lingzhu:{DEFAULT_LINGZHU_PASSIVE_CHAT_TYPE}:{lingzhu_agent_id(config)}"
200
+
201
+
202
+ def lingzhu_is_passive_conversation_id(value: Any, config: dict[str, Any] | None = None) -> bool:
203
+ normalized = str(value or "").strip()
204
+ if not normalized:
205
+ return False
206
+ if config is not None:
207
+ return normalized == lingzhu_passive_conversation_id(config)
208
+ return normalized.startswith(f"lingzhu:{DEFAULT_LINGZHU_PASSIVE_CHAT_TYPE}:")
209
+
210
+
211
+ def lingzhu_request_sender_id(body: dict[str, Any] | None) -> str:
212
+ value = str((body or {}).get("user_id") or (body or {}).get("agent_id") or "anonymous").strip()
213
+ return value or "anonymous"
214
+
215
+
216
+ def lingzhu_request_conversation_id(body: dict[str, Any] | None) -> str:
217
+ return f"lingzhu:direct:{lingzhu_request_sender_id(body)}"
218
+
219
+
220
+ def lingzhu_extract_user_text(messages: Any) -> str:
221
+ if not isinstance(messages, list):
222
+ return ""
223
+ preferred: list[str] = []
224
+ fallback: list[str] = []
225
+ for item in messages:
226
+ if not isinstance(item, dict):
227
+ continue
228
+ text = str(item.get("text") or item.get("content") or "").strip()
229
+ if not text:
230
+ continue
231
+ role = str(item.get("role") or "").strip().lower()
232
+ if role in {"", "user"}:
233
+ preferred.append(text)
234
+ else:
235
+ fallback.append(text)
236
+ parts = preferred or fallback
237
+ return "\n".join(parts).strip()
238
+
239
+
240
+ def lingzhu_extract_task_text(text: Any) -> str | None:
241
+ normalized = str(text or "").strip()
242
+ if not normalized.startswith(DEFAULT_LINGZHU_TASK_PREFIX):
243
+ return None
244
+ remainder = normalized[len(DEFAULT_LINGZHU_TASK_PREFIX) :].strip()
245
+ remainder = remainder.lstrip("::,,。.;;!!?? ")
246
+ return remainder or None
247
+
248
+
249
+ def lingzhu_verify_auth_header(auth_header: Any, expected_ak: str) -> bool:
250
+ if not expected_ak:
251
+ return True
252
+ if isinstance(auth_header, list):
253
+ header = str(auth_header[0] or "").strip()
254
+ else:
255
+ header = str(auth_header or "").strip()
256
+ if not header.lower().startswith("bearer "):
257
+ return False
258
+ return header[7:].strip() == expected_ak
259
+
260
+
261
+ def lingzhu_probe_payload(
262
+ config: dict[str, Any] | None,
263
+ *,
264
+ message_id: str = "ds-lingzhu-probe-001",
265
+ text: str = "你好",
266
+ ) -> dict[str, Any]:
267
+ return {
268
+ "message_id": message_id,
269
+ "agent_id": lingzhu_agent_id(config),
270
+ "message": [
271
+ {
272
+ "role": "user",
273
+ "type": "text",
274
+ "text": text,
275
+ }
276
+ ],
277
+ }
278
+
279
+
280
+ def lingzhu_generated_openclaw_config(config: dict[str, Any] | None) -> dict[str, Any]:
281
+ resolved = dict(config or {})
282
+ return {
283
+ "gateway": {
284
+ "port": lingzhu_gateway_port(resolved),
285
+ "http": {
286
+ "endpoints": {
287
+ "chatCompletions": {
288
+ "enabled": True,
289
+ }
290
+ }
291
+ },
292
+ },
293
+ "plugins": {
294
+ "entries": {
295
+ "lingzhu": {
296
+ "enabled": bool(resolved.get("enabled", False)),
297
+ "config": {
298
+ "authAk": str(resolved.get("auth_ak") or "").strip(),
299
+ "agentId": lingzhu_agent_id(resolved),
300
+ "includeMetadata": bool(resolved.get("include_metadata", True)),
301
+ "requestTimeoutMs": int(resolved.get("request_timeout_ms") or 60000),
302
+ "systemPrompt": str(resolved.get("system_prompt") or ""),
303
+ "defaultNavigationMode": str(resolved.get("default_navigation_mode") or "0"),
304
+ "enableFollowUp": bool(resolved.get("enable_follow_up", True)),
305
+ "followUpMaxCount": int(resolved.get("follow_up_max_count") or 3),
306
+ "maxImageBytes": int(resolved.get("max_image_bytes") or 5 * 1024 * 1024),
307
+ "sessionMode": str(resolved.get("session_mode") or "per_user"),
308
+ "sessionNamespace": str(
309
+ resolved.get("session_namespace") or DEFAULT_LINGZHU_SESSION_NAMESPACE
310
+ ),
311
+ "autoReceiptAck": bool(resolved.get("auto_receipt_ack", True)),
312
+ "visibleProgressHeartbeat": bool(
313
+ resolved.get("visible_progress_heartbeat", True)
314
+ ),
315
+ "visibleProgressHeartbeatSec": int(
316
+ resolved.get("visible_progress_heartbeat_sec") or 10
317
+ ),
318
+ "debugLogging": bool(resolved.get("debug_logging", False)),
319
+ "debugLogPayloads": bool(resolved.get("debug_log_payloads", False)),
320
+ "debugLogDir": str(resolved.get("debug_log_dir") or ""),
321
+ "enableExperimentalNativeActions": bool(
322
+ resolved.get("enable_experimental_native_actions", False)
323
+ ),
324
+ },
325
+ }
326
+ }
327
+ },
328
+ }
329
+
330
+
331
+ def lingzhu_generated_openclaw_config_text(config: dict[str, Any] | None) -> str:
332
+ return json.dumps(lingzhu_generated_openclaw_config(config), indent=2, ensure_ascii=False)
333
+
334
+
335
+ def lingzhu_generated_curl(config: dict[str, Any] | None, *, text: str = "你好") -> str:
336
+ auth_ak = str((config or {}).get("auth_ak") or "").strip()
337
+ payload = lingzhu_probe_payload(config, text=text)
338
+ endpoint_url = lingzhu_sse_url(config) or ""
339
+ return (
340
+ f"curl -X POST '{endpoint_url}' \\\n"
341
+ f" --header 'Authorization: Bearer {auth_ak}' \\\n"
342
+ " --header 'Content-Type: application/json' \\\n"
343
+ f" --data '{json.dumps(payload, ensure_ascii=False)}'"
344
+ )
345
+
346
+
347
+ def lingzhu_health_payload(
348
+ config: dict[str, Any] | None,
349
+ *,
350
+ chat_completions_enabled: bool = True,
351
+ ) -> dict[str, Any]:
352
+ resolved = dict(config or {})
353
+ experimental_enabled = bool(resolved.get("enable_experimental_native_actions", False))
354
+ return {
355
+ "ok": True,
356
+ "status": "ok",
357
+ "endpoint": "/metis/agent/api/sse",
358
+ "enabled": bool(resolved.get("enabled", False)),
359
+ "agentId": lingzhu_agent_id(resolved),
360
+ "supportedCommands": lingzhu_supported_commands(experimental_enabled=experimental_enabled),
361
+ "followUpEnabled": bool(resolved.get("enable_follow_up", True)),
362
+ "sessionMode": str(resolved.get("session_mode") or "per_user"),
363
+ "debugLogging": bool(resolved.get("debug_logging", False)),
364
+ "experimentalNativeActions": experimental_enabled,
365
+ "chatCompletionsEnabled": bool(chat_completions_enabled),
366
+ }
367
+
368
+
369
+ def lingzhu_sse_answer(
370
+ *,
371
+ message_id: str,
372
+ agent_id: str,
373
+ answer_stream: str,
374
+ is_finish: bool = True,
375
+ ) -> dict[str, Any]:
376
+ return {
377
+ "role": "agent",
378
+ "type": "answer",
379
+ "answer_stream": answer_stream,
380
+ "message_id": str(message_id or "").strip(),
381
+ "agent_id": str(agent_id or "").strip(),
382
+ "is_finish": bool(is_finish),
383
+ }
384
+
385
+
386
+ def lingzhu_sse_follow_up(
387
+ *,
388
+ message_id: str,
389
+ agent_id: str,
390
+ suggestions: list[str],
391
+ ) -> dict[str, Any]:
392
+ return {
393
+ "role": "agent",
394
+ "type": "follow_up",
395
+ "message_id": str(message_id or "").strip(),
396
+ "agent_id": str(agent_id or "").strip(),
397
+ "is_finish": True,
398
+ "follow_up": [str(item).strip() for item in suggestions if str(item).strip()],
399
+ }
400
+
401
+
402
+ def lingzhu_sse_tool_call(
403
+ *,
404
+ message_id: str,
405
+ agent_id: str,
406
+ tool_call: dict[str, Any],
407
+ is_finish: bool = True,
408
+ ) -> dict[str, Any]:
409
+ return {
410
+ "role": "agent",
411
+ "type": "tool_call",
412
+ "message_id": str(message_id or "").strip(),
413
+ "agent_id": str(agent_id or "").strip(),
414
+ "is_finish": bool(is_finish),
415
+ "tool_call": dict(tool_call or {}),
416
+ }
417
+
418
+
419
+ def lingzhu_resolve_command(raw_command: Any, *, experimental_enabled: bool = False) -> str | None:
420
+ normalized = str(raw_command or "").strip().lower()
421
+ if not normalized:
422
+ return None
423
+ resolved = _LINGZHU_TOOL_COMMAND_ALIASES.get(normalized, normalized)
424
+ if resolved in _LINGZHU_EXPERIMENTAL_COMMANDS and not experimental_enabled:
425
+ return None
426
+ if resolved not in lingzhu_supported_commands(experimental_enabled=experimental_enabled):
427
+ return None
428
+ return resolved
429
+
430
+
431
+ def _lingzhu_action_text_value(action: dict[str, Any], *keys: str) -> str:
432
+ for key in keys:
433
+ value = str(action.get(key) or "").strip()
434
+ if value:
435
+ return value
436
+ return ""
437
+
438
+
439
+ def _lingzhu_action_bool_value(action: dict[str, Any], key: str) -> bool | None:
440
+ raw = action.get(key)
441
+ if isinstance(raw, bool):
442
+ return raw
443
+ if isinstance(raw, str):
444
+ normalized = raw.strip().lower()
445
+ if normalized in {"true", "1", "yes", "on"}:
446
+ return True
447
+ if normalized in {"false", "0", "no", "off"}:
448
+ return False
449
+ return None
450
+
451
+
452
+ def _lingzhu_action_int_value(action: dict[str, Any], key: str) -> int | None:
453
+ raw = action.get(key)
454
+ try:
455
+ value = int(raw)
456
+ except (TypeError, ValueError):
457
+ return None
458
+ return value
459
+
460
+
461
+ def _lingzhu_command_payload_tool_call(
462
+ command: str,
463
+ payload: dict[str, Any],
464
+ *,
465
+ default_navigation_mode: str = "0",
466
+ ) -> dict[str, Any]:
467
+ resolved_navigation_mode = default_navigation_mode if default_navigation_mode in {"0", "1", "2"} else "0"
468
+ tool_call: dict[str, Any] = {
469
+ "handling_required": True,
470
+ "command": command,
471
+ "is_recall": bool(payload.get("is_recall", True)),
472
+ }
473
+
474
+ if command == "take_navigation":
475
+ tool_call["action"] = _lingzhu_action_text_value(payload, "action") or "open"
476
+ destination = _lingzhu_action_text_value(payload, "poi_name", "destination", "address", "name", "query")
477
+ if destination:
478
+ tool_call["poi_name"] = destination
479
+ navigation_mode = _lingzhu_action_text_value(payload, "navi_type", "type")
480
+ tool_call["navi_type"] = navigation_mode if navigation_mode in {"0", "1", "2"} else resolved_navigation_mode
481
+ return tool_call
482
+
483
+ if command == "control_calendar":
484
+ tool_call["action"] = _lingzhu_action_text_value(payload, "action") or "create"
485
+ title = _lingzhu_action_text_value(payload, "title", "name")
486
+ if title:
487
+ tool_call["title"] = title
488
+ start_time = _lingzhu_action_text_value(payload, "start_time", "startTime")
489
+ if start_time:
490
+ tool_call["start_time"] = start_time
491
+ end_time = _lingzhu_action_text_value(payload, "end_time", "endTime")
492
+ if end_time:
493
+ tool_call["end_time"] = end_time
494
+ return tool_call
495
+
496
+ if command in {"send_notification", "send_toast", "speak_tts"}:
497
+ title = _lingzhu_action_text_value(payload, "title")
498
+ body = _lingzhu_action_text_value(payload, "content", "body", "text", "message")
499
+ if title and body and body != title:
500
+ tool_call["content"] = f"{title}\n{body}"
501
+ else:
502
+ content = body or title
503
+ if content:
504
+ tool_call["content"] = content
505
+ play_tts = _lingzhu_action_bool_value(payload, "play_tts")
506
+ if play_tts is not None:
507
+ tool_call["play_tts"] = play_tts
508
+ icon_type = _lingzhu_action_text_value(payload, "icon_type")
509
+ if icon_type:
510
+ tool_call["icon_type"] = icon_type
511
+ return tool_call
512
+
513
+ if command == "start_video_record":
514
+ for key in ("duration_sec", "width", "height", "quality"):
515
+ value = _lingzhu_action_int_value(payload, key)
516
+ if value is not None:
517
+ tool_call[key] = value
518
+ return tool_call
519
+
520
+ if command == "open_custom_view":
521
+ view_name = _lingzhu_action_text_value(payload, "view_name", "title", "name")
522
+ if view_name:
523
+ tool_call["view_name"] = view_name
524
+ raw_payload = payload.get("view_payload", payload.get("payload", payload.get("data")))
525
+ if raw_payload is not None and raw_payload != "":
526
+ if isinstance(raw_payload, str):
527
+ rendered_payload = raw_payload.strip()
528
+ else:
529
+ rendered_payload = json.dumps(raw_payload, ensure_ascii=False)
530
+ if rendered_payload:
531
+ tool_call["view_payload"] = rendered_payload
532
+ return tool_call
533
+
534
+ return tool_call
535
+
536
+
537
+ def _lingzhu_decode_marker_params(raw_value: str) -> dict[str, Any]:
538
+ value = str(raw_value or "").strip()
539
+ if not value:
540
+ return {}
541
+ try:
542
+ decoded = json.loads(value)
543
+ return decoded if isinstance(decoded, dict) else {}
544
+ except json.JSONDecodeError:
545
+ pass
546
+ try:
547
+ padded = value + ("=" * (-len(value) % 4))
548
+ decoded_text = base64.urlsafe_b64decode(padded.encode("utf-8")).decode("utf-8")
549
+ decoded = json.loads(decoded_text)
550
+ return decoded if isinstance(decoded, dict) else {}
551
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
552
+ return {}
553
+
554
+
555
+ def lingzhu_detect_tool_call_from_text(
556
+ text: Any,
557
+ *,
558
+ default_navigation_mode: str = "0",
559
+ experimental_enabled: bool = False,
560
+ ) -> tuple[dict[str, Any] | None, str]:
561
+ normalized = str(text or "").strip()
562
+ if not normalized:
563
+ return None, ""
564
+
565
+ marker_match = _LINGZHU_TOOL_MARKER_RE.search(normalized)
566
+ if marker_match:
567
+ command = lingzhu_resolve_command(marker_match.group(1), experimental_enabled=experimental_enabled)
568
+ if command:
569
+ params = _lingzhu_decode_marker_params(marker_match.group(2))
570
+ cleaned = f"{normalized[:marker_match.start()]} {normalized[marker_match.end():]}".strip()
571
+ cleaned = re.sub(r"\s+", " ", cleaned)
572
+ return (
573
+ _lingzhu_command_payload_tool_call(
574
+ command,
575
+ params,
576
+ default_navigation_mode=default_navigation_mode,
577
+ ),
578
+ cleaned,
579
+ )
580
+
581
+ explicit_command_match = re.match(r"^\s*([A-Za-z_]+)\b", normalized)
582
+ if explicit_command_match:
583
+ command = lingzhu_resolve_command(explicit_command_match.group(1), experimental_enabled=experimental_enabled)
584
+ if command:
585
+ return (
586
+ _lingzhu_command_payload_tool_call(
587
+ command,
588
+ {},
589
+ default_navigation_mode=default_navigation_mode,
590
+ ),
591
+ normalized,
592
+ )
593
+
594
+ if _LINGZHU_PHOTO_REQUEST_RE.match(normalized):
595
+ return _lingzhu_command_payload_tool_call("take_photo", {}, default_navigation_mode=default_navigation_mode), normalized
596
+ if _LINGZHU_EXIT_REQUEST_RE.match(normalized):
597
+ return _lingzhu_command_payload_tool_call("notify_agent_off", {}, default_navigation_mode=default_navigation_mode), normalized
598
+
599
+ navigation_match = _LINGZHU_NAVIGATION_REQUEST_RE.match(normalized)
600
+ if navigation_match and navigation_match.group(1).strip():
601
+ return (
602
+ _lingzhu_command_payload_tool_call(
603
+ "take_navigation",
604
+ {"poi_name": navigation_match.group(1).strip()},
605
+ default_navigation_mode=default_navigation_mode,
606
+ ),
607
+ normalized,
608
+ )
609
+
610
+ if experimental_enabled:
611
+ if _LINGZHU_NOTIFICATION_REQUEST_RE.match(normalized):
612
+ return _lingzhu_command_payload_tool_call("send_notification", {}, default_navigation_mode=default_navigation_mode), normalized
613
+ if _LINGZHU_TOAST_REQUEST_RE.match(normalized):
614
+ return _lingzhu_command_payload_tool_call("send_toast", {}, default_navigation_mode=default_navigation_mode), normalized
615
+ if _LINGZHU_TTS_REQUEST_RE.match(normalized):
616
+ return _lingzhu_command_payload_tool_call("speak_tts", {}, default_navigation_mode=default_navigation_mode), normalized
617
+ if _LINGZHU_START_RECORD_RE.match(normalized):
618
+ return _lingzhu_command_payload_tool_call("start_video_record", {}, default_navigation_mode=default_navigation_mode), normalized
619
+ if _LINGZHU_STOP_RECORD_RE.match(normalized):
620
+ return _lingzhu_command_payload_tool_call("stop_video_record", {}, default_navigation_mode=default_navigation_mode), normalized
621
+ if _LINGZHU_OPEN_VIEW_RE.match(normalized):
622
+ return _lingzhu_command_payload_tool_call("open_custom_view", {}, default_navigation_mode=default_navigation_mode), normalized
623
+
624
+ return None, normalized
625
+
626
+
627
+ def lingzhu_surface_action_tool_call(
628
+ action: Any,
629
+ *,
630
+ default_navigation_mode: str = "0",
631
+ experimental_enabled: bool = False,
632
+ ) -> dict[str, Any] | None:
633
+ if not isinstance(action, dict):
634
+ return None
635
+
636
+ command = lingzhu_resolve_command(
637
+ action.get("command") or action.get("type"),
638
+ experimental_enabled=experimental_enabled,
639
+ )
640
+ if not command:
641
+ return None
642
+
643
+ return _lingzhu_command_payload_tool_call(
644
+ command,
645
+ dict(action),
646
+ default_navigation_mode=default_navigation_mode,
647
+ )
648
+
649
+
650
+ def lingzhu_supported_commands(*, experimental_enabled: bool) -> list[str]:
651
+ commands = [
652
+ "take_photo",
653
+ "take_navigation",
654
+ "control_calendar",
655
+ "notify_agent_off",
656
+ ]
657
+ if experimental_enabled:
658
+ commands.extend(
659
+ [
660
+ "send_notification",
661
+ "send_toast",
662
+ "speak_tts",
663
+ "start_video_record",
664
+ "stop_video_record",
665
+ "open_custom_view",
666
+ ]
667
+ )
668
+ return commands