@decentchat/decentclaw 0.1.0
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 +88 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +48 -0
- package/src/channel.ts +789 -0
- package/src/huddle/AudioPipeline.ts +174 -0
- package/src/huddle/BotHuddleManager.ts +882 -0
- package/src/huddle/SpeechToText.ts +223 -0
- package/src/huddle/TextToSpeech.ts +260 -0
- package/src/huddle/index.ts +8 -0
- package/src/monitor.ts +1266 -0
- package/src/peer/DecentChatNodePeer.ts +4570 -0
- package/src/peer/FileStore.ts +59 -0
- package/src/peer/NodeMessageProtocol.ts +1057 -0
- package/src/peer/SyncProtocol.ts +701 -0
- package/src/peer/polyfill.ts +43 -0
- package/src/peer-registry.ts +32 -0
- package/src/runtime.ts +63 -0
- package/src/types.ts +136 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { assertCompanyBootstrapAgentInstallation, ensureCompanyBootstrapRuntime, resolveCompanyManifestPath } from "@decentchat/company-sim";
|
|
8
|
+
import { startDecentChatPeer } from "./monitor.js";
|
|
9
|
+
import { getActivePeer, listActivePeerAccountIds } from "./peer-registry.js";
|
|
10
|
+
import { buildDecentChatRuntimeBootstrapKey, invalidateDecentChatBootstrapKey, runDecentChatBootstrapOnce } from "./runtime.js";
|
|
11
|
+
import type { DecentChatChannelConfig, OpenClawConfigShape, ResolvedDecentChatAccount } from "./types.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
14
|
+
const DECENTCHAT_STARTUP_STAGGER_MS = 6_000;
|
|
15
|
+
|
|
16
|
+
function sanitizeDecentChatAccountPathSegment(accountId: string): string {
|
|
17
|
+
const sanitized = accountId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
18
|
+
return sanitized || DEFAULT_ACCOUNT_ID;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveDecentChatDataDir(cfg: any, accountId: string, configuredDataDir: unknown): string | undefined {
|
|
22
|
+
if (typeof configuredDataDir === "string" && configuredDataDir.trim()) {
|
|
23
|
+
return configuredDataDir.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const accountIds = listDecentChatAccountIds(cfg);
|
|
27
|
+
if (accountIds.length === 1 && accountIds[0] === DEFAULT_ACCOUNT_ID && accountId === DEFAULT_ACCOUNT_ID) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return join(homedir(), ".openclaw", "data", "decentchat", sanitizeDecentChatAccountPathSegment(accountId));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatPairingApproveHint(channelId: string): string {
|
|
35
|
+
return `Approve via: \`openclaw pairing list ${channelId}\` / \`openclaw pairing approve ${channelId} <code>\``;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildChannelConfigSchema(schema: z.ZodTypeAny): { schema: Record<string, unknown> } {
|
|
39
|
+
const schemaWithJson = schema as z.ZodTypeAny & {
|
|
40
|
+
toJSONSchema?: (opts?: { target?: string; unrepresentable?: string }) => Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
if (typeof schemaWithJson.toJSONSchema === "function") {
|
|
43
|
+
return {
|
|
44
|
+
schema: schemaWithJson.toJSONSchema({
|
|
45
|
+
target: "draft-07",
|
|
46
|
+
unrepresentable: "any",
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
additionalProperties: true,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DecentChatConfigSchema = z.object({
|
|
59
|
+
enabled: z.boolean().optional(),
|
|
60
|
+
seedPhrase: z.string().optional(),
|
|
61
|
+
signalingServer: z.string().optional(),
|
|
62
|
+
invites: z.array(z.string()).optional(),
|
|
63
|
+
alias: z.string().optional().default("DecentChat Bot"),
|
|
64
|
+
dataDir: z.string().optional(),
|
|
65
|
+
streamEnabled: z.boolean().optional().default(true),
|
|
66
|
+
dmPolicy: z.enum(["open", "pairing", "allowlist", "disabled"]).optional().default("open"),
|
|
67
|
+
defaultAccount: z.string().optional(),
|
|
68
|
+
replyToMode: z.enum(["off", "first", "all"]).optional().default("all"),
|
|
69
|
+
// Flattened from replyToModeByChatType object (Control UI can't render nested objects)
|
|
70
|
+
replyToModeDirect: z.enum(["off", "first", "all"]).optional(),
|
|
71
|
+
replyToModeGroup: z.enum(["off", "first", "all"]).optional(),
|
|
72
|
+
replyToModeChannel: z.enum(["off", "first", "all"]).optional(),
|
|
73
|
+
// Flattened from thread object
|
|
74
|
+
threadHistoryScope: z.enum(["thread", "channel"]).optional().default("thread"),
|
|
75
|
+
threadInheritParent: z.boolean().optional().default(false),
|
|
76
|
+
threadInitialHistoryLimit: z.number().int().min(0).optional().default(20),
|
|
77
|
+
companySimBootstrapEnabled: z.boolean().optional().default(false),
|
|
78
|
+
companySimBootstrapMode: z.enum(["runtime", "off"]).optional().default("runtime"),
|
|
79
|
+
companySimBootstrapManifestPath: z.string().optional(),
|
|
80
|
+
companySimBootstrapTargetWorkspaceId: z.string().optional(),
|
|
81
|
+
companySimBootstrapTargetInviteCode: z.string().optional(),
|
|
82
|
+
// Legacy nested forms still accepted at runtime via passthrough
|
|
83
|
+
// (resolveDecentChatAccount reads ch.replyToModeByChatType, ch.thread, ch.channels)
|
|
84
|
+
// but excluded from schema so Control UI can render all fields cleanly.
|
|
85
|
+
}).passthrough();
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
88
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getDecentChatChannelConfig(cfg: any): DecentChatChannelConfig {
|
|
92
|
+
const ch = cfg?.channels?.decentchat;
|
|
93
|
+
return isRecord(ch) ? (ch as DecentChatChannelConfig) : {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function listDecentChatAccountIds(cfg: any): string[] {
|
|
97
|
+
const channelCfg = getDecentChatChannelConfig(cfg);
|
|
98
|
+
const accounts = channelCfg.accounts;
|
|
99
|
+
if (!isRecord(accounts)) return [DEFAULT_ACCOUNT_ID];
|
|
100
|
+
const ids = Object.keys(accounts).map((id) => id.trim()).filter(Boolean);
|
|
101
|
+
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
|
102
|
+
return [...new Set(ids)].sort((a, b) => a.localeCompare(b));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveDefaultDecentChatAccountId(cfg: any): string {
|
|
106
|
+
const channelCfg = getDecentChatChannelConfig(cfg);
|
|
107
|
+
const ids = listDecentChatAccountIds(cfg);
|
|
108
|
+
const preferred = typeof channelCfg.defaultAccount === "string" ? channelCfg.defaultAccount.trim() : "";
|
|
109
|
+
if (preferred && ids.includes(preferred)) return preferred;
|
|
110
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
|
111
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function listDecentChatStartupAccountIds(cfg: any): string[] {
|
|
115
|
+
const ids = listDecentChatAccountIds(cfg);
|
|
116
|
+
if (!ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
117
|
+
return ids;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
DEFAULT_ACCOUNT_ID,
|
|
122
|
+
...ids.filter((accountId) => accountId !== DEFAULT_ACCOUNT_ID),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveDecentChatStartupDelayMs(cfg: any, accountId?: string | null): number {
|
|
127
|
+
const resolvedAccountId = accountId?.trim() || resolveDefaultDecentChatAccountId(cfg);
|
|
128
|
+
const startupOrder = listDecentChatStartupAccountIds(cfg);
|
|
129
|
+
const startupIndex = startupOrder.indexOf(resolvedAccountId);
|
|
130
|
+
if (startupIndex <= 0) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return startupIndex * DECENTCHAT_STARTUP_STAGGER_MS;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function waitForDecentChatStartupSlot(delayMs: number, abortSignal?: AbortSignal): Promise<void> {
|
|
138
|
+
if (delayMs <= 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await new Promise<void>((resolve, reject) => {
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
145
|
+
resolve();
|
|
146
|
+
}, delayMs);
|
|
147
|
+
|
|
148
|
+
const onAbort = () => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
151
|
+
reject(new Error("DecentChat startup aborted"));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (abortSignal?.aborted) {
|
|
155
|
+
onAbort();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeStringList(values: string[]): string[] {
|
|
164
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))]
|
|
165
|
+
.sort((a, b) => a.localeCompare(b));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildCompanyBootstrapRuntimeScope(cfg: any, manifestPath: string): string {
|
|
169
|
+
const accounts = listDecentChatAccountIds(cfg).map((accountId) => {
|
|
170
|
+
const account = resolveDecentChatAccount(cfg, accountId);
|
|
171
|
+
const bootstrap = account.companySimBootstrap;
|
|
172
|
+
return {
|
|
173
|
+
accountId,
|
|
174
|
+
seedFingerprint: account.seedPhrase
|
|
175
|
+
? createHash('sha256').update(account.seedPhrase).digest('hex').slice(0, 16)
|
|
176
|
+
: '',
|
|
177
|
+
dataDir: account.dataDir?.trim() ?? '',
|
|
178
|
+
companySimManifestPath: account.companySim?.manifestPath?.trim() ?? '',
|
|
179
|
+
invites: normalizeStringList(account.invites),
|
|
180
|
+
bootstrap: bootstrap ? {
|
|
181
|
+
enabled: bootstrap.enabled !== false,
|
|
182
|
+
mode: bootstrap.mode,
|
|
183
|
+
manifestPath: bootstrap.manifestPath?.trim() ?? '',
|
|
184
|
+
targetWorkspaceId: bootstrap.targetWorkspaceId?.trim() ?? '',
|
|
185
|
+
targetInviteCode: bootstrap.targetInviteCode?.trim() ?? '',
|
|
186
|
+
} : null,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return createHash('sha256')
|
|
191
|
+
.update(JSON.stringify({ manifestPath, accounts }))
|
|
192
|
+
.digest('hex')
|
|
193
|
+
.slice(0, 20);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mergeObject<T extends Record<string, any> | undefined>(base: T, override: T): T | undefined {
|
|
197
|
+
if (!base && !override) return undefined;
|
|
198
|
+
return {
|
|
199
|
+
...(base ?? {}),
|
|
200
|
+
...(override ?? {}),
|
|
201
|
+
} as T;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveRawDecentChatAccountConfig(cfg: any, accountId?: string | null): DecentChatChannelConfig {
|
|
205
|
+
const channelCfg = getDecentChatChannelConfig(cfg);
|
|
206
|
+
const resolvedAccountId = (accountId?.trim() || resolveDefaultDecentChatAccountId(cfg));
|
|
207
|
+
const accounts = isRecord(channelCfg.accounts) ? channelCfg.accounts : undefined;
|
|
208
|
+
const accountCfg = accounts && isRecord(accounts[resolvedAccountId])
|
|
209
|
+
? accounts[resolvedAccountId] as DecentChatChannelConfig
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
const {
|
|
213
|
+
accounts: _accounts,
|
|
214
|
+
defaultAccount: _defaultAccount,
|
|
215
|
+
...base
|
|
216
|
+
} = channelCfg;
|
|
217
|
+
const inheritRootStartupConfig = !accounts || resolvedAccountId === DEFAULT_ACCOUNT_ID;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...base,
|
|
221
|
+
...(accountCfg ?? {}),
|
|
222
|
+
invites: inheritRootStartupConfig ? (accountCfg?.invites ?? base.invites) : accountCfg?.invites,
|
|
223
|
+
channels: mergeObject(base.channels, accountCfg?.channels),
|
|
224
|
+
replyToModeByChatType: mergeObject(base.replyToModeByChatType, accountCfg?.replyToModeByChatType),
|
|
225
|
+
thread: mergeObject(base.thread, accountCfg?.thread),
|
|
226
|
+
huddle: mergeObject(base.huddle, accountCfg?.huddle),
|
|
227
|
+
companySim: mergeObject(base.companySim, accountCfg?.companySim),
|
|
228
|
+
companySimBootstrap: mergeObject(
|
|
229
|
+
inheritRootStartupConfig ? base.companySimBootstrap : undefined,
|
|
230
|
+
accountCfg?.companySimBootstrap,
|
|
231
|
+
),
|
|
232
|
+
companySimBootstrapEnabled: inheritRootStartupConfig
|
|
233
|
+
? (accountCfg?.companySimBootstrapEnabled ?? base.companySimBootstrapEnabled)
|
|
234
|
+
: accountCfg?.companySimBootstrapEnabled,
|
|
235
|
+
companySimBootstrapMode: inheritRootStartupConfig
|
|
236
|
+
? (accountCfg?.companySimBootstrapMode ?? base.companySimBootstrapMode)
|
|
237
|
+
: accountCfg?.companySimBootstrapMode,
|
|
238
|
+
companySimBootstrapManifestPath: inheritRootStartupConfig
|
|
239
|
+
? (accountCfg?.companySimBootstrapManifestPath ?? base.companySimBootstrapManifestPath)
|
|
240
|
+
: accountCfg?.companySimBootstrapManifestPath,
|
|
241
|
+
companySimBootstrapTargetWorkspaceId: inheritRootStartupConfig
|
|
242
|
+
? (accountCfg?.companySimBootstrapTargetWorkspaceId ?? base.companySimBootstrapTargetWorkspaceId)
|
|
243
|
+
: accountCfg?.companySimBootstrapTargetWorkspaceId,
|
|
244
|
+
companySimBootstrapTargetInviteCode: inheritRootStartupConfig
|
|
245
|
+
? (accountCfg?.companySimBootstrapTargetInviteCode ?? base.companySimBootstrapTargetInviteCode)
|
|
246
|
+
: accountCfg?.companySimBootstrapTargetInviteCode,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function normalizeDecentChatMessagingTarget(raw: string): string | undefined {
|
|
251
|
+
const value = raw.trim();
|
|
252
|
+
if (!value) return undefined;
|
|
253
|
+
|
|
254
|
+
if (value.startsWith("decentchat:channel:")) {
|
|
255
|
+
const channelId = value.slice("decentchat:channel:".length).trim();
|
|
256
|
+
return channelId ? `decentchat:channel:${channelId}` : undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (value.startsWith("channel:")) {
|
|
260
|
+
const channelId = value.slice("channel:".length).trim();
|
|
261
|
+
return channelId ? `decentchat:channel:${channelId}` : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (value.startsWith("decentchat:")) {
|
|
265
|
+
const rest = value.slice("decentchat:".length).trim();
|
|
266
|
+
if (!rest) return undefined;
|
|
267
|
+
if (rest.startsWith("channel:")) {
|
|
268
|
+
const channelId = rest.slice("channel:".length).trim();
|
|
269
|
+
return channelId ? `decentchat:channel:${channelId}` : undefined;
|
|
270
|
+
}
|
|
271
|
+
return `decentchat:${rest}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return `decentchat:${value}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const DECENTCHAT_CHANNEL_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
278
|
+
const DECENTCHAT_PEER_ID_RE = /^[0-9a-f]{18}$/i;
|
|
279
|
+
const DECENTCHAT_TEST_PEER_ID_RE = /^peer-[a-z0-9-]+$/i;
|
|
280
|
+
|
|
281
|
+
type DecentChatTargetCandidate = {
|
|
282
|
+
kind: "user" | "group";
|
|
283
|
+
id: string;
|
|
284
|
+
name?: string;
|
|
285
|
+
handle?: string;
|
|
286
|
+
rank?: number;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
type DecentChatResolvedTarget = {
|
|
290
|
+
to: string;
|
|
291
|
+
kind: "user" | "group" | "channel";
|
|
292
|
+
display?: string;
|
|
293
|
+
source: "normalized" | "directory";
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function looksLikeBareDecentChatPeerId(value: string): boolean {
|
|
297
|
+
return DECENTCHAT_PEER_ID_RE.test(value) || DECENTCHAT_TEST_PEER_ID_RE.test(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeDecentChatLookupValue(value: string | undefined): string {
|
|
301
|
+
if (!value) return "";
|
|
302
|
+
return value.trim().toLowerCase()
|
|
303
|
+
.replace(/^decentchat:channel:/, "")
|
|
304
|
+
.replace(/^decentchat:/, "")
|
|
305
|
+
.replace(/^channel:/, "")
|
|
306
|
+
.replace(/^[@#]/, "")
|
|
307
|
+
.trim();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function scoreDecentChatTargetCandidate(candidate: DecentChatTargetCandidate, query: string): number {
|
|
311
|
+
const fields = [candidate.name, candidate.handle, candidate.id];
|
|
312
|
+
let best = -1;
|
|
313
|
+
for (const field of fields) {
|
|
314
|
+
const normalizedField = normalizeDecentChatLookupValue(field);
|
|
315
|
+
if (!normalizedField) continue;
|
|
316
|
+
if (normalizedField === query) return 300;
|
|
317
|
+
if (normalizedField.startsWith(query)) best = Math.max(best, 200);
|
|
318
|
+
else if (normalizedField.includes(query)) best = Math.max(best, 100);
|
|
319
|
+
}
|
|
320
|
+
return best;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function pickUniqueDecentChatCandidate(candidates: DecentChatTargetCandidate[], query: string): DecentChatTargetCandidate | null {
|
|
324
|
+
const scored = candidates
|
|
325
|
+
.map((candidate) => ({ candidate, score: scoreDecentChatTargetCandidate(candidate, query) }))
|
|
326
|
+
.filter((item) => item.score >= 0)
|
|
327
|
+
.sort((a, b) => b.score - a.score || (b.candidate.rank ?? 0) - (a.candidate.rank ?? 0) || (a.candidate.name ?? a.candidate.id).localeCompare(b.candidate.name ?? b.candidate.id));
|
|
328
|
+
|
|
329
|
+
if (scored.length === 0) return null;
|
|
330
|
+
const bestScore = scored[0]?.score ?? -1;
|
|
331
|
+
const best = scored.filter((item) => item.score == bestScore);
|
|
332
|
+
if (best.length !== 1) return null;
|
|
333
|
+
return best[0]?.candidate ?? null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildDecentChatResolvedTarget(candidate: DecentChatTargetCandidate): DecentChatResolvedTarget {
|
|
337
|
+
return {
|
|
338
|
+
to: candidate.kind === "group"
|
|
339
|
+
? (candidate.id.startsWith("decentchat:channel:") ? candidate.id : `decentchat:channel:${candidate.id}`)
|
|
340
|
+
: (candidate.id.startsWith("decentchat:") ? candidate.id : `decentchat:${candidate.id}`),
|
|
341
|
+
kind: candidate.kind,
|
|
342
|
+
display: candidate.name ?? candidate.handle ?? candidate.id,
|
|
343
|
+
source: "directory",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function resolveDecentChatTargetFromActivePeer(raw: string, accountId?: string | null, preferredKind?: "user" | "group" | "channel"): DecentChatResolvedTarget | null {
|
|
348
|
+
const query = normalizeDecentChatLookupValue(raw);
|
|
349
|
+
if (!query) return null;
|
|
350
|
+
|
|
351
|
+
const peer = getActivePeer(accountId?.trim() || DEFAULT_ACCOUNT_ID);
|
|
352
|
+
if (!peer) return null;
|
|
353
|
+
|
|
354
|
+
const peerMatches = peer.listDirectoryPeersLive({ query, limit: 50 }) as DecentChatTargetCandidate[];
|
|
355
|
+
const groupMatches = peer.listDirectoryGroupsLive({ query, limit: 50 }) as DecentChatTargetCandidate[];
|
|
356
|
+
|
|
357
|
+
const userCandidate = pickUniqueDecentChatCandidate(peerMatches, query);
|
|
358
|
+
const groupCandidate = pickUniqueDecentChatCandidate(groupMatches, query);
|
|
359
|
+
|
|
360
|
+
if (preferredKind === "user") return userCandidate ? buildDecentChatResolvedTarget(userCandidate) : null;
|
|
361
|
+
if (preferredKind === "group" || preferredKind === "channel") return groupCandidate ? buildDecentChatResolvedTarget(groupCandidate) : null;
|
|
362
|
+
|
|
363
|
+
if (userCandidate && !groupCandidate) return buildDecentChatResolvedTarget(userCandidate);
|
|
364
|
+
if (groupCandidate && !userCandidate) return buildDecentChatResolvedTarget(groupCandidate);
|
|
365
|
+
if (userCandidate && groupCandidate) return buildDecentChatResolvedTarget(userCandidate);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function resolveDecentChatTarget(params: {
|
|
370
|
+
accountId?: string | null;
|
|
371
|
+
input: string;
|
|
372
|
+
normalized: string;
|
|
373
|
+
preferredKind?: "user" | "group" | "channel";
|
|
374
|
+
}): Promise<DecentChatResolvedTarget | null> {
|
|
375
|
+
const rawValue = params.input.trim();
|
|
376
|
+
if (!rawValue) return null;
|
|
377
|
+
|
|
378
|
+
if (rawValue.startsWith("channel:")) {
|
|
379
|
+
const channelId = rawValue.slice("channel:".length).trim();
|
|
380
|
+
return channelId ? { to: `decentchat:channel:${channelId}`, kind: "group", display: channelId, source: "normalized" } : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (rawValue.startsWith("decentchat:channel:")) {
|
|
384
|
+
const channelId = rawValue.slice("decentchat:channel:".length).trim();
|
|
385
|
+
return channelId ? { to: `decentchat:channel:${channelId}`, kind: "group", display: channelId, source: "normalized" } : null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (rawValue.startsWith("decentchat:")) {
|
|
389
|
+
const rest = rawValue.slice("decentchat:".length).trim();
|
|
390
|
+
if (!rest) return null;
|
|
391
|
+
if (rest.startsWith("channel:")) {
|
|
392
|
+
const channelId = rest.slice("channel:".length).trim();
|
|
393
|
+
return channelId ? { to: `decentchat:channel:${channelId}`, kind: "group", display: channelId, source: "normalized" } : null;
|
|
394
|
+
}
|
|
395
|
+
return { to: `decentchat:${rest}`, kind: "user", display: rest, source: "normalized" };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (DECENTCHAT_CHANNEL_ID_RE.test(rawValue)) {
|
|
399
|
+
return { to: `decentchat:channel:${rawValue}`, kind: "group", display: rawValue, source: "normalized" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (looksLikeBareDecentChatPeerId(rawValue)) {
|
|
403
|
+
return { to: `decentchat:${rawValue}`, kind: "user", display: rawValue, source: "normalized" };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return resolveDecentChatTargetFromActivePeer(rawValue, params.accountId, params.preferredKind);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function looksLikeDecentChatTargetId(raw: string, normalized?: string): boolean {
|
|
410
|
+
const rawValue = raw.trim();
|
|
411
|
+
const normalizedValue = (normalized ?? raw).trim();
|
|
412
|
+
if (!rawValue && !normalizedValue) return false;
|
|
413
|
+
|
|
414
|
+
if (rawValue.startsWith("channel:")) return true;
|
|
415
|
+
if (rawValue.startsWith("decentchat:channel:")) return true;
|
|
416
|
+
if (rawValue.startsWith("decentchat:")) return true;
|
|
417
|
+
|
|
418
|
+
if (DECENTCHAT_CHANNEL_ID_RE.test(rawValue)) return true;
|
|
419
|
+
if (looksLikeBareDecentChatPeerId(rawValue)) return true;
|
|
420
|
+
|
|
421
|
+
const accountIds = listActivePeerAccountIds();
|
|
422
|
+
if (accountIds.length === 0) return false;
|
|
423
|
+
return accountIds.some((accountId) => !!resolveDecentChatTargetFromActivePeer(rawValue, accountId));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function resolveDecentChatAccount(cfg: any, accountId?: string | null): ResolvedDecentChatAccount {
|
|
427
|
+
const ch = resolveRawDecentChatAccountConfig(cfg, accountId);
|
|
428
|
+
const resolvedAccountId = accountId?.trim() || resolveDefaultDecentChatAccountId(cfg);
|
|
429
|
+
const seedPhrase = typeof ch.seedPhrase === "string" ? ch.seedPhrase : undefined;
|
|
430
|
+
const bootstrapEnabledRaw = (ch as any).companySimBootstrapEnabled ?? ch.companySimBootstrap?.enabled;
|
|
431
|
+
const bootstrapModeRaw = (ch as any).companySimBootstrapMode ?? ch.companySimBootstrap?.mode;
|
|
432
|
+
const bootstrapManifestPathRaw = (ch as any).companySimBootstrapManifestPath ?? ch.companySimBootstrap?.manifestPath;
|
|
433
|
+
const bootstrapTargetWorkspaceIdRaw = (ch as any).companySimBootstrapTargetWorkspaceId ?? ch.companySimBootstrap?.targetWorkspaceId;
|
|
434
|
+
const bootstrapTargetInviteCodeRaw = (ch as any).companySimBootstrapTargetInviteCode ?? ch.companySimBootstrap?.targetInviteCode;
|
|
435
|
+
const hasBootstrapConfig = bootstrapEnabledRaw !== undefined
|
|
436
|
+
|| bootstrapModeRaw !== undefined
|
|
437
|
+
|| typeof bootstrapManifestPathRaw === "string"
|
|
438
|
+
|| typeof bootstrapTargetWorkspaceIdRaw === "string"
|
|
439
|
+
|| typeof bootstrapTargetInviteCodeRaw === "string";
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
accountId: resolvedAccountId,
|
|
443
|
+
enabled: ch.enabled !== false,
|
|
444
|
+
dmPolicy: ch.dmPolicy ?? "open",
|
|
445
|
+
configured: !!seedPhrase?.trim(),
|
|
446
|
+
seedPhrase,
|
|
447
|
+
signalingServer: ch.signalingServer ?? "https://decentchat.app/peerjs",
|
|
448
|
+
invites: ch.invites ?? [],
|
|
449
|
+
alias: ch.alias ?? "DecentChat Bot",
|
|
450
|
+
dataDir: resolveDecentChatDataDir(cfg, resolvedAccountId, ch.dataDir),
|
|
451
|
+
streamEnabled: ch.streamEnabled !== false,
|
|
452
|
+
replyToMode: ch.replyToMode ?? "all",
|
|
453
|
+
replyToModeByChatType: {
|
|
454
|
+
direct: (ch as any).replyToModeDirect ?? ch.replyToModeByChatType?.direct,
|
|
455
|
+
group: (ch as any).replyToModeGroup ?? ch.replyToModeByChatType?.group,
|
|
456
|
+
channel: (ch as any).replyToModeChannel ?? ch.replyToModeByChatType?.channel,
|
|
457
|
+
},
|
|
458
|
+
thread: {
|
|
459
|
+
historyScope: (ch as any).threadHistoryScope ?? ch.thread?.historyScope ?? "thread",
|
|
460
|
+
inheritParent: (ch as any).threadInheritParent ?? ch.thread?.inheritParent ?? false,
|
|
461
|
+
initialHistoryLimit: (ch as any).threadInitialHistoryLimit ?? ch.thread?.initialHistoryLimit ?? 20,
|
|
462
|
+
},
|
|
463
|
+
huddle: ch.huddle ? {
|
|
464
|
+
enabled: ch.huddle.enabled,
|
|
465
|
+
autoJoin: ch.huddle.autoJoin,
|
|
466
|
+
sttEngine: ch.huddle.sttEngine,
|
|
467
|
+
whisperModel: ch.huddle.whisperModel,
|
|
468
|
+
sttLanguage: ch.huddle.sttLanguage,
|
|
469
|
+
sttApiKey: ch.huddle.sttApiKey,
|
|
470
|
+
ttsVoice: ch.huddle.ttsVoice,
|
|
471
|
+
vadSilenceMs: ch.huddle.vadSilenceMs,
|
|
472
|
+
vadThreshold: ch.huddle.vadThreshold,
|
|
473
|
+
} : undefined,
|
|
474
|
+
companySim: ch.companySim ? {
|
|
475
|
+
enabled: ch.companySim.enabled !== false,
|
|
476
|
+
manifestPath: ch.companySim.manifestPath,
|
|
477
|
+
companyId: ch.companySim.companyId,
|
|
478
|
+
employeeId: ch.companySim.employeeId,
|
|
479
|
+
roleFilesDir: ch.companySim.roleFilesDir,
|
|
480
|
+
silentChannelIds: Array.isArray(ch.companySim.silentChannelIds)
|
|
481
|
+
? normalizeStringList(ch.companySim.silentChannelIds.filter((value): value is string => typeof value === 'string'))
|
|
482
|
+
: undefined,
|
|
483
|
+
} : undefined,
|
|
484
|
+
companySimBootstrap: hasBootstrapConfig ? {
|
|
485
|
+
enabled: bootstrapEnabledRaw !== false,
|
|
486
|
+
mode: bootstrapModeRaw === "off" ? "off" : "runtime",
|
|
487
|
+
manifestPath: typeof bootstrapManifestPathRaw === "string" ? bootstrapManifestPathRaw : undefined,
|
|
488
|
+
targetWorkspaceId: typeof bootstrapTargetWorkspaceIdRaw === "string" && bootstrapTargetWorkspaceIdRaw.trim()
|
|
489
|
+
? bootstrapTargetWorkspaceIdRaw.trim()
|
|
490
|
+
: undefined,
|
|
491
|
+
targetInviteCode: typeof bootstrapTargetInviteCodeRaw === "string" && bootstrapTargetInviteCodeRaw.trim()
|
|
492
|
+
? bootstrapTargetInviteCodeRaw.trim()
|
|
493
|
+
: undefined,
|
|
494
|
+
} : undefined,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getPeerForContext(cfg: any, accountId?: string | null) {
|
|
499
|
+
const resolvedAccountId = accountId?.trim() || resolveDefaultDecentChatAccountId(cfg);
|
|
500
|
+
return getActivePeer(resolvedAccountId);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function bootstrapDecentChatCompanySimForStartup(params: {
|
|
504
|
+
cfg: any;
|
|
505
|
+
accountId: string;
|
|
506
|
+
account: ResolvedDecentChatAccount;
|
|
507
|
+
log?: { info?: (message: string) => void; warn?: (message: string) => void; error?: (message: string) => void };
|
|
508
|
+
}): Promise<void> {
|
|
509
|
+
const bootstrap = params.account.companySimBootstrap;
|
|
510
|
+
if (!bootstrap?.enabled || bootstrap.mode === "off") return;
|
|
511
|
+
|
|
512
|
+
const manifestPath = bootstrap.manifestPath?.trim();
|
|
513
|
+
if (!manifestPath) {
|
|
514
|
+
throw new Error(`Company bootstrap is enabled for account ${params.accountId} but companySimBootstrapManifestPath is missing`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const resolvedManifestPath = resolveCompanyManifestPath(manifestPath);
|
|
518
|
+
const runtimeScope = buildCompanyBootstrapRuntimeScope(params.cfg, resolvedManifestPath);
|
|
519
|
+
|
|
520
|
+
await runDecentChatBootstrapOnce(buildDecentChatRuntimeBootstrapKey(resolvedManifestPath, runtimeScope), async () => {
|
|
521
|
+
assertCompanyBootstrapAgentInstallation({
|
|
522
|
+
manifestPath: resolvedManifestPath,
|
|
523
|
+
cfg: params.cfg as OpenClawConfigShape,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await ensureCompanyBootstrapRuntime({
|
|
527
|
+
manifestPath: resolvedManifestPath,
|
|
528
|
+
accountIds: listDecentChatAccountIds(params.cfg),
|
|
529
|
+
resolveAccount: (accountId) => resolveDecentChatAccount(params.cfg, accountId),
|
|
530
|
+
log: params.log,
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export const decentChatPlugin: ChannelPlugin<ResolvedDecentChatAccount> = {
|
|
536
|
+
id: "decentchat",
|
|
537
|
+
meta: {
|
|
538
|
+
id: "decentchat",
|
|
539
|
+
label: "DecentChat",
|
|
540
|
+
selectionLabel: "DecentChat (P2P)",
|
|
541
|
+
docsPath: "/channels/decentchat",
|
|
542
|
+
blurb: "P2P encrypted chat via DecentChat.",
|
|
543
|
+
aliases: ["decent", "decentchat"],
|
|
544
|
+
},
|
|
545
|
+
capabilities: { chatTypes: ["direct", "group", "thread"], threads: true, media: true },
|
|
546
|
+
reload: { configPrefixes: ["channels.decentchat"] },
|
|
547
|
+
configSchema: {
|
|
548
|
+
...buildChannelConfigSchema(DecentChatConfigSchema),
|
|
549
|
+
uiHints: {
|
|
550
|
+
enabled: { label: "Enabled" },
|
|
551
|
+
seedPhrase: { label: "Seed Phrase (12 words)", sensitive: true, help: "BIP39 seed phrase — determines your bot's identity on the network" },
|
|
552
|
+
signalingServer: { label: "Signaling Server", placeholder: "https://decentchat.app/peerjs", advanced: true },
|
|
553
|
+
alias: { label: "Bot Display Name", placeholder: "DecentChat Bot" },
|
|
554
|
+
dataDir: { label: "Data Directory", advanced: true, help: "Path for persistent peer storage" },
|
|
555
|
+
streamEnabled: { label: "Enable streaming", help: "Stream token deltas to peers in real time" },
|
|
556
|
+
dmPolicy: { label: "DM Policy" },
|
|
557
|
+
defaultAccount: { label: "Default account", advanced: true, help: "Preferred DecentChat account id when multiple accounts are configured" },
|
|
558
|
+
replyToMode: { label: "Reply-to mode", help: "off|first|all — controls thread reply behavior" },
|
|
559
|
+
replyToModeDirect: { label: "Reply-to mode (DMs)", help: "Override for direct messages" },
|
|
560
|
+
replyToModeGroup: { label: "Reply-to mode (Groups)", help: "Override for group chats" },
|
|
561
|
+
replyToModeChannel: { label: "Reply-to mode (Channels)", help: "Override for channels" },
|
|
562
|
+
threadHistoryScope: { label: "Thread history scope", help: "thread = isolated, channel = shared context", advanced: true },
|
|
563
|
+
threadInheritParent: { label: "Thread inherit parent", help: "Thread sessions inherit parent channel context", advanced: true },
|
|
564
|
+
threadInitialHistoryLimit: { label: "Thread initial history limit", help: "Messages to bootstrap in new thread sessions", advanced: true },
|
|
565
|
+
companySimBootstrapEnabled: { label: "Company bootstrap enabled", advanced: true },
|
|
566
|
+
companySimBootstrapMode: { label: "Company bootstrap mode", advanced: true, help: "runtime = materialize company workspace on account startup" },
|
|
567
|
+
companySimBootstrapManifestPath: { label: "Company manifest path", advanced: true, help: "Path to company.yaml (supports relative paths from current working directory)" },
|
|
568
|
+
companySimBootstrapTargetWorkspaceId: { label: "Company target workspace id", advanced: true, help: "Pinned workspace id for runtime company bootstrap membership" },
|
|
569
|
+
companySimBootstrapTargetInviteCode: { label: "Company target invite code", advanced: true, help: "Pinned invite code for runtime company bootstrap membership" },
|
|
570
|
+
invites: { label: "Invite URLs", advanced: true, help: "DecentChat invite URIs for workspaces to join on startup" },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
config: {
|
|
575
|
+
listAccountIds: (cfg) => listDecentChatAccountIds(cfg),
|
|
576
|
+
resolveAccount: (cfg, accountId) => resolveDecentChatAccount(cfg, accountId),
|
|
577
|
+
defaultAccountId: (cfg) => resolveDefaultDecentChatAccountId(cfg),
|
|
578
|
+
isConfigured: (account) => account.configured,
|
|
579
|
+
describeAccount: (account) => ({
|
|
580
|
+
accountId: account.accountId,
|
|
581
|
+
enabled: account.enabled,
|
|
582
|
+
configured: account.configured,
|
|
583
|
+
signalingServer: account.signalingServer,
|
|
584
|
+
companySim: account.companySim?.enabled ? {
|
|
585
|
+
companyId: account.companySim.companyId,
|
|
586
|
+
employeeId: account.companySim.employeeId,
|
|
587
|
+
} : undefined,
|
|
588
|
+
companySimBootstrap: account.companySimBootstrap?.enabled ? {
|
|
589
|
+
mode: account.companySimBootstrap.mode,
|
|
590
|
+
manifestPath: account.companySimBootstrap.manifestPath,
|
|
591
|
+
targetWorkspaceId: account.companySimBootstrap.targetWorkspaceId,
|
|
592
|
+
targetInviteCode: account.companySimBootstrap.targetInviteCode,
|
|
593
|
+
} : undefined,
|
|
594
|
+
}),
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
security: {
|
|
598
|
+
resolveDmPolicy: ({ account }) => ({
|
|
599
|
+
policy: account.dmPolicy ?? "open",
|
|
600
|
+
allowFrom: [],
|
|
601
|
+
policyPath: "channels.decentchat.dmPolicy",
|
|
602
|
+
allowFromPath: "channels.decentchat.allowFrom",
|
|
603
|
+
approveHint: formatPairingApproveHint("decentchat"),
|
|
604
|
+
normalizeEntry: (raw: string) => raw.trim(),
|
|
605
|
+
}),
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
threading: {
|
|
609
|
+
resolveReplyToMode: ({ cfg, accountId, chatType }) => {
|
|
610
|
+
const account = resolveDecentChatAccount(cfg, accountId);
|
|
611
|
+
if (chatType === "direct") {
|
|
612
|
+
return account.replyToModeByChatType.direct ?? account.replyToMode;
|
|
613
|
+
}
|
|
614
|
+
if (chatType === "group") {
|
|
615
|
+
return account.replyToModeByChatType.group ?? account.replyToModeByChatType.channel ?? account.replyToMode;
|
|
616
|
+
}
|
|
617
|
+
if (chatType === "channel") {
|
|
618
|
+
return account.replyToModeByChatType.channel ?? account.replyToModeByChatType.group ?? account.replyToMode;
|
|
619
|
+
}
|
|
620
|
+
return account.replyToMode;
|
|
621
|
+
},
|
|
622
|
+
allowExplicitReplyTagsWhenOff: true,
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
streaming: {
|
|
626
|
+
blockStreamingCoalesceDefaults: { minChars: 1, idleMs: 0 },
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
groups: {
|
|
630
|
+
resolveRequireMention: ({ cfg, groupId }) => {
|
|
631
|
+
const chCfg = resolveRawDecentChatAccountConfig(cfg);
|
|
632
|
+
const grpCfg = chCfg.channels?.[groupId] ?? chCfg.channels?.["*"];
|
|
633
|
+
return grpCfg?.requireMention ?? true;
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
messaging: {
|
|
638
|
+
normalizeTarget: normalizeDecentChatMessagingTarget,
|
|
639
|
+
targetResolver: {
|
|
640
|
+
looksLikeId: looksLikeDecentChatTargetId,
|
|
641
|
+
hint: "<peerId|channel:<id>|decentchat:channel:<id>|peer alias>",
|
|
642
|
+
resolveTarget: async ({ accountId, input, normalized, preferredKind }) => resolveDecentChatTarget({
|
|
643
|
+
accountId,
|
|
644
|
+
input,
|
|
645
|
+
normalized,
|
|
646
|
+
preferredKind: preferredKind as "user" | "group" | "channel" | undefined,
|
|
647
|
+
}),
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
outbound: {
|
|
652
|
+
deliveryMode: "direct",
|
|
653
|
+
sendText: async (ctx) => {
|
|
654
|
+
const peer = getPeerForContext(ctx.cfg, ctx.accountId);
|
|
655
|
+
if (!peer) return { ok: false, error: new Error("DecentChat peer not running") };
|
|
656
|
+
|
|
657
|
+
const { to, text, replyToId, threadId } = ctx;
|
|
658
|
+
const threadIdStr = threadId != null
|
|
659
|
+
? String(threadId)
|
|
660
|
+
: (replyToId != null ? String(replyToId) : undefined);
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
if (to.startsWith("decentchat:channel:")) {
|
|
664
|
+
const channelId = to.slice("decentchat:channel:".length);
|
|
665
|
+
await peer.sendToChannel(channelId, text, threadIdStr, replyToId ?? undefined);
|
|
666
|
+
} else {
|
|
667
|
+
const peerId = to.startsWith("decentchat:") ? to.slice("decentchat:".length) : to;
|
|
668
|
+
await peer.sendDirectToPeer(peerId, text, threadIdStr, replyToId ?? undefined);
|
|
669
|
+
}
|
|
670
|
+
return { ok: true };
|
|
671
|
+
} catch (err) {
|
|
672
|
+
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
directory: {
|
|
678
|
+
self: async ({ cfg, accountId }) => {
|
|
679
|
+
const account = resolveDecentChatAccount(cfg, accountId);
|
|
680
|
+
const peer = getPeerForContext(cfg, accountId);
|
|
681
|
+
if (!peer?.peerId) return null;
|
|
682
|
+
return {
|
|
683
|
+
kind: "user" as const,
|
|
684
|
+
id: peer.peerId,
|
|
685
|
+
name: account.alias,
|
|
686
|
+
handle: `decentchat:${peer.peerId}`,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
690
|
+
const peer = getPeerForContext(cfg, accountId);
|
|
691
|
+
if (!peer) return [];
|
|
692
|
+
return peer.listDirectoryPeersLive({ query, limit });
|
|
693
|
+
},
|
|
694
|
+
listPeersLive: async ({ cfg, accountId, query, limit }) => {
|
|
695
|
+
const peer = getPeerForContext(cfg, accountId);
|
|
696
|
+
if (!peer) return [];
|
|
697
|
+
return peer.listDirectoryPeersLive({ query, limit });
|
|
698
|
+
},
|
|
699
|
+
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
700
|
+
const peer = getPeerForContext(cfg, accountId);
|
|
701
|
+
if (!peer) return [];
|
|
702
|
+
return peer.listDirectoryGroupsLive({ query, limit });
|
|
703
|
+
},
|
|
704
|
+
listGroupsLive: async ({ cfg, accountId, query, limit }) => {
|
|
705
|
+
const peer = getPeerForContext(cfg, accountId);
|
|
706
|
+
if (!peer) return [];
|
|
707
|
+
return peer.listDirectoryGroupsLive({ query, limit });
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
status: {
|
|
712
|
+
defaultRuntime: {
|
|
713
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
714
|
+
running: false,
|
|
715
|
+
lastError: null,
|
|
716
|
+
},
|
|
717
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
718
|
+
configured: snapshot.configured ?? false,
|
|
719
|
+
running: snapshot.running ?? false,
|
|
720
|
+
lastError: snapshot.lastError ?? null,
|
|
721
|
+
}),
|
|
722
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
723
|
+
accountId: account.accountId,
|
|
724
|
+
enabled: account.enabled,
|
|
725
|
+
configured: account.configured,
|
|
726
|
+
running: runtime?.running ?? false,
|
|
727
|
+
lastError: runtime?.lastError ?? null,
|
|
728
|
+
companySim: account.companySim?.enabled ? {
|
|
729
|
+
companyId: account.companySim.companyId,
|
|
730
|
+
employeeId: account.companySim.employeeId,
|
|
731
|
+
} : undefined,
|
|
732
|
+
companySimBootstrap: account.companySimBootstrap?.enabled ? {
|
|
733
|
+
mode: account.companySimBootstrap.mode,
|
|
734
|
+
manifestPath: account.companySimBootstrap.manifestPath,
|
|
735
|
+
targetWorkspaceId: account.companySimBootstrap.targetWorkspaceId,
|
|
736
|
+
targetInviteCode: account.companySimBootstrap.targetInviteCode,
|
|
737
|
+
} : undefined,
|
|
738
|
+
}),
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
gateway: {
|
|
742
|
+
startAccount: async (ctx) => {
|
|
743
|
+
ctx.setStatus({
|
|
744
|
+
accountId: ctx.accountId,
|
|
745
|
+
running: false,
|
|
746
|
+
configured: ctx.account.configured,
|
|
747
|
+
});
|
|
748
|
+
try {
|
|
749
|
+
// Invalidate bootstrap guard so re-bootstrap runs on gateway restart.
|
|
750
|
+
// Without this, the module-level `bootstrapCompleted` Set would skip
|
|
751
|
+
// bootstrap on the second startAccount call within the same process.
|
|
752
|
+
const bootstrap = ctx.account.companySimBootstrap;
|
|
753
|
+
if (bootstrap?.enabled && bootstrap.mode !== "off") {
|
|
754
|
+
const manifestPath = bootstrap.manifestPath?.trim();
|
|
755
|
+
if (manifestPath) {
|
|
756
|
+
const resolvedManifestPath = resolveCompanyManifestPath(manifestPath);
|
|
757
|
+
const runtimeScope = buildCompanyBootstrapRuntimeScope(ctx.cfg, resolvedManifestPath);
|
|
758
|
+
invalidateDecentChatBootstrapKey(buildDecentChatRuntimeBootstrapKey(resolvedManifestPath, runtimeScope));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
await bootstrapDecentChatCompanySimForStartup({
|
|
763
|
+
cfg: ctx.cfg,
|
|
764
|
+
accountId: ctx.accountId,
|
|
765
|
+
account: ctx.account,
|
|
766
|
+
log: ctx.log,
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const startupDelayMs = resolveDecentChatStartupDelayMs(ctx.cfg, ctx.accountId);
|
|
770
|
+
if (startupDelayMs > 0) {
|
|
771
|
+
ctx.log?.info?.(`[${ctx.accountId}] startup stagger ${startupDelayMs}ms`);
|
|
772
|
+
await waitForDecentChatStartupSlot(startupDelayMs, ctx.abortSignal);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
await startDecentChatPeer({
|
|
776
|
+
account: ctx.account,
|
|
777
|
+
accountId: ctx.accountId,
|
|
778
|
+
log: ctx.log,
|
|
779
|
+
setStatus: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
780
|
+
abortSignal: ctx.abortSignal,
|
|
781
|
+
});
|
|
782
|
+
} catch (err) {
|
|
783
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
784
|
+
ctx.setStatus({ accountId: ctx.accountId, running: false, lastError: message });
|
|
785
|
+
throw err;
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
};
|