@ascegu/teamily 1.0.5 → 1.0.7
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 +48 -30
- 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.7",
|
|
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,
|
|
@@ -80,8 +80,17 @@ export class TeamilyMonitor {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (this.ws) {
|
|
83
|
-
this.ws
|
|
83
|
+
const ws = this.ws;
|
|
84
|
+
ws.onopen = null;
|
|
85
|
+
ws.onmessage = null;
|
|
86
|
+
ws.onerror = null;
|
|
87
|
+
ws.onclose = null;
|
|
84
88
|
this.ws = null;
|
|
89
|
+
try {
|
|
90
|
+
ws.close(1000, "Monitoring stopped");
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore – socket may already be closed.
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
this.setState("disconnected");
|
|
@@ -128,13 +137,15 @@ export class TeamilyMonitor {
|
|
|
128
137
|
return;
|
|
129
138
|
}
|
|
130
139
|
try {
|
|
131
|
-
this.ws.send(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
this.ws.send(
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
reqIdentifier: WS_REQ.LOGIN,
|
|
143
|
+
operationID: generateOperationID(),
|
|
144
|
+
sendID: this.account.userID,
|
|
145
|
+
token: this.account.token,
|
|
146
|
+
platformID: 5,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
138
149
|
} catch (error) {
|
|
139
150
|
this.handleError(error);
|
|
140
151
|
}
|
|
@@ -201,15 +212,28 @@ export class TeamilyMonitor {
|
|
|
201
212
|
|
|
202
213
|
/**
|
|
203
214
|
* Handle WebSocket error.
|
|
215
|
+
* Detaches event handlers before closing to prevent recursive calls
|
|
216
|
+
* (ws.close() on an errored socket can re-fire onerror → stack overflow).
|
|
204
217
|
*/
|
|
205
218
|
private handleError(error: unknown): void {
|
|
206
219
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
220
|
this.setState("error", errorMessage);
|
|
208
221
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
// Detach handlers and grab ref before nulling, so close() cannot recurse.
|
|
223
|
+
const ws = this.ws;
|
|
224
|
+
if (ws) {
|
|
225
|
+
ws.onopen = null;
|
|
226
|
+
ws.onmessage = null;
|
|
227
|
+
ws.onerror = null;
|
|
228
|
+
ws.onclose = null;
|
|
212
229
|
this.ws = null;
|
|
230
|
+
try {
|
|
231
|
+
ws.close();
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore – socket may already be closed/invalid.
|
|
234
|
+
}
|
|
235
|
+
// onclose was detached, so manually trigger reconnect logic.
|
|
236
|
+
this.handleClose();
|
|
213
237
|
}
|
|
214
238
|
}
|
|
215
239
|
|
|
@@ -259,12 +283,14 @@ export class TeamilyMonitor {
|
|
|
259
283
|
private sendPing(): void {
|
|
260
284
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
261
285
|
try {
|
|
262
|
-
this.ws.send(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
this.ws.send(
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
reqIdentifier: WS_REQ.HEARTBEAT,
|
|
289
|
+
operationID: generateOperationID(),
|
|
290
|
+
sendID: this.account.userID,
|
|
291
|
+
sendTime: Date.now(),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
268
294
|
} catch (error) {
|
|
269
295
|
console.error("Teamily ping failed:", error);
|
|
270
296
|
}
|
|
@@ -291,25 +317,17 @@ export class TeamilyMonitor {
|
|
|
291
317
|
* Parse raw OpenIM message content into normalized internal format.
|
|
292
318
|
* OpenIM text messages use `{ content: "text" }`, not `{ text: "..." }`.
|
|
293
319
|
*/
|
|
294
|
-
function parseMessageContent(
|
|
295
|
-
raw: unknown,
|
|
296
|
-
contentType: number,
|
|
297
|
-
): TeamilyMessage["content"] {
|
|
320
|
+
function parseMessageContent(raw: unknown, contentType: number): TeamilyMessage["content"] {
|
|
298
321
|
if (!raw) {
|
|
299
322
|
return {};
|
|
300
323
|
}
|
|
301
324
|
|
|
302
|
-
const obj = (
|
|
303
|
-
typeof raw === "string" ? JSON.parse(raw) : raw
|
|
304
|
-
) as Record<string, unknown>;
|
|
325
|
+
const obj = (typeof raw === "string" ? JSON.parse(raw) : raw) as Record<string, unknown>;
|
|
305
326
|
|
|
306
327
|
switch (contentType) {
|
|
307
328
|
case CONTENT_TYPES.TEXT:
|
|
308
329
|
return {
|
|
309
|
-
text:
|
|
310
|
-
typeof obj.content === "string"
|
|
311
|
-
? obj.content
|
|
312
|
-
: String(obj.content ?? ""),
|
|
330
|
+
text: typeof obj.content === "string" ? obj.content : String(obj.content ?? ""),
|
|
313
331
|
};
|
|
314
332
|
case CONTENT_TYPES.PICTURE:
|
|
315
333
|
return { picture: obj as unknown as TeamilyPictureContent };
|
|
@@ -333,7 +351,7 @@ const monitors = new Map<string, TeamilyMonitor>();
|
|
|
333
351
|
export function startTeamilyMonitoring(
|
|
334
352
|
account: ResolvedTeamilyAccount,
|
|
335
353
|
onMessage: TeamilyMessageHandler,
|
|
336
|
-
onStateChange?: (state: TeamilyConnectionState, error?: string) => void
|
|
354
|
+
onStateChange?: (state: TeamilyConnectionState, error?: string) => void,
|
|
337
355
|
): () => void {
|
|
338
356
|
const monitor = new TeamilyMonitor({
|
|
339
357
|
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 {
|