@botcord/daemon 0.2.51 → 0.2.53
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/dist/gateway/channels/http-types.d.ts +4 -1
- package/dist/gateway/channels/wechat.js +152 -19
- package/dist/gateway/types.d.ts +10 -0
- package/dist/index.js +85 -15
- package/dist/openclaw-discovery.js +33 -2
- package/dist/provision.js +55 -13
- package/dist/start-auth.js +2 -2
- package/dist/turn-text.js +9 -0
- package/package.json +1 -1
- package/src/__tests__/openclaw-discovery.test.ts +37 -2
- package/src/__tests__/provision.test.ts +67 -1
- package/src/__tests__/start-auth.test.ts +2 -2
- package/src/__tests__/turn-text.test.ts +3 -0
- package/src/__tests__/wechat-channel.test.ts +126 -8
- package/src/gateway/channels/http-types.ts +2 -1
- package/src/gateway/channels/wechat.ts +180 -19
- package/src/gateway/types.ts +11 -0
- package/src/index.ts +83 -16
- package/src/openclaw-discovery.ts +33 -2
- package/src/provision.ts +51 -12
- package/src/start-auth.ts +1 -1
- package/src/turn-text.ts +13 -0
package/src/index.ts
CHANGED
|
@@ -231,6 +231,44 @@ function pidAlive(pid: number): boolean {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
235
|
+
const deadline = Date.now() + timeoutMs;
|
|
236
|
+
while (Date.now() < deadline) {
|
|
237
|
+
if (!pidAlive(pid)) return true;
|
|
238
|
+
await delay(100);
|
|
239
|
+
}
|
|
240
|
+
return !pidAlive(pid);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function stopExistingDaemonForRestart(pid: number): Promise<void> {
|
|
244
|
+
if (pid === process.pid) return;
|
|
245
|
+
log.info("existing daemon found; restarting", { pid });
|
|
246
|
+
try {
|
|
247
|
+
process.kill(pid, "SIGTERM");
|
|
248
|
+
} catch {
|
|
249
|
+
try {
|
|
250
|
+
unlinkSync(PID_PATH);
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!(await waitForPidExit(pid, 5_000))) {
|
|
257
|
+
log.warn("existing daemon did not stop after SIGTERM; sending SIGKILL", { pid });
|
|
258
|
+
try {
|
|
259
|
+
process.kill(pid, "SIGKILL");
|
|
260
|
+
} catch {
|
|
261
|
+
// ignore
|
|
262
|
+
}
|
|
263
|
+
await waitForPidExit(pid, 2_000);
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
unlinkSync(PID_PATH);
|
|
267
|
+
} catch {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
234
272
|
/**
|
|
235
273
|
* Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
|
|
236
274
|
* with sensible defaults on first run. `--agent` (repeated) pins explicit
|
|
@@ -323,9 +361,11 @@ async function redeemInstallToken(opts: {
|
|
|
323
361
|
hubUrl: string;
|
|
324
362
|
installToken: string;
|
|
325
363
|
label?: string;
|
|
364
|
+
daemonInstanceId?: string;
|
|
326
365
|
}): Promise<DaemonTokenResponse> {
|
|
327
366
|
const body: Record<string, unknown> = { install_token: opts.installToken };
|
|
328
367
|
if (opts.label) body.label = opts.label;
|
|
368
|
+
if (opts.daemonInstanceId) body.daemon_instance_id = opts.daemonInstanceId;
|
|
329
369
|
const resp = await fetch(`${opts.hubUrl.replace(/\/+$/, "")}/daemon/auth/install-token`, {
|
|
330
370
|
method: "POST",
|
|
331
371
|
headers: { "Content-Type": "application/json" },
|
|
@@ -334,7 +374,9 @@ async function redeemInstallToken(opts: {
|
|
|
334
374
|
});
|
|
335
375
|
if (!resp.ok) {
|
|
336
376
|
const text = await resp.text().catch(() => "");
|
|
337
|
-
|
|
377
|
+
const err = new Error(`daemon install-token redeem failed: ${resp.status} ${text}`);
|
|
378
|
+
(err as unknown as { status?: number }).status = resp.status;
|
|
379
|
+
throw err;
|
|
338
380
|
}
|
|
339
381
|
return parseDaemonTokenResponse(await resp.json(), opts.hubUrl);
|
|
340
382
|
}
|
|
@@ -418,10 +460,10 @@ async function runDeviceCodeFlow(opts: {
|
|
|
418
460
|
* plane (legacy P0 behavior — caller may still log a warning).
|
|
419
461
|
*
|
|
420
462
|
* Decision tree (plan §4.4 + §6.4):
|
|
421
|
-
* 1.
|
|
422
|
-
*
|
|
423
|
-
* the
|
|
424
|
-
* 2.
|
|
463
|
+
* 1. `--install-token` → redeem the one-time dashboard ticket. If local
|
|
464
|
+
* user-auth exists, include its daemonInstanceId so Hub can re-authorize
|
|
465
|
+
* the same device instead of creating a new one.
|
|
466
|
+
* 2. Have existing creds and no `--relogin` → return existing record.
|
|
425
467
|
* 3. `--relogin` → device-code login.
|
|
426
468
|
* 4. No creds + TTY → device-code login.
|
|
427
469
|
* 5. No creds + no TTY → exit 1 with the §6.4 hint.
|
|
@@ -452,9 +494,6 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
|
|
|
452
494
|
`note: --label "${labelFlag}" ignored (already logged in as "${existing.label ?? "<unset>"}"); pass --relogin to change it`,
|
|
453
495
|
);
|
|
454
496
|
}
|
|
455
|
-
if (installToken) {
|
|
456
|
-
console.error("note: --install-token ignored because daemon is already logged in; pass --relogin to re-bind");
|
|
457
|
-
}
|
|
458
497
|
return existing;
|
|
459
498
|
}
|
|
460
499
|
|
|
@@ -463,13 +502,37 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
|
|
|
463
502
|
const label = labelFlag ?? defaultLoginLabel();
|
|
464
503
|
|
|
465
504
|
if (authAction === "install-token" && installToken) {
|
|
466
|
-
|
|
467
|
-
|
|
505
|
+
let tok: DaemonTokenResponse;
|
|
506
|
+
try {
|
|
507
|
+
tok = await redeemInstallToken({
|
|
508
|
+
hubUrl,
|
|
509
|
+
installToken,
|
|
510
|
+
label,
|
|
511
|
+
daemonInstanceId: existing?.daemonInstanceId,
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
if (existing && !relogin && !existsSync(AUTH_EXPIRED_FLAG_PATH)) {
|
|
515
|
+
console.error(
|
|
516
|
+
`note: --install-token could not be redeemed (${err instanceof Error ? err.message : String(err)}); reusing existing daemon auth`,
|
|
517
|
+
);
|
|
518
|
+
return existing;
|
|
519
|
+
}
|
|
520
|
+
throw err;
|
|
521
|
+
}
|
|
522
|
+
const record = userAuthFromTokenResponse(tok, {
|
|
523
|
+
label,
|
|
524
|
+
loggedInAt:
|
|
525
|
+
existing?.daemonInstanceId && existing.daemonInstanceId === tok.daemonInstanceId
|
|
526
|
+
? existing.loggedInAt
|
|
527
|
+
: undefined,
|
|
528
|
+
});
|
|
468
529
|
saveUserAuth(record);
|
|
469
530
|
clearAuthExpiredFlag();
|
|
470
531
|
log.info("install-token flow: authorized", {
|
|
471
532
|
userId: record.userId,
|
|
472
533
|
daemonInstanceId: record.daemonInstanceId,
|
|
534
|
+
reusedExistingDaemonInstance:
|
|
535
|
+
existing?.daemonInstanceId === record.daemonInstanceId,
|
|
473
536
|
hubUrl: record.hubUrl,
|
|
474
537
|
label,
|
|
475
538
|
});
|
|
@@ -527,12 +590,6 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
527
590
|
child: process.env.BOTCORD_DAEMON_CHILD === "1",
|
|
528
591
|
});
|
|
529
592
|
|
|
530
|
-
const existing = readPid();
|
|
531
|
-
if (existing && pidAlive(existing)) {
|
|
532
|
-
console.error(`daemon already running (pid ${existing})`);
|
|
533
|
-
process.exit(1);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
593
|
// Login MUST happen before fork — once detached, stdio is gone and the
|
|
537
594
|
// user can't see the device code. We also run it for explicit
|
|
538
595
|
// --foreground so an interactive user can log in without the fork dance.
|
|
@@ -540,6 +597,16 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
540
597
|
// var so we don't try to re-prompt for credentials it already has.
|
|
541
598
|
if (process.env.BOTCORD_DAEMON_CHILD !== "1") {
|
|
542
599
|
await ensureUserAuthForStart(args);
|
|
600
|
+
const existing = readPid();
|
|
601
|
+
if (existing && pidAlive(existing)) {
|
|
602
|
+
await stopExistingDaemonForRestart(existing);
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
const existing = readPid();
|
|
606
|
+
if (existing && existing !== process.pid && pidAlive(existing)) {
|
|
607
|
+
console.error(`daemon already running (pid ${existing})`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
543
610
|
}
|
|
544
611
|
|
|
545
612
|
if (background) {
|
|
@@ -34,8 +34,8 @@ export interface MergeOpenclawGatewayResult {
|
|
|
34
34
|
added: OpenclawGatewayProfile[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const DEFAULT_SEARCH_PATHS = ["~/.openclaw/", "/etc/openclaw/"];
|
|
38
|
-
const DEFAULT_PORTS = [18789, 16200];
|
|
37
|
+
const DEFAULT_SEARCH_PATHS = ["~/.openclaw/", "~/.qclaw/", "/etc/openclaw/"];
|
|
38
|
+
const DEFAULT_PORTS = [18789, 16200, 28789];
|
|
39
39
|
const DEFAULT_TOKEN_FILE_PATHS = [
|
|
40
40
|
"/run/openclaw/gateway-token",
|
|
41
41
|
"/var/run/openclaw/gateway-token",
|
|
@@ -382,6 +382,8 @@ function discoverFromConfigDir(root: string): DiscoveredOpenclawGateway[] {
|
|
|
382
382
|
|
|
383
383
|
function parseJsonConfig(raw: string): { url?: string; token?: string; tokenFile?: string } | null {
|
|
384
384
|
const obj = JSON.parse(raw) as any;
|
|
385
|
+
const qclaw = pickQclawGatewayValues(obj);
|
|
386
|
+
if (qclaw) return qclaw;
|
|
385
387
|
// Prefer OpenClaw's native shape: `gateway.port` + `gateway.auth.token`.
|
|
386
388
|
// The legacy `acp.url` shape is also supported for explicit user-authored configs.
|
|
387
389
|
const native = pickOpenclawGatewayValues(obj?.gateway);
|
|
@@ -390,6 +392,35 @@ function parseJsonConfig(raw: string): { url?: string; token?: string; tokenFile
|
|
|
390
392
|
return pickConfigValues(acp);
|
|
391
393
|
}
|
|
392
394
|
|
|
395
|
+
function pickQclawGatewayValues(
|
|
396
|
+
obj: any,
|
|
397
|
+
): { url?: string; token?: string; tokenFile?: string } | null {
|
|
398
|
+
if (!obj || typeof obj !== "object") return null;
|
|
399
|
+
const port = typeof obj.port === "number" ? obj.port : undefined;
|
|
400
|
+
const configPath = typeof obj.configPath === "string" && obj.configPath.trim()
|
|
401
|
+
? obj.configPath.trim()
|
|
402
|
+
: undefined;
|
|
403
|
+
if (!port && !configPath) return null;
|
|
404
|
+
|
|
405
|
+
const fromConfig = configPath ? readGatewayValuesFromConfigPath(configPath) : null;
|
|
406
|
+
if (fromConfig) return fromConfig;
|
|
407
|
+
if (!port) return null;
|
|
408
|
+
return { url: `ws://127.0.0.1:${port}` };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function readGatewayValuesFromConfigPath(
|
|
412
|
+
configPath: string,
|
|
413
|
+
): { url?: string; token?: string; tokenFile?: string } | null {
|
|
414
|
+
try {
|
|
415
|
+
const raw = readFileSync(expandHome(configPath), "utf8");
|
|
416
|
+
const parsed = parseJsonConfig(raw);
|
|
417
|
+
if (parsed?.url) return parsed;
|
|
418
|
+
} catch {
|
|
419
|
+
// qclaw.json may be copied without its referenced openclaw.json.
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
393
424
|
function pickOpenclawGatewayValues(
|
|
394
425
|
gw: any,
|
|
395
426
|
): { url?: string; token?: string; tokenFile?: string } | null {
|
package/src/provision.ts
CHANGED
|
@@ -1362,9 +1362,9 @@ export async function adoptDiscoveredOpenclawAgents(ctx: {
|
|
|
1362
1362
|
function localOpenclawAcpDisabled(rawUrl: string): boolean {
|
|
1363
1363
|
if (!isLoopbackUrl(rawUrl)) return false;
|
|
1364
1364
|
try {
|
|
1365
|
-
const
|
|
1366
|
-
if (!
|
|
1367
|
-
const cfg = JSON.parse(readFileSync(file, "utf8")) as any;
|
|
1365
|
+
const source = pickLocalOpenclawConfig(rawUrl);
|
|
1366
|
+
if (!source) return false;
|
|
1367
|
+
const cfg = JSON.parse(readFileSync(source.file, "utf8")) as any;
|
|
1368
1368
|
return cfg?.acp?.enabled === false;
|
|
1369
1369
|
} catch {
|
|
1370
1370
|
return false;
|
|
@@ -1838,12 +1838,13 @@ export async function probeOpenclawAgents(
|
|
|
1838
1838
|
token: prepared.resolvedToken,
|
|
1839
1839
|
timeoutMs: opts.timeoutMs ?? 3000,
|
|
1840
1840
|
});
|
|
1841
|
-
// For loopback gateways the agent roster lives in
|
|
1841
|
+
// For loopback gateways the agent roster lives in local OpenClaw config
|
|
1842
|
+
// (`~/.openclaw/openclaw.json`, or QClaw's `~/.qclaw/openclaw.json`)
|
|
1842
1843
|
// and is the source of truth — listing it over the wire would require a
|
|
1843
1844
|
// paired device identity (operator.read scope). When the WS probe is the
|
|
1844
1845
|
// default (i.e. no test injection) we enrich the result from disk.
|
|
1845
1846
|
if (result.ok && !result.agents && !opts.probe && isLoopbackUrl(profile.url)) {
|
|
1846
|
-
const local = readLocalOpenclawAgents();
|
|
1847
|
+
const local = readLocalOpenclawAgents(profile.url);
|
|
1847
1848
|
if (local && local.length > 0) result.agents = local;
|
|
1848
1849
|
}
|
|
1849
1850
|
return result;
|
|
@@ -1858,22 +1859,23 @@ function isLoopbackUrl(raw: string): boolean {
|
|
|
1858
1859
|
}
|
|
1859
1860
|
}
|
|
1860
1861
|
|
|
1861
|
-
function readLocalOpenclawAgents(): Array<{
|
|
1862
|
+
function readLocalOpenclawAgents(rawUrl?: string): Array<{
|
|
1862
1863
|
id: string;
|
|
1863
1864
|
name?: string;
|
|
1864
1865
|
workspace?: string;
|
|
1865
1866
|
model?: { name?: string; provider?: string };
|
|
1866
1867
|
}> | null {
|
|
1867
1868
|
try {
|
|
1868
|
-
const
|
|
1869
|
-
if (!
|
|
1869
|
+
const source = pickLocalOpenclawConfig(rawUrl);
|
|
1870
|
+
if (!source) return readLocalOpenclawAgentDirs(path.join(homedir(), ".openclaw")) ?? [{ id: "default" }];
|
|
1871
|
+
const { file, stateDir } = source;
|
|
1870
1872
|
const cfg = JSON.parse(readFileSync(file, "utf8")) as any;
|
|
1871
1873
|
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
1872
1874
|
const explicitDefaultId =
|
|
1873
1875
|
typeof cfg?.agents?.defaults?.id === "string" && cfg.agents.defaults.id
|
|
1874
1876
|
? cfg.agents.defaults.id
|
|
1875
1877
|
: null;
|
|
1876
|
-
const dirAgents = readLocalOpenclawAgentDirs();
|
|
1878
|
+
const dirAgents = readLocalOpenclawAgentDirs(stateDir);
|
|
1877
1879
|
const defaultId = explicitDefaultId ?? (list.length === 0 && !dirAgents ? "default" : null);
|
|
1878
1880
|
const seen = new Set<string>();
|
|
1879
1881
|
const out: Array<{ id: string; name?: string; workspace?: string; model?: { name?: string; provider?: string } }> = [];
|
|
@@ -1906,18 +1908,50 @@ function readLocalOpenclawAgents(): Array<{
|
|
|
1906
1908
|
}
|
|
1907
1909
|
}
|
|
1908
1910
|
|
|
1909
|
-
function
|
|
1911
|
+
function pickLocalOpenclawConfig(rawUrl?: string): { file: string; stateDir: string } | null {
|
|
1912
|
+
const candidates = [
|
|
1913
|
+
{ file: path.join(homedir(), ".openclaw", "openclaw.json"), stateDir: path.join(homedir(), ".openclaw") },
|
|
1914
|
+
{ file: path.join(homedir(), ".qclaw", "openclaw.json"), stateDir: path.join(homedir(), ".qclaw") },
|
|
1915
|
+
];
|
|
1916
|
+
const targetPort = urlPort(rawUrl);
|
|
1917
|
+
let firstExisting: { file: string; stateDir: string } | null = null;
|
|
1918
|
+
for (const candidate of candidates) {
|
|
1919
|
+
if (!existsSync(candidate.file)) continue;
|
|
1920
|
+
firstExisting ??= candidate;
|
|
1921
|
+
if (!targetPort) continue;
|
|
1922
|
+
try {
|
|
1923
|
+
const cfg = JSON.parse(readFileSync(candidate.file, "utf8")) as any;
|
|
1924
|
+
if (Number(cfg?.gateway?.port) === targetPort) return candidate;
|
|
1925
|
+
} catch {
|
|
1926
|
+
// Try the next local config.
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return firstExisting;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function urlPort(rawUrl?: string): number | null {
|
|
1933
|
+
if (!rawUrl) return null;
|
|
1934
|
+
try {
|
|
1935
|
+
const u = new URL(rawUrl);
|
|
1936
|
+
const port = Number(u.port || (u.protocol === "wss:" ? 443 : 80));
|
|
1937
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
1938
|
+
} catch {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function readLocalOpenclawAgentDirs(stateDir: string): Array<{
|
|
1910
1944
|
id: string;
|
|
1911
1945
|
workspace?: string;
|
|
1912
1946
|
}> | null {
|
|
1913
1947
|
try {
|
|
1914
|
-
const dir = path.join(
|
|
1948
|
+
const dir = path.join(stateDir, "agents");
|
|
1915
1949
|
if (!existsSync(dir)) return null;
|
|
1916
1950
|
const agents = readdirSync(dir, { withFileTypes: true })
|
|
1917
1951
|
.filter((entry) => entry.isDirectory() && entry.name.length > 0)
|
|
1918
1952
|
.map((entry) => ({
|
|
1919
1953
|
id: entry.name,
|
|
1920
|
-
workspace:
|
|
1954
|
+
workspace: resolveAgentDirWorkspace(dir, entry.name),
|
|
1921
1955
|
}));
|
|
1922
1956
|
if (agents.length === 0) return null;
|
|
1923
1957
|
agents.sort((a, b) => {
|
|
@@ -1931,6 +1965,11 @@ function readLocalOpenclawAgentDirs(): Array<{
|
|
|
1931
1965
|
}
|
|
1932
1966
|
}
|
|
1933
1967
|
|
|
1968
|
+
function resolveAgentDirWorkspace(agentsDir: string, agentId: string): string {
|
|
1969
|
+
const nested = path.join(agentsDir, agentId, "agent");
|
|
1970
|
+
return existsSync(nested) ? nested : path.join(agentsDir, agentId);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1934
1973
|
function resolveOpenclawIdentityName(
|
|
1935
1974
|
agentId: string,
|
|
1936
1975
|
workspace?: string,
|
package/src/start-auth.ts
CHANGED
|
@@ -7,7 +7,7 @@ export function resolveStartAuthAction(opts: {
|
|
|
7
7
|
relogin: boolean;
|
|
8
8
|
installToken?: string;
|
|
9
9
|
}): StartAuthAction {
|
|
10
|
-
if (opts.existing && !opts.relogin) return "reuse-existing";
|
|
11
10
|
if (opts.installToken) return "install-token";
|
|
11
|
+
if (opts.existing && !opts.relogin) return "reuse-existing";
|
|
12
12
|
return "device-code";
|
|
13
13
|
}
|
package/src/turn-text.ts
CHANGED
|
@@ -76,6 +76,17 @@ function replyDeliveryHint(msg: GatewayInboundMessage): string {
|
|
|
76
76
|
: NON_OWNER_REPLY_HINT;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function appendConversationFields(
|
|
80
|
+
fields: string[],
|
|
81
|
+
msg: GatewayInboundMessage,
|
|
82
|
+
): void {
|
|
83
|
+
const conversationId = sanitizeSenderName(msg.conversation.id);
|
|
84
|
+
fields.push(`conversation_id: ${conversationId}`);
|
|
85
|
+
if (isThirdPartyConversation(msg.conversation.id)) {
|
|
86
|
+
fields.push(`channel: ${sanitizeSenderName(msg.channel)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
/** Minimal shape of one batched inbound entry. Matches the BotCord channel
|
|
80
91
|
* `BatchedInboxRaw.batch[]` elements but expressed structurally so the
|
|
81
92
|
* composer doesn't import channel internals. */
|
|
@@ -205,6 +216,7 @@ export function composeBotCordUserTurn(msg: GatewayInboundMessage): string {
|
|
|
205
216
|
`from: ${sanitizedSenderLabel}`,
|
|
206
217
|
`to: ${msg.accountId}`,
|
|
207
218
|
];
|
|
219
|
+
appendConversationFields(headerFields, msg);
|
|
208
220
|
if (isGroup && roomTitle) {
|
|
209
221
|
const safeRoom = sanitizeSenderName(roomTitle.replace(/[\r\n]+/g, " "));
|
|
210
222
|
headerFields.push(`room: ${safeRoom}`);
|
|
@@ -267,6 +279,7 @@ function composeBatchedTurn(
|
|
|
267
279
|
`[BotCord Messages (${batch.length} new)]`,
|
|
268
280
|
`to: ${msg.accountId}`,
|
|
269
281
|
];
|
|
282
|
+
appendConversationFields(header, msg);
|
|
270
283
|
if (isGroup && roomTitle) {
|
|
271
284
|
const safeRoom = sanitizeSenderName(roomTitle.replace(/[\r\n]+/g, " "));
|
|
272
285
|
header.push(`room: ${safeRoom}`);
|