@workclaw/openclaw-workchat 1.0.3 → 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/index.js +107 -89
- package/index.ts +7 -0
- package/package.json +1 -1
- package/src/auth/accounts.ts +113 -44
- package/src/auth/bind-agent.ts +22 -40
- package/src/auth/login-qr.ts +18 -23
- package/src/channel.ts +16 -4
- package/src/messaging/process-message.ts +15 -26
package/index.js
CHANGED
|
@@ -279,7 +279,8 @@ __export(accounts_exports, {
|
|
|
279
279
|
listIndexedWorkChatAccountIds: () => listIndexedWorkChatAccountIds,
|
|
280
280
|
listWorkChatAccountIds: () => listWorkChatAccountIds,
|
|
281
281
|
loadWorkChatAccount: () => loadWorkChatAccount,
|
|
282
|
-
|
|
282
|
+
migrateLegacyDefaultWorkChatAccount: () => migrateLegacyDefaultWorkChatAccount,
|
|
283
|
+
pruneOtherWorkChatAccounts: () => pruneOtherWorkChatAccounts,
|
|
283
284
|
registerWorkChatAccountId: () => registerWorkChatAccountId,
|
|
284
285
|
removeWorkChatAccountBinding: () => removeWorkChatAccountBinding,
|
|
285
286
|
resolveWorkChatAccount: () => resolveWorkChatAccount,
|
|
@@ -296,6 +297,9 @@ function resolveAccountIndexPath() {
|
|
|
296
297
|
return path4.join(resolveWorkChatStateDir(), "accounts.json");
|
|
297
298
|
}
|
|
298
299
|
function listIndexedWorkChatAccountIds() {
|
|
300
|
+
return readAccountIndex();
|
|
301
|
+
}
|
|
302
|
+
function readAccountIndex() {
|
|
299
303
|
const filePath = resolveAccountIndexPath();
|
|
300
304
|
try {
|
|
301
305
|
if (!fs3.existsSync(filePath)) return [];
|
|
@@ -306,19 +310,23 @@ function listIndexedWorkChatAccountIds() {
|
|
|
306
310
|
return [];
|
|
307
311
|
}
|
|
308
312
|
}
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
async function updateAccountIndex(mutator) {
|
|
314
|
+
const run = indexUpdateChain.then(() => {
|
|
315
|
+
const filePath = resolveAccountIndexPath();
|
|
316
|
+
fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
317
|
+
const updated = mutator(readAccountIndex());
|
|
318
|
+
fs3.writeFileSync(filePath, JSON.stringify(updated, null, 2), "utf-8");
|
|
319
|
+
});
|
|
320
|
+
indexUpdateChain = run.then(() => void 0, () => void 0);
|
|
321
|
+
await run;
|
|
315
322
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
323
|
+
async function registerWorkChatAccountId(accountId) {
|
|
324
|
+
await updateAccountIndex(
|
|
325
|
+
(ids) => ids.includes(accountId) ? ids : [...ids, accountId]
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
async function unregisterWorkChatAccountId(accountId) {
|
|
329
|
+
await updateAccountIndex((ids) => ids.filter((id) => id !== accountId));
|
|
322
330
|
}
|
|
323
331
|
function resolveAccountsDir() {
|
|
324
332
|
return path4.join(resolveWorkChatStateDir(), "accounts");
|
|
@@ -355,14 +363,14 @@ function saveWorkChatAccount(accountId, update) {
|
|
|
355
363
|
} catch {
|
|
356
364
|
}
|
|
357
365
|
}
|
|
358
|
-
function clearWorkChatAccount(accountId) {
|
|
366
|
+
async function clearWorkChatAccount(accountId) {
|
|
359
367
|
const filePath = resolveAccountPath(accountId);
|
|
360
368
|
try {
|
|
361
369
|
if (fs3.existsSync(filePath)) fs3.unlinkSync(filePath);
|
|
362
370
|
} catch (err) {
|
|
363
371
|
logger.warn(`clearWorkChatAccount: failed to remove ${filePath}: ${String(err)}`);
|
|
364
372
|
}
|
|
365
|
-
unregisterWorkChatAccountId(accountId);
|
|
373
|
+
await unregisterWorkChatAccountId(accountId);
|
|
366
374
|
}
|
|
367
375
|
async function removeWorkChatAccountBinding(accountId) {
|
|
368
376
|
clearWorkChatAccount(accountId);
|
|
@@ -373,40 +381,62 @@ async function removeWorkChatAccountBinding(accountId) {
|
|
|
373
381
|
logger.warn(`removeWorkChatAccountBinding: allowFrom cleanup failed: ${String(err)}`);
|
|
374
382
|
}
|
|
375
383
|
try {
|
|
376
|
-
|
|
377
|
-
const cfg = loadConfig();
|
|
378
|
-
const channels = cfg.channels ?? {};
|
|
379
|
-
const section = channels["openclaw-workchat"];
|
|
380
|
-
if (section?.accounts?.[accountId] !== void 0) {
|
|
381
|
-
const accounts = { ...section.accounts ?? {} };
|
|
382
|
-
delete accounts[accountId];
|
|
383
|
-
await writeConfigFile({
|
|
384
|
-
...cfg,
|
|
385
|
-
channels: {
|
|
386
|
-
...channels,
|
|
387
|
-
"openclaw-workchat": { ...section, accounts }
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
384
|
+
await triggerWorkChatChannelReload();
|
|
391
385
|
} catch (err) {
|
|
392
|
-
logger.warn(`removeWorkChatAccountBinding:
|
|
386
|
+
logger.warn(`removeWorkChatAccountBinding: channel reload trigger failed: ${String(err)}`);
|
|
393
387
|
}
|
|
394
388
|
}
|
|
395
|
-
async function
|
|
389
|
+
async function pruneOtherWorkChatAccounts(keepAccountId) {
|
|
390
|
+
const keep = keepAccountId.trim();
|
|
396
391
|
const removed = [];
|
|
397
392
|
for (const accountId of listIndexedWorkChatAccountIds()) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
removed
|
|
403
|
-
|
|
404
|
-
`pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`
|
|
405
|
-
);
|
|
406
|
-
}
|
|
393
|
+
if (accountId === keep) continue;
|
|
394
|
+
await removeWorkChatAccountBinding(accountId);
|
|
395
|
+
removed.push(accountId);
|
|
396
|
+
logger.info(
|
|
397
|
+
`pruneOtherWorkChatAccounts: removed account=${accountId} (superseded by ${keep})`
|
|
398
|
+
);
|
|
407
399
|
}
|
|
408
400
|
return removed;
|
|
409
401
|
}
|
|
402
|
+
async function migrateLegacyDefaultWorkChatAccount() {
|
|
403
|
+
const ids = listIndexedWorkChatAccountIds();
|
|
404
|
+
if (!ids.includes("default")) return false;
|
|
405
|
+
const data = loadWorkChatAccount("default");
|
|
406
|
+
const agentId = data?.agentId?.trim();
|
|
407
|
+
if (!agentId) return false;
|
|
408
|
+
const targetId = `agent-${agentId}`;
|
|
409
|
+
if (!ids.includes(targetId)) {
|
|
410
|
+
saveWorkChatAccount(targetId, { ...data });
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const { readFrameworkAllowFromList: readFrameworkAllowFromList2, registerUserInFrameworkStore: registerUserInFrameworkStore2 } = await Promise.resolve().then(() => (init_pairing(), pairing_exports));
|
|
414
|
+
for (const userId of readFrameworkAllowFromList2("default")) {
|
|
415
|
+
await registerUserInFrameworkStore2({ accountId: targetId, userId });
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
logger.warn(`migrateLegacyDefaultWorkChatAccount: allowFrom move failed: ${String(err)}`);
|
|
419
|
+
}
|
|
420
|
+
await updateAccountIndex((existing) => {
|
|
421
|
+
const next = existing.filter((id) => id !== "default" && id !== targetId);
|
|
422
|
+
next.push(targetId);
|
|
423
|
+
return next;
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
fs3.rmSync(path4.join(resolveWorkChatStateDir(), "accounts", "default.json"), { force: true });
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const { clearFrameworkAllowFromList: clearFrameworkAllowFromList2 } = await Promise.resolve().then(() => (init_pairing(), pairing_exports));
|
|
431
|
+
clearFrameworkAllowFromList2("default");
|
|
432
|
+
} catch (err) {
|
|
433
|
+
logger.warn(`migrateLegacyDefaultWorkChatAccount: allowFrom cleanup failed: ${String(err)}`);
|
|
434
|
+
}
|
|
435
|
+
logger.info(
|
|
436
|
+
`migrateLegacyDefaultWorkChatAccount: renamed default -> ${targetId} (agent=${agentId})`
|
|
437
|
+
);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
410
440
|
function listWorkChatAccountIds(_cfg) {
|
|
411
441
|
return listIndexedWorkChatAccountIds();
|
|
412
442
|
}
|
|
@@ -456,12 +486,14 @@ async function triggerWorkChatChannelReload() {
|
|
|
456
486
|
logger.warn(`triggerWorkChatChannelReload: failed to update config: ${String(err)}`);
|
|
457
487
|
}
|
|
458
488
|
}
|
|
489
|
+
var indexUpdateChain;
|
|
459
490
|
var init_accounts = __esm({
|
|
460
491
|
"src/auth/accounts.ts"() {
|
|
461
492
|
"use strict";
|
|
462
493
|
init_types();
|
|
463
494
|
init_state_dir();
|
|
464
495
|
init_logger();
|
|
496
|
+
indexUpdateChain = Promise.resolve();
|
|
465
497
|
}
|
|
466
498
|
});
|
|
467
499
|
|
|
@@ -26883,30 +26915,6 @@ __export(bind_agent_exports, {
|
|
|
26883
26915
|
bindAgentToAccountFromBinding: () => bindAgentToAccountFromBinding
|
|
26884
26916
|
});
|
|
26885
26917
|
async function bindAgentToAccount(accountId, agentId, userId) {
|
|
26886
|
-
const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
|
|
26887
|
-
const raw = loadConfig();
|
|
26888
|
-
const channels = raw.channels ?? {};
|
|
26889
|
-
const section = channels["openclaw-workchat"] ?? {};
|
|
26890
|
-
const accounts = { ...section.accounts ?? {} };
|
|
26891
|
-
accounts[accountId] = {
|
|
26892
|
-
...accounts[accountId] ?? {},
|
|
26893
|
-
enabled: true,
|
|
26894
|
-
agentId,
|
|
26895
|
-
...userId ? { userId } : {}
|
|
26896
|
-
};
|
|
26897
|
-
const updated = {
|
|
26898
|
-
...raw,
|
|
26899
|
-
channels: {
|
|
26900
|
-
...channels,
|
|
26901
|
-
"openclaw-workchat": {
|
|
26902
|
-
...section,
|
|
26903
|
-
accounts,
|
|
26904
|
-
...userId && !section.userId ? { userId } : {},
|
|
26905
|
-
channelConfigUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26906
|
-
}
|
|
26907
|
-
}
|
|
26908
|
-
};
|
|
26909
|
-
await writeConfigFile(updated);
|
|
26910
26918
|
if (userId) {
|
|
26911
26919
|
try {
|
|
26912
26920
|
const { registerUserInFrameworkStore: registerUserInFrameworkStore2 } = await Promise.resolve().then(() => (init_pairing(), pairing_exports));
|
|
@@ -26915,8 +26923,14 @@ async function bindAgentToAccount(accountId, agentId, userId) {
|
|
|
26915
26923
|
logger.warn(`bindAgentToAccount: allowFrom registration failed: ${String(err)}`);
|
|
26916
26924
|
}
|
|
26917
26925
|
}
|
|
26926
|
+
try {
|
|
26927
|
+
const { triggerWorkChatChannelReload: triggerWorkChatChannelReload2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
|
|
26928
|
+
await triggerWorkChatChannelReload2();
|
|
26929
|
+
} catch (err) {
|
|
26930
|
+
logger.warn(`bindAgentToAccount: channel reload trigger failed: ${String(err)}`);
|
|
26931
|
+
}
|
|
26918
26932
|
logger.info(
|
|
26919
|
-
`bindAgentToAccount: accountId=${accountId} agentId=${agentId} userId=${userId || "(none)"} (apiKey NOT written \u2014 env-injected)`
|
|
26933
|
+
`bindAgentToAccount: accountId=${accountId} agentId=${agentId} userId=${userId || "(none)"} (private-dir only; apiKey NOT written \u2014 env-injected)`
|
|
26920
26934
|
);
|
|
26921
26935
|
}
|
|
26922
26936
|
var bindAgentToAccountFromBinding;
|
|
@@ -27159,28 +27173,27 @@ async function handleListenerFrame(sessionKey, data, ws) {
|
|
|
27159
27173
|
if (result.type !== "init_agent") return;
|
|
27160
27174
|
const eventData = result.eventData;
|
|
27161
27175
|
if (!eventData) return;
|
|
27162
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
27176
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
27163
27177
|
if (!agentId) return;
|
|
27164
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
27178
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
27165
27179
|
const entry = bindingListeners.get(sessionKey);
|
|
27166
27180
|
if (entry) entry.done = true;
|
|
27167
|
-
const
|
|
27181
|
+
const accountId = `agent-${agentId}`;
|
|
27168
27182
|
try {
|
|
27169
|
-
const {
|
|
27170
|
-
const removed = await
|
|
27183
|
+
const { pruneOtherWorkChatAccounts: pruneOtherWorkChatAccounts2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
|
|
27184
|
+
const removed = await pruneOtherWorkChatAccounts2(accountId);
|
|
27171
27185
|
if (removed.length > 0) {
|
|
27172
|
-
logger.info(`binding listener: pruned
|
|
27186
|
+
logger.info(`binding listener: pruned previous account(s): ${removed.join(", ")}`);
|
|
27173
27187
|
}
|
|
27174
27188
|
} catch (err) {
|
|
27175
27189
|
logger.error(`binding listener: rebind cleanup failed: ${String(err)}`);
|
|
27176
27190
|
}
|
|
27177
|
-
const accountId = isRebind ? `agent-${agentId}` : "default";
|
|
27178
27191
|
try {
|
|
27179
27192
|
saveWorkChatAccount(accountId, {
|
|
27180
27193
|
agentId,
|
|
27181
27194
|
...userId ? { userId } : {}
|
|
27182
27195
|
});
|
|
27183
|
-
registerWorkChatAccountId(accountId);
|
|
27196
|
+
await registerWorkChatAccountId(accountId);
|
|
27184
27197
|
const { bindAgentToAccountFromBinding: bindAgentToAccountFromBinding2 } = await Promise.resolve().then(() => (init_bind_agent(), bind_agent_exports));
|
|
27185
27198
|
await bindAgentToAccountFromBinding2(accountId, agentId, userId);
|
|
27186
27199
|
logger.info(`binding listener: account=${accountId} agent=${agentId} persisted`);
|
|
@@ -27875,31 +27888,30 @@ function emitWorkChatMessageSent2(params) {
|
|
|
27875
27888
|
async function handleAgentLifecycleEvent(frameType, result, deps) {
|
|
27876
27889
|
const eventData = result.eventData;
|
|
27877
27890
|
if (!eventData) return;
|
|
27878
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
27891
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
27879
27892
|
if (!agentId) {
|
|
27880
27893
|
logger.warn(`${frameType}: missing agent id, ignoring`);
|
|
27881
27894
|
return;
|
|
27882
27895
|
}
|
|
27883
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
27896
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
27884
27897
|
logger.info(`${frameType}: agentId=${agentId} userId=${userId ? "present" : "missing"}`);
|
|
27885
27898
|
deps.log(`${frameType}: agentId=${agentId}`);
|
|
27886
|
-
const
|
|
27899
|
+
const { completeWorkChatBinding: completeWorkChatBinding2 } = await Promise.resolve().then(() => (init_login_qr(), login_qr_exports));
|
|
27900
|
+
const accountId = frameType === "init_agent" ? `agent-${agentId}` : deps.accountId;
|
|
27887
27901
|
try {
|
|
27888
|
-
const removed = await
|
|
27902
|
+
const removed = await pruneOtherWorkChatAccounts(accountId);
|
|
27889
27903
|
if (removed.length > 0) {
|
|
27890
|
-
deps.log(`rebind: removed
|
|
27904
|
+
deps.log(`rebind: removed previous account(s): ${removed.join(", ")}`);
|
|
27891
27905
|
}
|
|
27892
27906
|
} catch (err) {
|
|
27893
27907
|
logger.error(`rebind cleanup failed: ${String(err)}`);
|
|
27894
27908
|
}
|
|
27895
|
-
const { completeWorkChatBinding: completeWorkChatBinding2 } = await Promise.resolve().then(() => (init_login_qr(), login_qr_exports));
|
|
27896
|
-
const accountId = frameType === "init_agent" ? isRebind ? `agent-${agentId}` : "default" : deps.accountId;
|
|
27897
27909
|
try {
|
|
27898
27910
|
saveWorkChatAccount(accountId, {
|
|
27899
27911
|
agentId,
|
|
27900
27912
|
...userId ? { userId } : {}
|
|
27901
27913
|
});
|
|
27902
|
-
registerWorkChatAccountId(accountId);
|
|
27914
|
+
await registerWorkChatAccountId(accountId);
|
|
27903
27915
|
const { bindAgentToAccount: bindAgentToAccount2 } = await Promise.resolve().then(() => (init_bind_agent(), bind_agent_exports));
|
|
27904
27916
|
await bindAgentToAccount2(accountId, agentId, userId);
|
|
27905
27917
|
logger.info(`${frameType}: bound agent to account accountId=${accountId}`);
|
|
@@ -27921,9 +27933,6 @@ async function handleAgentLifecycleEvent(frameType, result, deps) {
|
|
|
27921
27933
|
void notifyAgentCreatedStatus(deps.apiOpts, agentId);
|
|
27922
27934
|
}
|
|
27923
27935
|
}
|
|
27924
|
-
function listWorkChatAccountIdsCount() {
|
|
27925
|
-
return listIndexedWorkChatAccountIds().length;
|
|
27926
|
-
}
|
|
27927
27936
|
function loadCreateTypingCallbacks() {
|
|
27928
27937
|
createTypingCallbacksPromise ??= (async () => {
|
|
27929
27938
|
for (const specifier of ["openclaw/plugin-sdk/channel-runtime", "openclaw/plugin-sdk/channel-message"]) {
|
|
@@ -29561,6 +29570,7 @@ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-sch
|
|
|
29561
29570
|
|
|
29562
29571
|
// src/channel.ts
|
|
29563
29572
|
init_accounts();
|
|
29573
|
+
init_types();
|
|
29564
29574
|
init_login_qr();
|
|
29565
29575
|
init_send();
|
|
29566
29576
|
init_logger();
|
|
@@ -29737,11 +29747,13 @@ var workchatPlugin = {
|
|
|
29737
29747
|
loginWithQrStart: async ({ accountId, force, verbose }) => {
|
|
29738
29748
|
const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
|
|
29739
29749
|
const cfg = loadConfig();
|
|
29740
|
-
const
|
|
29750
|
+
const requestedAccountId = accountId?.trim() || listWorkChatAccountIds(cfg)[0];
|
|
29751
|
+
const existing = requestedAccountId ? resolveWorkChatAccount(cfg, requestedAccountId) : void 0;
|
|
29752
|
+
const section = cfg.channels?.["openclaw-workchat"];
|
|
29741
29753
|
const result = await startWorkChatLoginWithQr({
|
|
29742
|
-
baseUrl:
|
|
29743
|
-
appKey:
|
|
29744
|
-
appSecret:
|
|
29754
|
+
baseUrl: existing?.baseUrl || section?.baseUrl || DEFAULT_BASE_URL,
|
|
29755
|
+
appKey: existing?.appKey || section?.appKey?.trim() || void 0,
|
|
29756
|
+
appSecret: existing?.appSecret || section?.appSecret?.trim() || void 0
|
|
29745
29757
|
});
|
|
29746
29758
|
return {
|
|
29747
29759
|
qrDataUrl: result.qrcodeUrl,
|
|
@@ -29848,6 +29860,9 @@ function assertHostCompatibility(hostVersion) {
|
|
|
29848
29860
|
);
|
|
29849
29861
|
}
|
|
29850
29862
|
|
|
29863
|
+
// index.ts
|
|
29864
|
+
init_accounts();
|
|
29865
|
+
|
|
29851
29866
|
// node_modules/zod/v4/classic/external.js
|
|
29852
29867
|
var external_exports = {};
|
|
29853
29868
|
__export(external_exports, {
|
|
@@ -49584,6 +49599,9 @@ var openclaw_workchat_default = {
|
|
|
49584
49599
|
configSchema: buildChannelConfigSchema(WorkChatConfigSchema),
|
|
49585
49600
|
register(api) {
|
|
49586
49601
|
assertHostCompatibility(api.runtime?.version);
|
|
49602
|
+
void migrateLegacyDefaultWorkChatAccount().catch((err) => {
|
|
49603
|
+
console.error("[openclaw-workchat] legacy default account migration failed:", err);
|
|
49604
|
+
});
|
|
49587
49605
|
api.registerChannel({ plugin: workchatPlugin });
|
|
49588
49606
|
}
|
|
49589
49607
|
};
|
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-sch
|
|
|
3
3
|
|
|
4
4
|
import { workchatPlugin } from "./src/channel.js";
|
|
5
5
|
import { assertHostCompatibility } from "./src/compat.js";
|
|
6
|
+
import { migrateLegacyDefaultWorkChatAccount } from "./src/auth/accounts.js";
|
|
6
7
|
import { WorkChatConfigSchema } from "./src/config/config-schema.js";
|
|
7
8
|
|
|
8
9
|
export default {
|
|
@@ -14,6 +15,12 @@ export default {
|
|
|
14
15
|
// Fail-fast: reject incompatible host versions before any side-effects.
|
|
15
16
|
assertHostCompatibility(api.runtime?.version);
|
|
16
17
|
|
|
18
|
+
// Rename legacy `default` accounts to the agent-scoped key before the
|
|
19
|
+
// channel starts enumerating accounts (one-way, idempotent).
|
|
20
|
+
void migrateLegacyDefaultWorkChatAccount().catch(err => {
|
|
21
|
+
console.error("[openclaw-workchat] legacy default account migration failed:", err);
|
|
22
|
+
});
|
|
23
|
+
|
|
17
24
|
api.registerChannel({ plugin: workchatPlugin });
|
|
18
25
|
},
|
|
19
26
|
};
|
package/package.json
CHANGED
package/src/auth/accounts.ts
CHANGED
|
@@ -21,6 +21,10 @@ function resolveAccountIndexPath(): string {
|
|
|
21
21
|
|
|
22
22
|
/** Returns all accountIds registered via binding QR. */
|
|
23
23
|
export function listIndexedWorkChatAccountIds(): string[] {
|
|
24
|
+
return readAccountIndex();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readAccountIndex(): string[] {
|
|
24
28
|
const filePath = resolveAccountIndexPath();
|
|
25
29
|
try {
|
|
26
30
|
if (!fs.existsSync(filePath)) return [];
|
|
@@ -32,22 +36,43 @@ export function listIndexedWorkChatAccountIds(): string[] {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Serialize index mutations inside the plugin process. All writers (binding
|
|
41
|
+
* registration, rebind pruning, legacy migration) run in this one process;
|
|
42
|
+
* the shared promise chain guarantees read-modify-write never interleaves.
|
|
43
|
+
*/
|
|
44
|
+
let indexUpdateChain: Promise<void> = Promise.resolve();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read-modify-write the account index. Mutations are serialized in-process
|
|
48
|
+
* (see indexUpdateChain); the framework file lock is not used because the
|
|
49
|
+
* index is only ever written by this plugin process, and the lock sidecar
|
|
50
|
+
* behaves inconsistently under test-runner module isolation.
|
|
51
|
+
*/
|
|
52
|
+
async function updateAccountIndex(
|
|
53
|
+
mutator: (ids: string[]) => string[],
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const run = indexUpdateChain.then(() => {
|
|
56
|
+
const filePath = resolveAccountIndexPath();
|
|
57
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
58
|
+
const updated = mutator(readAccountIndex());
|
|
59
|
+
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), "utf-8");
|
|
60
|
+
});
|
|
61
|
+
// Keep the chain alive even when a mutation fails.
|
|
62
|
+
indexUpdateChain = run.then(() => undefined, () => undefined);
|
|
63
|
+
await run;
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
/** Add accountId to the persistent index (no-op if already present). */
|
|
36
|
-
export function registerWorkChatAccountId(accountId: string): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (existing.includes(accountId)) return;
|
|
41
|
-
fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([...existing, accountId], null, 2), "utf-8");
|
|
67
|
+
export async function registerWorkChatAccountId(accountId: string): Promise<void> {
|
|
68
|
+
await updateAccountIndex(ids =>
|
|
69
|
+
ids.includes(accountId) ? ids : [...ids, accountId],
|
|
70
|
+
);
|
|
42
71
|
}
|
|
43
72
|
|
|
44
73
|
/** Remove accountId from the persistent index. */
|
|
45
|
-
export function unregisterWorkChatAccountId(accountId: string): void {
|
|
46
|
-
|
|
47
|
-
const updated = existing.filter(id => id !== accountId);
|
|
48
|
-
if (updated.length !== existing.length) {
|
|
49
|
-
fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), "utf-8");
|
|
50
|
-
}
|
|
74
|
+
export async function unregisterWorkChatAccountId(accountId: string): Promise<void> {
|
|
75
|
+
await updateAccountIndex(ids => ids.filter(id => id !== accountId));
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
// ---------------------------------------------------------------------------
|
|
@@ -115,14 +140,14 @@ export function saveWorkChatAccount(
|
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
/** Remove all files associated with an account. */
|
|
118
|
-
export function clearWorkChatAccount(accountId: string): void {
|
|
143
|
+
export async function clearWorkChatAccount(accountId: string): Promise<void> {
|
|
119
144
|
const filePath = resolveAccountPath(accountId);
|
|
120
145
|
try {
|
|
121
146
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
122
147
|
} catch (err) {
|
|
123
148
|
logger.warn(`clearWorkChatAccount: failed to remove ${filePath}: ${String(err)}`);
|
|
124
149
|
}
|
|
125
|
-
unregisterWorkChatAccountId(accountId);
|
|
150
|
+
await unregisterWorkChatAccountId(accountId);
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
/**
|
|
@@ -141,45 +166,33 @@ export async function removeWorkChatAccountBinding(accountId: string): Promise<v
|
|
|
141
166
|
logger.warn(`removeWorkChatAccountBinding: allowFrom cleanup failed: ${String(err)}`);
|
|
142
167
|
}
|
|
143
168
|
|
|
169
|
+
// Account entries no longer live in openclaw.json (weixin-style private-dir
|
|
170
|
+
// ownership), so there is no shared config to clean up here — the channel
|
|
171
|
+
// reload bump below lets the gateway drop the removed account.
|
|
144
172
|
try {
|
|
145
|
-
|
|
146
|
-
const cfg = loadConfig();
|
|
147
|
-
const channels = (cfg.channels ?? {}) as Record<string, any>;
|
|
148
|
-
const section = channels["openclaw-workchat"];
|
|
149
|
-
if (section?.accounts?.[accountId] !== undefined) {
|
|
150
|
-
const accounts = { ...(section.accounts ?? {}) };
|
|
151
|
-
delete accounts[accountId];
|
|
152
|
-
await writeConfigFile({
|
|
153
|
-
...cfg,
|
|
154
|
-
channels: {
|
|
155
|
-
...channels,
|
|
156
|
-
"openclaw-workchat": { ...section, accounts },
|
|
157
|
-
},
|
|
158
|
-
} as import("openclaw/plugin-sdk").OpenClawConfig);
|
|
159
|
-
}
|
|
173
|
+
await triggerWorkChatChannelReload();
|
|
160
174
|
} catch (err) {
|
|
161
|
-
logger.warn(`removeWorkChatAccountBinding:
|
|
175
|
+
logger.warn(`removeWorkChatAccountBinding: channel reload trigger failed: ${String(err)}`);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
/**
|
|
166
|
-
* Remove every registered account
|
|
167
|
-
*
|
|
168
|
-
* `
|
|
169
|
-
*
|
|
180
|
+
* Remove every registered account other than `keepAccountId` (rebind cleanup).
|
|
181
|
+
* One active agent per host: a completed binding owns the agent-scoped key
|
|
182
|
+
* `agent-${agentId}`, and every other account — a previous agent, or a legacy
|
|
183
|
+
* `default` registered by older plugin versions even for the SAME agent — is
|
|
184
|
+
* removed so a re-scan can never leave duplicate accounts behind.
|
|
170
185
|
*/
|
|
171
|
-
export async function
|
|
186
|
+
export async function pruneOtherWorkChatAccounts(keepAccountId: string): Promise<string[]> {
|
|
187
|
+
const keep = keepAccountId.trim();
|
|
172
188
|
const removed: string[] = [];
|
|
173
189
|
for (const accountId of listIndexedWorkChatAccountIds()) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
removed
|
|
179
|
-
|
|
180
|
-
`pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
190
|
+
if (accountId === keep) continue;
|
|
191
|
+
await removeWorkChatAccountBinding(accountId);
|
|
192
|
+
removed.push(accountId);
|
|
193
|
+
logger.info(
|
|
194
|
+
`pruneOtherWorkChatAccounts: removed account=${accountId} (superseded by ${keep})`,
|
|
195
|
+
);
|
|
183
196
|
}
|
|
184
197
|
return removed;
|
|
185
198
|
}
|
|
@@ -222,6 +235,62 @@ type WorkChatSectionConfig = WorkChatAccountConfig & {
|
|
|
222
235
|
channelConfigUpdatedAt?: string;
|
|
223
236
|
};
|
|
224
237
|
|
|
238
|
+
/**
|
|
239
|
+
* One-time migration for accounts registered by older plugin versions under
|
|
240
|
+
* the `default` key: rename them to the agent-scoped key `agent-${agentId}`.
|
|
241
|
+
* Moves the account data file and the framework allowFrom credentials so no
|
|
242
|
+
* orphaned files remain (the gateway startup migration treats orphaned
|
|
243
|
+
* allowFrom files as a fatal unresolved state). Idempotent — a no-op once no
|
|
244
|
+
* `default` account exists. Runs once at plugin registration (see index.ts),
|
|
245
|
+
* before any binding listener can start, so it cannot interleave with
|
|
246
|
+
* concurrent binding writes (register/unregister hold the index file lock).
|
|
247
|
+
*/
|
|
248
|
+
export async function migrateLegacyDefaultWorkChatAccount(): Promise<boolean> {
|
|
249
|
+
const ids = listIndexedWorkChatAccountIds();
|
|
250
|
+
if (!ids.includes("default")) return false;
|
|
251
|
+
const data = loadWorkChatAccount("default");
|
|
252
|
+
const agentId = data?.agentId?.trim();
|
|
253
|
+
if (!agentId) return false;
|
|
254
|
+
const targetId = `agent-${agentId}`;
|
|
255
|
+
|
|
256
|
+
// 1. Persist the agent-scoped copy (unless it already exists — the
|
|
257
|
+
// agent-scoped file is the newer binding when both are present).
|
|
258
|
+
if (!ids.includes(targetId)) {
|
|
259
|
+
saveWorkChatAccount(targetId, { ...data });
|
|
260
|
+
}
|
|
261
|
+
// 2. Move the framework allowFrom credentials to the new key.
|
|
262
|
+
try {
|
|
263
|
+
const { readFrameworkAllowFromList, registerUserInFrameworkStore } = await import("./pairing.js");
|
|
264
|
+
for (const userId of readFrameworkAllowFromList("default")) {
|
|
265
|
+
await registerUserInFrameworkStore({ accountId: targetId, userId });
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
logger.warn(`migrateLegacyDefaultWorkChatAccount: allowFrom move failed: ${String(err)}`);
|
|
269
|
+
}
|
|
270
|
+
// 3. Register the new key and drop the legacy one, both under the index
|
|
271
|
+
// file lock so a concurrent binding cannot be lost.
|
|
272
|
+
await updateAccountIndex(existing => {
|
|
273
|
+
const next = existing.filter(id => id !== "default" && id !== targetId);
|
|
274
|
+
next.push(targetId);
|
|
275
|
+
return next;
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
fs.rmSync(path.join(resolveWorkChatStateDir(), "accounts", "default.json"), { force: true });
|
|
279
|
+
} catch {
|
|
280
|
+
// Best-effort data file cleanup; the index above is authoritative.
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const { clearFrameworkAllowFromList } = await import("./pairing.js");
|
|
284
|
+
clearFrameworkAllowFromList("default");
|
|
285
|
+
} catch (err) {
|
|
286
|
+
logger.warn(`migrateLegacyDefaultWorkChatAccount: allowFrom cleanup failed: ${String(err)}`);
|
|
287
|
+
}
|
|
288
|
+
logger.info(
|
|
289
|
+
`migrateLegacyDefaultWorkChatAccount: renamed default -> ${targetId} (agent=${agentId})`,
|
|
290
|
+
);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
225
294
|
/** List accountIds from the index file (written at binding). */
|
|
226
295
|
export function listWorkChatAccountIds(_cfg: OpenClawConfig): string[] {
|
|
227
296
|
return listIndexedWorkChatAccountIds();
|
package/src/auth/bind-agent.ts
CHANGED
|
@@ -1,53 +1,26 @@
|
|
|
1
1
|
import { logger } from "../util/logger.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* channels.openclaw-workchat.accounts of openclaw.json so inbound
|
|
6
|
-
* AGENT_MESSAGE frames route to the right account and outbound sends find
|
|
7
|
-
* the agent.
|
|
4
|
+
* Complete the binding on the plugin side.
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* WORKCHAT_API_KEY / WORKCHAT_APP_SECRET env injection.
|
|
6
|
+
* The account payload (agentId/userId) lives exclusively in the plugin's
|
|
7
|
+
* private state dir (`state/openclaw-workchat/accounts/`) — weixin-style. The
|
|
8
|
+
* caller (binding listener / monitor INIT_AGENT handling) persists it via
|
|
9
|
+
* saveWorkChatAccount + registerWorkChatAccountId before calling this.
|
|
14
10
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* This function only bumps `channelConfigUpdatedAt` in openclaw.json so the
|
|
12
|
+
* gateway reloads config from disk and restarts the channel with the fresh
|
|
13
|
+
* private-dir account data. Writing account entries into
|
|
14
|
+
* channels.openclaw-workchat.accounts here would race the desktop configSync
|
|
15
|
+
* (which rebuilds that field) and orphan the credentials/
|
|
16
|
+
* <account>-allowFrom.json files — the gateway's startup migration treats
|
|
17
|
+
* those as a fatal unresolved state.
|
|
17
18
|
*/
|
|
18
19
|
export async function bindAgentToAccount(
|
|
19
20
|
accountId: string,
|
|
20
21
|
agentId: string,
|
|
21
22
|
userId?: string,
|
|
22
23
|
): Promise<void> {
|
|
23
|
-
const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
|
|
24
|
-
const raw = loadConfig() as Record<string, any>;
|
|
25
|
-
const channels = (raw.channels ?? {}) as Record<string, any>;
|
|
26
|
-
const section = (channels["openclaw-workchat"] ?? {}) as Record<string, any>;
|
|
27
|
-
const accounts = { ...(section.accounts ?? {}) };
|
|
28
|
-
|
|
29
|
-
accounts[accountId] = {
|
|
30
|
-
...(accounts[accountId] ?? {}),
|
|
31
|
-
enabled: true,
|
|
32
|
-
agentId,
|
|
33
|
-
...(userId ? { userId } : {}),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const updated = {
|
|
37
|
-
...raw,
|
|
38
|
-
channels: {
|
|
39
|
-
...channels,
|
|
40
|
-
"openclaw-workchat": {
|
|
41
|
-
...section,
|
|
42
|
-
accounts,
|
|
43
|
-
...(userId && !section.userId ? { userId } : {}),
|
|
44
|
-
channelConfigUpdatedAt: new Date().toISOString(),
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
} as Record<string, any>;
|
|
48
|
-
|
|
49
|
-
await writeConfigFile(updated as import("openclaw/plugin-sdk").OpenClawConfig);
|
|
50
|
-
|
|
51
24
|
// Persist the binding user in the framework allowFrom store so command
|
|
52
25
|
// authorization has an explicit source of truth (the inbound path also
|
|
53
26
|
// falls back to the account's userId, but writing it keeps parity with the
|
|
@@ -61,8 +34,17 @@ export async function bindAgentToAccount(
|
|
|
61
34
|
}
|
|
62
35
|
}
|
|
63
36
|
|
|
37
|
+
// Trigger the channel hot reload; account data itself stays in the private
|
|
38
|
+
// state dir (already persisted by the caller before this runs).
|
|
39
|
+
try {
|
|
40
|
+
const { triggerWorkChatChannelReload } = await import("./accounts.js");
|
|
41
|
+
await triggerWorkChatChannelReload();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.warn(`bindAgentToAccount: channel reload trigger failed: ${String(err)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
64
46
|
logger.info(
|
|
65
|
-
`bindAgentToAccount: accountId=${accountId} agentId=${agentId} userId=${userId || "(none)"} (apiKey NOT written — env-injected)`,
|
|
47
|
+
`bindAgentToAccount: accountId=${accountId} agentId=${agentId} userId=${userId || "(none)"} (private-dir only; apiKey NOT written — env-injected)`,
|
|
66
48
|
);
|
|
67
49
|
}
|
|
68
50
|
|
package/src/auth/login-qr.ts
CHANGED
|
@@ -293,7 +293,6 @@ import { buildPongFrame } from "../messaging/inbound.js";
|
|
|
293
293
|
import {
|
|
294
294
|
saveWorkChatAccount,
|
|
295
295
|
registerWorkChatAccountId,
|
|
296
|
-
listIndexedWorkChatAccountIds,
|
|
297
296
|
} from "./accounts.js";
|
|
298
297
|
|
|
299
298
|
const bindingListeners = new Map<string, { ws: WebSocket; done: boolean }>();
|
|
@@ -388,46 +387,42 @@ async function handleListenerFrame(sessionKey: string, data: string, ws: WebSock
|
|
|
388
387
|
|
|
389
388
|
const eventData = result.eventData;
|
|
390
389
|
if (!eventData) return;
|
|
391
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
390
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
392
391
|
if (!agentId) return;
|
|
393
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
392
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
394
393
|
|
|
395
394
|
const entry = bindingListeners.get(sessionKey);
|
|
396
395
|
if (entry) entry.done = true;
|
|
397
396
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// channel (marked manually-stopped in the
|
|
401
|
-
// would stay stopped after reload —
|
|
402
|
-
//
|
|
403
|
-
const
|
|
397
|
+
// One active agent per host (mirrors handleAgentLifecycleEvent): every
|
|
398
|
+
// binding registers under the agent-scoped key `agent-${agentId}`. The QR
|
|
399
|
+
// login flow force-stops the channel (marked manually-stopped in the
|
|
400
|
+
// gateway), and a reused account id would stay stopped after reload — the
|
|
401
|
+
// fresh agent-scoped key plus the channel reload restarts it.
|
|
402
|
+
const accountId = `agent-${agentId}`;
|
|
404
403
|
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
404
|
+
// A re-scan replaces the previously bound agent: drop every OTHER
|
|
405
|
+
// registered account (including a same-agent legacy `default`) before
|
|
406
|
+
// registering this one.
|
|
408
407
|
try {
|
|
409
|
-
const {
|
|
410
|
-
const removed = await
|
|
408
|
+
const { pruneOtherWorkChatAccounts } = await import("./accounts.js");
|
|
409
|
+
const removed = await pruneOtherWorkChatAccounts(accountId);
|
|
411
410
|
if (removed.length > 0) {
|
|
412
|
-
logger.info(`binding listener: pruned
|
|
411
|
+
logger.info(`binding listener: pruned previous account(s): ${removed.join(", ")}`);
|
|
413
412
|
}
|
|
414
413
|
} catch (err) {
|
|
415
414
|
logger.error(`binding listener: rebind cleanup failed: ${String(err)}`);
|
|
416
415
|
}
|
|
417
416
|
|
|
418
|
-
// First bound agent becomes the `default` account; a rebind gets a fresh
|
|
419
|
-
// agent-scoped key (see isRebind note above).
|
|
420
|
-
const accountId = isRebind
|
|
421
|
-
? `agent-${agentId}`
|
|
422
|
-
: "default";
|
|
423
|
-
|
|
424
417
|
try {
|
|
425
418
|
saveWorkChatAccount(accountId, {
|
|
426
419
|
agentId,
|
|
427
420
|
...(userId ? { userId } : {}),
|
|
428
421
|
});
|
|
429
|
-
registerWorkChatAccountId(accountId);
|
|
430
|
-
//
|
|
422
|
+
await registerWorkChatAccountId(accountId);
|
|
423
|
+
// Complete the binding: account data is already in the private state
|
|
424
|
+
// dir; this call registers the allowFrom user and triggers the channel
|
|
425
|
+
// hot reload.
|
|
431
426
|
const { bindAgentToAccountFromBinding } = await import("./bind-agent.js");
|
|
432
427
|
await bindAgentToAccountFromBinding(accountId, agentId, userId);
|
|
433
428
|
logger.info(`binding listener: account=${accountId} agent=${agentId} persisted`);
|
package/src/channel.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
listWorkChatAccountIds,
|
|
6
6
|
resolveWorkChatAccount,
|
|
7
7
|
} from "./auth/accounts.js";
|
|
8
|
+
import { DEFAULT_BASE_URL } from "./api/types.js";
|
|
8
9
|
import {
|
|
9
10
|
startWorkChatLoginWithQr,
|
|
10
11
|
waitForWorkChatLogin,
|
|
@@ -205,13 +206,24 @@ export const workchatPlugin: ChannelPlugin<ResolvedWorkChatAccount> = {
|
|
|
205
206
|
// The gateway adapter has no cfg in scope here — read the live config
|
|
206
207
|
// from disk (the gateway's own config, same file triggerWorkChatChannelReload
|
|
207
208
|
// writes to) so the ${WORKCHAT_API_KEY} placeholder is resolved by the host.
|
|
209
|
+
// Account ids are always agent-scoped (`agent-${agentId}`); an explicit
|
|
210
|
+
// accountId targets that account, otherwise fall back to the first
|
|
211
|
+
// registered one. A first-time bind (empty index) still resolves an
|
|
212
|
+
// account-like view of the shared channel credentials — the QR flow
|
|
213
|
+
// only consumes baseUrl/appKey/appSecret.
|
|
208
214
|
const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
|
|
209
215
|
const cfg = loadConfig() as OpenClawConfig;
|
|
210
|
-
const
|
|
216
|
+
const requestedAccountId = accountId?.trim() || listWorkChatAccountIds(cfg)[0];
|
|
217
|
+
const existing = requestedAccountId
|
|
218
|
+
? resolveWorkChatAccount(cfg, requestedAccountId)
|
|
219
|
+
: undefined;
|
|
220
|
+
const section = cfg.channels?.["openclaw-workchat"] as
|
|
221
|
+
| { baseUrl?: string; appKey?: string; appSecret?: string }
|
|
222
|
+
| undefined;
|
|
211
223
|
const result: WorkChatQrStartResult = await startWorkChatLoginWithQr({
|
|
212
|
-
baseUrl:
|
|
213
|
-
appKey:
|
|
214
|
-
appSecret:
|
|
224
|
+
baseUrl: existing?.baseUrl || section?.baseUrl || DEFAULT_BASE_URL,
|
|
225
|
+
appKey: existing?.appKey || section?.appKey?.trim() || undefined,
|
|
226
|
+
appSecret: existing?.appSecret || section?.appSecret?.trim() || undefined,
|
|
215
227
|
});
|
|
216
228
|
return {
|
|
217
229
|
qrDataUrl: result.qrcodeUrl,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
registerWorkChatAccountId,
|
|
13
13
|
saveWorkChatAccount,
|
|
14
14
|
triggerWorkChatChannelReload,
|
|
15
|
-
|
|
15
|
+
pruneOtherWorkChatAccounts,
|
|
16
16
|
removeWorkChatAccountBinding,
|
|
17
17
|
} from "../auth/accounts.js";
|
|
18
18
|
import type { ResolvedWorkChatAccount } from "../auth/accounts.js";
|
|
@@ -79,48 +79,41 @@ async function handleAgentLifecycleEvent(
|
|
|
79
79
|
const eventData = result.eventData;
|
|
80
80
|
if (!eventData) return;
|
|
81
81
|
|
|
82
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
82
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
83
83
|
if (!agentId) {
|
|
84
84
|
logger.warn(`${frameType}: missing agent id, ignoring`);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
87
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
88
88
|
|
|
89
89
|
logger.info(`${frameType}: agentId=${agentId} userId=${userId ? "present" : "missing"}`);
|
|
90
90
|
deps.log(`${frameType}: agentId=${agentId}`);
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
const
|
|
92
|
+
// One active agent per host: every binding (first scan included) registers
|
|
93
|
+
// under the agent-scoped key `agent-${agentId}`, and every OTHER registered
|
|
94
|
+
// account is removed before persisting. A reused account id would also stay
|
|
95
|
+
// stopped after the QR flow's manual-stop mark, so rebinds always land on
|
|
96
|
+
// the fresh key and the gateway restarts it via the channel reload below.
|
|
97
|
+
const { completeWorkChatBinding } = await import("../auth/login-qr.js");
|
|
98
|
+
const accountId = frameType === "init_agent"
|
|
99
|
+
? `agent-${agentId}`
|
|
100
|
+
: deps.accountId;
|
|
98
101
|
|
|
99
|
-
// Rebind semantics: one active agent per host. Before persisting the new
|
|
100
|
-
// binding, remove every registered account bound to a different agent
|
|
101
|
-
// (a re-scan creates a new platform agent; the old one is deleted there).
|
|
102
102
|
try {
|
|
103
|
-
const removed = await
|
|
103
|
+
const removed = await pruneOtherWorkChatAccounts(accountId);
|
|
104
104
|
if (removed.length > 0) {
|
|
105
|
-
deps.log(`rebind: removed
|
|
105
|
+
deps.log(`rebind: removed previous account(s): ${removed.join(", ")}`);
|
|
106
106
|
}
|
|
107
107
|
} catch (err) {
|
|
108
108
|
logger.error(`rebind cleanup failed: ${String(err)}`);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// The bound agent becomes the account: `default` for the primary install,
|
|
112
|
-
// or a fresh agent-scoped key on rebind.
|
|
113
|
-
const { completeWorkChatBinding } = await import("../auth/login-qr.js");
|
|
114
|
-
const accountId = frameType === "init_agent"
|
|
115
|
-
? (isRebind ? `agent-${agentId}` : "default")
|
|
116
|
-
: deps.accountId;
|
|
117
|
-
|
|
118
111
|
try {
|
|
119
112
|
saveWorkChatAccount(accountId, {
|
|
120
113
|
agentId,
|
|
121
114
|
...(userId ? { userId } : {}),
|
|
122
115
|
});
|
|
123
|
-
registerWorkChatAccountId(accountId);
|
|
116
|
+
await registerWorkChatAccountId(accountId);
|
|
124
117
|
// Bind the account to the incoming agent for message routing. agentId
|
|
125
118
|
// comes from the event; userId (futureId) is the WorkChat user.
|
|
126
119
|
const { bindAgentToAccount } = await import("../auth/bind-agent.js");
|
|
@@ -149,10 +142,6 @@ async function handleAgentLifecycleEvent(
|
|
|
149
142
|
}
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
function listWorkChatAccountIdsCount(): number {
|
|
153
|
-
return listIndexedWorkChatAccountIds().length;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
145
|
// Typing-indicator helper versions: the host renamed the subpath between
|
|
157
146
|
// releases (2026.3.x exports openclaw/plugin-sdk/channel-runtime, 2026.8.x
|
|
158
147
|
// exports openclaw/plugin-sdk/channel-message). Load whichever resolves —
|