@researai/deepscientist 1.5.2 → 1.5.4
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.
- package/README.md +22 -0
- package/bin/ds.js +399 -175
- package/docs/en/00_QUICK_START.md +22 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +13 -4
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- package/docs/images/connectors/discord-setup-overview.svg +52 -0
- package/docs/images/connectors/feishu-setup-overview.svg +53 -0
- package/docs/images/connectors/slack-setup-overview.svg +51 -0
- package/docs/images/connectors/telegram-setup-overview.svg +55 -0
- package/docs/images/connectors/whatsapp-setup-overview.svg +51 -0
- package/docs/images/lingzhu/lingzhu-openclaw-config.svg +17 -0
- package/docs/images/lingzhu/lingzhu-platform-values.svg +16 -0
- package/docs/images/lingzhu/lingzhu-settings-overview.svg +30 -0
- package/docs/images/qq/tencent-cloud-qq-chat.png +0 -0
- package/docs/images/qq/tencent-cloud-qq-register.png +0 -0
- package/docs/images/quickstart/00-home.png +0 -0
- package/docs/images/quickstart/01-start-research.png +0 -0
- package/docs/images/quickstart/02-list-quest.png +0 -0
- package/docs/zh/00_QUICK_START.md +22 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +14 -5
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
- package/install.sh +120 -4
- package/package.json +8 -4
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +1 -1
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +12 -20
- package/src/deepscientist/bridges/connectors.py +2 -1
- package/src/deepscientist/channels/discord_gateway.py +27 -4
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +24 -5
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +31 -7
- package/src/deepscientist/channels/telegram_polling.py +27 -3
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +31 -1
- package/src/deepscientist/config/models.py +13 -43
- package/src/deepscientist/config/service.py +216 -157
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +53 -16
- package/src/deepscientist/daemon/api/router.py +2 -2
- package/src/deepscientist/daemon/app.py +747 -228
- package/src/deepscientist/mcp/server.py +60 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/network.py +78 -0
- package/src/deepscientist/prompts/builder.py +50 -4
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/service.py +1 -1
- package/src/deepscientist/skills/installer.py +77 -1
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +162 -6
- package/src/skills/analysis-campaign/SKILL.md +19 -5
- package/src/skills/baseline/SKILL.md +66 -31
- package/src/skills/decision/SKILL.md +1 -1
- package/src/skills/experiment/SKILL.md +11 -5
- package/src/skills/finalize/SKILL.md +1 -1
- package/src/skills/idea/SKILL.md +246 -4
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/rebuttal/SKILL.md +1 -1
- package/src/skills/review/SKILL.md +1 -1
- package/src/skills/scout/SKILL.md +1 -1
- package/src/skills/write/SKILL.md +152 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-CZpg376x.js → AiManusChatView-BGLArZRn.js} +14 -37
- package/src/ui/dist/assets/{AnalysisPlugin-CtHA22g3.js → AnalysisPlugin-BgDGSigG.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-BSWmLMmF.js → AutoFigurePlugin-B65HD7L4.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CJ7jdm_s.js → CliPlugin-CUqgsFHC.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DhInVGFf.js → CodeEditorPlugin-CF5EdvaS.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D1n8S9r5.js → CodeViewerPlugin-DEeU063D.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-C4XM_kqk.js → DocViewerPlugin-Df-FuDlZ.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-W6kS9r6v.js → GitDiffViewerPlugin-RAnNaRxM.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-DPeUx_Oz.js → ImageViewerPlugin-DXJ0ZJGg.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-eAelUaub.js → LabCopilotPanel-BlO-sKsj.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BbOrBxKY.js → LabPlugin-BajPZW5v.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C-HhkVXY.js → LatexPlugin-F1OEol8D.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BDIzIBfh.js → MarkdownViewerPlugin-MhUupqwT.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DAOJphwr.js → MarketplacePlugin-DxhIEsv0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BsoMvDoU.js → NotebookEditor-q7TkhewC.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-fiC7RtHf.js → PdfLoader-B8ZOTKFc.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-C5OxZBFK.js → PdfMarkdownPlugin-xFPvzvWh.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-CAbxQebk.js → PdfViewerPlugin-EjEcsIB8.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-SE33Lb9B.js → SearchPlugin-ixY-1lgW.js} +1 -1
- package/src/ui/dist/assets/{Stepper-0Av7GfV7.js → Stepper-gYFK2Pgz.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Daf2gJDI.js → TextViewerPlugin-Cym6pv_n.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BKrMUIOX.js → VNCViewer-BPmIHcmK.js} +9 -9
- package/src/ui/dist/assets/{bibtex-JBdOEe45.js → bibtex-Btv6Wi7f.js} +1 -1
- package/src/ui/dist/assets/{code-B0TDFCZz.js → code-BlG7g85c.js} +1 -1
- package/src/ui/dist/assets/{file-content-3YtrSacz.js → file-content-DBT5OfTZ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CJEg5OG1.js → file-diff-panel-BWXYzqHk.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CYQYdmB1.js → file-socket-wDlx6byM.js} +1 -1
- package/src/ui/dist/assets/{file-utils-Cd1C9Ppl.js → file-utils-Ba3nJmH0.js} +1 -1
- package/src/ui/dist/assets/{image-B33ctrvC.js → image-BwtCyguk.js} +1 -1
- package/src/ui/dist/assets/{index-BNQWqmJ2.js → index-B-2scqCJ.js} +11 -11
- package/src/ui/dist/assets/{index-BVXsmS7V.js → index-Bz5AaWL7.js} +52383 -51440
- package/src/ui/dist/assets/{index-Buw_N1VQ.js → index-CfRpE209.js} +2 -2
- package/src/ui/dist/assets/{index-9CLPVeZh.js → index-DcqvKzeJ.js} +1 -1
- package/src/ui/dist/assets/{index-SwmFAld3.css → index-DpMZw8aM.css} +49 -2
- package/src/ui/dist/assets/{message-square-D0cUJ9yU.js → message-square-BnlyWVH0.js} +1 -1
- package/src/ui/dist/assets/{monaco-UZLYkp2n.js → monaco-CXe0pAVe.js} +1 -1
- package/src/ui/dist/assets/{popover-CTeiY-dK.js → popover-BCHmVhHj.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Dbs01Xky.js → project-sync-Brk6kaOD.js} +1 -1
- package/src/ui/dist/assets/{sigma-CM08S-xT.js → sigma-D72eSUep.js} +1 -1
- package/src/ui/dist/assets/{tooltip-pDtzvU9p.js → tooltip-BMWd0dqX.js} +1 -1
- package/src/ui/dist/assets/{trash-YvPCP-da.js → trash-BIt_eWIS.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-Bavi74Ac.js → useCliAccess-N1hkTRrR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-CVXY6oeg.js → useFileDiffOverlay-DPRPv6rv.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Cf4flRW7.js → wrap-text-E5-UheyP.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-Hb0Z1YpT.js → zoom-out-D4TR-ZZ_.js} +1 -1
- package/src/ui/dist/index.html +2 -2
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .connector_runtime import infer_connector_transport
|
|
7
|
+
from .shared import slugify
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PROFILEABLE_CONNECTOR_NAMES = ("telegram", "discord", "slack", "feishu", "whatsapp")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
|
|
14
|
+
"telegram": {
|
|
15
|
+
"profile_id_prefix": "telegram-profile",
|
|
16
|
+
"shared_fields": (
|
|
17
|
+
"enabled",
|
|
18
|
+
"profiles",
|
|
19
|
+
"transport",
|
|
20
|
+
"bot_name",
|
|
21
|
+
"bot_token",
|
|
22
|
+
"bot_token_env",
|
|
23
|
+
"command_prefix",
|
|
24
|
+
"dm_policy",
|
|
25
|
+
"allow_from",
|
|
26
|
+
"group_policy",
|
|
27
|
+
"group_allow_from",
|
|
28
|
+
"groups",
|
|
29
|
+
"require_mention_in_groups",
|
|
30
|
+
"auto_bind_dm_to_active_quest",
|
|
31
|
+
),
|
|
32
|
+
"profile_defaults": {
|
|
33
|
+
"profile_id": None,
|
|
34
|
+
"enabled": True,
|
|
35
|
+
"transport": "polling",
|
|
36
|
+
"bot_name": "DeepScientist",
|
|
37
|
+
"bot_token": None,
|
|
38
|
+
"bot_token_env": "TELEGRAM_BOT_TOKEN",
|
|
39
|
+
},
|
|
40
|
+
"profile_fields": (
|
|
41
|
+
"enabled",
|
|
42
|
+
"transport",
|
|
43
|
+
"bot_name",
|
|
44
|
+
"bot_token",
|
|
45
|
+
"bot_token_env",
|
|
46
|
+
),
|
|
47
|
+
"migration_keys": ("bot_token",),
|
|
48
|
+
"label_fields": ("bot_name",),
|
|
49
|
+
"id_fields": ("bot_name",),
|
|
50
|
+
},
|
|
51
|
+
"discord": {
|
|
52
|
+
"profile_id_prefix": "discord-profile",
|
|
53
|
+
"shared_fields": (
|
|
54
|
+
"enabled",
|
|
55
|
+
"profiles",
|
|
56
|
+
"transport",
|
|
57
|
+
"bot_name",
|
|
58
|
+
"bot_token",
|
|
59
|
+
"bot_token_env",
|
|
60
|
+
"command_prefix",
|
|
61
|
+
"application_id",
|
|
62
|
+
"dm_policy",
|
|
63
|
+
"allow_from",
|
|
64
|
+
"group_policy",
|
|
65
|
+
"group_allow_from",
|
|
66
|
+
"groups",
|
|
67
|
+
"require_mention_in_groups",
|
|
68
|
+
"auto_bind_dm_to_active_quest",
|
|
69
|
+
"guild_allowlist",
|
|
70
|
+
),
|
|
71
|
+
"profile_defaults": {
|
|
72
|
+
"profile_id": None,
|
|
73
|
+
"enabled": True,
|
|
74
|
+
"transport": "gateway",
|
|
75
|
+
"bot_name": "DeepScientist",
|
|
76
|
+
"bot_token": None,
|
|
77
|
+
"bot_token_env": "DISCORD_BOT_TOKEN",
|
|
78
|
+
"application_id": None,
|
|
79
|
+
},
|
|
80
|
+
"profile_fields": (
|
|
81
|
+
"enabled",
|
|
82
|
+
"transport",
|
|
83
|
+
"bot_name",
|
|
84
|
+
"bot_token",
|
|
85
|
+
"bot_token_env",
|
|
86
|
+
"application_id",
|
|
87
|
+
),
|
|
88
|
+
"migration_keys": ("bot_token", "application_id"),
|
|
89
|
+
"label_fields": ("bot_name", "application_id"),
|
|
90
|
+
"id_fields": ("application_id", "bot_name"),
|
|
91
|
+
},
|
|
92
|
+
"slack": {
|
|
93
|
+
"profile_id_prefix": "slack-profile",
|
|
94
|
+
"shared_fields": (
|
|
95
|
+
"enabled",
|
|
96
|
+
"profiles",
|
|
97
|
+
"transport",
|
|
98
|
+
"bot_name",
|
|
99
|
+
"bot_token",
|
|
100
|
+
"bot_token_env",
|
|
101
|
+
"bot_user_id",
|
|
102
|
+
"app_token",
|
|
103
|
+
"app_token_env",
|
|
104
|
+
"command_prefix",
|
|
105
|
+
"dm_policy",
|
|
106
|
+
"allow_from",
|
|
107
|
+
"group_policy",
|
|
108
|
+
"group_allow_from",
|
|
109
|
+
"groups",
|
|
110
|
+
"require_mention_in_groups",
|
|
111
|
+
"auto_bind_dm_to_active_quest",
|
|
112
|
+
),
|
|
113
|
+
"profile_defaults": {
|
|
114
|
+
"profile_id": None,
|
|
115
|
+
"enabled": True,
|
|
116
|
+
"transport": "socket_mode",
|
|
117
|
+
"bot_name": "DeepScientist",
|
|
118
|
+
"bot_token": None,
|
|
119
|
+
"bot_token_env": "SLACK_BOT_TOKEN",
|
|
120
|
+
"bot_user_id": None,
|
|
121
|
+
"app_token": None,
|
|
122
|
+
"app_token_env": "SLACK_APP_TOKEN",
|
|
123
|
+
},
|
|
124
|
+
"profile_fields": (
|
|
125
|
+
"enabled",
|
|
126
|
+
"transport",
|
|
127
|
+
"bot_name",
|
|
128
|
+
"bot_token",
|
|
129
|
+
"bot_token_env",
|
|
130
|
+
"bot_user_id",
|
|
131
|
+
"app_token",
|
|
132
|
+
"app_token_env",
|
|
133
|
+
),
|
|
134
|
+
"migration_keys": ("bot_token", "bot_user_id", "app_token"),
|
|
135
|
+
"label_fields": ("bot_name", "bot_user_id"),
|
|
136
|
+
"id_fields": ("bot_user_id", "bot_name"),
|
|
137
|
+
},
|
|
138
|
+
"feishu": {
|
|
139
|
+
"profile_id_prefix": "feishu-profile",
|
|
140
|
+
"shared_fields": (
|
|
141
|
+
"enabled",
|
|
142
|
+
"profiles",
|
|
143
|
+
"transport",
|
|
144
|
+
"bot_name",
|
|
145
|
+
"app_id",
|
|
146
|
+
"app_secret",
|
|
147
|
+
"app_secret_env",
|
|
148
|
+
"api_base_url",
|
|
149
|
+
"command_prefix",
|
|
150
|
+
"dm_policy",
|
|
151
|
+
"allow_from",
|
|
152
|
+
"group_policy",
|
|
153
|
+
"group_allow_from",
|
|
154
|
+
"groups",
|
|
155
|
+
"require_mention_in_groups",
|
|
156
|
+
"auto_bind_dm_to_active_quest",
|
|
157
|
+
),
|
|
158
|
+
"profile_defaults": {
|
|
159
|
+
"profile_id": None,
|
|
160
|
+
"enabled": True,
|
|
161
|
+
"transport": "long_connection",
|
|
162
|
+
"bot_name": "DeepScientist",
|
|
163
|
+
"app_id": None,
|
|
164
|
+
"app_secret": None,
|
|
165
|
+
"app_secret_env": "FEISHU_APP_SECRET",
|
|
166
|
+
"api_base_url": "https://open.feishu.cn",
|
|
167
|
+
},
|
|
168
|
+
"profile_fields": (
|
|
169
|
+
"enabled",
|
|
170
|
+
"transport",
|
|
171
|
+
"bot_name",
|
|
172
|
+
"app_id",
|
|
173
|
+
"app_secret",
|
|
174
|
+
"app_secret_env",
|
|
175
|
+
"api_base_url",
|
|
176
|
+
),
|
|
177
|
+
"migration_keys": ("app_id", "app_secret"),
|
|
178
|
+
"label_fields": ("bot_name", "app_id"),
|
|
179
|
+
"id_fields": ("app_id", "bot_name"),
|
|
180
|
+
},
|
|
181
|
+
"whatsapp": {
|
|
182
|
+
"profile_id_prefix": "whatsapp-profile",
|
|
183
|
+
"shared_fields": (
|
|
184
|
+
"enabled",
|
|
185
|
+
"profiles",
|
|
186
|
+
"transport",
|
|
187
|
+
"bot_name",
|
|
188
|
+
"auth_method",
|
|
189
|
+
"session_dir",
|
|
190
|
+
"command_prefix",
|
|
191
|
+
"dm_policy",
|
|
192
|
+
"allow_from",
|
|
193
|
+
"group_policy",
|
|
194
|
+
"group_allow_from",
|
|
195
|
+
"groups",
|
|
196
|
+
"auto_bind_dm_to_active_quest",
|
|
197
|
+
),
|
|
198
|
+
"profile_defaults": {
|
|
199
|
+
"profile_id": None,
|
|
200
|
+
"enabled": True,
|
|
201
|
+
"transport": "local_session",
|
|
202
|
+
"bot_name": "DeepScientist",
|
|
203
|
+
"auth_method": "qr_browser",
|
|
204
|
+
"session_dir": "~/.deepscientist/connectors/whatsapp",
|
|
205
|
+
},
|
|
206
|
+
"profile_fields": (
|
|
207
|
+
"enabled",
|
|
208
|
+
"transport",
|
|
209
|
+
"bot_name",
|
|
210
|
+
"auth_method",
|
|
211
|
+
"session_dir",
|
|
212
|
+
),
|
|
213
|
+
"migration_keys": ("session_dir",),
|
|
214
|
+
"label_fields": ("bot_name",),
|
|
215
|
+
"id_fields": ("bot_name",),
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _as_text(value: Any) -> str | None:
|
|
221
|
+
text = str(value or "").strip()
|
|
222
|
+
return text or None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _profile_seed(connector_name: str, raw: dict[str, Any], *, index: int) -> str:
|
|
226
|
+
spec = CONNECTOR_PROFILE_SPECS[connector_name]
|
|
227
|
+
explicit = _as_text(raw.get("profile_id"))
|
|
228
|
+
if explicit:
|
|
229
|
+
return explicit
|
|
230
|
+
for key in spec["id_fields"]:
|
|
231
|
+
candidate = _as_text(raw.get(key))
|
|
232
|
+
if candidate:
|
|
233
|
+
return f"{connector_name}-{candidate}"
|
|
234
|
+
return f"{spec['profile_id_prefix']}-{index:03d}"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _unique_profile_id(seed: str, *, prefix: str, used: set[str]) -> str:
|
|
238
|
+
base = slugify(seed, default=prefix)
|
|
239
|
+
candidate = base
|
|
240
|
+
suffix = 2
|
|
241
|
+
while candidate in used:
|
|
242
|
+
candidate = f"{base}-{suffix}"
|
|
243
|
+
suffix += 1
|
|
244
|
+
used.add(candidate)
|
|
245
|
+
return candidate
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def default_connector_profile(connector_name: str) -> dict[str, Any]:
|
|
249
|
+
spec = CONNECTOR_PROFILE_SPECS[connector_name]
|
|
250
|
+
return deepcopy(spec["profile_defaults"])
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def connector_profile_label(connector_name: str, profile: dict[str, Any] | None) -> str:
|
|
254
|
+
if not isinstance(profile, dict):
|
|
255
|
+
return connector_name.capitalize()
|
|
256
|
+
spec = CONNECTOR_PROFILE_SPECS[connector_name]
|
|
257
|
+
parts = [_as_text(profile.get(key)) for key in spec["label_fields"]]
|
|
258
|
+
filtered = [item for item in parts if item]
|
|
259
|
+
return " · ".join(filtered) if filtered else connector_name.capitalize()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def normalize_connector_config(connector_name: str, config: dict[str, Any] | None) -> dict[str, Any]:
|
|
263
|
+
if connector_name not in CONNECTOR_PROFILE_SPECS:
|
|
264
|
+
raise KeyError(f"Connector `{connector_name}` does not support generic profile normalization.")
|
|
265
|
+
spec = CONNECTOR_PROFILE_SPECS[connector_name]
|
|
266
|
+
payload = deepcopy(config or {})
|
|
267
|
+
shared = {
|
|
268
|
+
key: deepcopy(payload.get(key))
|
|
269
|
+
for key in spec["shared_fields"]
|
|
270
|
+
if key in payload
|
|
271
|
+
}
|
|
272
|
+
shared["profiles"] = []
|
|
273
|
+
|
|
274
|
+
raw_profiles = payload.get("profiles")
|
|
275
|
+
items = list(raw_profiles) if isinstance(raw_profiles, list) else []
|
|
276
|
+
if not items and any(_as_text(payload.get(key)) for key in spec["migration_keys"]):
|
|
277
|
+
items = [{key: payload.get(key) for key in spec["profile_fields"]}]
|
|
278
|
+
|
|
279
|
+
used_ids: set[str] = set()
|
|
280
|
+
profiles: list[dict[str, Any]] = []
|
|
281
|
+
for index, raw in enumerate(items, start=1):
|
|
282
|
+
if not isinstance(raw, dict):
|
|
283
|
+
continue
|
|
284
|
+
current = default_connector_profile(connector_name)
|
|
285
|
+
for key in ("profile_id", *spec["profile_fields"]):
|
|
286
|
+
if key in raw:
|
|
287
|
+
current[key] = deepcopy(raw.get(key))
|
|
288
|
+
current["enabled"] = bool(current.get("enabled", True))
|
|
289
|
+
for key in spec["profile_fields"]:
|
|
290
|
+
if key in {"enabled", "transport", "mode"}:
|
|
291
|
+
continue
|
|
292
|
+
if isinstance(current.get(key), list):
|
|
293
|
+
continue
|
|
294
|
+
if current.get(key) is None:
|
|
295
|
+
continue
|
|
296
|
+
current[key] = _as_text(current.get(key))
|
|
297
|
+
current["transport"] = infer_connector_transport(connector_name, current)
|
|
298
|
+
if "mode" in spec["profile_defaults"] or current.get("mode") is not None:
|
|
299
|
+
current["mode"] = _as_text(current.get("mode")) or str(spec["profile_defaults"].get("mode") or "")
|
|
300
|
+
current["profile_id"] = _unique_profile_id(
|
|
301
|
+
_profile_seed(connector_name, current, index=index),
|
|
302
|
+
prefix=str(spec["profile_id_prefix"]),
|
|
303
|
+
used=used_ids,
|
|
304
|
+
)
|
|
305
|
+
profiles.append(current)
|
|
306
|
+
|
|
307
|
+
shared["transport"] = infer_connector_transport(connector_name, shared)
|
|
308
|
+
shared["profiles"] = profiles
|
|
309
|
+
if len(profiles) == 1:
|
|
310
|
+
for key in spec["profile_fields"]:
|
|
311
|
+
shared[key] = profiles[0].get(key)
|
|
312
|
+
return shared
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def list_connector_profiles(connector_name: str, config: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
316
|
+
normalized = normalize_connector_config(connector_name, config)
|
|
317
|
+
profiles = normalized.get("profiles")
|
|
318
|
+
return [dict(item) for item in profiles] if isinstance(profiles, list) else []
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def find_connector_profile(
|
|
322
|
+
connector_name: str,
|
|
323
|
+
config: dict[str, Any] | None,
|
|
324
|
+
*,
|
|
325
|
+
profile_id: str | None = None,
|
|
326
|
+
) -> dict[str, Any] | None:
|
|
327
|
+
normalized_profile_id = _as_text(profile_id)
|
|
328
|
+
for profile in list_connector_profiles(connector_name, config):
|
|
329
|
+
if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
|
|
330
|
+
return profile
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def merge_connector_profile_config(
|
|
335
|
+
connector_name: str,
|
|
336
|
+
shared_config: dict[str, Any] | None,
|
|
337
|
+
profile: dict[str, Any],
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
normalized = normalize_connector_config(connector_name, shared_config)
|
|
340
|
+
merged = deepcopy(normalized)
|
|
341
|
+
merged.pop("profiles", None)
|
|
342
|
+
for key in CONNECTOR_PROFILE_SPECS[connector_name]["profile_fields"]:
|
|
343
|
+
merged[key] = profile.get(key)
|
|
344
|
+
merged["profile_id"] = str(profile.get("profile_id") or "").strip() or None
|
|
345
|
+
merged["enabled"] = bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True))
|
|
346
|
+
return merged
|
|
@@ -3,69 +3,68 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
CONNECTOR_PROFILE_CHAT_ID_SEPARATOR = "::"
|
|
7
|
+
QQ_PROFILE_CHAT_ID_SEPARATOR = CONNECTOR_PROFILE_CHAT_ID_SEPARATOR
|
|
8
|
+
|
|
9
|
+
|
|
6
10
|
def infer_connector_transport(name: str, config: dict[str, Any] | None) -> str:
|
|
7
11
|
normalized = str(name or "").strip().lower()
|
|
8
12
|
payload = config or {}
|
|
9
13
|
explicit = str(payload.get("transport") or "").strip().lower()
|
|
10
|
-
if explicit
|
|
14
|
+
if explicit and explicit not in {
|
|
15
|
+
"relay",
|
|
16
|
+
"legacy_webhook",
|
|
17
|
+
"legacy_interactions",
|
|
18
|
+
"legacy_events_api",
|
|
19
|
+
"legacy_meta_cloud",
|
|
20
|
+
}:
|
|
11
21
|
return explicit
|
|
12
22
|
|
|
13
|
-
relay_url = str(payload.get("relay_url") or "").strip()
|
|
14
|
-
mode = str(payload.get("mode") or "").strip().lower()
|
|
15
|
-
public_callback_url = str(payload.get("public_callback_url") or "").strip()
|
|
16
|
-
|
|
17
23
|
if normalized == "qq":
|
|
18
24
|
return "gateway_direct"
|
|
19
25
|
if normalized == "telegram":
|
|
20
|
-
if relay_url and mode == "relay":
|
|
21
|
-
return "relay"
|
|
22
|
-
if public_callback_url or str(payload.get("webhook_secret") or "").strip():
|
|
23
|
-
return "legacy_webhook"
|
|
24
26
|
return "polling"
|
|
25
27
|
if normalized == "discord":
|
|
26
|
-
if relay_url and mode == "relay":
|
|
27
|
-
return "relay"
|
|
28
|
-
if str(payload.get("public_interactions_url") or "").strip() or str(payload.get("public_key") or "").strip():
|
|
29
|
-
return "legacy_interactions"
|
|
30
28
|
return "gateway"
|
|
31
29
|
if normalized == "slack":
|
|
32
|
-
if relay_url and mode == "relay":
|
|
33
|
-
return "relay"
|
|
34
30
|
if str(payload.get("app_token") or "").strip():
|
|
35
31
|
return "socket_mode"
|
|
36
|
-
if public_callback_url or str(payload.get("signing_secret") or "").strip():
|
|
37
|
-
return "legacy_events_api"
|
|
38
32
|
return "socket_mode"
|
|
39
33
|
if normalized == "feishu":
|
|
40
|
-
if relay_url and mode == "relay":
|
|
41
|
-
return "relay"
|
|
42
|
-
if (
|
|
43
|
-
public_callback_url
|
|
44
|
-
or str(payload.get("verification_token") or "").strip()
|
|
45
|
-
or str(payload.get("encrypt_key") or "").strip()
|
|
46
|
-
):
|
|
47
|
-
return "legacy_webhook"
|
|
48
34
|
return "long_connection"
|
|
49
35
|
if normalized == "whatsapp":
|
|
50
|
-
provider = str(payload.get("provider") or "").strip().lower()
|
|
51
|
-
if relay_url and mode == "relay" and provider == "relay":
|
|
52
|
-
return "relay"
|
|
53
|
-
if (
|
|
54
|
-
provider == "meta"
|
|
55
|
-
or str(payload.get("access_token") or "").strip()
|
|
56
|
-
or str(payload.get("phone_number_id") or "").strip()
|
|
57
|
-
or str(payload.get("verify_token") or "").strip()
|
|
58
|
-
or public_callback_url
|
|
59
|
-
):
|
|
60
|
-
return "legacy_meta_cloud"
|
|
61
36
|
return "local_session"
|
|
62
37
|
if normalized == "lingzhu":
|
|
63
38
|
return "openclaw_sse"
|
|
64
|
-
if relay_url and mode == "relay":
|
|
65
|
-
return "relay"
|
|
66
39
|
return "direct"
|
|
67
40
|
|
|
68
41
|
|
|
42
|
+
def _decode_chat_id(*, connector: str, chat_id: str) -> tuple[str | None, str]:
|
|
43
|
+
if CONNECTOR_PROFILE_CHAT_ID_SEPARATOR not in chat_id:
|
|
44
|
+
return None, chat_id
|
|
45
|
+
profile_id, resolved_chat_id = chat_id.split(CONNECTOR_PROFILE_CHAT_ID_SEPARATOR, 1)
|
|
46
|
+
normalized_profile_id = str(profile_id or "").strip() or None
|
|
47
|
+
normalized_chat_id = str(resolved_chat_id or "").strip() or chat_id
|
|
48
|
+
return normalized_profile_id, normalized_chat_id
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def encode_chat_id(*, connector: str, chat_id: Any, profile_id: Any = None) -> str:
|
|
52
|
+
normalized_chat_id = str(chat_id or "").strip()
|
|
53
|
+
if not normalized_chat_id:
|
|
54
|
+
return ""
|
|
55
|
+
normalized_profile_id = str(profile_id or "").strip()
|
|
56
|
+
if not normalized_profile_id:
|
|
57
|
+
return normalized_chat_id
|
|
58
|
+
return f"{normalized_profile_id}{CONNECTOR_PROFILE_CHAT_ID_SEPARATOR}{normalized_chat_id}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_conversation_id(connector: str, chat_type: str, chat_id: Any, *, profile_id: Any = None) -> str:
|
|
62
|
+
normalized_connector = str(connector or "").strip().lower()
|
|
63
|
+
normalized_chat_type = str(chat_type or "").strip().lower()
|
|
64
|
+
encoded_chat_id = encode_chat_id(connector=normalized_connector, chat_id=chat_id, profile_id=profile_id)
|
|
65
|
+
return f"{normalized_connector}:{normalized_chat_type}:{encoded_chat_id}"
|
|
66
|
+
|
|
67
|
+
|
|
69
68
|
def parse_conversation_id(conversation_id: Any) -> dict[str, str] | None:
|
|
70
69
|
raw = str(conversation_id or "").strip()
|
|
71
70
|
parts = raw.split(":", 2)
|
|
@@ -74,11 +73,14 @@ def parse_conversation_id(conversation_id: Any) -> dict[str, str] | None:
|
|
|
74
73
|
connector, chat_type, chat_id = parts
|
|
75
74
|
if not connector or not chat_type or not chat_id:
|
|
76
75
|
return None
|
|
76
|
+
profile_id, resolved_chat_id = _decode_chat_id(connector=connector, chat_id=chat_id)
|
|
77
77
|
return {
|
|
78
78
|
"conversation_id": raw,
|
|
79
79
|
"connector": connector,
|
|
80
80
|
"chat_type": chat_type,
|
|
81
|
-
"chat_id":
|
|
81
|
+
"chat_id": resolved_chat_id,
|
|
82
|
+
"chat_id_raw": chat_id,
|
|
83
|
+
"profile_id": profile_id or "",
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
|
|
@@ -91,7 +93,12 @@ def normalize_conversation_id(conversation_id: Any) -> str:
|
|
|
91
93
|
return "local:default"
|
|
92
94
|
parsed = parse_conversation_id(raw)
|
|
93
95
|
if parsed is not None:
|
|
94
|
-
return
|
|
96
|
+
return format_conversation_id(
|
|
97
|
+
parsed["connector"].lower(),
|
|
98
|
+
parsed["chat_type"].lower(),
|
|
99
|
+
parsed["chat_id"],
|
|
100
|
+
profile_id=parsed.get("profile_id") or None,
|
|
101
|
+
)
|
|
95
102
|
if ":" in raw:
|
|
96
103
|
return raw
|
|
97
104
|
return f"{lowered}:default"
|
|
@@ -102,7 +109,17 @@ def conversation_identity_key(conversation_id: Any) -> str:
|
|
|
102
109
|
parsed = parse_conversation_id(normalized)
|
|
103
110
|
if parsed is None:
|
|
104
111
|
return normalized.lower()
|
|
105
|
-
|
|
112
|
+
profile_key = str(parsed.get("profile_id") or "").strip().lower()
|
|
113
|
+
return ":".join(
|
|
114
|
+
item
|
|
115
|
+
for item in (
|
|
116
|
+
parsed["connector"].lower(),
|
|
117
|
+
profile_key,
|
|
118
|
+
parsed["chat_type"].lower(),
|
|
119
|
+
parsed["chat_id"].lower(),
|
|
120
|
+
)
|
|
121
|
+
if item
|
|
122
|
+
)
|
|
106
123
|
|
|
107
124
|
|
|
108
125
|
def build_discovered_target(
|
|
@@ -113,6 +130,8 @@ def build_discovered_target(
|
|
|
113
130
|
label: str | None = None,
|
|
114
131
|
quest_id: str | None = None,
|
|
115
132
|
updated_at: str | None = None,
|
|
133
|
+
profile_id: str | None = None,
|
|
134
|
+
profile_label: str | None = None,
|
|
116
135
|
) -> dict[str, Any] | None:
|
|
117
136
|
parsed = parse_conversation_id(conversation_id)
|
|
118
137
|
if parsed is None:
|
|
@@ -123,6 +142,10 @@ def build_discovered_target(
|
|
|
123
142
|
"sources": [source],
|
|
124
143
|
"label": label or f"{parsed['chat_type']} · {parsed['chat_id']}",
|
|
125
144
|
}
|
|
145
|
+
if profile_id or parsed.get("profile_id"):
|
|
146
|
+
target["profile_id"] = str(profile_id or parsed.get("profile_id") or "").strip() or None
|
|
147
|
+
if profile_label:
|
|
148
|
+
target["profile_label"] = profile_label
|
|
126
149
|
if is_default:
|
|
127
150
|
target["is_default"] = True
|
|
128
151
|
if quest_id:
|
|
@@ -140,9 +163,10 @@ def merge_discovered_targets(items: list[dict[str, Any] | None]) -> list[dict[st
|
|
|
140
163
|
conversation_id = str(item.get("conversation_id") or "").strip()
|
|
141
164
|
if not conversation_id:
|
|
142
165
|
continue
|
|
143
|
-
|
|
166
|
+
identity = conversation_identity_key(conversation_id)
|
|
167
|
+
existing = merged.get(identity)
|
|
144
168
|
if existing is None:
|
|
145
|
-
merged[
|
|
169
|
+
merged[identity] = dict(item)
|
|
146
170
|
continue
|
|
147
171
|
sources = list(existing.get("sources") or [])
|
|
148
172
|
for source in item.get("sources") or []:
|
|
@@ -161,6 +185,27 @@ def merge_discovered_targets(items: list[dict[str, Any] | None]) -> list[dict[st
|
|
|
161
185
|
existing["label"] = item["label"]
|
|
162
186
|
if not existing.get("source") and item.get("source"):
|
|
163
187
|
existing["source"] = item["source"]
|
|
188
|
+
for key, value in item.items():
|
|
189
|
+
if key in {
|
|
190
|
+
"conversation_id",
|
|
191
|
+
"connector",
|
|
192
|
+
"chat_type",
|
|
193
|
+
"chat_id",
|
|
194
|
+
"sources",
|
|
195
|
+
"is_default",
|
|
196
|
+
"quest_id",
|
|
197
|
+
"updated_at",
|
|
198
|
+
"label",
|
|
199
|
+
"source",
|
|
200
|
+
}:
|
|
201
|
+
continue
|
|
202
|
+
if value is None:
|
|
203
|
+
continue
|
|
204
|
+
if key not in existing or existing.get(key) in {None, ""}:
|
|
205
|
+
existing[key] = value
|
|
206
|
+
continue
|
|
207
|
+
if key in {"bound_quest_id", "bound_quest_title", "warning", "first_seen_at"}:
|
|
208
|
+
existing[key] = value
|
|
164
209
|
|
|
165
210
|
return sorted(
|
|
166
211
|
merged.values(),
|