@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,4 @@
1
+ from .connector_profiles import * # noqa: F401,F403
2
+ from .lingzhu_support import * # noqa: F401,F403
3
+ from .qq_profiles import * # noqa: F401,F403
4
+ from .weixin_support import * # noqa: F401,F403
@@ -0,0 +1,481 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ import os
5
+ from typing import Any
6
+
7
+ from ..connector_runtime import infer_connector_transport
8
+ from ..shared import slugify
9
+
10
+
11
+ PROFILEABLE_CONNECTOR_NAMES = ("telegram", "discord", "slack", "feishu", "whatsapp")
12
+
13
+
14
+ def _normalize_secret_pair(payload: dict[str, Any], direct_key: str, env_key: str) -> None:
15
+ direct = _as_text(payload.get(direct_key))
16
+ env_name = _as_text(payload.get(env_key))
17
+ payload[direct_key] = direct
18
+ payload[env_key] = None if direct else env_name
19
+
20
+
21
+ CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
22
+ "telegram": {
23
+ "profile_id_prefix": "telegram-profile",
24
+ "shared_fields": (
25
+ "enabled",
26
+ "profiles",
27
+ "transport",
28
+ "bot_name",
29
+ "bot_token",
30
+ "bot_token_env",
31
+ "command_prefix",
32
+ "dm_policy",
33
+ "allow_from",
34
+ "group_policy",
35
+ "group_allow_from",
36
+ "groups",
37
+ "require_mention_in_groups",
38
+ "auto_bind_dm_to_active_quest",
39
+ ),
40
+ "profile_defaults": {
41
+ "profile_id": None,
42
+ "enabled": True,
43
+ "transport": "polling",
44
+ "bot_name": "DeepScientist",
45
+ "bot_token": None,
46
+ "bot_token_env": None,
47
+ },
48
+ "profile_fields": (
49
+ "enabled",
50
+ "transport",
51
+ "bot_name",
52
+ "bot_token",
53
+ "bot_token_env",
54
+ ),
55
+ "migration_keys": ("bot_token",),
56
+ "label_fields": ("bot_name",),
57
+ "id_fields": ("bot_name",),
58
+ "secret_pairs": (("bot_token", "bot_token_env"),),
59
+ "activation_env_defaults": {"bot_token_env": "TELEGRAM_BOT_TOKEN"},
60
+ },
61
+ "discord": {
62
+ "profile_id_prefix": "discord-profile",
63
+ "shared_fields": (
64
+ "enabled",
65
+ "profiles",
66
+ "transport",
67
+ "bot_name",
68
+ "bot_token",
69
+ "bot_token_env",
70
+ "command_prefix",
71
+ "application_id",
72
+ "dm_policy",
73
+ "allow_from",
74
+ "group_policy",
75
+ "group_allow_from",
76
+ "groups",
77
+ "require_mention_in_groups",
78
+ "auto_bind_dm_to_active_quest",
79
+ "guild_allowlist",
80
+ ),
81
+ "profile_defaults": {
82
+ "profile_id": None,
83
+ "enabled": True,
84
+ "transport": "gateway",
85
+ "bot_name": "DeepScientist",
86
+ "bot_token": None,
87
+ "bot_token_env": None,
88
+ "application_id": None,
89
+ },
90
+ "profile_fields": (
91
+ "enabled",
92
+ "transport",
93
+ "bot_name",
94
+ "bot_token",
95
+ "bot_token_env",
96
+ "application_id",
97
+ ),
98
+ "migration_keys": ("bot_token", "application_id"),
99
+ "label_fields": ("bot_name", "application_id"),
100
+ "id_fields": ("application_id", "bot_name"),
101
+ "secret_pairs": (("bot_token", "bot_token_env"),),
102
+ "activation_env_defaults": {"bot_token_env": "DISCORD_BOT_TOKEN"},
103
+ },
104
+ "slack": {
105
+ "profile_id_prefix": "slack-profile",
106
+ "shared_fields": (
107
+ "enabled",
108
+ "profiles",
109
+ "transport",
110
+ "bot_name",
111
+ "bot_token",
112
+ "bot_token_env",
113
+ "bot_user_id",
114
+ "app_token",
115
+ "app_token_env",
116
+ "command_prefix",
117
+ "dm_policy",
118
+ "allow_from",
119
+ "group_policy",
120
+ "group_allow_from",
121
+ "groups",
122
+ "require_mention_in_groups",
123
+ "auto_bind_dm_to_active_quest",
124
+ ),
125
+ "profile_defaults": {
126
+ "profile_id": None,
127
+ "enabled": True,
128
+ "transport": "socket_mode",
129
+ "bot_name": "DeepScientist",
130
+ "bot_token": None,
131
+ "bot_token_env": None,
132
+ "bot_user_id": None,
133
+ "app_token": None,
134
+ "app_token_env": None,
135
+ },
136
+ "profile_fields": (
137
+ "enabled",
138
+ "transport",
139
+ "bot_name",
140
+ "bot_token",
141
+ "bot_token_env",
142
+ "bot_user_id",
143
+ "app_token",
144
+ "app_token_env",
145
+ ),
146
+ "migration_keys": ("bot_token", "bot_user_id", "app_token"),
147
+ "label_fields": ("bot_name", "bot_user_id"),
148
+ "id_fields": ("bot_user_id", "bot_name"),
149
+ "secret_pairs": (
150
+ ("bot_token", "bot_token_env"),
151
+ ("app_token", "app_token_env"),
152
+ ),
153
+ "activation_env_defaults": {
154
+ "bot_token_env": "SLACK_BOT_TOKEN",
155
+ "app_token_env": "SLACK_APP_TOKEN",
156
+ },
157
+ },
158
+ "feishu": {
159
+ "profile_id_prefix": "feishu-profile",
160
+ "shared_fields": (
161
+ "enabled",
162
+ "profiles",
163
+ "transport",
164
+ "bot_name",
165
+ "app_id",
166
+ "app_secret",
167
+ "app_secret_env",
168
+ "api_base_url",
169
+ "command_prefix",
170
+ "dm_policy",
171
+ "allow_from",
172
+ "group_policy",
173
+ "group_allow_from",
174
+ "groups",
175
+ "require_mention_in_groups",
176
+ "auto_bind_dm_to_active_quest",
177
+ ),
178
+ "profile_defaults": {
179
+ "profile_id": None,
180
+ "enabled": True,
181
+ "transport": "long_connection",
182
+ "bot_name": "DeepScientist",
183
+ "app_id": None,
184
+ "app_secret": None,
185
+ "app_secret_env": None,
186
+ "api_base_url": "https://open.feishu.cn",
187
+ },
188
+ "profile_fields": (
189
+ "enabled",
190
+ "transport",
191
+ "bot_name",
192
+ "app_id",
193
+ "app_secret",
194
+ "app_secret_env",
195
+ "api_base_url",
196
+ ),
197
+ "migration_keys": ("app_id", "app_secret"),
198
+ "label_fields": ("bot_name", "app_id"),
199
+ "id_fields": ("app_id", "bot_name"),
200
+ "secret_pairs": (("app_secret", "app_secret_env"),),
201
+ "activation_env_defaults": {"app_secret_env": "FEISHU_APP_SECRET"},
202
+ },
203
+ "whatsapp": {
204
+ "profile_id_prefix": "whatsapp-profile",
205
+ "shared_fields": (
206
+ "enabled",
207
+ "profiles",
208
+ "transport",
209
+ "bot_name",
210
+ "auth_method",
211
+ "session_dir",
212
+ "command_prefix",
213
+ "dm_policy",
214
+ "allow_from",
215
+ "group_policy",
216
+ "group_allow_from",
217
+ "groups",
218
+ "auto_bind_dm_to_active_quest",
219
+ ),
220
+ "profile_defaults": {
221
+ "profile_id": None,
222
+ "enabled": True,
223
+ "transport": "local_session",
224
+ "bot_name": "DeepScientist",
225
+ "auth_method": "qr_browser",
226
+ "session_dir": "~/.deepscientist/connectors/whatsapp",
227
+ },
228
+ "profile_fields": (
229
+ "enabled",
230
+ "transport",
231
+ "bot_name",
232
+ "auth_method",
233
+ "session_dir",
234
+ ),
235
+ "migration_keys": ("session_dir",),
236
+ "label_fields": ("bot_name",),
237
+ "id_fields": ("bot_name",),
238
+ "secret_pairs": (),
239
+ },
240
+ }
241
+
242
+ CONNECTOR_PROFILE_READY_REQUIREMENTS: dict[str, tuple[object, ...]] = {
243
+ "telegram": (("bot_token", "bot_token_env"),),
244
+ "discord": (("bot_token", "bot_token_env"),),
245
+ "slack": (
246
+ ("bot_token", "bot_token_env"),
247
+ ("app_token", "app_token_env"),
248
+ ),
249
+ "feishu": (
250
+ "app_id",
251
+ ("app_secret", "app_secret_env"),
252
+ ),
253
+ "whatsapp": ("session_dir",),
254
+ }
255
+
256
+
257
+ def _as_text(value: Any) -> str | None:
258
+ text = str(value or "").strip()
259
+ return text or None
260
+
261
+
262
+ def _resolved_env_secret(env_name: Any) -> str | None:
263
+ normalized = _as_text(env_name)
264
+ if not normalized:
265
+ return None
266
+ resolved = str(os.environ.get(normalized) or "").strip()
267
+ return resolved or None
268
+
269
+
270
+ def _has_secret_value(payload: dict[str, Any], direct_key: str, env_key: str) -> bool:
271
+ return bool(_as_text(payload.get(direct_key)) or _resolved_env_secret(payload.get(env_key)))
272
+
273
+
274
+ def _secret_pair_has_activation_seed(
275
+ payload: dict[str, Any],
276
+ defaults: dict[str, Any],
277
+ *,
278
+ direct_key: str,
279
+ env_key: str,
280
+ placeholder_env_defaults: dict[str, Any] | None = None,
281
+ ) -> bool:
282
+ direct_value = _as_text(payload.get(direct_key))
283
+ if direct_value:
284
+ return True
285
+ env_name = _as_text(payload.get(env_key))
286
+ if not env_name:
287
+ return False
288
+ default_env_name = _as_text((placeholder_env_defaults or {}).get(env_key) or defaults.get(env_key))
289
+ return bool(_resolved_env_secret(env_name) or env_name != default_env_name)
290
+
291
+
292
+ def connector_profile_is_configured(connector_name: str, profile: dict[str, Any] | None) -> bool:
293
+ if connector_name not in CONNECTOR_PROFILE_READY_REQUIREMENTS or not isinstance(profile, dict):
294
+ return False
295
+ for requirement in CONNECTOR_PROFILE_READY_REQUIREMENTS[connector_name]:
296
+ if isinstance(requirement, tuple):
297
+ direct_key, env_key = requirement
298
+ if not _has_secret_value(profile, str(direct_key), str(env_key)):
299
+ return False
300
+ continue
301
+ if not _as_text(profile.get(str(requirement))):
302
+ return False
303
+ return True
304
+
305
+
306
+ def connector_profile_has_activation_seed(connector_name: str, profile: dict[str, Any] | None) -> bool:
307
+ if connector_name not in CONNECTOR_PROFILE_SPECS or not isinstance(profile, dict):
308
+ return False
309
+ if bool(profile.get("enabled")):
310
+ return True
311
+ spec = CONNECTOR_PROFILE_SPECS[connector_name]
312
+ defaults = spec["profile_defaults"]
313
+ secret_direct_keys = {str(direct_key) for direct_key, _ in spec.get("secret_pairs", ())}
314
+ for direct_key, env_key in spec.get("secret_pairs", ()):
315
+ if _secret_pair_has_activation_seed(
316
+ profile,
317
+ defaults,
318
+ direct_key=str(direct_key),
319
+ env_key=str(env_key),
320
+ placeholder_env_defaults=spec.get("activation_env_defaults"),
321
+ ):
322
+ return True
323
+ for key in spec["migration_keys"]:
324
+ normalized_key = str(key)
325
+ if normalized_key in secret_direct_keys:
326
+ continue
327
+ value = _as_text(profile.get(normalized_key))
328
+ default_value = _as_text(defaults.get(normalized_key))
329
+ if value and value != default_value:
330
+ return True
331
+ return False
332
+
333
+
334
+ def _profile_seed(connector_name: str, raw: dict[str, Any], *, index: int) -> str:
335
+ spec = CONNECTOR_PROFILE_SPECS[connector_name]
336
+ explicit = _as_text(raw.get("profile_id"))
337
+ if explicit:
338
+ return explicit
339
+ for key in spec["id_fields"]:
340
+ candidate = _as_text(raw.get(key))
341
+ if candidate:
342
+ return f"{connector_name}-{candidate}"
343
+ return f"{spec['profile_id_prefix']}-{index:03d}"
344
+
345
+
346
+ def _unique_profile_id(seed: str, *, prefix: str, used: set[str]) -> str:
347
+ base = slugify(seed, default=prefix)
348
+ candidate = base
349
+ suffix = 2
350
+ while candidate in used:
351
+ candidate = f"{base}-{suffix}"
352
+ suffix += 1
353
+ used.add(candidate)
354
+ return candidate
355
+
356
+
357
+ def default_connector_profile(connector_name: str) -> dict[str, Any]:
358
+ spec = CONNECTOR_PROFILE_SPECS[connector_name]
359
+ return deepcopy(spec["profile_defaults"])
360
+
361
+
362
+ def connector_profile_label(connector_name: str, profile: dict[str, Any] | None) -> str:
363
+ if not isinstance(profile, dict):
364
+ return connector_name.capitalize()
365
+ spec = CONNECTOR_PROFILE_SPECS[connector_name]
366
+ parts = [_as_text(profile.get(key)) for key in spec["label_fields"]]
367
+ filtered = [item for item in parts if item]
368
+ return " · ".join(filtered) if filtered else connector_name.capitalize()
369
+
370
+
371
+ def normalize_connector_config(connector_name: str, config: dict[str, Any] | None) -> dict[str, Any]:
372
+ if connector_name not in CONNECTOR_PROFILE_SPECS:
373
+ raise KeyError(f"Connector `{connector_name}` does not support generic profile normalization.")
374
+ spec = CONNECTOR_PROFILE_SPECS[connector_name]
375
+ payload = deepcopy(config or {})
376
+ shared = {
377
+ key: deepcopy(payload.get(key))
378
+ for key in spec["shared_fields"]
379
+ if key in payload
380
+ }
381
+ shared["profiles"] = []
382
+ for direct_key, env_key in spec.get("secret_pairs", ()):
383
+ _normalize_secret_pair(shared, direct_key, env_key)
384
+
385
+ raw_profiles = payload.get("profiles")
386
+ items = list(raw_profiles) if isinstance(raw_profiles, list) else []
387
+ has_direct_migration_value = any(
388
+ _as_text(payload.get(key))
389
+ and _as_text(payload.get(key)) != _as_text(spec["profile_defaults"].get(key))
390
+ for key in spec["migration_keys"]
391
+ )
392
+ has_env_only_secret = any(
393
+ _secret_pair_has_activation_seed(
394
+ payload,
395
+ spec["profile_defaults"],
396
+ direct_key=str(direct_key),
397
+ env_key=str(env_key),
398
+ placeholder_env_defaults=spec.get("activation_env_defaults"),
399
+ )
400
+ for direct_key, env_key in spec.get("secret_pairs", ())
401
+ )
402
+ if not items and (has_direct_migration_value or has_env_only_secret or bool(payload.get("enabled"))):
403
+ items = [{key: payload.get(key) for key in spec["profile_fields"]}]
404
+
405
+ used_ids: set[str] = set()
406
+ profiles: list[dict[str, Any]] = []
407
+ for index, raw in enumerate(items, start=1):
408
+ if not isinstance(raw, dict):
409
+ continue
410
+ current = default_connector_profile(connector_name)
411
+ for key in ("profile_id", *spec["profile_fields"]):
412
+ if key in raw:
413
+ current[key] = deepcopy(raw.get(key))
414
+ for key in spec["profile_fields"]:
415
+ if key in {"enabled", "transport", "mode"}:
416
+ continue
417
+ if isinstance(current.get(key), list):
418
+ continue
419
+ if current.get(key) is None:
420
+ continue
421
+ current[key] = _as_text(current.get(key))
422
+ current["transport"] = infer_connector_transport(connector_name, current)
423
+ if "mode" in spec["profile_defaults"] or current.get("mode") is not None:
424
+ current["mode"] = _as_text(current.get("mode")) or str(spec["profile_defaults"].get("mode") or "")
425
+ for direct_key, env_key in spec.get("secret_pairs", ()):
426
+ _normalize_secret_pair(current, direct_key, env_key)
427
+ if not connector_profile_has_activation_seed(connector_name, current):
428
+ continue
429
+ current["enabled"] = connector_profile_is_configured(connector_name, current)
430
+ current["profile_id"] = _unique_profile_id(
431
+ _profile_seed(connector_name, current, index=index),
432
+ prefix=str(spec["profile_id_prefix"]),
433
+ used=used_ids,
434
+ )
435
+ profiles.append(current)
436
+
437
+ shared["transport"] = infer_connector_transport(connector_name, shared)
438
+ shared["profiles"] = profiles
439
+ shared["enabled"] = any(bool(item.get("enabled")) for item in profiles)
440
+ if len(profiles) == 1:
441
+ for key in spec["profile_fields"]:
442
+ shared[key] = profiles[0].get(key)
443
+ elif len(profiles) > 1:
444
+ for direct_key, env_key in spec.get("secret_pairs", ()):
445
+ shared[direct_key] = None
446
+ shared[env_key] = None
447
+ return shared
448
+
449
+
450
+ def list_connector_profiles(connector_name: str, config: dict[str, Any] | None) -> list[dict[str, Any]]:
451
+ normalized = normalize_connector_config(connector_name, config)
452
+ profiles = normalized.get("profiles")
453
+ return [dict(item) for item in profiles] if isinstance(profiles, list) else []
454
+
455
+
456
+ def find_connector_profile(
457
+ connector_name: str,
458
+ config: dict[str, Any] | None,
459
+ *,
460
+ profile_id: str | None = None,
461
+ ) -> dict[str, Any] | None:
462
+ normalized_profile_id = _as_text(profile_id)
463
+ for profile in list_connector_profiles(connector_name, config):
464
+ if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
465
+ return profile
466
+ return None
467
+
468
+
469
+ def merge_connector_profile_config(
470
+ connector_name: str,
471
+ shared_config: dict[str, Any] | None,
472
+ profile: dict[str, Any],
473
+ ) -> dict[str, Any]:
474
+ normalized = normalize_connector_config(connector_name, shared_config)
475
+ merged = deepcopy(normalized)
476
+ merged.pop("profiles", None)
477
+ for key in CONNECTOR_PROFILE_SPECS[connector_name]["profile_fields"]:
478
+ merged[key] = profile.get(key)
479
+ merged["profile_id"] = str(profile.get("profile_id") or "").strip() or None
480
+ merged["enabled"] = bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True))
481
+ return merged