@ascegu/teamily 1.0.4 → 1.0.5
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/package.json +2 -1
- package/src/accounts.ts +51 -0
- package/src/channel.ts +411 -0
- package/src/config-schema.ts +67 -0
- package/src/monitor.ts +370 -0
- package/src/normalize.ts +90 -0
- package/src/probe.ts +86 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +273 -0
- package/src/types.ts +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascegu/teamily",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"*.ts",
|
|
9
9
|
"*.js",
|
|
10
10
|
"*.json",
|
|
11
|
+
"src/",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"keywords": [
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CoreConfig } from "./config-schema.js";
|
|
2
|
+
import type { ResolvedTeamilyAccount } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function listTeamilyAccountIds(cfg: CoreConfig): string[] {
|
|
5
|
+
const config = cfg.channels?.teamily;
|
|
6
|
+
if (!config?.enabled || !config.accounts) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return Object.keys(config.accounts).filter(
|
|
10
|
+
(key) => config.accounts![key].token !== undefined
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveDefaultTeamilyAccountId(cfg: CoreConfig): string {
|
|
15
|
+
const accountIds = listTeamilyAccountIds(cfg);
|
|
16
|
+
return accountIds[0] || "default";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveTeamilyAccount(
|
|
20
|
+
cfg: CoreConfig,
|
|
21
|
+
accountId?: string | null
|
|
22
|
+
): ResolvedTeamilyAccount {
|
|
23
|
+
const config = cfg.channels?.teamily;
|
|
24
|
+
if (!config?.enabled) {
|
|
25
|
+
throw new Error("Teamily channel is not enabled");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const accountIds = listTeamilyAccountIds(cfg);
|
|
29
|
+
const targetAccountId = accountId || accountIds[0];
|
|
30
|
+
|
|
31
|
+
if (!targetAccountId) {
|
|
32
|
+
throw new Error("No Teamily account configured");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const account = config.accounts?.[targetAccountId];
|
|
36
|
+
if (!account) {
|
|
37
|
+
throw new Error(`Teamily account ${targetAccountId} not found`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
accountId: targetAccountId,
|
|
42
|
+
enabled: true,
|
|
43
|
+
platformUrl: config.server.platformUrl,
|
|
44
|
+
apiURL: config.server.apiURL,
|
|
45
|
+
wsURL: config.server.wsURL,
|
|
46
|
+
userID: account.userID,
|
|
47
|
+
token: account.token,
|
|
48
|
+
nickname: account.nickname,
|
|
49
|
+
faceURL: account.faceURL,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAccountNameToChannelSection,
|
|
3
|
+
buildChannelConfigSchema,
|
|
4
|
+
DEFAULT_ACCOUNT_ID,
|
|
5
|
+
deleteAccountFromConfigSection,
|
|
6
|
+
formatPairingApproveHint,
|
|
7
|
+
normalizeAccountId,
|
|
8
|
+
PAIRING_APPROVED_MESSAGE,
|
|
9
|
+
setAccountEnabledInConfigSection,
|
|
10
|
+
type ChannelPlugin,
|
|
11
|
+
type ChannelOutboundContext,
|
|
12
|
+
type ChannelOutboundAdapter,
|
|
13
|
+
type ChannelStatusAdapter,
|
|
14
|
+
type ChannelStatusIssue,
|
|
15
|
+
} from "openclaw/plugin-sdk";
|
|
16
|
+
import { TeamilyConfigSchema } from "./config-schema.js";
|
|
17
|
+
import {
|
|
18
|
+
listTeamilyAccountIds,
|
|
19
|
+
resolveDefaultTeamilyAccountId,
|
|
20
|
+
resolveTeamilyAccount,
|
|
21
|
+
type ResolvedTeamilyAccount,
|
|
22
|
+
} from "./accounts.js";
|
|
23
|
+
import { probeTeamily } from "./probe.js";
|
|
24
|
+
import { sendMessageTeamily, sendMediaTeamily } from "./send.js";
|
|
25
|
+
import { startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
26
|
+
import { normalizeTeamilyTarget, normalizeTeamilyAllowEntry } from "./normalize.js";
|
|
27
|
+
import { SESSION_TYPES } from "./types.js";
|
|
28
|
+
import { getTeamilyRuntime } from "./runtime.js";
|
|
29
|
+
import type { CoreConfig } from "./config-schema.js";
|
|
30
|
+
|
|
31
|
+
const meta = {
|
|
32
|
+
id: "teamily",
|
|
33
|
+
label: "Teamily",
|
|
34
|
+
selectionLabel: "Teamily (self-hosted)",
|
|
35
|
+
docsPath: "/channels/teamily",
|
|
36
|
+
docsLabel: "teamily",
|
|
37
|
+
blurb: "Team instant messaging server",
|
|
38
|
+
order: 75,
|
|
39
|
+
quickstartAllowFrom: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const capabilities = {
|
|
43
|
+
chatTypes: ["direct", "group"] as const,
|
|
44
|
+
media: true,
|
|
45
|
+
reactions: true,
|
|
46
|
+
threads: false,
|
|
47
|
+
polls: false,
|
|
48
|
+
streaming: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
52
|
+
id: "teamily",
|
|
53
|
+
meta,
|
|
54
|
+
capabilities,
|
|
55
|
+
onboarding: {
|
|
56
|
+
promptAccountId,
|
|
57
|
+
resolveAccountId,
|
|
58
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
59
|
+
applyAccountNameToChannelSection({
|
|
60
|
+
cfg: cfg as CoreConfig,
|
|
61
|
+
sectionKey: "teamily",
|
|
62
|
+
accountId,
|
|
63
|
+
name,
|
|
64
|
+
allowTopLevel: true,
|
|
65
|
+
}),
|
|
66
|
+
applyAccountConfig: ({ cfg, accountId, input }) =>
|
|
67
|
+
applyTeamilyAccountConfig({ cfg: cfg as CoreConfig, accountId, input }),
|
|
68
|
+
resolveBindingAccountId: ({ cfg }) => resolveDefaultTeamilyAccountId(cfg as CoreConfig),
|
|
69
|
+
},
|
|
70
|
+
pairing: {
|
|
71
|
+
idLabel: "teamilyUserId",
|
|
72
|
+
normalizeAllowEntry: (entry) => {
|
|
73
|
+
try {
|
|
74
|
+
return normalizeTeamilyAllowEntry(entry);
|
|
75
|
+
} catch {
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
notifyApproval: async ({ id, cfg }) => {
|
|
80
|
+
try {
|
|
81
|
+
const accountId = resolveDefaultTeamilyAccountId(cfg as CoreConfig);
|
|
82
|
+
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
83
|
+
const target = normalizeTeamilyTarget(id);
|
|
84
|
+
await sendMessageTeamily({
|
|
85
|
+
account,
|
|
86
|
+
target,
|
|
87
|
+
text: PAIRING_APPROVED_MESSAGE,
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// Silently fail on notification
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
configSchema: buildChannelConfigSchema(TeamilyConfigSchema),
|
|
95
|
+
config: {
|
|
96
|
+
listAccountIds: (cfg) => listTeamilyAccountIds(cfg as CoreConfig),
|
|
97
|
+
resolveAccount: (cfg, accountId) =>
|
|
98
|
+
resolveTeamilyAccount(cfg as CoreConfig, accountId),
|
|
99
|
+
defaultAccountId: (cfg) => resolveDefaultTeamilyAccountId(cfg as CoreConfig),
|
|
100
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
101
|
+
setAccountEnabledInConfigSection({
|
|
102
|
+
cfg: cfg as CoreConfig,
|
|
103
|
+
sectionKey: "teamily",
|
|
104
|
+
accountId,
|
|
105
|
+
enabled,
|
|
106
|
+
allowTopLevel: true,
|
|
107
|
+
}),
|
|
108
|
+
deleteAccount: ({ cfg, accountId }) =>
|
|
109
|
+
deleteAccountFromConfigSection({
|
|
110
|
+
cfg: cfg as CoreConfig,
|
|
111
|
+
sectionKey: "teamily",
|
|
112
|
+
accountId,
|
|
113
|
+
clearBaseFields: [
|
|
114
|
+
"name",
|
|
115
|
+
"userID",
|
|
116
|
+
"token",
|
|
117
|
+
"nickname",
|
|
118
|
+
"faceURL",
|
|
119
|
+
],
|
|
120
|
+
}),
|
|
121
|
+
isConfigured: (account) => !!account.token,
|
|
122
|
+
describeAccount: (account) => ({
|
|
123
|
+
accountId: account.accountId,
|
|
124
|
+
name: account.nickname || account.userID,
|
|
125
|
+
enabled: account.enabled,
|
|
126
|
+
configured: !!account.token,
|
|
127
|
+
}),
|
|
128
|
+
resolveAllowFrom: (cfg, accountId) => {
|
|
129
|
+
// Return empty array - allowFrom needs to be manually configured
|
|
130
|
+
return [];
|
|
131
|
+
},
|
|
132
|
+
formatAllowFrom: (cfg, allowFrom) => {
|
|
133
|
+
return allowFrom.map((id) => id.toString());
|
|
134
|
+
},
|
|
135
|
+
resolveDefaultTo: (cfg) => {
|
|
136
|
+
// No default target - user must specify
|
|
137
|
+
return undefined;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
outbound: {
|
|
141
|
+
sendText: async (ctx: ChannelOutboundContext) => {
|
|
142
|
+
const { to, text, accountId } = ctx;
|
|
143
|
+
const account = resolveTeamilyAccount(ctx.cfg, accountId);
|
|
144
|
+
const target = normalizeTeamilyTarget(to);
|
|
145
|
+
|
|
146
|
+
const result = await sendMessageTeamily({
|
|
147
|
+
account,
|
|
148
|
+
target,
|
|
149
|
+
text,
|
|
150
|
+
replyToId: ctx.replyToId || undefined,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!result.success) {
|
|
154
|
+
throw new Error(result.error || "Failed to send message");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { messageId: result.messageId };
|
|
158
|
+
},
|
|
159
|
+
sendMedia: async (ctx: ChannelOutboundContext) => {
|
|
160
|
+
const { to, mediaUrl, text, accountId } = ctx;
|
|
161
|
+
const account = resolveTeamilyAccount(ctx.cfg, accountId);
|
|
162
|
+
const target = normalizeTeamilyTarget(to);
|
|
163
|
+
|
|
164
|
+
// Determine media type from URL or assume image
|
|
165
|
+
let mediaType: "image" | "video" | "audio" | "file" = "image";
|
|
166
|
+
const urlLower = mediaUrl.toLowerCase();
|
|
167
|
+
if (urlLower.endsWith(".mp4") || urlLower.endsWith(".mov") || urlLower.endsWith(".webm")) {
|
|
168
|
+
mediaType = "video";
|
|
169
|
+
} else if (urlLower.endsWith(".mp3") || urlLower.endsWith(".m4a") || urlLower.endsWith(".wav")) {
|
|
170
|
+
mediaType = "audio";
|
|
171
|
+
} else if (
|
|
172
|
+
urlLower.endsWith(".pdf") ||
|
|
173
|
+
urlLower.endsWith(".doc") ||
|
|
174
|
+
urlLower.endsWith(".docx") ||
|
|
175
|
+
urlLower.endsWith(".zip")
|
|
176
|
+
) {
|
|
177
|
+
mediaType = "file";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = await sendMediaTeamily({
|
|
181
|
+
account,
|
|
182
|
+
target,
|
|
183
|
+
mediaUrl,
|
|
184
|
+
mediaType,
|
|
185
|
+
caption: text,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!result.success) {
|
|
189
|
+
throw new Error(result.error || "Failed to send media");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { messageId: result.messageId };
|
|
193
|
+
},
|
|
194
|
+
resolveTarget: (raw) => {
|
|
195
|
+
return normalizeTeamilyTarget(raw).id;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
status: {
|
|
199
|
+
probeAccount: async (cfg, accountId) => {
|
|
200
|
+
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
201
|
+
const result = await probeTeamily(account);
|
|
202
|
+
|
|
203
|
+
if (!result.connected) {
|
|
204
|
+
return {
|
|
205
|
+
connected: false,
|
|
206
|
+
error: result.error || "Failed to connect to Teamily server",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
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
|
+
};
|
|
220
|
+
},
|
|
221
|
+
collectStatusIssues: async (cfg, accountId) => {
|
|
222
|
+
const issues: ChannelStatusIssue[] = [];
|
|
223
|
+
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
224
|
+
|
|
225
|
+
if (!account.token) {
|
|
226
|
+
issues.push({
|
|
227
|
+
channel: "teamily",
|
|
228
|
+
accountId,
|
|
229
|
+
kind: "config",
|
|
230
|
+
message: "User token is not configured",
|
|
231
|
+
fix: "Run `openclaw channel configure teamily` to set up authentication",
|
|
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
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return issues;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
gateway: {
|
|
260
|
+
startAccount: async (ctx) => {
|
|
261
|
+
const { cfg, accountId, account, log } = ctx;
|
|
262
|
+
|
|
263
|
+
if (!account.token) {
|
|
264
|
+
log?.warn?.(`Teamily account ${accountId} not configured (missing token)`);
|
|
265
|
+
return { stop: () => {} };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
log?.info?.(`Starting Teamily channel (account: ${accountId})`);
|
|
269
|
+
|
|
270
|
+
const stopFn = startTeamilyMonitoring(account, async (message) => {
|
|
271
|
+
const rt = getTeamilyRuntime();
|
|
272
|
+
const currentCfg = rt.config.loadConfig();
|
|
273
|
+
|
|
274
|
+
const isGroup = message.sessionType === SESSION_TYPES.GROUP;
|
|
275
|
+
const from = message.sendID;
|
|
276
|
+
const text = message.content?.text || "";
|
|
277
|
+
const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
|
|
278
|
+
|
|
279
|
+
let mediaUrl: string | undefined;
|
|
280
|
+
if (message.content?.picture?.sourcePicture?.url) {
|
|
281
|
+
mediaUrl = message.content.picture.sourcePicture.url;
|
|
282
|
+
} else if (message.content?.video?.videoUrl) {
|
|
283
|
+
mediaUrl = message.content.video.videoUrl;
|
|
284
|
+
} else if (message.content?.audio?.sourceUrl) {
|
|
285
|
+
mediaUrl = message.content.audio.sourceUrl;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const msgCtx = {
|
|
289
|
+
Body: text,
|
|
290
|
+
From: from,
|
|
291
|
+
To: account.userID,
|
|
292
|
+
SessionKey: sessionKey,
|
|
293
|
+
AccountId: accountId,
|
|
294
|
+
OriginatingChannel: "teamily" as any,
|
|
295
|
+
OriginatingTo: from,
|
|
296
|
+
ChatType: isGroup ? "group" : "direct",
|
|
297
|
+
MediaUrl: mediaUrl,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
301
|
+
ctx: msgCtx,
|
|
302
|
+
cfg: currentCfg,
|
|
303
|
+
dispatcherOptions: {
|
|
304
|
+
deliver: async (payload: { text?: string; body?: string }) => {
|
|
305
|
+
const replyText = payload?.text ?? payload?.body;
|
|
306
|
+
if (replyText) {
|
|
307
|
+
const target = normalizeTeamilyTarget(from);
|
|
308
|
+
await sendMessageTeamily({ account, target, text: replyText });
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
onReplyStart: () => {
|
|
312
|
+
log?.info?.(`Agent reply started for ${from}`);
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Respect abort signal from the gateway framework
|
|
319
|
+
ctx.abortSignal.addEventListener("abort", () => {
|
|
320
|
+
stopFn();
|
|
321
|
+
stopTeamilyMonitoring(accountId);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Return a promise that never resolves (monitor runs indefinitely)
|
|
325
|
+
return new Promise<void>(() => {});
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
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
|
+
function applyTeamilyAccountConfig(params: {
|
|
357
|
+
cfg: CoreConfig;
|
|
358
|
+
accountId: string;
|
|
359
|
+
input: Record<string, unknown>;
|
|
360
|
+
}): CoreConfig {
|
|
361
|
+
const { cfg, accountId, input } = params;
|
|
362
|
+
const existing = cfg.channels?.teamily || { enabled: false, server: {}, accounts: {} };
|
|
363
|
+
|
|
364
|
+
const accountUpdate: Record<string, unknown> = {};
|
|
365
|
+
if (input.userID) {
|
|
366
|
+
accountUpdate.userID = String(input.userID);
|
|
367
|
+
}
|
|
368
|
+
if (input.token) {
|
|
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
|
+
}
|
|
377
|
+
|
|
378
|
+
// Server configuration from input or existing
|
|
379
|
+
const serverUpdate: Record<string, unknown> = {};
|
|
380
|
+
if (input.platformUrl) {
|
|
381
|
+
serverUpdate.platformUrl = String(input.platformUrl);
|
|
382
|
+
}
|
|
383
|
+
if (input.apiURL) {
|
|
384
|
+
serverUpdate.apiURL = String(input.apiURL);
|
|
385
|
+
}
|
|
386
|
+
if (input.wsURL) {
|
|
387
|
+
serverUpdate.wsURL = String(input.wsURL);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
...cfg,
|
|
392
|
+
channels: {
|
|
393
|
+
...cfg.channels,
|
|
394
|
+
teamily: {
|
|
395
|
+
...existing,
|
|
396
|
+
enabled: true,
|
|
397
|
+
server: {
|
|
398
|
+
...existing.server,
|
|
399
|
+
...serverUpdate,
|
|
400
|
+
},
|
|
401
|
+
accounts: {
|
|
402
|
+
...existing.accounts,
|
|
403
|
+
[accountId]: {
|
|
404
|
+
...(existing.accounts?.[accountId] || {}),
|
|
405
|
+
...accountUpdate,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ChannelConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { buildChannelConfigSchema } from "openclaw/plugin-sdk";
|
|
4
|
+
import type { TeamilyConfig } from "./types.js";
|
|
5
|
+
|
|
6
|
+
// Server configuration schema
|
|
7
|
+
export const TeamilyServerConfigSchema = z.object({
|
|
8
|
+
platformUrl: z
|
|
9
|
+
.string()
|
|
10
|
+
.url()
|
|
11
|
+
.default("http://localhost:10002")
|
|
12
|
+
.describe("Teamily platform URL"),
|
|
13
|
+
apiURL: z
|
|
14
|
+
.string()
|
|
15
|
+
.url()
|
|
16
|
+
.default("http://localhost:10002")
|
|
17
|
+
.describe("Teamily REST API URL"),
|
|
18
|
+
wsURL: z
|
|
19
|
+
.string()
|
|
20
|
+
.url()
|
|
21
|
+
.default("ws://localhost:10001")
|
|
22
|
+
.describe("Teamily WebSocket URL"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// User account configuration schema
|
|
26
|
+
export const TeamilyUserAccountSchema = z.object({
|
|
27
|
+
userID: z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.describe("User ID for the bot account"),
|
|
31
|
+
token: z
|
|
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"),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Main Teamily configuration schema
|
|
47
|
+
export const TeamilyConfigSchema = z.object({
|
|
48
|
+
enabled: z
|
|
49
|
+
.boolean()
|
|
50
|
+
.default(true)
|
|
51
|
+
.describe("Enable Teamily channel"),
|
|
52
|
+
server: TeamilyServerConfigSchema.describe("Teamily server configuration"),
|
|
53
|
+
accounts: z
|
|
54
|
+
.record(z.string(), TeamilyUserAccountSchema)
|
|
55
|
+
.default({})
|
|
56
|
+
.describe("Teamily bot accounts"),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const TeamilyChannelConfigSchema = buildChannelConfigSchema(
|
|
60
|
+
TeamilyConfigSchema
|
|
61
|
+
) as ChannelConfigSchema;
|
|
62
|
+
|
|
63
|
+
export type CoreConfig = {
|
|
64
|
+
channels?: {
|
|
65
|
+
teamily?: TeamilyConfig;
|
|
66
|
+
};
|
|
67
|
+
};
|