@ascegu/teamily 1.0.6 → 1.0.8
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 +1 -0
- package/package.json +16 -16
- package/src/accounts.ts +3 -4
- package/src/channel.ts +125 -165
- package/src/config-schema.ts +21 -38
- package/src/monitor.ts +25 -26
- package/src/normalize.ts +10 -12
- package/src/probe.ts +4 -4
- package/src/runtime.ts +4 -12
- package/src/send.ts +9 -11
- package/src/types.ts +8 -0
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ openclaw channel configure teamily
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Required server settings:
|
|
26
|
+
|
|
26
27
|
- `platformUrl`: Teamily platform URL (default: `http://localhost:10002`)
|
|
27
28
|
- `apiURL`: Teamily REST API URL (default: `http://localhost:10002`)
|
|
28
29
|
- `wsURL`: Teamily WebSocket URL (default: `ws://localhost:10001`)
|
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascegu/teamily",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"*.ts",
|
|
9
|
-
"*.js",
|
|
10
|
-
"*.json",
|
|
11
|
-
"src/",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
5
|
"keywords": [
|
|
15
|
-
"openclaw",
|
|
16
|
-
"plugin",
|
|
17
6
|
"channel",
|
|
18
|
-
"
|
|
19
|
-
"openim",
|
|
7
|
+
"communication",
|
|
20
8
|
"messaging",
|
|
9
|
+
"openclaw",
|
|
10
|
+
"openim",
|
|
11
|
+
"plugin",
|
|
21
12
|
"team",
|
|
22
|
-
"
|
|
13
|
+
"teamily"
|
|
23
14
|
],
|
|
24
|
-
"author": "ascegu",
|
|
25
15
|
"license": "MIT",
|
|
16
|
+
"author": "ascegu",
|
|
26
17
|
"repository": {
|
|
27
18
|
"type": "git",
|
|
28
19
|
"url": "git+https://github.com/ascegu/openclaw.git",
|
|
29
20
|
"directory": "extensions/teamily"
|
|
30
21
|
},
|
|
22
|
+
"files": [
|
|
23
|
+
"*.ts",
|
|
24
|
+
"*.js",
|
|
25
|
+
"*.json",
|
|
26
|
+
"src/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "index.ts",
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"zod": "^4.3.6"
|
|
33
33
|
},
|
package/src/accounts.ts
CHANGED
|
@@ -6,9 +6,7 @@ export function listTeamilyAccountIds(cfg: CoreConfig): string[] {
|
|
|
6
6
|
if (!config?.enabled || !config.accounts) {
|
|
7
7
|
return [];
|
|
8
8
|
}
|
|
9
|
-
return Object.keys(config.accounts).filter(
|
|
10
|
-
(key) => config.accounts![key].token !== undefined
|
|
11
|
-
);
|
|
9
|
+
return Object.keys(config.accounts).filter((key) => config.accounts![key].token !== undefined);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export function resolveDefaultTeamilyAccountId(cfg: CoreConfig): string {
|
|
@@ -18,7 +16,7 @@ export function resolveDefaultTeamilyAccountId(cfg: CoreConfig): string {
|
|
|
18
16
|
|
|
19
17
|
export function resolveTeamilyAccount(
|
|
20
18
|
cfg: CoreConfig,
|
|
21
|
-
accountId?: string | null
|
|
19
|
+
accountId?: string | null,
|
|
22
20
|
): ResolvedTeamilyAccount {
|
|
23
21
|
const config = cfg.channels?.teamily;
|
|
24
22
|
if (!config?.enabled) {
|
|
@@ -47,5 +45,6 @@ export function resolveTeamilyAccount(
|
|
|
47
45
|
token: account.token,
|
|
48
46
|
nickname: account.nickname,
|
|
49
47
|
faceURL: account.faceURL,
|
|
48
|
+
dm: account.dm ?? config.dm,
|
|
50
49
|
};
|
|
51
50
|
}
|
package/src/channel.ts
CHANGED
|
@@ -3,30 +3,31 @@ import {
|
|
|
3
3
|
buildChannelConfigSchema,
|
|
4
4
|
DEFAULT_ACCOUNT_ID,
|
|
5
5
|
deleteAccountFromConfigSection,
|
|
6
|
-
formatPairingApproveHint,
|
|
7
6
|
normalizeAccountId,
|
|
8
7
|
PAIRING_APPROVED_MESSAGE,
|
|
9
8
|
setAccountEnabledInConfigSection,
|
|
10
9
|
type ChannelPlugin,
|
|
11
10
|
type ChannelOutboundContext,
|
|
12
|
-
type ChannelOutboundAdapter,
|
|
13
|
-
type ChannelStatusAdapter,
|
|
14
11
|
type ChannelStatusIssue,
|
|
15
12
|
} from "openclaw/plugin-sdk";
|
|
16
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildAccountScopedDmSecurityPolicy,
|
|
15
|
+
createScopedAccountConfigAccessors,
|
|
16
|
+
} from "openclaw/plugin-sdk/compat";
|
|
17
17
|
import {
|
|
18
18
|
listTeamilyAccountIds,
|
|
19
19
|
resolveDefaultTeamilyAccountId,
|
|
20
20
|
resolveTeamilyAccount,
|
|
21
|
-
type ResolvedTeamilyAccount,
|
|
22
21
|
} from "./accounts.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { TeamilyConfigSchema } from "./config-schema.js";
|
|
23
|
+
import type { CoreConfig } from "./config-schema.js";
|
|
25
24
|
import { startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
26
25
|
import { normalizeTeamilyTarget, normalizeTeamilyAllowEntry } from "./normalize.js";
|
|
27
|
-
import {
|
|
26
|
+
import { probeTeamily } from "./probe.js";
|
|
28
27
|
import { getTeamilyRuntime } from "./runtime.js";
|
|
29
|
-
import
|
|
28
|
+
import { sendMessageTeamily, sendMediaTeamily } from "./send.js";
|
|
29
|
+
import type { ResolvedTeamilyAccount } from "./types.js";
|
|
30
|
+
import { SESSION_TYPES } from "./types.js";
|
|
30
31
|
|
|
31
32
|
const meta = {
|
|
32
33
|
id: "teamily",
|
|
@@ -39,33 +40,42 @@ const meta = {
|
|
|
39
40
|
quickstartAllowFrom: true,
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
polls: false,
|
|
48
|
-
streaming: false,
|
|
49
|
-
};
|
|
43
|
+
const teamilyConfigAccessors = createScopedAccountConfigAccessors({
|
|
44
|
+
resolveAccount: ({ cfg, accountId }) => resolveTeamilyAccount(cfg as CoreConfig, accountId),
|
|
45
|
+
resolveAllowFrom: (account) => account.dm?.allowFrom,
|
|
46
|
+
formatAllowFrom: (allowFrom) => allowFrom.map((id) => String(id)),
|
|
47
|
+
});
|
|
50
48
|
|
|
51
49
|
export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
52
50
|
id: "teamily",
|
|
53
51
|
meta,
|
|
54
|
-
capabilities
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
capabilities: {
|
|
53
|
+
chatTypes: ["direct", "group"],
|
|
54
|
+
media: true,
|
|
55
|
+
reactions: false,
|
|
56
|
+
threads: false,
|
|
57
|
+
polls: false,
|
|
58
|
+
},
|
|
59
|
+
reload: { configPrefixes: ["channels.teamily"] },
|
|
60
|
+
setup: {
|
|
61
|
+
resolveAccountId: ({ accountId, input }) => {
|
|
62
|
+
if (accountId) return accountId;
|
|
63
|
+
if (input?.name) return normalizeAccountId(String(input.name));
|
|
64
|
+
return DEFAULT_ACCOUNT_ID;
|
|
65
|
+
},
|
|
58
66
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
59
67
|
applyAccountNameToChannelSection({
|
|
60
|
-
cfg
|
|
61
|
-
|
|
68
|
+
cfg,
|
|
69
|
+
channelKey: "teamily",
|
|
62
70
|
accountId,
|
|
63
71
|
name,
|
|
64
|
-
allowTopLevel: true,
|
|
65
72
|
}),
|
|
66
73
|
applyAccountConfig: ({ cfg, accountId, input }) =>
|
|
67
|
-
applyTeamilyAccountConfig({
|
|
68
|
-
|
|
74
|
+
applyTeamilyAccountConfig({
|
|
75
|
+
cfg: cfg as CoreConfig,
|
|
76
|
+
accountId,
|
|
77
|
+
input: input as Record<string, unknown>,
|
|
78
|
+
}),
|
|
69
79
|
},
|
|
70
80
|
pairing: {
|
|
71
81
|
idLabel: "teamilyUserId",
|
|
@@ -94,8 +104,7 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
94
104
|
configSchema: buildChannelConfigSchema(TeamilyConfigSchema),
|
|
95
105
|
config: {
|
|
96
106
|
listAccountIds: (cfg) => listTeamilyAccountIds(cfg as CoreConfig),
|
|
97
|
-
resolveAccount: (cfg, accountId) =>
|
|
98
|
-
resolveTeamilyAccount(cfg as CoreConfig, accountId),
|
|
107
|
+
resolveAccount: (cfg, accountId) => resolveTeamilyAccount(cfg as CoreConfig, accountId),
|
|
99
108
|
defaultAccountId: (cfg) => resolveDefaultTeamilyAccountId(cfg as CoreConfig),
|
|
100
109
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
101
110
|
setAccountEnabledInConfigSection({
|
|
@@ -110,13 +119,7 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
110
119
|
cfg: cfg as CoreConfig,
|
|
111
120
|
sectionKey: "teamily",
|
|
112
121
|
accountId,
|
|
113
|
-
clearBaseFields: [
|
|
114
|
-
"name",
|
|
115
|
-
"userID",
|
|
116
|
-
"token",
|
|
117
|
-
"nickname",
|
|
118
|
-
"faceURL",
|
|
119
|
-
],
|
|
122
|
+
clearBaseFields: ["name", "userID", "token", "nickname", "faceURL"],
|
|
120
123
|
}),
|
|
121
124
|
isConfigured: (account) => !!account.token,
|
|
122
125
|
describeAccount: (account) => ({
|
|
@@ -125,22 +128,38 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
125
128
|
enabled: account.enabled,
|
|
126
129
|
configured: !!account.token,
|
|
127
130
|
}),
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
...teamilyConfigAccessors,
|
|
132
|
+
},
|
|
133
|
+
security: {
|
|
134
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
135
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
136
|
+
cfg: cfg as CoreConfig,
|
|
137
|
+
channelKey: "teamily",
|
|
138
|
+
accountId,
|
|
139
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
140
|
+
policy: account.dm?.policy,
|
|
141
|
+
allowFrom: account.dm?.allowFrom ?? [],
|
|
142
|
+
allowFromPathSuffix: "dm.",
|
|
143
|
+
normalizeEntry: (raw) => normalizeTeamilyAllowEntry(raw),
|
|
144
|
+
});
|
|
138
145
|
},
|
|
139
146
|
},
|
|
140
147
|
outbound: {
|
|
148
|
+
deliveryMode: "gateway",
|
|
149
|
+
resolveTarget: ({ to }) => {
|
|
150
|
+
if (!to?.trim()) {
|
|
151
|
+
return { ok: false, error: new Error("Teamily requires --to <userId|group:groupId>") };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const target = normalizeTeamilyTarget(to);
|
|
155
|
+
return { ok: true, to: target.id };
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
|
|
158
|
+
}
|
|
159
|
+
},
|
|
141
160
|
sendText: async (ctx: ChannelOutboundContext) => {
|
|
142
161
|
const { to, text, accountId } = ctx;
|
|
143
|
-
const account = resolveTeamilyAccount(ctx.cfg, accountId);
|
|
162
|
+
const account = resolveTeamilyAccount(ctx.cfg as CoreConfig, accountId);
|
|
144
163
|
const target = normalizeTeamilyTarget(to);
|
|
145
164
|
|
|
146
165
|
const result = await sendMessageTeamily({
|
|
@@ -154,11 +173,15 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
154
173
|
throw new Error(result.error || "Failed to send message");
|
|
155
174
|
}
|
|
156
175
|
|
|
157
|
-
return { messageId: result.messageId };
|
|
176
|
+
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
158
177
|
},
|
|
159
178
|
sendMedia: async (ctx: ChannelOutboundContext) => {
|
|
160
|
-
const { to,
|
|
161
|
-
const
|
|
179
|
+
const { to, text, accountId } = ctx;
|
|
180
|
+
const mediaUrl = ctx.mediaUrl;
|
|
181
|
+
if (!mediaUrl) {
|
|
182
|
+
throw new Error("Media URL is required");
|
|
183
|
+
}
|
|
184
|
+
const account = resolveTeamilyAccount(ctx.cfg as CoreConfig, accountId);
|
|
162
185
|
const target = normalizeTeamilyTarget(to);
|
|
163
186
|
|
|
164
187
|
// Determine media type from URL or assume image
|
|
@@ -166,7 +189,11 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
166
189
|
const urlLower = mediaUrl.toLowerCase();
|
|
167
190
|
if (urlLower.endsWith(".mp4") || urlLower.endsWith(".mov") || urlLower.endsWith(".webm")) {
|
|
168
191
|
mediaType = "video";
|
|
169
|
-
} else if (
|
|
192
|
+
} else if (
|
|
193
|
+
urlLower.endsWith(".mp3") ||
|
|
194
|
+
urlLower.endsWith(".m4a") ||
|
|
195
|
+
urlLower.endsWith(".wav")
|
|
196
|
+
) {
|
|
170
197
|
mediaType = "audio";
|
|
171
198
|
} else if (
|
|
172
199
|
urlLower.endsWith(".pdf") ||
|
|
@@ -189,80 +216,54 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
189
216
|
throw new Error(result.error || "Failed to send media");
|
|
190
217
|
}
|
|
191
218
|
|
|
192
|
-
return { messageId: result.messageId };
|
|
193
|
-
},
|
|
194
|
-
resolveTarget: (raw) => {
|
|
195
|
-
return normalizeTeamilyTarget(raw).id;
|
|
219
|
+
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
196
220
|
},
|
|
197
221
|
},
|
|
198
222
|
status: {
|
|
199
|
-
probeAccount: async (
|
|
200
|
-
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
223
|
+
probeAccount: async ({ account }) => {
|
|
201
224
|
const result = await probeTeamily(account);
|
|
202
|
-
|
|
203
225
|
if (!result.connected) {
|
|
204
226
|
return {
|
|
205
|
-
|
|
227
|
+
ok: false,
|
|
206
228
|
error: result.error || "Failed to connect to Teamily server",
|
|
207
229
|
};
|
|
208
230
|
}
|
|
209
|
-
|
|
210
|
-
return { connected: true };
|
|
211
|
-
},
|
|
212
|
-
buildAccountSnapshot: (cfg, accountId) => {
|
|
213
|
-
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
214
|
-
return {
|
|
215
|
-
accountId,
|
|
216
|
-
name: account.nickname || account.userID,
|
|
217
|
-
enabled: account.enabled,
|
|
218
|
-
configured: !!account.token,
|
|
219
|
-
};
|
|
231
|
+
return { ok: true };
|
|
220
232
|
},
|
|
221
|
-
|
|
233
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
234
|
+
accountId: account.accountId,
|
|
235
|
+
name: account.nickname || account.userID,
|
|
236
|
+
enabled: account.enabled,
|
|
237
|
+
configured: !!account.token,
|
|
238
|
+
running: runtime?.running ?? false,
|
|
239
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
240
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
241
|
+
lastError: runtime?.lastError ?? null,
|
|
242
|
+
probe,
|
|
243
|
+
}),
|
|
244
|
+
collectStatusIssues: (accounts) => {
|
|
222
245
|
const issues: ChannelStatusIssue[] = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (!account.apiURL || account.apiURL === "http://localhost:10002") {
|
|
236
|
-
issues.push({
|
|
237
|
-
channel: "teamily",
|
|
238
|
-
accountId,
|
|
239
|
-
kind: "config",
|
|
240
|
-
message: "Teamily API URL is set to default localhost",
|
|
241
|
-
fix: "Update the API URL to your Teamily server address",
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const probeResult = await probeTeamily(account);
|
|
246
|
-
if (!probeResult.connected) {
|
|
247
|
-
issues.push({
|
|
248
|
-
channel: "teamily",
|
|
249
|
-
accountId,
|
|
250
|
-
kind: "runtime",
|
|
251
|
-
message: probeResult.error || "Cannot connect to Teamily server",
|
|
252
|
-
fix: "Check that the Teamily server is running and accessible",
|
|
253
|
-
});
|
|
246
|
+
for (const snap of accounts) {
|
|
247
|
+
if (snap.lastError) {
|
|
248
|
+
issues.push({
|
|
249
|
+
channel: "teamily",
|
|
250
|
+
accountId: snap.accountId,
|
|
251
|
+
kind: "runtime",
|
|
252
|
+
message: snap.lastError,
|
|
253
|
+
fix: "Check that the Teamily server is running and accessible",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
254
256
|
}
|
|
255
|
-
|
|
256
257
|
return issues;
|
|
257
258
|
},
|
|
258
259
|
},
|
|
259
260
|
gateway: {
|
|
260
261
|
startAccount: async (ctx) => {
|
|
261
|
-
const {
|
|
262
|
+
const { accountId, account, log } = ctx;
|
|
262
263
|
|
|
263
264
|
if (!account.token) {
|
|
264
265
|
log?.warn?.(`Teamily account ${accountId} not configured (missing token)`);
|
|
265
|
-
return
|
|
266
|
+
return;
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
log?.info?.(`Starting Teamily channel (account: ${accountId})`);
|
|
@@ -291,7 +292,7 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
291
292
|
To: account.userID,
|
|
292
293
|
SessionKey: sessionKey,
|
|
293
294
|
AccountId: accountId,
|
|
294
|
-
OriginatingChannel: "teamily" as
|
|
295
|
+
OriginatingChannel: "teamily" as const,
|
|
295
296
|
OriginatingTo: from,
|
|
296
297
|
ChatType: isGroup ? "group" : "direct",
|
|
297
298
|
MediaUrl: mediaUrl,
|
|
@@ -321,91 +322,50 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
321
322
|
stopTeamilyMonitoring(accountId);
|
|
322
323
|
});
|
|
323
324
|
|
|
324
|
-
//
|
|
325
|
-
|
|
325
|
+
// Block until aborted — monitor runs indefinitely
|
|
326
|
+
await new Promise<void>((resolve) => {
|
|
327
|
+
ctx.abortSignal.addEventListener("abort", () => resolve());
|
|
328
|
+
});
|
|
326
329
|
},
|
|
327
330
|
},
|
|
328
331
|
};
|
|
329
332
|
|
|
330
|
-
/**
|
|
331
|
-
* Normalize account ID for Teamily.
|
|
332
|
-
*/
|
|
333
|
-
function promptAccountId(): string {
|
|
334
|
-
return DEFAULT_ACCOUNT_ID;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function resolveAccountId(params: {
|
|
338
|
-
cfg: CoreConfig;
|
|
339
|
-
accountId?: string;
|
|
340
|
-
input?: { name?: string };
|
|
341
|
-
}): string {
|
|
342
|
-
const { cfg, accountId, input } = params;
|
|
343
|
-
if (accountId) {
|
|
344
|
-
return accountId;
|
|
345
|
-
}
|
|
346
|
-
if (input?.name) {
|
|
347
|
-
return normalizeAccountId(input.name);
|
|
348
|
-
}
|
|
349
|
-
const accountIds = listTeamilyAccountIds(cfg);
|
|
350
|
-
return accountIds[0] || DEFAULT_ACCOUNT_ID;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Apply Teamily account configuration.
|
|
355
|
-
*/
|
|
356
333
|
function applyTeamilyAccountConfig(params: {
|
|
357
334
|
cfg: CoreConfig;
|
|
358
335
|
accountId: string;
|
|
359
336
|
input: Record<string, unknown>;
|
|
360
337
|
}): CoreConfig {
|
|
361
338
|
const { cfg, accountId, input } = params;
|
|
362
|
-
const existing = cfg.channels?.teamily
|
|
339
|
+
const existing = cfg.channels?.teamily;
|
|
340
|
+
const server = existing?.server ?? { platformUrl: "", apiURL: "", wsURL: "" };
|
|
341
|
+
const accounts = existing?.accounts ?? {};
|
|
363
342
|
|
|
364
343
|
const accountUpdate: Record<string, unknown> = {};
|
|
365
|
-
if (input.userID)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (input.
|
|
369
|
-
accountUpdate.token = String(input.token);
|
|
370
|
-
}
|
|
371
|
-
if (input.nickname) {
|
|
372
|
-
accountUpdate.nickname = String(input.nickname);
|
|
373
|
-
}
|
|
374
|
-
if (input.faceURL) {
|
|
375
|
-
accountUpdate.faceURL = String(input.faceURL);
|
|
376
|
-
}
|
|
344
|
+
if (input.userID) accountUpdate.userID = String(input.userID);
|
|
345
|
+
if (input.token) accountUpdate.token = String(input.token);
|
|
346
|
+
if (input.nickname) accountUpdate.nickname = String(input.nickname);
|
|
347
|
+
if (input.faceURL) accountUpdate.faceURL = String(input.faceURL);
|
|
377
348
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (input.
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
if (input.apiURL) {
|
|
384
|
-
serverUpdate.apiURL = String(input.apiURL);
|
|
385
|
-
}
|
|
386
|
-
if (input.wsURL) {
|
|
387
|
-
serverUpdate.wsURL = String(input.wsURL);
|
|
388
|
-
}
|
|
349
|
+
const serverUpdate: Record<string, string> = {};
|
|
350
|
+
if (input.platformUrl) serverUpdate.platformUrl = String(input.platformUrl);
|
|
351
|
+
if (input.apiURL) serverUpdate.apiURL = String(input.apiURL);
|
|
352
|
+
if (input.wsURL) serverUpdate.wsURL = String(input.wsURL);
|
|
389
353
|
|
|
390
354
|
return {
|
|
391
355
|
...cfg,
|
|
392
356
|
channels: {
|
|
393
357
|
...cfg.channels,
|
|
394
358
|
teamily: {
|
|
395
|
-
...existing,
|
|
396
359
|
enabled: true,
|
|
397
|
-
server: {
|
|
398
|
-
...existing.server,
|
|
399
|
-
...serverUpdate,
|
|
400
|
-
},
|
|
360
|
+
server: { ...server, ...serverUpdate },
|
|
401
361
|
accounts: {
|
|
402
|
-
...
|
|
362
|
+
...accounts,
|
|
403
363
|
[accountId]: {
|
|
404
|
-
...
|
|
364
|
+
...accounts[accountId],
|
|
405
365
|
...accountUpdate,
|
|
406
366
|
},
|
|
407
367
|
},
|
|
408
368
|
},
|
|
409
369
|
},
|
|
410
|
-
};
|
|
370
|
+
} as CoreConfig;
|
|
411
371
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -1,63 +1,46 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import type { ChannelConfigSchema } from "openclaw/plugin-sdk";
|
|
3
2
|
import { buildChannelConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
import type { TeamilyConfig } from "./types.js";
|
|
5
5
|
|
|
6
6
|
// Server configuration schema
|
|
7
7
|
export const TeamilyServerConfigSchema = z.object({
|
|
8
|
-
platformUrl: z
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
.url()
|
|
21
|
-
.default("ws://localhost:10001")
|
|
22
|
-
.describe("Teamily WebSocket URL"),
|
|
8
|
+
platformUrl: z.string().url().default("http://localhost:10002").describe("Teamily platform URL"),
|
|
9
|
+
apiURL: z.string().url().default("http://localhost:10002").describe("Teamily REST API URL"),
|
|
10
|
+
wsURL: z.string().url().default("ws://localhost:10001").describe("Teamily WebSocket URL"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// DM security configuration schema
|
|
14
|
+
export const TeamilyDmConfigSchema = z.object({
|
|
15
|
+
policy: z.string().optional().describe("DM security policy (pairing, allowlist, open)"),
|
|
16
|
+
allowFrom: z
|
|
17
|
+
.array(z.union([z.string(), z.number()]))
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("List of allowed sender IDs"),
|
|
23
20
|
});
|
|
24
21
|
|
|
25
22
|
// User account configuration schema
|
|
26
23
|
export const TeamilyUserAccountSchema = z.object({
|
|
27
|
-
userID: z
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.string()
|
|
33
|
-
.min(1)
|
|
34
|
-
.describe("User token for authentication"),
|
|
35
|
-
nickname: z
|
|
36
|
-
.string()
|
|
37
|
-
.optional()
|
|
38
|
-
.describe("Display nickname for the bot"),
|
|
39
|
-
faceURL: z
|
|
40
|
-
.string()
|
|
41
|
-
.url()
|
|
42
|
-
.optional()
|
|
43
|
-
.describe("Avatar URL for the bot"),
|
|
24
|
+
userID: z.string().min(1).describe("User ID for the bot account"),
|
|
25
|
+
token: z.string().min(1).describe("User token for authentication"),
|
|
26
|
+
nickname: z.string().optional().describe("Display nickname for the bot"),
|
|
27
|
+
faceURL: z.string().url().optional().describe("Avatar URL for the bot"),
|
|
28
|
+
dm: TeamilyDmConfigSchema.optional().describe("Per-account DM security settings"),
|
|
44
29
|
});
|
|
45
30
|
|
|
46
31
|
// Main Teamily configuration schema
|
|
47
32
|
export const TeamilyConfigSchema = z.object({
|
|
48
|
-
enabled: z
|
|
49
|
-
.boolean()
|
|
50
|
-
.default(true)
|
|
51
|
-
.describe("Enable Teamily channel"),
|
|
33
|
+
enabled: z.boolean().default(true).describe("Enable Teamily channel"),
|
|
52
34
|
server: TeamilyServerConfigSchema.describe("Teamily server configuration"),
|
|
53
35
|
accounts: z
|
|
54
36
|
.record(z.string(), TeamilyUserAccountSchema)
|
|
55
37
|
.default({})
|
|
56
38
|
.describe("Teamily bot accounts"),
|
|
39
|
+
dm: TeamilyDmConfigSchema.optional().describe("Channel-level DM security settings"),
|
|
57
40
|
});
|
|
58
41
|
|
|
59
42
|
export const TeamilyChannelConfigSchema = buildChannelConfigSchema(
|
|
60
|
-
TeamilyConfigSchema
|
|
43
|
+
TeamilyConfigSchema,
|
|
61
44
|
) as ChannelConfigSchema;
|
|
62
45
|
|
|
63
46
|
export type CoreConfig = {
|
package/src/monitor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateOperationID } from "./probe.js";
|
|
1
2
|
import type {
|
|
2
3
|
ResolvedTeamilyAccount,
|
|
3
4
|
TeamilyMessage,
|
|
@@ -6,7 +7,6 @@ import type {
|
|
|
6
7
|
TeamilyAudioContent,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
|
|
9
|
-
import { generateOperationID } from "./probe.js";
|
|
10
10
|
|
|
11
11
|
const WS_REQ = {
|
|
12
12
|
LOGIN: 1001,
|
|
@@ -107,7 +107,10 @@ export class TeamilyMonitor {
|
|
|
107
107
|
this.setState("connecting");
|
|
108
108
|
|
|
109
109
|
const wsUrl = new URL(this.account.wsURL);
|
|
110
|
+
wsUrl.searchParams.set("sendID", this.account.userID);
|
|
110
111
|
wsUrl.searchParams.set("token", this.account.token);
|
|
112
|
+
wsUrl.searchParams.set("platformID", "5");
|
|
113
|
+
wsUrl.searchParams.set("sdkType", "js");
|
|
111
114
|
|
|
112
115
|
try {
|
|
113
116
|
const ws = new this.wsImpl(wsUrl.toString()) as WebSocket & { isMock?: boolean };
|
|
@@ -137,13 +140,15 @@ export class TeamilyMonitor {
|
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
142
|
try {
|
|
140
|
-
this.ws.send(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
this.ws.send(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
reqIdentifier: WS_REQ.LOGIN,
|
|
146
|
+
operationID: generateOperationID(),
|
|
147
|
+
sendID: this.account.userID,
|
|
148
|
+
token: this.account.token,
|
|
149
|
+
platformID: 5,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
147
152
|
} catch (error) {
|
|
148
153
|
this.handleError(error);
|
|
149
154
|
}
|
|
@@ -281,12 +286,14 @@ export class TeamilyMonitor {
|
|
|
281
286
|
private sendPing(): void {
|
|
282
287
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
283
288
|
try {
|
|
284
|
-
this.ws.send(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
this.ws.send(
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
reqIdentifier: WS_REQ.HEARTBEAT,
|
|
292
|
+
operationID: generateOperationID(),
|
|
293
|
+
sendID: this.account.userID,
|
|
294
|
+
sendTime: Date.now(),
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
290
297
|
} catch (error) {
|
|
291
298
|
console.error("Teamily ping failed:", error);
|
|
292
299
|
}
|
|
@@ -313,25 +320,17 @@ export class TeamilyMonitor {
|
|
|
313
320
|
* Parse raw OpenIM message content into normalized internal format.
|
|
314
321
|
* OpenIM text messages use `{ content: "text" }`, not `{ text: "..." }`.
|
|
315
322
|
*/
|
|
316
|
-
function parseMessageContent(
|
|
317
|
-
raw: unknown,
|
|
318
|
-
contentType: number,
|
|
319
|
-
): TeamilyMessage["content"] {
|
|
323
|
+
function parseMessageContent(raw: unknown, contentType: number): TeamilyMessage["content"] {
|
|
320
324
|
if (!raw) {
|
|
321
325
|
return {};
|
|
322
326
|
}
|
|
323
327
|
|
|
324
|
-
const obj = (
|
|
325
|
-
typeof raw === "string" ? JSON.parse(raw) : raw
|
|
326
|
-
) as Record<string, unknown>;
|
|
328
|
+
const obj = (typeof raw === "string" ? JSON.parse(raw) : raw) as Record<string, unknown>;
|
|
327
329
|
|
|
328
330
|
switch (contentType) {
|
|
329
331
|
case CONTENT_TYPES.TEXT:
|
|
330
332
|
return {
|
|
331
|
-
text:
|
|
332
|
-
typeof obj.content === "string"
|
|
333
|
-
? obj.content
|
|
334
|
-
: String(obj.content ?? ""),
|
|
333
|
+
text: typeof obj.content === "string" ? obj.content : String(obj.content ?? ""),
|
|
335
334
|
};
|
|
336
335
|
case CONTENT_TYPES.PICTURE:
|
|
337
336
|
return { picture: obj as unknown as TeamilyPictureContent };
|
|
@@ -355,7 +354,7 @@ const monitors = new Map<string, TeamilyMonitor>();
|
|
|
355
354
|
export function startTeamilyMonitoring(
|
|
356
355
|
account: ResolvedTeamilyAccount,
|
|
357
356
|
onMessage: TeamilyMessageHandler,
|
|
358
|
-
onStateChange?: (state: TeamilyConnectionState, error?: string) => void
|
|
357
|
+
onStateChange?: (state: TeamilyConnectionState, error?: string) => void,
|
|
359
358
|
): () => void {
|
|
360
359
|
const monitor = new TeamilyMonitor({
|
|
361
360
|
account,
|
package/src/normalize.ts
CHANGED
|
@@ -25,13 +25,8 @@ export function normalizeTeamilyTarget(raw: string): TeamilyMessageTarget {
|
|
|
25
25
|
: trimmed;
|
|
26
26
|
|
|
27
27
|
// Group target
|
|
28
|
-
if (
|
|
29
|
-
withoutPrefix.
|
|
30
|
-
withoutPrefix.startsWith("g:")
|
|
31
|
-
) {
|
|
32
|
-
const groupId = withoutPrefix
|
|
33
|
-
.replace(/^(group:|g:)/i, "")
|
|
34
|
-
.trim();
|
|
28
|
+
if (withoutPrefix.startsWith("group:") || withoutPrefix.startsWith("g:")) {
|
|
29
|
+
const groupId = withoutPrefix.replace(/^(group:|g:)/i, "").trim();
|
|
35
30
|
if (!groupId) {
|
|
36
31
|
throw new Error("Group ID cannot be empty");
|
|
37
32
|
}
|
|
@@ -39,9 +34,7 @@ export function normalizeTeamilyTarget(raw: string): TeamilyMessageTarget {
|
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
// User target
|
|
42
|
-
const userId = withoutPrefix
|
|
43
|
-
.replace(/^(user:|u:)/i, "")
|
|
44
|
-
.trim();
|
|
37
|
+
const userId = withoutPrefix.replace(/^(user:|u:)/i, "").trim();
|
|
45
38
|
if (!userId) {
|
|
46
39
|
throw new Error("User ID cannot be empty");
|
|
47
40
|
}
|
|
@@ -79,8 +72,13 @@ export function looksLikeTeamilyTargetId(raw: string): boolean {
|
|
|
79
72
|
const lowered = trimmed.toLowerCase();
|
|
80
73
|
|
|
81
74
|
// Check for explicit prefixes
|
|
82
|
-
if (
|
|
83
|
-
|
|
75
|
+
if (
|
|
76
|
+
lowered.startsWith("teamily:") ||
|
|
77
|
+
lowered.startsWith("user:") ||
|
|
78
|
+
lowered.startsWith("u:") ||
|
|
79
|
+
lowered.startsWith("group:") ||
|
|
80
|
+
lowered.startsWith("g:")
|
|
81
|
+
) {
|
|
84
82
|
return true;
|
|
85
83
|
}
|
|
86
84
|
|
package/src/probe.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function generateOperationID(): string {
|
|
|
18
18
|
*/
|
|
19
19
|
export async function probeTeamily(
|
|
20
20
|
account: ResolvedTeamilyAccount,
|
|
21
|
-
fetchImpl: typeof fetch = fetch
|
|
21
|
+
fetchImpl: typeof fetch = fetch,
|
|
22
22
|
): Promise<TeamilyProbeResult> {
|
|
23
23
|
try {
|
|
24
24
|
const url = `${account.apiURL}/user/get_users_info`;
|
|
@@ -27,8 +27,8 @@ export async function probeTeamily(
|
|
|
27
27
|
method: "POST",
|
|
28
28
|
headers: {
|
|
29
29
|
"Content-Type": "application/json",
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
operationID: generateOperationID(),
|
|
31
|
+
token: account.token,
|
|
32
32
|
},
|
|
33
33
|
body: JSON.stringify({
|
|
34
34
|
userIDs: [account.userID],
|
|
@@ -44,7 +44,7 @@ export async function probeTeamily(
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const data = await response.json() as {
|
|
47
|
+
const data = (await response.json()) as {
|
|
48
48
|
errCode: number;
|
|
49
49
|
errMsg: string;
|
|
50
50
|
data?: Array<{ userID: string }>;
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
teamilyRuntime = runtime;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getTeamilyRuntime(): PluginRuntime {
|
|
10
|
-
if (!teamilyRuntime) {
|
|
11
|
-
throw new Error("Teamily runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return teamilyRuntime;
|
|
14
|
-
}
|
|
4
|
+
const { setRuntime: setTeamilyRuntime, getRuntime: getTeamilyRuntime } =
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>("Teamily runtime not initialized");
|
|
6
|
+
export { getTeamilyRuntime, setTeamilyRuntime };
|
package/src/send.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { generateOperationID } from "./probe.js";
|
|
1
2
|
import type { ResolvedTeamilyAccount, TeamilyMessageTarget } from "./types.js";
|
|
2
3
|
import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
|
|
3
|
-
import { generateOperationID } from "./probe.js";
|
|
4
4
|
|
|
5
5
|
export interface SendTeamilyMessageParams {
|
|
6
6
|
account: ResolvedTeamilyAccount;
|
|
@@ -34,7 +34,7 @@ export interface TeamilySendResult {
|
|
|
34
34
|
* @returns Send result with message ID or error
|
|
35
35
|
*/
|
|
36
36
|
export async function sendMessageTeamily(
|
|
37
|
-
params: SendTeamilyMessageParams
|
|
37
|
+
params: SendTeamilyMessageParams,
|
|
38
38
|
): Promise<TeamilySendResult> {
|
|
39
39
|
const { account, target, text, replyToId, fetchImpl = fetch } = params;
|
|
40
40
|
|
|
@@ -53,8 +53,8 @@ export async function sendMessageTeamily(
|
|
|
53
53
|
method: "POST",
|
|
54
54
|
headers: {
|
|
55
55
|
"Content-Type": "application/json",
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
operationID: generateOperationID(),
|
|
57
|
+
token: account.token,
|
|
58
58
|
},
|
|
59
59
|
body: JSON.stringify(payload),
|
|
60
60
|
});
|
|
@@ -67,7 +67,7 @@ export async function sendMessageTeamily(
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const data = await response.json() as {
|
|
70
|
+
const data = (await response.json()) as {
|
|
71
71
|
errCode: number;
|
|
72
72
|
errMsg: string;
|
|
73
73
|
data?: {
|
|
@@ -104,9 +104,7 @@ export async function sendMessageTeamily(
|
|
|
104
104
|
* @param params - Send parameters
|
|
105
105
|
* @returns Send result with message ID or error
|
|
106
106
|
*/
|
|
107
|
-
export async function sendMediaTeamily(
|
|
108
|
-
params: SendTeamilyMediaParams
|
|
109
|
-
): Promise<TeamilySendResult> {
|
|
107
|
+
export async function sendMediaTeamily(params: SendTeamilyMediaParams): Promise<TeamilySendResult> {
|
|
110
108
|
const { account, target, mediaUrl, mediaType, caption, fetchImpl = fetch } = params;
|
|
111
109
|
|
|
112
110
|
const url = `${account.apiURL}/msg/send_msg`;
|
|
@@ -192,8 +190,8 @@ export async function sendMediaTeamily(
|
|
|
192
190
|
method: "POST",
|
|
193
191
|
headers: {
|
|
194
192
|
"Content-Type": "application/json",
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
operationID: generateOperationID(),
|
|
194
|
+
token: account.token,
|
|
197
195
|
},
|
|
198
196
|
body: JSON.stringify(payload),
|
|
199
197
|
});
|
|
@@ -206,7 +204,7 @@ export async function sendMediaTeamily(
|
|
|
206
204
|
};
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
const data = await response.json() as {
|
|
207
|
+
const data = (await response.json()) as {
|
|
210
208
|
errCode: number;
|
|
211
209
|
errMsg: string;
|
|
212
210
|
data?: {
|
package/src/types.ts
CHANGED
|
@@ -6,17 +6,24 @@ export interface TeamilyServerConfig {
|
|
|
6
6
|
wsURL: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface TeamilyDmConfig {
|
|
10
|
+
policy?: string;
|
|
11
|
+
allowFrom?: Array<string | number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export interface TeamilyUserAccount {
|
|
10
15
|
userID: string;
|
|
11
16
|
token: string;
|
|
12
17
|
nickname?: string;
|
|
13
18
|
faceURL?: string;
|
|
19
|
+
dm?: TeamilyDmConfig;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export interface TeamilyConfig {
|
|
17
23
|
enabled: boolean;
|
|
18
24
|
server: TeamilyServerConfig;
|
|
19
25
|
accounts: Record<string, TeamilyUserAccount>;
|
|
26
|
+
dm?: TeamilyDmConfig;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export interface ResolvedTeamilyAccount {
|
|
@@ -29,6 +36,7 @@ export interface ResolvedTeamilyAccount {
|
|
|
29
36
|
token: string;
|
|
30
37
|
nickname?: string;
|
|
31
38
|
faceURL?: string;
|
|
39
|
+
dm?: TeamilyDmConfig;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export interface TeamilyMessageTarget {
|