@shadowob/connector 1.1.3-dev.271 → 1.1.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 +33 -7
- package/dist/cli.js +593 -81
- package/dist/index.cjs +52 -18
- package/dist/index.js +50 -18
- package/hermes-shadowob-plugin/README.md +2 -2
- package/hermes-shadowob-plugin/adapter.py +124 -32
- package/hermes-shadowob-plugin/shadow_sdk.py +33 -2
- package/hermes-shadowob-plugin/tests/test_adapter_env.py +102 -0
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,14 @@ __export(index_exports, {
|
|
|
24
24
|
createConnectorPlans: () => createConnectorPlans
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/cc-connect-fork.ts
|
|
29
|
+
var CC_CONNECT_FORK_REPO = "buggyblues/cc-connect";
|
|
30
|
+
var CC_CONNECT_FORK_REF = "63b5d59127b3004bc7002f2d51892b1f2a91ea83";
|
|
31
|
+
var CC_CONNECT_FORK_SHORT_REF = CC_CONNECT_FORK_REF.slice(0, 7);
|
|
32
|
+
var CC_CONNECT_FORK_DOCS_URL = `https://github.com/${CC_CONNECT_FORK_REPO}/blob/main/docs/shadowob.md`;
|
|
33
|
+
|
|
34
|
+
// src/index.ts
|
|
27
35
|
var DEFAULT_SERVER_URL = "https://shadowob.com";
|
|
28
36
|
var DEFAULT_WORK_DIR = ".";
|
|
29
37
|
var DEFAULT_PROJECT_NAME = "shadow-buddy";
|
|
@@ -110,7 +118,14 @@ function buildOpenClawPlan(input) {
|
|
|
110
118
|
"typing",
|
|
111
119
|
"activityStatus",
|
|
112
120
|
"reactions",
|
|
113
|
-
"editDelete"
|
|
121
|
+
"editDelete",
|
|
122
|
+
"statusChecks",
|
|
123
|
+
"usageCosts",
|
|
124
|
+
"multiAgentBinding",
|
|
125
|
+
"shadowCliLogin",
|
|
126
|
+
"notifications",
|
|
127
|
+
"officialSkills",
|
|
128
|
+
"cronTasks"
|
|
114
129
|
]
|
|
115
130
|
};
|
|
116
131
|
}
|
|
@@ -196,7 +211,12 @@ function buildHermesPlan(input) {
|
|
|
196
211
|
"onlineStatus",
|
|
197
212
|
"typing",
|
|
198
213
|
"activityStatus",
|
|
199
|
-
"cronDelivery"
|
|
214
|
+
"cronDelivery",
|
|
215
|
+
"statusChecks",
|
|
216
|
+
"usageCosts",
|
|
217
|
+
"shadowCliLogin",
|
|
218
|
+
"notifications",
|
|
219
|
+
"officialSkills"
|
|
200
220
|
]
|
|
201
221
|
};
|
|
202
222
|
}
|
|
@@ -211,8 +231,12 @@ function buildCcConnectPlan(input) {
|
|
|
211
231
|
"",
|
|
212
232
|
"[[projects]]",
|
|
213
233
|
`name = "${projectName}"`,
|
|
234
|
+
"",
|
|
235
|
+
"[projects.agent]",
|
|
236
|
+
`type = "${agentType}"`,
|
|
237
|
+
"",
|
|
238
|
+
"[projects.agent.options]",
|
|
214
239
|
`work_dir = "${workDir}"`,
|
|
215
|
-
`agent_type = "${agentType}"`,
|
|
216
240
|
"",
|
|
217
241
|
"[[projects.platforms]]",
|
|
218
242
|
'type = "shadowob"',
|
|
@@ -225,15 +249,6 @@ function buildCcConnectPlan(input) {
|
|
|
225
249
|
"share_session_in_channel = false",
|
|
226
250
|
'progress_style = "compact"'
|
|
227
251
|
].join("\n");
|
|
228
|
-
const commands = [
|
|
229
|
-
{ label: "Install cc-connect", command: "npm install -g cc-connect" },
|
|
230
|
-
{ label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
|
|
231
|
-
{
|
|
232
|
-
label: "Edit config",
|
|
233
|
-
command: "$EDITOR ~/.cc-connect/config.toml"
|
|
234
|
-
},
|
|
235
|
-
{ label: "Start cc-connect", command: "cc-connect" }
|
|
236
|
-
];
|
|
237
252
|
const connectCommand = [
|
|
238
253
|
"npx @shadowob/connector@latest connect",
|
|
239
254
|
"--target cc-connect",
|
|
@@ -243,12 +258,26 @@ function buildCcConnectPlan(input) {
|
|
|
243
258
|
`--project-name ${shellQuote(projectName)}`,
|
|
244
259
|
`--agent-type ${shellQuote(agentType)}`
|
|
245
260
|
].join(" ");
|
|
261
|
+
const installCommand = `${connectCommand} --install`;
|
|
262
|
+
const startCommand = `${connectCommand} --install --start`;
|
|
263
|
+
const commands = [
|
|
264
|
+
{
|
|
265
|
+
label: "Install ShadowOB cc-connect fork",
|
|
266
|
+
command: installCommand
|
|
267
|
+
},
|
|
268
|
+
{ label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
|
|
269
|
+
{
|
|
270
|
+
label: "Edit config",
|
|
271
|
+
command: "$EDITOR ~/.cc-connect/config.toml"
|
|
272
|
+
},
|
|
273
|
+
{ label: "Start ShadowOB cc-connect fork", command: startCommand }
|
|
274
|
+
];
|
|
246
275
|
return {
|
|
247
276
|
target: "cc-connect",
|
|
248
277
|
title: "cc-connect",
|
|
249
|
-
summary:
|
|
250
|
-
connectCommand,
|
|
251
|
-
quickCommand:
|
|
278
|
+
summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
|
|
279
|
+
connectCommand: startCommand,
|
|
280
|
+
quickCommand: startCommand,
|
|
252
281
|
commands,
|
|
253
282
|
configBlocks: [{ label: "~/.cc-connect/config.toml", language: "toml", content: tomlConfig }],
|
|
254
283
|
aiPrompt: [
|
|
@@ -259,9 +288,9 @@ function buildCcConnectPlan(input) {
|
|
|
259
288
|
`Project work_dir: ${workDir}`,
|
|
260
289
|
`Agent type: ${agentType}`,
|
|
261
290
|
"",
|
|
262
|
-
|
|
291
|
+
`Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
|
|
263
292
|
].join("\n"),
|
|
264
|
-
docsUrl:
|
|
293
|
+
docsUrl: CC_CONNECT_FORK_DOCS_URL,
|
|
265
294
|
capabilities: [
|
|
266
295
|
"channelMessages",
|
|
267
296
|
"dms",
|
|
@@ -271,7 +300,12 @@ function buildCcConnectPlan(input) {
|
|
|
271
300
|
"slashCommands",
|
|
272
301
|
"typing",
|
|
273
302
|
"streamingPreviews",
|
|
274
|
-
"forms"
|
|
303
|
+
"forms",
|
|
304
|
+
"statusChecks",
|
|
305
|
+
"usageCosts",
|
|
306
|
+
"multiAgentBinding",
|
|
307
|
+
"shadowCliLogin",
|
|
308
|
+
"notifications"
|
|
275
309
|
]
|
|
276
310
|
};
|
|
277
311
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// src/cc-connect-fork.ts
|
|
2
|
+
var CC_CONNECT_FORK_REPO = "buggyblues/cc-connect";
|
|
3
|
+
var CC_CONNECT_FORK_REF = "63b5d59127b3004bc7002f2d51892b1f2a91ea83";
|
|
4
|
+
var CC_CONNECT_FORK_SHORT_REF = CC_CONNECT_FORK_REF.slice(0, 7);
|
|
5
|
+
var CC_CONNECT_FORK_DOCS_URL = `https://github.com/${CC_CONNECT_FORK_REPO}/blob/main/docs/shadowob.md`;
|
|
6
|
+
|
|
1
7
|
// src/index.ts
|
|
2
8
|
var DEFAULT_SERVER_URL = "https://shadowob.com";
|
|
3
9
|
var DEFAULT_WORK_DIR = ".";
|
|
@@ -85,7 +91,14 @@ function buildOpenClawPlan(input) {
|
|
|
85
91
|
"typing",
|
|
86
92
|
"activityStatus",
|
|
87
93
|
"reactions",
|
|
88
|
-
"editDelete"
|
|
94
|
+
"editDelete",
|
|
95
|
+
"statusChecks",
|
|
96
|
+
"usageCosts",
|
|
97
|
+
"multiAgentBinding",
|
|
98
|
+
"shadowCliLogin",
|
|
99
|
+
"notifications",
|
|
100
|
+
"officialSkills",
|
|
101
|
+
"cronTasks"
|
|
89
102
|
]
|
|
90
103
|
};
|
|
91
104
|
}
|
|
@@ -171,7 +184,12 @@ function buildHermesPlan(input) {
|
|
|
171
184
|
"onlineStatus",
|
|
172
185
|
"typing",
|
|
173
186
|
"activityStatus",
|
|
174
|
-
"cronDelivery"
|
|
187
|
+
"cronDelivery",
|
|
188
|
+
"statusChecks",
|
|
189
|
+
"usageCosts",
|
|
190
|
+
"shadowCliLogin",
|
|
191
|
+
"notifications",
|
|
192
|
+
"officialSkills"
|
|
175
193
|
]
|
|
176
194
|
};
|
|
177
195
|
}
|
|
@@ -186,8 +204,12 @@ function buildCcConnectPlan(input) {
|
|
|
186
204
|
"",
|
|
187
205
|
"[[projects]]",
|
|
188
206
|
`name = "${projectName}"`,
|
|
207
|
+
"",
|
|
208
|
+
"[projects.agent]",
|
|
209
|
+
`type = "${agentType}"`,
|
|
210
|
+
"",
|
|
211
|
+
"[projects.agent.options]",
|
|
189
212
|
`work_dir = "${workDir}"`,
|
|
190
|
-
`agent_type = "${agentType}"`,
|
|
191
213
|
"",
|
|
192
214
|
"[[projects.platforms]]",
|
|
193
215
|
'type = "shadowob"',
|
|
@@ -200,15 +222,6 @@ function buildCcConnectPlan(input) {
|
|
|
200
222
|
"share_session_in_channel = false",
|
|
201
223
|
'progress_style = "compact"'
|
|
202
224
|
].join("\n");
|
|
203
|
-
const commands = [
|
|
204
|
-
{ label: "Install cc-connect", command: "npm install -g cc-connect" },
|
|
205
|
-
{ label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
|
|
206
|
-
{
|
|
207
|
-
label: "Edit config",
|
|
208
|
-
command: "$EDITOR ~/.cc-connect/config.toml"
|
|
209
|
-
},
|
|
210
|
-
{ label: "Start cc-connect", command: "cc-connect" }
|
|
211
|
-
];
|
|
212
225
|
const connectCommand = [
|
|
213
226
|
"npx @shadowob/connector@latest connect",
|
|
214
227
|
"--target cc-connect",
|
|
@@ -218,12 +231,26 @@ function buildCcConnectPlan(input) {
|
|
|
218
231
|
`--project-name ${shellQuote(projectName)}`,
|
|
219
232
|
`--agent-type ${shellQuote(agentType)}`
|
|
220
233
|
].join(" ");
|
|
234
|
+
const installCommand = `${connectCommand} --install`;
|
|
235
|
+
const startCommand = `${connectCommand} --install --start`;
|
|
236
|
+
const commands = [
|
|
237
|
+
{
|
|
238
|
+
label: "Install ShadowOB cc-connect fork",
|
|
239
|
+
command: installCommand
|
|
240
|
+
},
|
|
241
|
+
{ label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
|
|
242
|
+
{
|
|
243
|
+
label: "Edit config",
|
|
244
|
+
command: "$EDITOR ~/.cc-connect/config.toml"
|
|
245
|
+
},
|
|
246
|
+
{ label: "Start ShadowOB cc-connect fork", command: startCommand }
|
|
247
|
+
];
|
|
221
248
|
return {
|
|
222
249
|
target: "cc-connect",
|
|
223
250
|
title: "cc-connect",
|
|
224
|
-
summary:
|
|
225
|
-
connectCommand,
|
|
226
|
-
quickCommand:
|
|
251
|
+
summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
|
|
252
|
+
connectCommand: startCommand,
|
|
253
|
+
quickCommand: startCommand,
|
|
227
254
|
commands,
|
|
228
255
|
configBlocks: [{ label: "~/.cc-connect/config.toml", language: "toml", content: tomlConfig }],
|
|
229
256
|
aiPrompt: [
|
|
@@ -234,9 +261,9 @@ function buildCcConnectPlan(input) {
|
|
|
234
261
|
`Project work_dir: ${workDir}`,
|
|
235
262
|
`Agent type: ${agentType}`,
|
|
236
263
|
"",
|
|
237
|
-
|
|
264
|
+
`Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
|
|
238
265
|
].join("\n"),
|
|
239
|
-
docsUrl:
|
|
266
|
+
docsUrl: CC_CONNECT_FORK_DOCS_URL,
|
|
240
267
|
capabilities: [
|
|
241
268
|
"channelMessages",
|
|
242
269
|
"dms",
|
|
@@ -246,7 +273,12 @@ function buildCcConnectPlan(input) {
|
|
|
246
273
|
"slashCommands",
|
|
247
274
|
"typing",
|
|
248
275
|
"streamingPreviews",
|
|
249
|
-
"forms"
|
|
276
|
+
"forms",
|
|
277
|
+
"statusChecks",
|
|
278
|
+
"usageCosts",
|
|
279
|
+
"multiAgentBinding",
|
|
280
|
+
"shadowCliLogin",
|
|
281
|
+
"notifications"
|
|
250
282
|
]
|
|
251
283
|
};
|
|
252
284
|
}
|
|
@@ -150,8 +150,8 @@ This iteration fixes issues found during static review against the uploaded Shad
|
|
|
150
150
|
- Fixed REST polling shutdown condition so the polling loop stops when the adapter is disconnected.
|
|
151
151
|
- Fixed `env_enablement_fn` to return a flat seed dict, because Hermes merges all non-`home_channel` keys directly into `PlatformConfig.extra`.
|
|
152
152
|
- Changed env auto-enable and connector setup to require only Shadow endpoint/token; Buddy id and channel policy are now resolved dynamically from Shadow.
|
|
153
|
-
- Added dynamic handling for `channel:member-added
|
|
154
|
-
-
|
|
153
|
+
- Added OpenClaw-aligned dynamic handling for `channel:member-added`, `channel:member-removed`, `server:joined`, and `agent:policy-changed` events.
|
|
154
|
+
- Changed empty-channel startup from fatal to tolerant waiting. The adapter stays online, refreshes channel policy periodically, and uses the owner DM as the default home channel when available.
|
|
155
155
|
- Updated standalone media delivery to support local paths, `MEDIA:` refs, relative paths, Shadow private URLs and remote URLs through the SDK helper.
|
|
156
156
|
|
|
157
157
|
## Known limits in this first version
|
|
@@ -465,6 +465,13 @@ def _remote_listen_channel_entries(
|
|
|
465
465
|
return entries
|
|
466
466
|
|
|
467
467
|
|
|
468
|
+
def _owner_id_from_remote_config(remote_config: dict[str, Any] | None) -> str | None:
|
|
469
|
+
if not isinstance(remote_config, dict):
|
|
470
|
+
return None
|
|
471
|
+
owner_id = str(remote_config.get("ownerId") or remote_config.get("owner_id") or "").strip()
|
|
472
|
+
return owner_id or None
|
|
473
|
+
|
|
474
|
+
|
|
468
475
|
class ShadowOBAdapter(BasePlatformAdapter):
|
|
469
476
|
"""Hermes ``BasePlatformAdapter`` implementation for Shadow."""
|
|
470
477
|
|
|
@@ -479,6 +486,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
479
486
|
self.socket: ShadowSocketClient | None = None
|
|
480
487
|
self._poll_task: asyncio.Task | None = None
|
|
481
488
|
self._heartbeat_task: asyncio.Task | None = None
|
|
489
|
+
self._channel_refresh_task: asyncio.Task | None = None
|
|
482
490
|
self._channel_ids: list[str] = _channel_ids_from_config(config)
|
|
483
491
|
self._configured_channel_ids: set[str] = set(self._channel_ids)
|
|
484
492
|
self._remote_channel_ids: set[str] = set()
|
|
@@ -532,26 +540,26 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
532
540
|
await self.client.open()
|
|
533
541
|
await self._load_identity()
|
|
534
542
|
await self._register_slash_commands()
|
|
535
|
-
await self._start_heartbeat()
|
|
536
543
|
await self._resolve_channels()
|
|
537
544
|
if not self._channel_ids:
|
|
538
|
-
|
|
539
|
-
"
|
|
540
|
-
"
|
|
541
|
-
retryable=False,
|
|
545
|
+
logger.warning(
|
|
546
|
+
"[Shadow] No channels are available yet for this Buddy token. "
|
|
547
|
+
"Waiting for the Buddy to be added to a channel or DM.",
|
|
542
548
|
)
|
|
543
|
-
return False
|
|
544
549
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
else:
|
|
550
|
+
use_polling = self._rest_only
|
|
551
|
+
if not self._rest_only:
|
|
548
552
|
try:
|
|
549
553
|
await self._start_socket()
|
|
550
554
|
except Exception as exc:
|
|
551
555
|
logger.warning("[Shadow] Socket.IO connection failed, falling back to REST polling: %s", exc)
|
|
552
|
-
|
|
556
|
+
use_polling = True
|
|
553
557
|
|
|
554
558
|
self._mark_connected()
|
|
559
|
+
if use_polling:
|
|
560
|
+
await self._start_polling()
|
|
561
|
+
await self._start_heartbeat()
|
|
562
|
+
await self._start_channel_refresh()
|
|
555
563
|
logger.info("[Shadow] Connected to %s; channels=%s", self.base_url, ",".join(self._channel_ids))
|
|
556
564
|
return True
|
|
557
565
|
except Exception as exc:
|
|
@@ -573,6 +581,13 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
573
581
|
except asyncio.CancelledError:
|
|
574
582
|
pass
|
|
575
583
|
self._heartbeat_task = None
|
|
584
|
+
if self._channel_refresh_task is not None and not self._channel_refresh_task.done():
|
|
585
|
+
self._channel_refresh_task.cancel()
|
|
586
|
+
try:
|
|
587
|
+
await self._channel_refresh_task
|
|
588
|
+
except asyncio.CancelledError:
|
|
589
|
+
pass
|
|
590
|
+
self._channel_refresh_task = None
|
|
576
591
|
if self._poll_task is not None and not self._poll_task.done():
|
|
577
592
|
self._poll_task.cancel()
|
|
578
593
|
try:
|
|
@@ -909,19 +924,47 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
909
924
|
self._remote_channel_ids = new_remote_ids
|
|
910
925
|
logger.info("[Shadow] Refreshed remote config for agent %s; channels=%s", self._agent_id, len(new_remote_ids))
|
|
911
926
|
|
|
912
|
-
if sync_socket
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
927
|
+
if sync_socket:
|
|
928
|
+
await self._sync_socket_channels(old_channel_ids)
|
|
929
|
+
|
|
930
|
+
async def _sync_socket_channels(self, old_channel_ids: set[str]) -> None:
|
|
931
|
+
if self.socket is None:
|
|
932
|
+
return
|
|
933
|
+
next_channel_ids = set(self._channel_ids)
|
|
934
|
+
for channel_id in old_channel_ids - next_channel_ids:
|
|
935
|
+
try:
|
|
936
|
+
await self.socket.leave_channel(channel_id)
|
|
937
|
+
except Exception:
|
|
938
|
+
pass
|
|
939
|
+
for channel_id in next_channel_ids - old_channel_ids:
|
|
940
|
+
try:
|
|
941
|
+
ack = await self.socket.join_channel(channel_id)
|
|
942
|
+
logger.info("[Shadow] Joined channel %s after config refresh ack=%s", channel_id, ack)
|
|
943
|
+
except Exception as exc:
|
|
944
|
+
logger.warning("[Shadow] Failed to join refreshed channel %s: %s", channel_id, exc)
|
|
945
|
+
|
|
946
|
+
async def _ensure_owner_dm_home_channel(self) -> None:
|
|
947
|
+
if self.client is None:
|
|
948
|
+
return
|
|
949
|
+
owner_id = _owner_id_from_remote_config(self._remote_config)
|
|
950
|
+
if not owner_id or owner_id == self._bot_user_id:
|
|
951
|
+
return
|
|
952
|
+
try:
|
|
953
|
+
channel = await self.client.create_direct_channel(owner_id)
|
|
954
|
+
except Exception as exc:
|
|
955
|
+
logger.debug("[Shadow] Owner DM home channel is not available yet: %s", exc)
|
|
956
|
+
return
|
|
957
|
+
channel_id = str(channel.get("id") or "").strip()
|
|
958
|
+
if not channel_id:
|
|
959
|
+
return
|
|
960
|
+
self._channel_cache[channel_id] = {
|
|
961
|
+
**channel,
|
|
962
|
+
"kind": channel.get("kind") or channel.get("type") or "dm",
|
|
963
|
+
}
|
|
964
|
+
self._channel_policies.setdefault(channel_id, _default_policy_from_remote_config(self._remote_config))
|
|
965
|
+
if channel_id not in self._channel_ids:
|
|
966
|
+
self._channel_ids.append(channel_id)
|
|
967
|
+
logger.info("[Shadow] Using owner DM %s as the default home channel", channel_id)
|
|
925
968
|
|
|
926
969
|
async def _register_slash_commands(self) -> None:
|
|
927
970
|
if self.client is None or not self._agent_id or not self._slash_commands:
|
|
@@ -952,6 +995,26 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
952
995
|
except Exception as exc:
|
|
953
996
|
logger.debug("[Shadow] heartbeat failed for agent %s: %s", self._agent_id, exc)
|
|
954
997
|
|
|
998
|
+
async def _start_channel_refresh(self) -> None:
|
|
999
|
+
if self._channel_refresh_task is None or self._channel_refresh_task.done():
|
|
1000
|
+
self._channel_refresh_task = asyncio.create_task(
|
|
1001
|
+
self._channel_refresh_loop(),
|
|
1002
|
+
name="shadowob-channel-refresh",
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
async def _channel_refresh_loop(self) -> None:
|
|
1006
|
+
interval = max(10.0, min(60.0, self._heartbeat_interval))
|
|
1007
|
+
while self._running and not self.has_fatal_error:
|
|
1008
|
+
await asyncio.sleep(interval)
|
|
1009
|
+
try:
|
|
1010
|
+
await self._resolve_channels(sync_socket=True)
|
|
1011
|
+
if not self._channel_ids:
|
|
1012
|
+
logger.debug("[Shadow] Still waiting for a channel or owner DM")
|
|
1013
|
+
except asyncio.CancelledError:
|
|
1014
|
+
raise
|
|
1015
|
+
except Exception as exc:
|
|
1016
|
+
logger.debug("[Shadow] Channel refresh failed: %s", exc)
|
|
1017
|
+
|
|
955
1018
|
async def _send_slash_interactive_prompt(
|
|
956
1019
|
self,
|
|
957
1020
|
match: tuple[dict[str, Any], str, str],
|
|
@@ -986,9 +1049,10 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
986
1049
|
logger.info("[Shadow] Sent interactive prompt for slash command /%s", name)
|
|
987
1050
|
return True
|
|
988
1051
|
|
|
989
|
-
async def _resolve_channels(self) -> None:
|
|
1052
|
+
async def _resolve_channels(self, *, sync_socket: bool = False) -> None:
|
|
990
1053
|
if self.client is None:
|
|
991
1054
|
return
|
|
1055
|
+
old_channel_ids = set(self._channel_ids)
|
|
992
1056
|
if self._agent_id:
|
|
993
1057
|
try:
|
|
994
1058
|
await self._refresh_remote_config()
|
|
@@ -1044,6 +1108,8 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1044
1108
|
except Exception as exc:
|
|
1045
1109
|
logger.debug("[Shadow] Failed to list direct channels: %s", exc)
|
|
1046
1110
|
|
|
1111
|
+
await self._ensure_owner_dm_home_channel()
|
|
1112
|
+
|
|
1047
1113
|
# Best-effort metadata cache for explicitly configured channels.
|
|
1048
1114
|
for channel_id in list(self._channel_ids):
|
|
1049
1115
|
if channel_id in self._channel_cache:
|
|
@@ -1053,6 +1119,9 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1053
1119
|
except Exception:
|
|
1054
1120
|
self._channel_cache[channel_id] = {"id": channel_id, "name": channel_id, "kind": "channel"}
|
|
1055
1121
|
|
|
1122
|
+
if sync_socket:
|
|
1123
|
+
await self._sync_socket_channels(old_channel_ids)
|
|
1124
|
+
|
|
1056
1125
|
async def _start_socket(self) -> None:
|
|
1057
1126
|
self.socket = ShadowSocketClient(self.base_url, self.token, transports=self._transports, logger=logger)
|
|
1058
1127
|
self.socket.on("connect", self._on_socket_connect)
|
|
@@ -1070,13 +1139,27 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1070
1139
|
self.socket.on("server:joined", self._on_server_joined)
|
|
1071
1140
|
self.socket.on("agent:policy-changed", self._on_agent_policy_changed)
|
|
1072
1141
|
await self.socket.connect()
|
|
1073
|
-
await self.
|
|
1074
|
-
for channel_id in self._channel_ids:
|
|
1075
|
-
ack = await self.socket.join_channel(channel_id)
|
|
1076
|
-
logger.info("[Shadow] Joined channel %s ack=%s", channel_id, ack)
|
|
1142
|
+
await self._join_current_socket_channels()
|
|
1077
1143
|
if self._catchup_minutes > 0:
|
|
1078
1144
|
await self._catchup_recent_messages()
|
|
1079
1145
|
|
|
1146
|
+
async def _join_current_socket_channels(self) -> None:
|
|
1147
|
+
if self.socket is None:
|
|
1148
|
+
return
|
|
1149
|
+
try:
|
|
1150
|
+
await self.socket.update_presence("online")
|
|
1151
|
+
except Exception as exc:
|
|
1152
|
+
logger.debug("[Shadow] Failed to update socket presence: %s", exc)
|
|
1153
|
+
if not self._channel_ids:
|
|
1154
|
+
logger.info("[Shadow] Socket connected with no channels yet; waiting for channel membership events")
|
|
1155
|
+
return
|
|
1156
|
+
for channel_id in list(self._channel_ids):
|
|
1157
|
+
try:
|
|
1158
|
+
ack = await self.socket.join_channel(channel_id)
|
|
1159
|
+
logger.info("[Shadow] Joined channel %s ack=%s", channel_id, ack)
|
|
1160
|
+
except Exception as exc:
|
|
1161
|
+
logger.warning("[Shadow] Failed to join channel %s: %s", channel_id, exc)
|
|
1162
|
+
|
|
1080
1163
|
async def _start_polling(self) -> None:
|
|
1081
1164
|
if self._catchup_minutes > 0:
|
|
1082
1165
|
await self._catchup_recent_messages()
|
|
@@ -1142,6 +1225,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1142
1225
|
|
|
1143
1226
|
async def _on_socket_connect(self) -> None:
|
|
1144
1227
|
logger.info("[Shadow] Socket connected")
|
|
1228
|
+
await self._join_current_socket_channels()
|
|
1145
1229
|
|
|
1146
1230
|
async def _on_socket_disconnect(self, reason: str | None = None) -> None:
|
|
1147
1231
|
logger.info("[Shadow] Socket disconnected: %s", reason)
|
|
@@ -1163,6 +1247,11 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1163
1247
|
channel_id = str(payload.get("channelId") or payload.get("channel_id") or "").strip()
|
|
1164
1248
|
if not channel_id:
|
|
1165
1249
|
return
|
|
1250
|
+
old_channel_ids = set(self._channel_ids)
|
|
1251
|
+
try:
|
|
1252
|
+
await self._resolve_channels(sync_socket=False)
|
|
1253
|
+
except Exception as exc:
|
|
1254
|
+
logger.warning("[Shadow] Failed to refresh config after channel member add: %s", exc)
|
|
1166
1255
|
if channel_id not in self._channel_ids:
|
|
1167
1256
|
self._channel_ids.append(channel_id)
|
|
1168
1257
|
if self.client is not None:
|
|
@@ -1170,10 +1259,13 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1170
1259
|
self._channel_cache[channel_id] = await self.client.get_channel(channel_id)
|
|
1171
1260
|
except Exception:
|
|
1172
1261
|
self._channel_cache[channel_id] = {"id": channel_id, "name": channel_id, "kind": "channel"}
|
|
1262
|
+
self._channel_policies.setdefault(
|
|
1263
|
+
channel_id,
|
|
1264
|
+
_default_policy_from_remote_config(self._remote_config),
|
|
1265
|
+
)
|
|
1173
1266
|
if self.socket is not None:
|
|
1174
1267
|
try:
|
|
1175
|
-
|
|
1176
|
-
logger.info("[Shadow] Joined newly added channel %s ack=%s", channel_id, ack)
|
|
1268
|
+
await self._sync_socket_channels(old_channel_ids)
|
|
1177
1269
|
except Exception as exc:
|
|
1178
1270
|
logger.warning("[Shadow] Failed to join newly added channel %s: %s", channel_id, exc)
|
|
1179
1271
|
if self._catchup_minutes > 0:
|
|
@@ -1200,7 +1292,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1200
1292
|
if payload_agent_id and self._agent_id and payload_agent_id != self._agent_id:
|
|
1201
1293
|
return
|
|
1202
1294
|
try:
|
|
1203
|
-
await self.
|
|
1295
|
+
await self._resolve_channels(sync_socket=True)
|
|
1204
1296
|
except Exception as exc:
|
|
1205
1297
|
logger.warning("[Shadow] Failed to refresh remote config after server join: %s", exc)
|
|
1206
1298
|
|
|
@@ -1209,7 +1301,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
|
|
|
1209
1301
|
if payload_agent_id and self._agent_id and payload_agent_id != self._agent_id:
|
|
1210
1302
|
return
|
|
1211
1303
|
try:
|
|
1212
|
-
await self.
|
|
1304
|
+
await self._resolve_channels(sync_socket=True)
|
|
1213
1305
|
except Exception as exc:
|
|
1214
1306
|
logger.warning("[Shadow] Failed to refresh remote config after policy change: %s", exc)
|
|
1215
1307
|
|
|
@@ -231,9 +231,31 @@ class ShadowAsyncClient:
|
|
|
231
231
|
async def list_direct_channels(self) -> list[JsonDict]:
|
|
232
232
|
return await self.request("GET", "/api/channels/dm")
|
|
233
233
|
|
|
234
|
+
async def create_direct_channel(self, user_id: str) -> JsonDict:
|
|
235
|
+
return await self.request(
|
|
236
|
+
"POST",
|
|
237
|
+
"/api/channels/dm",
|
|
238
|
+
json_body={"userId": str(user_id)},
|
|
239
|
+
)
|
|
240
|
+
|
|
234
241
|
async def get_channel(self, channel_id: str) -> JsonDict:
|
|
235
242
|
return await self.request("GET", f"/api/channels/{quote(str(channel_id), safe='')}")
|
|
236
243
|
|
|
244
|
+
async def get_channel_bootstrap(
|
|
245
|
+
self,
|
|
246
|
+
channel_id: str,
|
|
247
|
+
*,
|
|
248
|
+
messages_limit: int | None = None,
|
|
249
|
+
) -> JsonDict:
|
|
250
|
+
params: JsonDict = {}
|
|
251
|
+
if messages_limit is not None:
|
|
252
|
+
params["messagesLimit"] = int(messages_limit)
|
|
253
|
+
return await self.request(
|
|
254
|
+
"GET",
|
|
255
|
+
f"/api/channels/{quote(str(channel_id), safe='')}/bootstrap",
|
|
256
|
+
params=params or None,
|
|
257
|
+
)
|
|
258
|
+
|
|
237
259
|
async def get_messages(
|
|
238
260
|
self,
|
|
239
261
|
channel_id: str,
|
|
@@ -256,11 +278,20 @@ class ShadowAsyncClient:
|
|
|
256
278
|
async def get_message(self, message_id: str) -> JsonDict:
|
|
257
279
|
return await self.request("GET", f"/api/messages/{quote(str(message_id), safe='')}")
|
|
258
280
|
|
|
259
|
-
async def resolve_attachment_media_url(
|
|
281
|
+
async def resolve_attachment_media_url(
|
|
282
|
+
self,
|
|
283
|
+
attachment_id: str,
|
|
284
|
+
*,
|
|
285
|
+
disposition: str = "inline",
|
|
286
|
+
variant: str | None = None,
|
|
287
|
+
) -> JsonDict:
|
|
288
|
+
params: JsonDict = {"disposition": disposition}
|
|
289
|
+
if variant:
|
|
290
|
+
params["variant"] = variant
|
|
260
291
|
return await self.request(
|
|
261
292
|
"GET",
|
|
262
293
|
f"/api/attachments/{quote(str(attachment_id), safe='')}/media-url",
|
|
263
|
-
params=
|
|
294
|
+
params=params,
|
|
264
295
|
)
|
|
265
296
|
|
|
266
297
|
async def send_message(
|