@ascegu/teamily 1.0.15 → 1.0.17
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 +92 -46
- package/package.json +1 -1
- package/src/channel.ts +61 -6
package/README.md
CHANGED
|
@@ -1,41 +1,54 @@
|
|
|
1
1
|
# Teamily Channel Plugin for OpenClaw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Integrates [Teamily](https://teamily.ai/) (based on OpenIM) with OpenClaw as a self-hosted team messaging channel.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @ascegu/teamily
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Or update an existing install:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
openclaw plugins
|
|
14
|
+
openclaw plugins update teamily
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
> **Note:** The plugin update command uses the plugin ID `teamily`, not the npm package name.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Configuration
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
### Interactive Setup
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
24
|
openclaw channel configure teamily
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
### Server Settings
|
|
28
|
+
|
|
29
|
+
| Field | Description | Default |
|
|
30
|
+
|---------------|--------------------------|---------------------------|
|
|
31
|
+
| `platformUrl` | Teamily platform URL | `http://localhost:10002` |
|
|
32
|
+
| `apiURL` | Teamily REST API URL | `http://localhost:10002` |
|
|
33
|
+
| `wsURL` | Teamily WebSocket URL | `ws://localhost:10001` |
|
|
34
|
+
|
|
35
|
+
### Bot Account Settings
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
| Field | Required | Description |
|
|
38
|
+
|------------|----------|---------------------------------|
|
|
39
|
+
| `userID` | Yes | User ID for the bot account |
|
|
40
|
+
| `token` | Yes | User token for authentication |
|
|
41
|
+
| `nickname` | No | Display nickname |
|
|
42
|
+
| `faceURL` | No | Avatar URL |
|
|
30
43
|
|
|
31
|
-
###
|
|
44
|
+
### DM Security
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
Per-account or channel-level DM security can be configured:
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
| Field | Description |
|
|
49
|
+
|--------------------|------------------------------------------------------|
|
|
50
|
+
| `dm.policy` | DM security policy (`pairing`, `allowlist`, `open`) |
|
|
51
|
+
| `dm.allowFrom` | List of allowed sender IDs |
|
|
39
52
|
|
|
40
53
|
### Example Configuration
|
|
41
54
|
|
|
@@ -44,27 +57,29 @@ channels:
|
|
|
44
57
|
teamily:
|
|
45
58
|
enabled: true
|
|
46
59
|
server:
|
|
47
|
-
platformUrl: http://
|
|
48
|
-
apiURL: http://
|
|
49
|
-
wsURL: ws://
|
|
60
|
+
platformUrl: http://your-server:10002
|
|
61
|
+
apiURL: http://your-server:10002
|
|
62
|
+
wsURL: ws://your-server:10001
|
|
50
63
|
accounts:
|
|
51
64
|
default:
|
|
52
|
-
userID: "
|
|
53
|
-
token: "
|
|
65
|
+
userID: "bot-user-id"
|
|
66
|
+
token: "bot-token"
|
|
54
67
|
nickname: "OpenClaw Bot"
|
|
68
|
+
dm:
|
|
69
|
+
policy: open
|
|
55
70
|
```
|
|
56
71
|
|
|
72
|
+
Multiple accounts are supported under the `accounts` key.
|
|
73
|
+
|
|
57
74
|
## Usage
|
|
58
75
|
|
|
59
|
-
### Send
|
|
76
|
+
### Send Messages
|
|
60
77
|
|
|
61
78
|
```bash
|
|
79
|
+
# Send to a user
|
|
62
80
|
openclaw message send teamily:user:userID "Hello!"
|
|
63
|
-
```
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
82
|
+
# Send to a group
|
|
68
83
|
openclaw message send teamily:group:groupID "Hello group!"
|
|
69
84
|
```
|
|
70
85
|
|
|
@@ -74,34 +89,65 @@ openclaw message send teamily:group:groupID "Hello group!"
|
|
|
74
89
|
openclaw message send teamily:user:userID --media /path/to/image.jpg
|
|
75
90
|
```
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
Supported media types are auto-detected by file extension:
|
|
78
93
|
|
|
79
|
-
|
|
94
|
+
| Extension | Type |
|
|
95
|
+
|------------------------------|-------|
|
|
96
|
+
| `.jpg`, `.png`, `.gif`, etc. | Image |
|
|
97
|
+
| `.mp4`, `.mov`, `.webm` | Video |
|
|
98
|
+
| `.mp3`, `.m4a`, `.wav` | Audio |
|
|
99
|
+
| `.pdf`, `.doc`, `.docx`, `.zip` | File |
|
|
80
100
|
|
|
81
|
-
|
|
101
|
+
## Group Chat Behavior
|
|
82
102
|
|
|
83
|
-
|
|
103
|
+
- The bot only **replies** to messages where it is **@-mentioned** (`@BotName`).
|
|
104
|
+
- Non-@-mention group messages are **buffered in memory** (up to 50 per group) and injected as conversation context when an @-mention triggers a reply. The buffer is cleared after each dispatch so context doesn't repeat.
|
|
105
|
+
- In direct messages, the bot always replies.
|
|
106
|
+
- Both regular groups (`sessionType=3`) and super groups (`sessionType=2`) are supported.
|
|
84
107
|
|
|
85
|
-
|
|
108
|
+
## Capabilities
|
|
86
109
|
|
|
87
|
-
|
|
110
|
+
| Feature | Supported |
|
|
111
|
+
|---------------------------|-----------|
|
|
112
|
+
| Direct messaging | Yes |
|
|
113
|
+
| Group messaging | Yes |
|
|
114
|
+
| Text messages | Yes |
|
|
115
|
+
| Media (image/video/audio/file) | Yes |
|
|
116
|
+
| @-mention gating (groups) | Yes |
|
|
117
|
+
| Group history context (50 msg buffer) | Yes |
|
|
118
|
+
| WebSocket real-time monitoring | Yes |
|
|
119
|
+
| Automatic reconnection | Yes |
|
|
120
|
+
| Connection health probes | Yes |
|
|
121
|
+
| Reactions | No |
|
|
122
|
+
| Threads | No |
|
|
123
|
+
| Polls | No |
|
|
88
124
|
|
|
89
|
-
|
|
125
|
+
## Setting Up Teamily Server
|
|
90
126
|
|
|
91
|
-
|
|
127
|
+
1. **Deploy OpenIM server** -- follow the [OpenIM Quick Start](https://docs.openim.io/guides/gettingStarted/introduction) guide.
|
|
128
|
+
2. **Create a bot user** -- use the Teamily management API to create a bot account and obtain its `userID` and `token`.
|
|
129
|
+
3. **Configure OpenClaw** -- run `openclaw channel configure teamily` and provide the server URLs and bot credentials.
|
|
92
130
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
131
|
+
## Architecture
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
index.ts Plugin entry point (registers channel with OpenClaw)
|
|
135
|
+
src/
|
|
136
|
+
channel.ts Channel plugin definition (gateway, outbound, config, security)
|
|
137
|
+
monitor.ts WebSocket monitor using @openim/client-sdk (inbound + send)
|
|
138
|
+
types.ts Shared types and constants (session types, content types, message shapes)
|
|
139
|
+
config-schema.ts Zod config schema (server, accounts, DM security)
|
|
140
|
+
accounts.ts Account resolution and listing
|
|
141
|
+
normalize.ts Target ID normalization (user:ID, group:ID)
|
|
142
|
+
probe.ts Health check via REST API
|
|
143
|
+
runtime.ts Plugin runtime store
|
|
144
|
+
send.ts REST API message/media send (fallback path)
|
|
145
|
+
```
|
|
100
146
|
|
|
101
|
-
##
|
|
147
|
+
## Compatibility
|
|
102
148
|
|
|
103
|
-
|
|
149
|
+
Designed for OpenIM API v2/v3. Requires `@openim/client-sdk` ^3.8.3.
|
|
104
150
|
|
|
105
151
|
## License
|
|
106
152
|
|
|
107
|
-
|
|
153
|
+
MIT
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyAccountNameToChannelSection,
|
|
3
3
|
buildChannelConfigSchema,
|
|
4
|
+
buildPendingHistoryContextFromMap,
|
|
5
|
+
clearHistoryEntriesIfEnabled,
|
|
4
6
|
DEFAULT_ACCOUNT_ID,
|
|
7
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
5
8
|
deleteAccountFromConfigSection,
|
|
6
9
|
normalizeAccountId,
|
|
7
10
|
PAIRING_APPROVED_MESSAGE,
|
|
11
|
+
recordPendingHistoryEntryIfEnabled,
|
|
8
12
|
setAccountEnabledInConfigSection,
|
|
9
13
|
type ChannelPlugin,
|
|
10
14
|
type ChannelOutboundContext,
|
|
11
15
|
type ChannelStatusIssue,
|
|
16
|
+
type HistoryEntry,
|
|
12
17
|
} from "openclaw/plugin-sdk";
|
|
13
18
|
import {
|
|
14
19
|
buildAccountScopedDmSecurityPolicy,
|
|
@@ -238,24 +243,43 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
238
243
|
log?.info?.(`Starting Teamily channel (account: ${accountId})`);
|
|
239
244
|
|
|
240
245
|
const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
246
|
+
const historyLimit = DEFAULT_GROUP_HISTORY_LIMIT; // 50 messages per group
|
|
247
|
+
const groupHistories = new Map<string, HistoryEntry[]>();
|
|
241
248
|
|
|
242
249
|
const stopFn = startTeamilyMonitoring(account, async (message) => {
|
|
250
|
+
try {
|
|
243
251
|
const rt = getTeamilyRuntime();
|
|
244
252
|
const currentCfg = rt.config.loadConfig();
|
|
245
253
|
|
|
246
254
|
const isGroup = isGroupSession(message.sessionType);
|
|
247
255
|
const from = message.sendID;
|
|
256
|
+
const rawText = message.content?.text || "";
|
|
248
257
|
|
|
249
258
|
log?.info?.(
|
|
250
259
|
`[${accountId}] Incoming message: sessionType=${message.sessionType}, isGroup=${isGroup}, ` +
|
|
251
260
|
`from=${from}, recvID=${message.recvID}, isAtSelf=${message.isAtSelf ?? false}`,
|
|
252
261
|
);
|
|
253
262
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
const historyKey = isGroup ? `teamily:group:${message.recvID}` : undefined;
|
|
264
|
+
|
|
265
|
+
// In group chats, buffer non-@-mention messages for context and skip dispatch.
|
|
266
|
+
// Only @-mention messages are dispatched to the agent, with the buffer injected.
|
|
267
|
+
if (isGroup && !message.isAtSelf) {
|
|
268
|
+
if (historyKey && rawText) {
|
|
269
|
+
recordPendingHistoryEntryIfEnabled({
|
|
270
|
+
historyMap: groupHistories,
|
|
271
|
+
historyKey,
|
|
272
|
+
limit: historyLimit,
|
|
273
|
+
entry: {
|
|
274
|
+
sender: from,
|
|
275
|
+
body: rawText,
|
|
276
|
+
timestamp: message.sendTime,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
257
282
|
|
|
258
|
-
const text = message.content?.text || "";
|
|
259
283
|
const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
|
|
260
284
|
|
|
261
285
|
let mediaUrl: string | undefined;
|
|
@@ -287,9 +311,21 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
287
311
|
}
|
|
288
312
|
}
|
|
289
313
|
|
|
314
|
+
// For group @-mention messages, prepend buffered history as context.
|
|
315
|
+
const body =
|
|
316
|
+
isGroup && historyKey
|
|
317
|
+
? buildPendingHistoryContextFromMap({
|
|
318
|
+
historyMap: groupHistories,
|
|
319
|
+
historyKey,
|
|
320
|
+
limit: historyLimit,
|
|
321
|
+
currentMessage: rawText,
|
|
322
|
+
formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
|
|
323
|
+
})
|
|
324
|
+
: rawText;
|
|
325
|
+
|
|
290
326
|
const replyTarget = isGroup ? `group:${message.recvID}` : from;
|
|
291
327
|
const msgCtx = {
|
|
292
|
-
Body:
|
|
328
|
+
Body: body,
|
|
293
329
|
From: from,
|
|
294
330
|
To: account.userID,
|
|
295
331
|
SessionKey: sessionKey,
|
|
@@ -304,12 +340,15 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
304
340
|
MediaType: mediaType,
|
|
305
341
|
};
|
|
306
342
|
|
|
343
|
+
log?.info?.(
|
|
344
|
+
`[${accountId}] Dispatching reply: sessionKey=${sessionKey}, chatType=${isGroup ? "group" : "direct"}, replyTarget=${replyTarget}`,
|
|
345
|
+
);
|
|
346
|
+
|
|
307
347
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
308
348
|
ctx: msgCtx,
|
|
309
349
|
cfg: currentCfg,
|
|
310
350
|
dispatcherOptions: {
|
|
311
351
|
deliver: async (payload: { text?: string; body?: string }) => {
|
|
312
|
-
if (!shouldReply) return;
|
|
313
352
|
const replyText = payload?.text ?? payload?.body;
|
|
314
353
|
if (replyText) {
|
|
315
354
|
const monitor = getTeamilyMonitor(accountId);
|
|
@@ -324,6 +363,22 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
324
363
|
},
|
|
325
364
|
},
|
|
326
365
|
});
|
|
366
|
+
|
|
367
|
+
log?.info?.(`[${accountId}] Dispatch completed for ${sessionKey}`);
|
|
368
|
+
|
|
369
|
+
// Clear history buffer after dispatch so context doesn't repeat.
|
|
370
|
+
if (isGroup && historyKey) {
|
|
371
|
+
clearHistoryEntriesIfEnabled({
|
|
372
|
+
historyMap: groupHistories,
|
|
373
|
+
historyKey,
|
|
374
|
+
limit: historyLimit,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
log?.error?.(
|
|
379
|
+
`[${accountId}] Error handling message from ${message.sendID}: ${err instanceof Error ? err.stack || err.message : String(err)}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
327
382
|
});
|
|
328
383
|
|
|
329
384
|
// Respect abort signal from the gateway framework
|