@ascegu/teamily 1.0.15 → 1.0.16

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 (3) hide show
  1. package/README.md +92 -46
  2. package/package.json +1 -1
  3. package/src/channel.ts +49 -6
package/README.md CHANGED
@@ -1,41 +1,54 @@
1
1
  # Teamily Channel Plugin for OpenClaw
2
2
 
3
- This plugin integrates Teamily Server (based on OpenIM) with OpenClaw, allowing you to use OpenClaw as a bot in your self-hosted team messaging platform.
3
+ Integrates [Teamily](https://teamily.ai/) (based on OpenIM) with OpenClaw as a self-hosted team messaging channel.
4
4
 
5
- ## Teamily Server
5
+ ## Installation
6
6
 
7
- Teamily is a team instant messaging server based on OpenIM open-source platform. For more information about OpenIM, visit: https://github.com/openimsdk/open-im-server
7
+ ```bash
8
+ openclaw plugins install @ascegu/teamily
9
+ ```
8
10
 
9
- ## Installation
11
+ Or update an existing install:
10
12
 
11
13
  ```bash
12
- openclaw plugins install @openclaw/teamily
14
+ openclaw plugins update teamily
13
15
  ```
14
16
 
15
- ## Configuration
17
+ > **Note:** The plugin update command uses the plugin ID `teamily`, not the npm package name.
16
18
 
17
- ### Server Configuration
19
+ ## Configuration
18
20
 
19
- Configure your Teamily server connection:
21
+ ### Interactive Setup
20
22
 
21
23
  ```bash
22
24
  openclaw channel configure teamily
23
25
  ```
24
26
 
25
- Required server settings:
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
- - `platformUrl`: Teamily platform URL (default: `http://localhost:10002`)
28
- - `apiURL`: Teamily REST API URL (default: `http://localhost:10002`)
29
- - `wsURL`: Teamily WebSocket URL (default: `ws://localhost:10001`)
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
- ### Bot Account Configuration
44
+ ### DM Security
32
45
 
33
- Configure a bot user account:
46
+ Per-account or channel-level DM security can be configured:
34
47
 
35
- - `userID`: User ID for the bot account
36
- - `token`: User token for authentication
37
- - `nickname`: (optional) Display nickname
38
- - `faceURL`: (optional) Avatar URL
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://localhost:10002
48
- apiURL: http://localhost:10002
49
- wsURL: ws://localhost:10001
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: "your-bot-user-id"
53
- token: "your-bot-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 a Message to a User
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
- ### Send a Message to a Group
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
- ## Setting Up Teamily
92
+ Supported media types are auto-detected by file extension:
78
93
 
79
- ### 1. Start Teamily Server
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
- Follow the [OpenIM Quick Start](https://docs.openim.io/guides/gettingStarted/introduction) guide to deploy the server (Teamily is based on OpenIM).
101
+ ## Group Chat Behavior
82
102
 
83
- ### 2. Create a Bot User
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
- Use the Teamily management API to create a bot user account and obtain its user ID and token.
108
+ ## Capabilities
86
109
 
87
- ### 3. Configure OpenClaw
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
- Configure the Teamily channel with your server details and bot credentials.
125
+ ## Setting Up Teamily Server
90
126
 
91
- ## Features
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
- - ✅ Direct messaging
94
- - ✅ Group messaging
95
- - ✅ Text messages
96
- - Media messages (images, videos, audio, files)
97
- - ✅ WebSocket real-time message monitoring
98
- - Automatic reconnection
99
- - Connection health checks
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
- ## Supported API Versions
147
+ ## Compatibility
102
148
 
103
- This plugin is designed to work with OpenIM API v2/v3 (which Teamily is based on). Please refer to the official OpenIM documentation for API details: https://docs.openim.io
149
+ Designed for OpenIM API v2/v3. Requires `@openim/client-sdk` ^3.8.3.
104
150
 
105
151
  ## License
106
152
 
107
- This plugin is part of OpenClaw and follows the same license terms.
153
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascegu/teamily",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
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,6 +243,8 @@ 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) => {
243
250
  const rt = getTeamilyRuntime();
@@ -245,17 +252,33 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
245
252
 
246
253
  const isGroup = isGroupSession(message.sessionType);
247
254
  const from = message.sendID;
255
+ const rawText = message.content?.text || "";
248
256
 
249
257
  log?.info?.(
250
258
  `[${accountId}] Incoming message: sessionType=${message.sessionType}, isGroup=${isGroup}, ` +
251
259
  `from=${from}, recvID=${message.recvID}, isAtSelf=${message.isAtSelf ?? false}`,
252
260
  );
253
261
 
254
- // In group chats, all messages are dispatched to the agent for context,
255
- // but only @-mentioned messages trigger a reply.
256
- const shouldReply = !isGroup || !!message.isAtSelf;
262
+ const historyKey = isGroup ? `teamily:group:${message.recvID}` : undefined;
263
+
264
+ // In group chats, buffer non-@-mention messages for context and skip dispatch.
265
+ // Only @-mention messages are dispatched to the agent, with the buffer injected.
266
+ if (isGroup && !message.isAtSelf) {
267
+ if (historyKey && rawText) {
268
+ recordPendingHistoryEntryIfEnabled({
269
+ historyMap: groupHistories,
270
+ historyKey,
271
+ limit: historyLimit,
272
+ entry: {
273
+ sender: from,
274
+ body: rawText,
275
+ timestamp: message.sendTime,
276
+ },
277
+ });
278
+ }
279
+ return;
280
+ }
257
281
 
258
- const text = message.content?.text || "";
259
282
  const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
260
283
 
261
284
  let mediaUrl: string | undefined;
@@ -287,9 +310,21 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
287
310
  }
288
311
  }
289
312
 
313
+ // For group @-mention messages, prepend buffered history as context.
314
+ const body =
315
+ isGroup && historyKey
316
+ ? buildPendingHistoryContextFromMap({
317
+ historyMap: groupHistories,
318
+ historyKey,
319
+ limit: historyLimit,
320
+ currentMessage: rawText,
321
+ formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
322
+ })
323
+ : rawText;
324
+
290
325
  const replyTarget = isGroup ? `group:${message.recvID}` : from;
291
326
  const msgCtx = {
292
- Body: text,
327
+ Body: body,
293
328
  From: from,
294
329
  To: account.userID,
295
330
  SessionKey: sessionKey,
@@ -309,7 +344,6 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
309
344
  cfg: currentCfg,
310
345
  dispatcherOptions: {
311
346
  deliver: async (payload: { text?: string; body?: string }) => {
312
- if (!shouldReply) return;
313
347
  const replyText = payload?.text ?? payload?.body;
314
348
  if (replyText) {
315
349
  const monitor = getTeamilyMonitor(accountId);
@@ -324,6 +358,15 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
324
358
  },
325
359
  },
326
360
  });
361
+
362
+ // Clear history buffer after dispatch so context doesn't repeat.
363
+ if (isGroup && historyKey) {
364
+ clearHistoryEntriesIfEnabled({
365
+ historyMap: groupHistories,
366
+ historyKey,
367
+ limit: historyLimit,
368
+ });
369
+ }
327
370
  });
328
371
 
329
372
  // Respect abort signal from the gateway framework