@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
|
@@ -6,8 +6,9 @@ import subprocess
|
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from urllib.error import URLError
|
|
9
|
-
from urllib.request import Request
|
|
9
|
+
from urllib.request import Request
|
|
10
10
|
|
|
11
|
+
from ..connector_profiles import PROFILEABLE_CONNECTOR_NAMES, normalize_connector_config
|
|
11
12
|
from ..connector_runtime import infer_connector_transport
|
|
12
13
|
from ..home import repo_root
|
|
13
14
|
from ..lingzhu_support import (
|
|
@@ -22,6 +23,13 @@ from ..lingzhu_support import (
|
|
|
22
23
|
lingzhu_sse_url,
|
|
23
24
|
lingzhu_supported_commands,
|
|
24
25
|
)
|
|
26
|
+
from ..qq_profiles import (
|
|
27
|
+
find_qq_profile,
|
|
28
|
+
list_qq_profiles,
|
|
29
|
+
normalize_qq_connector_config,
|
|
30
|
+
qq_profile_label,
|
|
31
|
+
)
|
|
32
|
+
from ..network import urlopen_with_proxy as urlopen
|
|
25
33
|
from ..shared import read_json, read_text, read_yaml, resolve_runner_binary, run_command, sha256_text, utc_now, which, write_text, write_yaml
|
|
26
34
|
from .models import (
|
|
27
35
|
CONFIG_NAMES,
|
|
@@ -125,27 +133,42 @@ class ConfigManager:
|
|
|
125
133
|
def save_named_payload(self, name: str, payload: dict) -> dict:
|
|
126
134
|
return self.save_named_text(name, self.render_named_payload(name, payload))
|
|
127
135
|
|
|
128
|
-
def bind_qq_main_chat(self, *, chat_id: str) -> dict:
|
|
136
|
+
def bind_qq_main_chat(self, *, profile_id: str | None = None, chat_id: str) -> dict:
|
|
129
137
|
normalized_chat_id = str(chat_id or "").strip()
|
|
130
138
|
if not normalized_chat_id:
|
|
131
139
|
return {"ok": False, "saved": False, "message": "QQ main chat id is empty."}
|
|
132
140
|
connectors = self.load_named_normalized("connectors")
|
|
133
141
|
qq = connectors.get("qq") if isinstance(connectors.get("qq"), dict) else {}
|
|
134
|
-
|
|
142
|
+
profiles = list_qq_profiles(qq)
|
|
143
|
+
if not profiles:
|
|
144
|
+
return {"ok": False, "saved": False, "message": "QQ profile is not configured yet."}
|
|
145
|
+
resolved_profile = find_qq_profile(qq, profile_id=profile_id)
|
|
146
|
+
if resolved_profile is None and len(profiles) == 1:
|
|
147
|
+
resolved_profile = profiles[0]
|
|
148
|
+
if resolved_profile is None:
|
|
149
|
+
return {"ok": False, "saved": False, "message": "Unable to determine which QQ profile should save this OpenID."}
|
|
150
|
+
configured = str((resolved_profile or {}).get("main_chat_id") or "").strip()
|
|
135
151
|
if configured:
|
|
136
152
|
return {
|
|
137
153
|
"ok": True,
|
|
138
154
|
"saved": False,
|
|
139
155
|
"chat_id": configured,
|
|
140
156
|
"already_configured": True,
|
|
157
|
+
"profile_id": resolved_profile.get("profile_id"),
|
|
141
158
|
}
|
|
142
|
-
|
|
159
|
+
for item in profiles:
|
|
160
|
+
if str(item.get("profile_id") or "").strip() == str(resolved_profile.get("profile_id") or "").strip():
|
|
161
|
+
item["main_chat_id"] = normalized_chat_id
|
|
162
|
+
qq["profiles"] = profiles
|
|
163
|
+
qq = normalize_qq_connector_config(qq)
|
|
143
164
|
connectors["qq"] = qq
|
|
144
165
|
result = self.save_named_payload("connectors", connectors)
|
|
145
166
|
return {
|
|
146
167
|
"ok": bool(result.get("ok")),
|
|
147
168
|
"saved": bool(result.get("ok")),
|
|
148
169
|
"chat_id": normalized_chat_id,
|
|
170
|
+
"profile_id": resolved_profile.get("profile_id"),
|
|
171
|
+
"profile_label": qq_profile_label(resolved_profile),
|
|
149
172
|
"saved_at": result.get("saved_at"),
|
|
150
173
|
"errors": result.get("errors") or [],
|
|
151
174
|
"warnings": result.get("warnings") or [],
|
|
@@ -231,8 +254,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
231
254
|
- connect Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
232
255
|
- choose one preferred connector for proactive artifact updates
|
|
233
256
|
- decide whether artifact updates fan out or stay focused
|
|
234
|
-
-
|
|
235
|
-
- keep legacy webhook or relay settings only as a fallback path
|
|
257
|
+
- use the built-in direct runtime for each connector
|
|
236
258
|
- keep all secrets in one visible place
|
|
237
259
|
|
|
238
260
|
## Recommended order
|
|
@@ -252,22 +274,12 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
252
274
|
- WhatsApp: `local_session`
|
|
253
275
|
- QQ: `gateway_direct`
|
|
254
276
|
|
|
255
|
-
## Legacy fallback routes
|
|
256
|
-
|
|
257
|
-
- `POST /api/bridges/telegram/webhook`
|
|
258
|
-
- `POST /api/bridges/slack/webhook`
|
|
259
|
-
- `POST /api/bridges/feishu/webhook`
|
|
260
|
-
- `GET /api/bridges/whatsapp/webhook`
|
|
261
|
-
- `POST /api/bridges/whatsapp/webhook`
|
|
262
|
-
- `POST /api/bridges/discord/webhook`
|
|
263
|
-
|
|
264
277
|
## Practical notes
|
|
265
278
|
|
|
266
279
|
### Telegram
|
|
267
280
|
|
|
268
281
|
- set `bot_token`
|
|
269
282
|
- prefer `transport: polling`
|
|
270
|
-
- use webhook fields only in the legacy section
|
|
271
283
|
- readiness test uses `getMe`
|
|
272
284
|
|
|
273
285
|
### Slack
|
|
@@ -281,32 +293,30 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
281
293
|
|
|
282
294
|
- set `bot_token`
|
|
283
295
|
- prefer `transport: gateway`
|
|
284
|
-
- keep interaction callback fields only for legacy compatibility
|
|
285
296
|
|
|
286
297
|
### Feishu
|
|
287
298
|
|
|
288
299
|
- set `app_id`
|
|
289
300
|
- set `app_secret`
|
|
290
301
|
- prefer `transport: long_connection`
|
|
291
|
-
- keep verification or encrypt keys only for legacy webhook fallback
|
|
292
302
|
- test checks whether tenant token exchange succeeds
|
|
293
303
|
|
|
294
304
|
### WhatsApp
|
|
295
305
|
|
|
296
306
|
- prefer `transport: local_session`
|
|
297
307
|
- the local-session path is designed to avoid public callbacks
|
|
298
|
-
-
|
|
299
|
-
- current live test can still probe Meta credentials when configured
|
|
308
|
+
- keep one writable `session_dir` for the local auth state
|
|
300
309
|
|
|
301
310
|
### QQ
|
|
302
311
|
|
|
303
312
|
- QQ only uses the built-in gateway direct path with `app_id` + `app_secret`
|
|
304
|
-
-
|
|
305
|
-
-
|
|
313
|
+
- each QQ bot is stored as one item under `qq.profiles`
|
|
314
|
+
- save one QQ bot profile first, then ask the user to send one private QQ message to that specific bot
|
|
315
|
+
- the daemon auto-detects that user's `openid` and saves it into that profile's `main_chat_id`
|
|
306
316
|
- private QQ chats can then auto-follow the latest quest by default, unless disabled in settings
|
|
307
317
|
- readiness test exchanges `access_token` and probes `/gateway`
|
|
308
318
|
- active send targets use QQ user `openid` or group `group_openid`
|
|
309
|
-
- the settings page also surfaces recently discovered targets from runtime activity
|
|
319
|
+
- the settings page also surfaces recently discovered targets from runtime activity, grouped by QQ bot profile
|
|
310
320
|
- milestone delivery toggles default to enabled; adjust them only if you want less outbound push
|
|
311
321
|
- the recommended first-run path is: save credentials -> send one QQ private message -> confirm `Detected OpenID` -> run a probe
|
|
312
322
|
|
|
@@ -377,7 +387,7 @@ This page edits `{home_text}/config/runners.yaml`.
|
|
|
377
387
|
- `claude` remains TODO / reserved in the current open-source release and is not runnable yet
|
|
378
388
|
- keep `codex.model_reasoning_effort: xhigh` unless you explicitly want a lighter default
|
|
379
389
|
- keep `codex.retry_on_failure: true` so transient Codex failures can resume automatically
|
|
380
|
-
- keep retry timing near `
|
|
390
|
+
- keep retry timing near `10s / 6x / 1800s max` so Codex backs off exponentially and the last retry waits about 30 minutes
|
|
381
391
|
- DeepScientist hard-limits one turn to at most `5` total attempts, even if the config says more
|
|
382
392
|
|
|
383
393
|
## Test behavior
|
|
@@ -643,17 +653,39 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
643
653
|
enabled_connectors.append(str(name))
|
|
644
654
|
|
|
645
655
|
if name == "qq":
|
|
646
|
-
|
|
656
|
+
profiles = list_qq_profiles(config)
|
|
657
|
+
if not profiles:
|
|
658
|
+
errors.append("qq: requires at least one configured profile under `qq.profiles`.")
|
|
659
|
+
continue
|
|
660
|
+
legacy_missing_app_id = False
|
|
661
|
+
legacy_missing_secret = False
|
|
662
|
+
seen_profile_ids: set[str] = set()
|
|
663
|
+
seen_app_ids: set[str] = set()
|
|
664
|
+
for profile in profiles:
|
|
665
|
+
profile_id = str(profile.get("profile_id") or "").strip() or "unknown"
|
|
666
|
+
app_id = str(profile.get("app_id") or "").strip()
|
|
667
|
+
if not profile_id:
|
|
668
|
+
errors.append("qq: every profile requires a stable `profile_id`.")
|
|
669
|
+
elif profile_id in seen_profile_ids:
|
|
670
|
+
errors.append(f"qq: duplicate profile_id `{profile_id}`.")
|
|
671
|
+
else:
|
|
672
|
+
seen_profile_ids.add(profile_id)
|
|
673
|
+
if not app_id:
|
|
674
|
+
legacy_missing_app_id = True
|
|
675
|
+
errors.append(f"qq[{profile_id}]: requires `app_id`.")
|
|
676
|
+
elif app_id in seen_app_ids:
|
|
677
|
+
errors.append(f"qq: duplicate app_id `{app_id}` across profiles.")
|
|
678
|
+
else:
|
|
679
|
+
seen_app_ids.add(app_id)
|
|
680
|
+
if not self._has_secret(profile, "app_secret", "app_secret_env"):
|
|
681
|
+
legacy_missing_secret = True
|
|
682
|
+
errors.append(f"qq[{profile_id}]: requires `app_secret` or `app_secret_env`.")
|
|
683
|
+
if len(profiles) == 1 and legacy_missing_app_id:
|
|
647
684
|
errors.append("qq: requires `app_id` for the built-in gateway direct connector.")
|
|
648
|
-
if
|
|
685
|
+
if len(profiles) == 1 and legacy_missing_secret:
|
|
649
686
|
errors.append("qq: requires `app_secret` or `app_secret_env` for the built-in gateway direct connector.")
|
|
650
687
|
continue
|
|
651
688
|
transport = infer_connector_transport(name, config)
|
|
652
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
653
|
-
public_callback_url = str(config.get("public_callback_url") or "").strip()
|
|
654
|
-
|
|
655
|
-
if transport == "relay" and not relay_url:
|
|
656
|
-
errors.append(f"{name}: `transport: relay` requires `relay_url`.")
|
|
657
689
|
|
|
658
690
|
policy_validation = self._validate_access_policies(name, config)
|
|
659
691
|
warnings.extend(policy_validation["warnings"])
|
|
@@ -661,75 +693,41 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
661
693
|
|
|
662
694
|
if name == "telegram":
|
|
663
695
|
has_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
664
|
-
if transport
|
|
665
|
-
errors.append("telegram: `transport
|
|
666
|
-
|
|
667
|
-
errors.append("telegram: `transport:
|
|
668
|
-
elif not has_token and not relay_url:
|
|
669
|
-
warnings.append("telegram: set `bot_token` or `bot_token_env` so the connector can authenticate with the Telegram Bot API.")
|
|
696
|
+
if transport != "polling":
|
|
697
|
+
errors.append("telegram: `transport` must stay `polling`.")
|
|
698
|
+
if not has_token:
|
|
699
|
+
errors.append("telegram: `transport: polling` requires `bot_token` or `bot_token_env`.")
|
|
670
700
|
elif name == "discord":
|
|
671
701
|
has_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
672
|
-
if transport
|
|
702
|
+
if transport != "gateway":
|
|
703
|
+
errors.append("discord: `transport` must stay `gateway`.")
|
|
704
|
+
if not has_token:
|
|
673
705
|
errors.append("discord: `transport: gateway` requires `bot_token` or `bot_token_env`.")
|
|
674
|
-
elif transport == "legacy_interactions":
|
|
675
|
-
if not has_token:
|
|
676
|
-
errors.append("discord: `transport: legacy_interactions` requires `bot_token` or `bot_token_env`.")
|
|
677
|
-
if public_callback_url or str(config.get("public_interactions_url") or "").strip():
|
|
678
|
-
if not self._has_secret(config, "public_key", "public_key_env"):
|
|
679
|
-
errors.append("discord: interaction callback mode requires `public_key` or `public_key_env`.")
|
|
680
|
-
elif transport == "relay" and not relay_url:
|
|
681
|
-
errors.append("discord: `transport: relay` requires `relay_url`.")
|
|
682
706
|
if not str(config.get("application_id") or "").strip():
|
|
683
707
|
warnings.append("discord: `application_id` is recommended for richer routing and future slash command support.")
|
|
684
708
|
elif name == "slack":
|
|
685
709
|
has_bot_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
686
710
|
has_app_token = self._has_secret(config, "app_token", "app_token_env")
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
elif transport == "legacy_events_api":
|
|
694
|
-
if not has_bot_token:
|
|
695
|
-
errors.append("slack: `transport: legacy_events_api` requires `bot_token` or `bot_token_env`.")
|
|
696
|
-
if public_callback_url and not has_signing_secret:
|
|
697
|
-
errors.append("slack: callback-based setup requires `signing_secret` or `signing_secret_env`.")
|
|
698
|
-
elif transport == "relay" and not relay_url:
|
|
699
|
-
errors.append("slack: `transport: relay` requires `relay_url`.")
|
|
711
|
+
if transport != "socket_mode":
|
|
712
|
+
errors.append("slack: `transport` must stay `socket_mode`.")
|
|
713
|
+
if not has_bot_token:
|
|
714
|
+
errors.append("slack: `transport: socket_mode` requires `bot_token` or `bot_token_env`.")
|
|
715
|
+
if not has_app_token:
|
|
716
|
+
errors.append("slack: `transport: socket_mode` requires `app_token` or `app_token_env`.")
|
|
700
717
|
elif name == "feishu":
|
|
701
718
|
has_app_id = bool(str(config.get("app_id") or "").strip())
|
|
702
719
|
has_app_secret = self._has_secret(config, "app_secret", "app_secret_env")
|
|
703
|
-
if transport
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
errors.append("feishu: `transport: relay` requires `relay_url`.")
|
|
710
|
-
if public_callback_url and not self._has_secret(
|
|
711
|
-
config,
|
|
712
|
-
"verification_token",
|
|
713
|
-
"verification_token_env",
|
|
714
|
-
):
|
|
715
|
-
errors.append("feishu: webhook-style bridge configuration requires `verification_token` or `verification_token_env`.")
|
|
720
|
+
if transport != "long_connection":
|
|
721
|
+
errors.append("feishu: `transport` must stay `long_connection`.")
|
|
722
|
+
if not has_app_id:
|
|
723
|
+
errors.append("feishu: `transport: long_connection` requires `app_id`.")
|
|
724
|
+
if not has_app_secret:
|
|
725
|
+
errors.append("feishu: `transport: long_connection` requires `app_secret` or `app_secret_env`.")
|
|
716
726
|
elif name == "whatsapp":
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
elif transport == "relay":
|
|
722
|
-
if not relay_url:
|
|
723
|
-
errors.append("whatsapp: `transport: relay` requires `relay_url`.")
|
|
724
|
-
elif transport == "legacy_meta_cloud":
|
|
725
|
-
if provider not in {"relay", "meta"}:
|
|
726
|
-
errors.append(f"whatsapp: unsupported provider `{provider}`. Supported providers: meta, relay.")
|
|
727
|
-
if not self._has_secret(config, "access_token", "access_token_env"):
|
|
728
|
-
errors.append("whatsapp: `provider: meta` requires `access_token` or `access_token_env`.")
|
|
729
|
-
if not str(config.get("phone_number_id") or "").strip():
|
|
730
|
-
errors.append("whatsapp: `provider: meta` requires `phone_number_id`.")
|
|
731
|
-
if not self._has_secret(config, "verify_token", "verify_token_env"):
|
|
732
|
-
errors.append("whatsapp: `provider: meta` requires `verify_token` or `verify_token_env`.")
|
|
727
|
+
if transport != "local_session":
|
|
728
|
+
errors.append("whatsapp: `transport` must stay `local_session`.")
|
|
729
|
+
if not str(config.get("session_dir") or "").strip():
|
|
730
|
+
warnings.append("whatsapp: `transport: local_session` should set `session_dir` for local auth state.")
|
|
733
731
|
elif name == "lingzhu":
|
|
734
732
|
if transport != "openclaw_sse":
|
|
735
733
|
errors.append("lingzhu: `transport` must stay `openclaw_sse`.")
|
|
@@ -794,57 +792,49 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
794
792
|
return {
|
|
795
793
|
"ok": all(item["ok"] for item in items) if items else True,
|
|
796
794
|
"name": "connectors",
|
|
797
|
-
"summary": "Connector
|
|
795
|
+
"summary": "Connector test completed." if items else "No enabled connectors to test.",
|
|
798
796
|
"warnings": [],
|
|
799
797
|
"errors": [],
|
|
800
798
|
"items": items,
|
|
801
799
|
}
|
|
802
800
|
|
|
803
801
|
def _test_single_connector(self, name: str, config: dict, *, live: bool, delivery_target: dict[str, object] | None = None) -> dict:
|
|
804
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
805
802
|
transport = infer_connector_transport(name, config)
|
|
806
803
|
warnings: list[str] = []
|
|
807
804
|
errors: list[str] = []
|
|
808
805
|
details: dict[str, object] = {
|
|
809
|
-
"mode": "gateway-direct" if name == "qq" else config.get("mode"
|
|
806
|
+
"mode": "gateway-direct" if name == "qq" else str(config.get("mode") or transport),
|
|
810
807
|
"transport": transport,
|
|
811
808
|
}
|
|
812
|
-
if name != "qq" and relay_url:
|
|
813
|
-
details["relay_url"] = relay_url
|
|
814
|
-
if relay_url and name != "qq" and transport == "relay":
|
|
815
|
-
warnings.append("Configured with relay_url. The live test checks local prerequisites, not the external bridge health.")
|
|
816
809
|
|
|
817
810
|
try:
|
|
818
811
|
if name == "telegram":
|
|
819
812
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
820
|
-
if transport
|
|
813
|
+
if transport == "polling" and live and token:
|
|
821
814
|
payload = self._http_json(f"https://api.telegram.org/bot{token}/getMe")
|
|
822
815
|
if not payload.get("ok", False):
|
|
823
816
|
errors.append("Telegram getMe did not return ok=true.")
|
|
824
817
|
else:
|
|
825
818
|
details["identity"] = (payload.get("result") or {}).get("username")
|
|
826
|
-
elif transport
|
|
827
|
-
errors.append("Telegram requires `bot_token` for polling
|
|
828
|
-
|
|
829
|
-
errors.append("Telegram
|
|
819
|
+
elif transport == "polling" and not token:
|
|
820
|
+
errors.append("Telegram requires `bot_token` for polling.")
|
|
821
|
+
else:
|
|
822
|
+
errors.append("Telegram transport must stay `polling`.")
|
|
830
823
|
elif name == "slack":
|
|
831
824
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
832
825
|
app_token = self._secret(config, "app_token", "app_token_env")
|
|
833
|
-
signing_secret = self._secret(config, "signing_secret", "signing_secret_env")
|
|
834
826
|
if transport == "socket_mode" and not app_token:
|
|
835
827
|
errors.append("Slack Socket Mode requires `app_token` or `app_token_env`.")
|
|
836
|
-
if transport == "legacy_events_api" and not signing_secret:
|
|
837
|
-
warnings.append("Slack signing_secret is empty; inbound verification will be skipped.")
|
|
838
828
|
if live and token:
|
|
839
829
|
payload = self._http_json("https://slack.com/api/auth.test", method="POST", headers={"Authorization": f"Bearer {token}"})
|
|
840
830
|
if not payload.get("ok", False):
|
|
841
831
|
errors.append(str(payload.get("error") or "Slack auth.test failed."))
|
|
842
832
|
else:
|
|
843
833
|
details["identity"] = payload.get("user")
|
|
844
|
-
elif transport
|
|
834
|
+
elif transport == "socket_mode" and not token:
|
|
845
835
|
errors.append("Slack requires `bot_token` for native runtime access.")
|
|
846
|
-
elif transport
|
|
847
|
-
errors.append("Slack
|
|
836
|
+
elif transport != "socket_mode":
|
|
837
|
+
errors.append("Slack transport must stay `socket_mode`.")
|
|
848
838
|
elif name == "discord":
|
|
849
839
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
850
840
|
if live and token:
|
|
@@ -853,10 +843,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
853
843
|
errors.append(str(payload.get("message") or "Discord identity check failed."))
|
|
854
844
|
else:
|
|
855
845
|
details["identity"] = payload.get("username")
|
|
856
|
-
elif transport
|
|
857
|
-
errors.append("Discord requires `bot_token` for gateway
|
|
858
|
-
elif transport
|
|
859
|
-
errors.append("Discord
|
|
846
|
+
elif transport == "gateway" and not token:
|
|
847
|
+
errors.append("Discord requires `bot_token` for gateway access.")
|
|
848
|
+
elif transport != "gateway":
|
|
849
|
+
errors.append("Discord transport must stay `gateway`.")
|
|
860
850
|
elif name == "feishu":
|
|
861
851
|
app_id = str(config.get("app_id") or "").strip()
|
|
862
852
|
app_secret = self._secret(config, "app_secret", "app_secret_env")
|
|
@@ -869,15 +859,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
869
859
|
)
|
|
870
860
|
if not payload.get("tenant_access_token"):
|
|
871
861
|
errors.append(str(payload.get("msg") or "Feishu tenant token exchange failed."))
|
|
872
|
-
elif transport
|
|
873
|
-
errors.append("Feishu requires `app_id` + `app_secret` for long-connection
|
|
874
|
-
elif transport
|
|
875
|
-
errors.append("Feishu
|
|
862
|
+
elif transport == "long_connection" and not (app_id and app_secret):
|
|
863
|
+
errors.append("Feishu requires `app_id` + `app_secret` for long-connection access.")
|
|
864
|
+
elif transport != "long_connection":
|
|
865
|
+
errors.append("Feishu transport must stay `long_connection`.")
|
|
876
866
|
elif name == "whatsapp":
|
|
877
|
-
provider = str(config.get("provider") or "relay").strip().lower()
|
|
878
|
-
details["provider"] = provider
|
|
879
|
-
token = self._secret(config, "access_token", "access_token_env")
|
|
880
|
-
phone_number_id = str(config.get("phone_number_id") or "").strip()
|
|
881
867
|
if transport == "local_session":
|
|
882
868
|
session_dir = str(config.get("session_dir") or "").strip()
|
|
883
869
|
details["session_dir"] = session_dir or None
|
|
@@ -885,49 +871,66 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
885
871
|
details["session_dir_exists"] = Path(session_dir).expanduser().exists()
|
|
886
872
|
if not session_dir:
|
|
887
873
|
warnings.append("WhatsApp local-session mode still needs a local `session_dir` for auth state.")
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
api_version = str(config.get("api_version") or "v21.0").strip()
|
|
891
|
-
payload = self._http_json(
|
|
892
|
-
f"{api_base_url}/{api_version}/{phone_number_id}",
|
|
893
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
894
|
-
)
|
|
895
|
-
if payload.get("error"):
|
|
896
|
-
errors.append(str(payload["error"].get("message") or "WhatsApp phone number probe failed."))
|
|
897
|
-
else:
|
|
898
|
-
details["identity"] = payload.get("display_phone_number")
|
|
899
|
-
elif transport == "legacy_meta_cloud" and not (token and phone_number_id):
|
|
900
|
-
errors.append("WhatsApp Meta Cloud fallback requires `access_token` + `phone_number_id`.")
|
|
901
|
-
elif transport == "relay" and not relay_url:
|
|
902
|
-
errors.append("WhatsApp requires `relay_url` when `transport: relay` is selected.")
|
|
874
|
+
else:
|
|
875
|
+
errors.append("WhatsApp transport must stay `local_session`.")
|
|
903
876
|
elif name == "qq":
|
|
904
|
-
app_id = str(config.get("app_id") or "").strip()
|
|
905
|
-
app_secret = self._secret(config, "app_secret", "app_secret_env")
|
|
906
877
|
details["transport"] = "gateway_direct"
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
878
|
+
profile_results: list[dict[str, object]] = []
|
|
879
|
+
profiles = list_qq_profiles(config)
|
|
880
|
+
if not profiles:
|
|
881
|
+
errors.append("QQ requires at least one configured profile.")
|
|
882
|
+
for profile in profiles:
|
|
883
|
+
profile_id = str(profile.get("profile_id") or "").strip() or "unknown"
|
|
884
|
+
app_id = str(profile.get("app_id") or "").strip()
|
|
885
|
+
app_secret = self._secret(profile, "app_secret", "app_secret_env")
|
|
886
|
+
profile_details: dict[str, object] = {
|
|
887
|
+
"profile_id": profile_id,
|
|
888
|
+
"label": qq_profile_label(profile),
|
|
889
|
+
"app_id": app_id or None,
|
|
890
|
+
"main_chat_id": str(profile.get("main_chat_id") or "").strip() or None,
|
|
891
|
+
}
|
|
892
|
+
if not app_id or not app_secret:
|
|
893
|
+
profile_details["ok"] = False
|
|
894
|
+
profile_details["error"] = "QQ requires `app_id` + `app_secret` for each configured profile."
|
|
895
|
+
errors.append(f"QQ profile `{profile_id}` is missing `app_id` or `app_secret`.")
|
|
896
|
+
profile_results.append(profile_details)
|
|
897
|
+
continue
|
|
898
|
+
if live:
|
|
899
|
+
token_payload = self._http_json(
|
|
900
|
+
"https://bots.qq.com/app/getAppAccessToken",
|
|
901
|
+
method="POST",
|
|
902
|
+
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
903
|
+
body={"appId": app_id, "clientSecret": app_secret},
|
|
904
|
+
)
|
|
905
|
+
access_token = str(token_payload.get("access_token") or "").strip()
|
|
906
|
+
if not access_token:
|
|
907
|
+
message = str(token_payload.get("message") or "QQ access token exchange failed.")
|
|
908
|
+
errors.append(f"QQ profile `{profile_id}`: {message}")
|
|
909
|
+
profile_details["ok"] = False
|
|
910
|
+
profile_details["error"] = message
|
|
911
|
+
profile_results.append(profile_details)
|
|
912
|
+
continue
|
|
922
913
|
gateway_payload = self._http_json(
|
|
923
914
|
"https://api.sgroup.qq.com/gateway",
|
|
924
915
|
headers={"Authorization": f"QQBot {access_token}"},
|
|
925
916
|
)
|
|
926
917
|
gateway_url = str(gateway_payload.get("url") or "").strip()
|
|
927
918
|
if not gateway_url:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
919
|
+
message = str(gateway_payload.get("message") or "QQ gateway probe failed.")
|
|
920
|
+
errors.append(f"QQ profile `{profile_id}`: {message}")
|
|
921
|
+
profile_details["ok"] = False
|
|
922
|
+
profile_details["error"] = message
|
|
923
|
+
profile_results.append(profile_details)
|
|
924
|
+
continue
|
|
925
|
+
profile_details["gateway_url"] = gateway_url
|
|
926
|
+
profile_details["token_expires_in"] = token_payload.get("expires_in")
|
|
927
|
+
profile_details["ok"] = True
|
|
928
|
+
profile_results.append(profile_details)
|
|
929
|
+
details["profiles"] = profile_results
|
|
930
|
+
if len(profile_results) == 1 and profile_results[0].get("ok"):
|
|
931
|
+
details["identity"] = profile_results[0].get("app_id")
|
|
932
|
+
details["gateway_url"] = profile_results[0].get("gateway_url")
|
|
933
|
+
details["token_expires_in"] = profile_results[0].get("token_expires_in")
|
|
931
934
|
elif name == "lingzhu":
|
|
932
935
|
details.update(self._lingzhu_snapshot_details(config))
|
|
933
936
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
@@ -952,7 +955,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
952
955
|
delivery_message = str(delivery_target.get("text") or "").strip()
|
|
953
956
|
chat_type = str(delivery_target.get("chat_type") or "direct").strip().lower()
|
|
954
957
|
chat_id = str(delivery_target.get("chat_id") or "").strip()
|
|
955
|
-
default_chat_id =
|
|
958
|
+
default_chat_id = ""
|
|
959
|
+
if name == "qq":
|
|
960
|
+
profiles = list_qq_profiles(config)
|
|
961
|
+
if len(profiles) == 1:
|
|
962
|
+
default_chat_id = str(profiles[0].get("main_chat_id") or "").strip()
|
|
956
963
|
if not default_chat_id:
|
|
957
964
|
default_chat_id = self._connector_recent_chat_id(name, chat_type)
|
|
958
965
|
if not chat_id and default_chat_id:
|
|
@@ -994,7 +1001,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
994
1001
|
delivery = bridge.deliver(outbound, config)
|
|
995
1002
|
if delivery is None:
|
|
996
1003
|
warnings.append(
|
|
997
|
-
"The current connector mode cannot actively send a test message.
|
|
1004
|
+
"The current connector mode cannot actively send a test message yet. Finish the native direct setup first."
|
|
998
1005
|
)
|
|
999
1006
|
else:
|
|
1000
1007
|
details["delivery"] = delivery
|
|
@@ -1197,7 +1204,6 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1197
1204
|
"mode": "openclaw_companion",
|
|
1198
1205
|
"transport": "openclaw_sse",
|
|
1199
1206
|
"enabled": bool(resolved.get("enabled", False)),
|
|
1200
|
-
"relay_url": None,
|
|
1201
1207
|
"main_chat_id": None,
|
|
1202
1208
|
"last_conversation_id": None,
|
|
1203
1209
|
"inbox_count": 0,
|
|
@@ -1315,11 +1321,20 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1315
1321
|
prepared = deepcopy(payload)
|
|
1316
1322
|
if name == "config":
|
|
1317
1323
|
prepared.pop("reports", None)
|
|
1324
|
+
return self._normalize_config_payload(prepared)
|
|
1318
1325
|
if name == "plugins":
|
|
1319
1326
|
prepared = self._normalize_plugins_payload(prepared)
|
|
1320
1327
|
elif name == "mcp_servers":
|
|
1321
1328
|
prepared = self._normalize_mcp_payload(prepared)
|
|
1322
1329
|
defaults = default_payload(name, self.home)
|
|
1330
|
+
if name == "runners":
|
|
1331
|
+
normalized = self._deep_merge(defaults, prepared)
|
|
1332
|
+
codex = normalized.get("codex")
|
|
1333
|
+
if isinstance(codex, dict) and self._looks_like_legacy_codex_retry_profile(codex):
|
|
1334
|
+
codex["retry_initial_backoff_sec"] = 10.0
|
|
1335
|
+
codex["retry_backoff_multiplier"] = 6.0
|
|
1336
|
+
codex["retry_max_backoff_sec"] = 1800.0
|
|
1337
|
+
return normalized
|
|
1323
1338
|
if name == "connectors":
|
|
1324
1339
|
normalized = deepcopy(defaults)
|
|
1325
1340
|
for connector_name, connector_payload in prepared.items():
|
|
@@ -1337,7 +1352,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1337
1352
|
if connector_name == "qq":
|
|
1338
1353
|
for legacy_key in ("mode", "relay_url", "relay_auth_token", "public_callback_url", "webhook_verify_signature"):
|
|
1339
1354
|
sanitized_payload.pop(legacy_key, None)
|
|
1340
|
-
|
|
1355
|
+
normalized["qq"] = normalize_qq_connector_config({**base, **sanitized_payload})
|
|
1356
|
+
continue
|
|
1357
|
+
if connector_name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1358
|
+
normalized[connector_name] = normalize_connector_config(connector_name, {**base, **sanitized_payload})
|
|
1359
|
+
continue
|
|
1341
1360
|
elif connector_name == "lingzhu":
|
|
1342
1361
|
sanitized_payload["transport"] = "openclaw_sse"
|
|
1343
1362
|
elif "transport" not in sanitized_payload:
|
|
@@ -1349,6 +1368,46 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1349
1368
|
return normalized
|
|
1350
1369
|
return self._deep_merge(defaults, prepared)
|
|
1351
1370
|
|
|
1371
|
+
def _normalize_config_payload(self, payload: dict) -> dict:
|
|
1372
|
+
defaults = default_payload("config", self.home)
|
|
1373
|
+
normalized = self._deep_merge(defaults, payload)
|
|
1374
|
+
bootstrap = normalized.get("bootstrap") if isinstance(normalized.get("bootstrap"), dict) else {}
|
|
1375
|
+
raw_bootstrap = payload.get("bootstrap") if isinstance(payload.get("bootstrap"), dict) else {}
|
|
1376
|
+
default_locale = str(defaults.get("default_locale") or "").strip()
|
|
1377
|
+
current_locale = str(normalized.get("default_locale") or "").strip()
|
|
1378
|
+
locale_source = str(raw_bootstrap.get("locale_source") or "").strip().lower()
|
|
1379
|
+
locale_initialized_from_browser = bool(
|
|
1380
|
+
raw_bootstrap.get("locale_initialized_from_browser", bootstrap.get("locale_initialized_from_browser", False))
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
if locale_source not in {"default", "browser", "user"}:
|
|
1384
|
+
if current_locale and current_locale != default_locale:
|
|
1385
|
+
locale_source = "user"
|
|
1386
|
+
elif locale_initialized_from_browser:
|
|
1387
|
+
locale_source = "browser"
|
|
1388
|
+
else:
|
|
1389
|
+
locale_source = "default"
|
|
1390
|
+
|
|
1391
|
+
if locale_source == "browser":
|
|
1392
|
+
locale_initialized_from_browser = True
|
|
1393
|
+
|
|
1394
|
+
bootstrap["locale_source"] = locale_source
|
|
1395
|
+
bootstrap["locale_initialized_from_browser"] = locale_initialized_from_browser
|
|
1396
|
+
bootstrap["locale_initialized_at"] = bootstrap.get("locale_initialized_at")
|
|
1397
|
+
bootstrap["locale_initialized_browser_locale"] = bootstrap.get("locale_initialized_browser_locale")
|
|
1398
|
+
normalized["bootstrap"] = bootstrap
|
|
1399
|
+
return normalized
|
|
1400
|
+
|
|
1401
|
+
@staticmethod
|
|
1402
|
+
def _looks_like_legacy_codex_retry_profile(payload: dict) -> bool:
|
|
1403
|
+
try:
|
|
1404
|
+
initial = float(payload.get("retry_initial_backoff_sec"))
|
|
1405
|
+
multiplier = float(payload.get("retry_backoff_multiplier"))
|
|
1406
|
+
max_backoff = float(payload.get("retry_max_backoff_sec"))
|
|
1407
|
+
except (TypeError, ValueError):
|
|
1408
|
+
return False
|
|
1409
|
+
return abs(initial - 1.0) < 1e-9 and abs(multiplier - 2.0) < 1e-9 and abs(max_backoff - 8.0) < 1e-9
|
|
1410
|
+
|
|
1352
1411
|
def _normalize_plugins_payload(self, payload: dict) -> dict:
|
|
1353
1412
|
normalized = deepcopy(payload)
|
|
1354
1413
|
if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):
|