@workclaw/openclaw-workchat 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +98 -48
- package/index.ts +7 -0
- package/package.json +1 -1
- package/src/auth/accounts.ts +108 -28
- package/src/auth/login-qr.ts +15 -22
- 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);
|
|
@@ -378,21 +386,57 @@ async function removeWorkChatAccountBinding(accountId) {
|
|
|
378
386
|
logger.warn(`removeWorkChatAccountBinding: channel reload trigger failed: ${String(err)}`);
|
|
379
387
|
}
|
|
380
388
|
}
|
|
381
|
-
async function
|
|
389
|
+
async function pruneOtherWorkChatAccounts(keepAccountId) {
|
|
390
|
+
const keep = keepAccountId.trim();
|
|
382
391
|
const removed = [];
|
|
383
392
|
for (const accountId of listIndexedWorkChatAccountIds()) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
removed
|
|
389
|
-
|
|
390
|
-
`pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
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
|
+
);
|
|
393
399
|
}
|
|
394
400
|
return removed;
|
|
395
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
|
+
}
|
|
396
440
|
function listWorkChatAccountIds(_cfg) {
|
|
397
441
|
return listIndexedWorkChatAccountIds();
|
|
398
442
|
}
|
|
@@ -442,12 +486,14 @@ async function triggerWorkChatChannelReload() {
|
|
|
442
486
|
logger.warn(`triggerWorkChatChannelReload: failed to update config: ${String(err)}`);
|
|
443
487
|
}
|
|
444
488
|
}
|
|
489
|
+
var indexUpdateChain;
|
|
445
490
|
var init_accounts = __esm({
|
|
446
491
|
"src/auth/accounts.ts"() {
|
|
447
492
|
"use strict";
|
|
448
493
|
init_types();
|
|
449
494
|
init_state_dir();
|
|
450
495
|
init_logger();
|
|
496
|
+
indexUpdateChain = Promise.resolve();
|
|
451
497
|
}
|
|
452
498
|
});
|
|
453
499
|
|
|
@@ -27127,28 +27173,27 @@ async function handleListenerFrame(sessionKey, data, ws) {
|
|
|
27127
27173
|
if (result.type !== "init_agent") return;
|
|
27128
27174
|
const eventData = result.eventData;
|
|
27129
27175
|
if (!eventData) return;
|
|
27130
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
27176
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
27131
27177
|
if (!agentId) return;
|
|
27132
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
27178
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
27133
27179
|
const entry = bindingListeners.get(sessionKey);
|
|
27134
27180
|
if (entry) entry.done = true;
|
|
27135
|
-
const
|
|
27181
|
+
const accountId = `agent-${agentId}`;
|
|
27136
27182
|
try {
|
|
27137
|
-
const {
|
|
27138
|
-
const removed = await
|
|
27183
|
+
const { pruneOtherWorkChatAccounts: pruneOtherWorkChatAccounts2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
|
|
27184
|
+
const removed = await pruneOtherWorkChatAccounts2(accountId);
|
|
27139
27185
|
if (removed.length > 0) {
|
|
27140
|
-
logger.info(`binding listener: pruned
|
|
27186
|
+
logger.info(`binding listener: pruned previous account(s): ${removed.join(", ")}`);
|
|
27141
27187
|
}
|
|
27142
27188
|
} catch (err) {
|
|
27143
27189
|
logger.error(`binding listener: rebind cleanup failed: ${String(err)}`);
|
|
27144
27190
|
}
|
|
27145
|
-
const accountId = isRebind ? `agent-${agentId}` : "default";
|
|
27146
27191
|
try {
|
|
27147
27192
|
saveWorkChatAccount(accountId, {
|
|
27148
27193
|
agentId,
|
|
27149
27194
|
...userId ? { userId } : {}
|
|
27150
27195
|
});
|
|
27151
|
-
registerWorkChatAccountId(accountId);
|
|
27196
|
+
await registerWorkChatAccountId(accountId);
|
|
27152
27197
|
const { bindAgentToAccountFromBinding: bindAgentToAccountFromBinding2 } = await Promise.resolve().then(() => (init_bind_agent(), bind_agent_exports));
|
|
27153
27198
|
await bindAgentToAccountFromBinding2(accountId, agentId, userId);
|
|
27154
27199
|
logger.info(`binding listener: account=${accountId} agent=${agentId} persisted`);
|
|
@@ -27843,31 +27888,30 @@ function emitWorkChatMessageSent2(params) {
|
|
|
27843
27888
|
async function handleAgentLifecycleEvent(frameType, result, deps) {
|
|
27844
27889
|
const eventData = result.eventData;
|
|
27845
27890
|
if (!eventData) return;
|
|
27846
|
-
const agentId = String(eventData.id ?? eventData.agentId ?? "");
|
|
27891
|
+
const agentId = String(eventData.id ?? eventData.agentId ?? "").trim();
|
|
27847
27892
|
if (!agentId) {
|
|
27848
27893
|
logger.warn(`${frameType}: missing agent id, ignoring`);
|
|
27849
27894
|
return;
|
|
27850
27895
|
}
|
|
27851
|
-
const userId = String(eventData.futureId ?? eventData.userId ?? "");
|
|
27896
|
+
const userId = String(eventData.futureId ?? eventData.userId ?? "").trim();
|
|
27852
27897
|
logger.info(`${frameType}: agentId=${agentId} userId=${userId ? "present" : "missing"}`);
|
|
27853
27898
|
deps.log(`${frameType}: agentId=${agentId}`);
|
|
27854
|
-
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;
|
|
27855
27901
|
try {
|
|
27856
|
-
const removed = await
|
|
27902
|
+
const removed = await pruneOtherWorkChatAccounts(accountId);
|
|
27857
27903
|
if (removed.length > 0) {
|
|
27858
|
-
deps.log(`rebind: removed
|
|
27904
|
+
deps.log(`rebind: removed previous account(s): ${removed.join(", ")}`);
|
|
27859
27905
|
}
|
|
27860
27906
|
} catch (err) {
|
|
27861
27907
|
logger.error(`rebind cleanup failed: ${String(err)}`);
|
|
27862
27908
|
}
|
|
27863
|
-
const { completeWorkChatBinding: completeWorkChatBinding2 } = await Promise.resolve().then(() => (init_login_qr(), login_qr_exports));
|
|
27864
|
-
const accountId = frameType === "init_agent" ? isRebind ? `agent-${agentId}` : "default" : deps.accountId;
|
|
27865
27909
|
try {
|
|
27866
27910
|
saveWorkChatAccount(accountId, {
|
|
27867
27911
|
agentId,
|
|
27868
27912
|
...userId ? { userId } : {}
|
|
27869
27913
|
});
|
|
27870
|
-
registerWorkChatAccountId(accountId);
|
|
27914
|
+
await registerWorkChatAccountId(accountId);
|
|
27871
27915
|
const { bindAgentToAccount: bindAgentToAccount2 } = await Promise.resolve().then(() => (init_bind_agent(), bind_agent_exports));
|
|
27872
27916
|
await bindAgentToAccount2(accountId, agentId, userId);
|
|
27873
27917
|
logger.info(`${frameType}: bound agent to account accountId=${accountId}`);
|
|
@@ -27889,9 +27933,6 @@ async function handleAgentLifecycleEvent(frameType, result, deps) {
|
|
|
27889
27933
|
void notifyAgentCreatedStatus(deps.apiOpts, agentId);
|
|
27890
27934
|
}
|
|
27891
27935
|
}
|
|
27892
|
-
function listWorkChatAccountIdsCount() {
|
|
27893
|
-
return listIndexedWorkChatAccountIds().length;
|
|
27894
|
-
}
|
|
27895
27936
|
function loadCreateTypingCallbacks() {
|
|
27896
27937
|
createTypingCallbacksPromise ??= (async () => {
|
|
27897
27938
|
for (const specifier of ["openclaw/plugin-sdk/channel-runtime", "openclaw/plugin-sdk/channel-message"]) {
|
|
@@ -29529,6 +29570,7 @@ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-sch
|
|
|
29529
29570
|
|
|
29530
29571
|
// src/channel.ts
|
|
29531
29572
|
init_accounts();
|
|
29573
|
+
init_types();
|
|
29532
29574
|
init_login_qr();
|
|
29533
29575
|
init_send();
|
|
29534
29576
|
init_logger();
|
|
@@ -29705,11 +29747,13 @@ var workchatPlugin = {
|
|
|
29705
29747
|
loginWithQrStart: async ({ accountId, force, verbose }) => {
|
|
29706
29748
|
const { loadConfig } = await import("openclaw/plugin-sdk/config-runtime");
|
|
29707
29749
|
const cfg = loadConfig();
|
|
29708
|
-
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"];
|
|
29709
29753
|
const result = await startWorkChatLoginWithQr({
|
|
29710
|
-
baseUrl:
|
|
29711
|
-
appKey:
|
|
29712
|
-
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
|
|
29713
29757
|
});
|
|
29714
29758
|
return {
|
|
29715
29759
|
qrDataUrl: result.qrcodeUrl,
|
|
@@ -29816,6 +29860,9 @@ function assertHostCompatibility(hostVersion) {
|
|
|
29816
29860
|
);
|
|
29817
29861
|
}
|
|
29818
29862
|
|
|
29863
|
+
// index.ts
|
|
29864
|
+
init_accounts();
|
|
29865
|
+
|
|
29819
29866
|
// node_modules/zod/v4/classic/external.js
|
|
29820
29867
|
var external_exports = {};
|
|
29821
29868
|
__export(external_exports, {
|
|
@@ -49552,6 +49599,9 @@ var openclaw_workchat_default = {
|
|
|
49552
49599
|
configSchema: buildChannelConfigSchema(WorkChatConfigSchema),
|
|
49553
49600
|
register(api) {
|
|
49554
49601
|
assertHostCompatibility(api.runtime?.version);
|
|
49602
|
+
void migrateLegacyDefaultWorkChatAccount().catch((err) => {
|
|
49603
|
+
console.error("[openclaw-workchat] legacy default account migration failed:", err);
|
|
49604
|
+
});
|
|
49555
49605
|
api.registerChannel({ plugin: workchatPlugin });
|
|
49556
49606
|
}
|
|
49557
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
|
/**
|
|
@@ -152,23 +177,22 @@ export async function removeWorkChatAccountBinding(accountId: string): Promise<v
|
|
|
152
177
|
}
|
|
153
178
|
|
|
154
179
|
/**
|
|
155
|
-
* Remove every registered account
|
|
156
|
-
*
|
|
157
|
-
* `
|
|
158
|
-
*
|
|
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.
|
|
159
185
|
*/
|
|
160
|
-
export async function
|
|
186
|
+
export async function pruneOtherWorkChatAccounts(keepAccountId: string): Promise<string[]> {
|
|
187
|
+
const keep = keepAccountId.trim();
|
|
161
188
|
const removed: string[] = [];
|
|
162
189
|
for (const accountId of listIndexedWorkChatAccountIds()) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
removed
|
|
168
|
-
|
|
169
|
-
`pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
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
|
+
);
|
|
172
196
|
}
|
|
173
197
|
return removed;
|
|
174
198
|
}
|
|
@@ -211,6 +235,62 @@ type WorkChatSectionConfig = WorkChatAccountConfig & {
|
|
|
211
235
|
channelConfigUpdatedAt?: string;
|
|
212
236
|
};
|
|
213
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
|
+
|
|
214
294
|
/** List accountIds from the index file (written at binding). */
|
|
215
295
|
export function listWorkChatAccountIds(_cfg: OpenClawConfig): string[] {
|
|
216
296
|
return listIndexedWorkChatAccountIds();
|
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,45 +387,39 @@ 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);
|
|
422
|
+
await registerWorkChatAccountId(accountId);
|
|
430
423
|
// Complete the binding: account data is already in the private state
|
|
431
424
|
// dir; this call registers the allowFrom user and triggers the channel
|
|
432
425
|
// hot reload.
|
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 —
|