@workclaw/openclaw-workchat 1.0.0 → 1.0.3

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.
@@ -1,290 +1,290 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
5
-
6
- import { DEFAULT_BASE_URL } from "../api/types.js";
7
- import { resolveStateDir } from "../storage/state-dir.js";
8
- import { logger } from "../util/logger.js";
9
-
10
- // ---------------------------------------------------------------------------
11
- // Account index (persistent list of bound account IDs)
12
- // ---------------------------------------------------------------------------
13
-
14
- function resolveWorkChatStateDir(): string {
15
- return path.join(resolveStateDir(), "openclaw-workchat");
16
- }
17
-
18
- function resolveAccountIndexPath(): string {
19
- return path.join(resolveWorkChatStateDir(), "accounts.json");
20
- }
21
-
22
- /** Returns all accountIds registered via binding QR. */
23
- export function listIndexedWorkChatAccountIds(): string[] {
24
- const filePath = resolveAccountIndexPath();
25
- try {
26
- if (!fs.existsSync(filePath)) return [];
27
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
28
- if (!Array.isArray(parsed)) return [];
29
- return parsed.filter((id): id is string => typeof id === "string" && id.trim() !== "");
30
- } catch {
31
- return [];
32
- }
33
- }
34
-
35
- /** Add accountId to the persistent index (no-op if already present). */
36
- export function registerWorkChatAccountId(accountId: string): void {
37
- const dir = resolveWorkChatStateDir();
38
- fs.mkdirSync(dir, { recursive: true });
39
- const existing = listIndexedWorkChatAccountIds();
40
- if (existing.includes(accountId)) return;
41
- fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([...existing, accountId], null, 2), "utf-8");
42
- }
43
-
44
- /** Remove accountId from the persistent index. */
45
- export function unregisterWorkChatAccountId(accountId: string): void {
46
- const existing = listIndexedWorkChatAccountIds();
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
- }
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Account store (per-account data files)
55
- // ---------------------------------------------------------------------------
56
-
57
- /** Unified per-account data: agent/user ids discovered at binding time. */
58
- export type WorkChatAccountData = {
59
- agentId?: string;
60
- userId?: string;
61
- savedAt?: string;
62
- baseUrl?: string;
63
- };
64
-
65
- function resolveAccountsDir(): string {
66
- return path.join(resolveWorkChatStateDir(), "accounts");
67
- }
68
-
69
- function resolveAccountPath(accountId: string): string {
70
- return path.join(resolveAccountsDir(), `${accountId}.json`);
71
- }
72
-
73
- function readAccountFile(filePath: string): WorkChatAccountData | null {
74
- try {
75
- if (fs.existsSync(filePath)) {
76
- return JSON.parse(fs.readFileSync(filePath, "utf-8")) as WorkChatAccountData;
77
- }
78
- } catch {
79
- // ignore
80
- }
81
- return null;
82
- }
83
-
84
- /** Load account data by ID. */
85
- export function loadWorkChatAccount(accountId: string): WorkChatAccountData | null {
86
- return readAccountFile(resolveAccountPath(accountId));
87
- }
88
-
89
- /**
90
- * Persist account data after binding (merges into existing file).
91
- * agentId/userId are the WorkChat cloud ids discovered via AGENT_CREATED /
92
- * INIT_AGENT events or the binding QR poll.
93
- */
94
- export function saveWorkChatAccount(
95
- accountId: string,
96
- update: { agentId?: string; userId?: string; baseUrl?: string },
97
- ): void {
98
- const dir = resolveAccountsDir();
99
- fs.mkdirSync(dir, { recursive: true });
100
-
101
- const existing = loadWorkChatAccount(accountId) ?? {};
102
- const data: WorkChatAccountData = {
103
- agentId: update.agentId?.trim() || existing.agentId,
104
- userId: update.userId?.trim() || existing.userId,
105
- baseUrl: update.baseUrl?.trim() || existing.baseUrl,
106
- savedAt: new Date().toISOString(),
107
- };
108
- const filePath = resolveAccountPath(accountId);
109
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
110
- try {
111
- fs.chmodSync(filePath, 0o600);
112
- } catch {
113
- // best-effort
114
- }
115
- }
116
-
117
- /** Remove all files associated with an account. */
118
- export function clearWorkChatAccount(accountId: string): void {
119
- const filePath = resolveAccountPath(accountId);
120
- try {
121
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
122
- } catch (err) {
123
- logger.warn(`clearWorkChatAccount: failed to remove ${filePath}: ${String(err)}`);
124
- }
125
- unregisterWorkChatAccountId(accountId);
126
- }
127
-
128
- /**
129
- * Fully remove an account binding: state file, index entry, allowFrom store
130
- * and the `channels.openclaw-workchat.accounts` routing entry in openclaw.json.
131
- * Used when a re-scan replaces the previously bound agent (rebind semantics:
132
- * one active agent per host) and when the platform reports AGENT_DELETED.
133
- */
134
- export async function removeWorkChatAccountBinding(accountId: string): Promise<void> {
135
- clearWorkChatAccount(accountId);
136
-
137
- try {
138
- const { clearFrameworkAllowFromList } = await import("./pairing.js");
139
- clearFrameworkAllowFromList(accountId);
140
- } catch (err) {
141
- logger.warn(`removeWorkChatAccountBinding: allowFrom cleanup failed: ${String(err)}`);
142
- }
143
-
144
- try {
145
- const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
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
- }
160
- } catch (err) {
161
- logger.warn(`removeWorkChatAccountBinding: config cleanup failed: ${String(err)}`);
162
- }
163
- }
164
-
165
- /**
166
- * Remove every registered account whose stored agentId differs from
167
- * `keepAgentId` (rebind cleanup). Returns the removed accountIds.
168
- * `default` is normalized: if it holds a foreign agentId it is removed too;
169
- * the surviving binding keeps whatever account key it was registered under.
170
- */
171
- export async function pruneStaleWorkChatAccounts(keepAgentId: string): Promise<string[]> {
172
- const removed: string[] = [];
173
- for (const accountId of listIndexedWorkChatAccountIds()) {
174
- const data = loadWorkChatAccount(accountId);
175
- const storedAgentId = data?.agentId?.trim();
176
- if (storedAgentId && storedAgentId !== keepAgentId) {
177
- await removeWorkChatAccountBinding(accountId);
178
- removed.push(accountId);
179
- logger.info(
180
- `pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`,
181
- );
182
- }
183
- }
184
- return removed;
185
- }
186
-
187
- // ---------------------------------------------------------------------------
188
- // Account resolution (merge config + stored ids)
189
- // ---------------------------------------------------------------------------
190
-
191
- export type ResolvedWorkChatAccount = {
192
- accountId: string;
193
- baseUrl: string;
194
- appKey?: string;
195
- appSecret?: string;
196
- agentId?: string;
197
- userId?: string;
198
- enabled: boolean;
199
- /** true when appKey + appSecret are available (via ${WORKCHAT_*} env placeholders). */
200
- configured: boolean;
201
- name?: string;
202
- allowInsecureTls?: boolean;
203
- requestTimeout?: number;
204
- };
205
-
206
- type WorkChatAccountConfig = {
207
- name?: string;
208
- enabled?: boolean;
209
- agentId?: string | number;
210
- userId?: string | number;
211
- };
212
-
213
- type WorkChatSectionConfig = WorkChatAccountConfig & {
214
- baseUrl?: string;
215
- websocketUrl?: string;
216
- appKey?: string;
217
- appSecret?: string;
218
- allowInsecureTls?: boolean;
219
- requestTimeout?: number;
220
- accounts?: Record<string, WorkChatAccountConfig>;
221
- /** Written on each successful binding; see triggerWorkChatChannelReload. */
222
- channelConfigUpdatedAt?: string;
223
- };
224
-
225
- /** List accountIds from the index file (written at binding). */
226
- export function listWorkChatAccountIds(_cfg: OpenClawConfig): string[] {
227
- return listIndexedWorkChatAccountIds();
228
- }
229
-
230
- /** Resolve a workchat account by ID, merging config and stored ids. */
231
- export function resolveWorkChatAccount(
232
- cfg: OpenClawConfig,
233
- accountId?: string | null,
234
- ): ResolvedWorkChatAccount {
235
- const raw = accountId?.trim();
236
- if (!raw) {
237
- throw new Error("workchat: accountId is required (no default account)");
238
- }
239
- const section = cfg.channels?.["openclaw-workchat"] as WorkChatSectionConfig | undefined;
240
- const accountCfg: WorkChatAccountConfig = section?.accounts?.[raw] ?? section ?? {};
241
-
242
- const accountData = loadWorkChatAccount(raw);
243
- const appKey = section?.appKey?.trim() || undefined;
244
- const appSecret = section?.appSecret?.trim() || undefined;
245
-
246
- return {
247
- accountId: raw,
248
- baseUrl: section?.baseUrl?.trim() || DEFAULT_BASE_URL,
249
- appKey,
250
- appSecret,
251
- agentId: String(accountCfg.agentId ?? "") || accountData?.agentId,
252
- userId: String(accountCfg.userId ?? "") || accountData?.userId,
253
- enabled: accountCfg.enabled !== false,
254
- configured: Boolean(appKey && appSecret),
255
- name: accountCfg.name?.trim() || undefined,
256
- allowInsecureTls: section?.allowInsecureTls,
257
- requestTimeout: section?.requestTimeout,
258
- };
259
- }
260
-
261
- // ---------------------------------------------------------------------------
262
- // Config reload trigger
263
- // ---------------------------------------------------------------------------
264
-
265
- /**
266
- * Bump `channels.openclaw-workchat.channelConfigUpdatedAt` in openclaw.json on
267
- * each successful binding so the gateway reloads config from disk.
268
- */
269
- export async function triggerWorkChatChannelReload(): Promise<void> {
270
- try {
271
- const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
272
- const cfg = loadConfig();
273
- const channels = (cfg.channels ?? {}) as Record<string, unknown>;
274
- const existing = (channels["openclaw-workchat"] as Record<string, unknown> | undefined) ?? {};
275
- const updated: OpenClawConfig = {
276
- ...cfg,
277
- channels: {
278
- ...channels,
279
- "openclaw-workchat": {
280
- ...existing,
281
- channelConfigUpdatedAt: new Date().toISOString(),
282
- },
283
- },
284
- };
285
- await writeConfigFile(updated);
286
- logger.info("triggerWorkChatChannelReload: wrote channel config to openclaw.json");
287
- } catch (err) {
288
- logger.warn(`triggerWorkChatChannelReload: failed to update config: ${String(err)}`);
289
- }
290
- }
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
5
+
6
+ import { DEFAULT_BASE_URL } from "../api/types.js";
7
+ import { resolveStateDir } from "../storage/state-dir.js";
8
+ import { logger } from "../util/logger.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Account index (persistent list of bound account IDs)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function resolveWorkChatStateDir(): string {
15
+ return path.join(resolveStateDir(), "openclaw-workchat");
16
+ }
17
+
18
+ function resolveAccountIndexPath(): string {
19
+ return path.join(resolveWorkChatStateDir(), "accounts.json");
20
+ }
21
+
22
+ /** Returns all accountIds registered via binding QR. */
23
+ export function listIndexedWorkChatAccountIds(): string[] {
24
+ const filePath = resolveAccountIndexPath();
25
+ try {
26
+ if (!fs.existsSync(filePath)) return [];
27
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
28
+ if (!Array.isArray(parsed)) return [];
29
+ return parsed.filter((id): id is string => typeof id === "string" && id.trim() !== "");
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /** Add accountId to the persistent index (no-op if already present). */
36
+ export function registerWorkChatAccountId(accountId: string): void {
37
+ const dir = resolveWorkChatStateDir();
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ const existing = listIndexedWorkChatAccountIds();
40
+ if (existing.includes(accountId)) return;
41
+ fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([...existing, accountId], null, 2), "utf-8");
42
+ }
43
+
44
+ /** Remove accountId from the persistent index. */
45
+ export function unregisterWorkChatAccountId(accountId: string): void {
46
+ const existing = listIndexedWorkChatAccountIds();
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
+ }
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Account store (per-account data files)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Unified per-account data: agent/user ids discovered at binding time. */
58
+ export type WorkChatAccountData = {
59
+ agentId?: string;
60
+ userId?: string;
61
+ savedAt?: string;
62
+ baseUrl?: string;
63
+ };
64
+
65
+ function resolveAccountsDir(): string {
66
+ return path.join(resolveWorkChatStateDir(), "accounts");
67
+ }
68
+
69
+ function resolveAccountPath(accountId: string): string {
70
+ return path.join(resolveAccountsDir(), `${accountId}.json`);
71
+ }
72
+
73
+ function readAccountFile(filePath: string): WorkChatAccountData | null {
74
+ try {
75
+ if (fs.existsSync(filePath)) {
76
+ return JSON.parse(fs.readFileSync(filePath, "utf-8")) as WorkChatAccountData;
77
+ }
78
+ } catch {
79
+ // ignore
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /** Load account data by ID. */
85
+ export function loadWorkChatAccount(accountId: string): WorkChatAccountData | null {
86
+ return readAccountFile(resolveAccountPath(accountId));
87
+ }
88
+
89
+ /**
90
+ * Persist account data after binding (merges into existing file).
91
+ * agentId/userId are the WorkChat cloud ids discovered via AGENT_CREATED /
92
+ * INIT_AGENT events or the binding QR poll.
93
+ */
94
+ export function saveWorkChatAccount(
95
+ accountId: string,
96
+ update: { agentId?: string; userId?: string; baseUrl?: string },
97
+ ): void {
98
+ const dir = resolveAccountsDir();
99
+ fs.mkdirSync(dir, { recursive: true });
100
+
101
+ const existing = loadWorkChatAccount(accountId) ?? {};
102
+ const data: WorkChatAccountData = {
103
+ agentId: update.agentId?.trim() || existing.agentId,
104
+ userId: update.userId?.trim() || existing.userId,
105
+ baseUrl: update.baseUrl?.trim() || existing.baseUrl,
106
+ savedAt: new Date().toISOString(),
107
+ };
108
+ const filePath = resolveAccountPath(accountId);
109
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
110
+ try {
111
+ fs.chmodSync(filePath, 0o600);
112
+ } catch {
113
+ // best-effort
114
+ }
115
+ }
116
+
117
+ /** Remove all files associated with an account. */
118
+ export function clearWorkChatAccount(accountId: string): void {
119
+ const filePath = resolveAccountPath(accountId);
120
+ try {
121
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
122
+ } catch (err) {
123
+ logger.warn(`clearWorkChatAccount: failed to remove ${filePath}: ${String(err)}`);
124
+ }
125
+ unregisterWorkChatAccountId(accountId);
126
+ }
127
+
128
+ /**
129
+ * Fully remove an account binding: state file, index entry, allowFrom store
130
+ * and the `channels.openclaw-workchat.accounts` routing entry in openclaw.json.
131
+ * Used when a re-scan replaces the previously bound agent (rebind semantics:
132
+ * one active agent per host) and when the platform reports AGENT_DELETED.
133
+ */
134
+ export async function removeWorkChatAccountBinding(accountId: string): Promise<void> {
135
+ clearWorkChatAccount(accountId);
136
+
137
+ try {
138
+ const { clearFrameworkAllowFromList } = await import("./pairing.js");
139
+ clearFrameworkAllowFromList(accountId);
140
+ } catch (err) {
141
+ logger.warn(`removeWorkChatAccountBinding: allowFrom cleanup failed: ${String(err)}`);
142
+ }
143
+
144
+ try {
145
+ const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
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
+ }
160
+ } catch (err) {
161
+ logger.warn(`removeWorkChatAccountBinding: config cleanup failed: ${String(err)}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Remove every registered account whose stored agentId differs from
167
+ * `keepAgentId` (rebind cleanup). Returns the removed accountIds.
168
+ * `default` is normalized: if it holds a foreign agentId it is removed too;
169
+ * the surviving binding keeps whatever account key it was registered under.
170
+ */
171
+ export async function pruneStaleWorkChatAccounts(keepAgentId: string): Promise<string[]> {
172
+ const removed: string[] = [];
173
+ for (const accountId of listIndexedWorkChatAccountIds()) {
174
+ const data = loadWorkChatAccount(accountId);
175
+ const storedAgentId = data?.agentId?.trim();
176
+ if (storedAgentId && storedAgentId !== keepAgentId) {
177
+ await removeWorkChatAccountBinding(accountId);
178
+ removed.push(accountId);
179
+ logger.info(
180
+ `pruneStaleWorkChatAccounts: removed account=${accountId} agent=${storedAgentId} (superseded by agent=${keepAgentId})`,
181
+ );
182
+ }
183
+ }
184
+ return removed;
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Account resolution (merge config + stored ids)
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export type ResolvedWorkChatAccount = {
192
+ accountId: string;
193
+ baseUrl: string;
194
+ appKey?: string;
195
+ appSecret?: string;
196
+ agentId?: string;
197
+ userId?: string;
198
+ enabled: boolean;
199
+ /** true when appKey + appSecret are available (via ${WORKCHAT_*} env placeholders). */
200
+ configured: boolean;
201
+ name?: string;
202
+ allowInsecureTls?: boolean;
203
+ requestTimeout?: number;
204
+ };
205
+
206
+ type WorkChatAccountConfig = {
207
+ name?: string;
208
+ enabled?: boolean;
209
+ agentId?: string | number;
210
+ userId?: string | number;
211
+ };
212
+
213
+ type WorkChatSectionConfig = WorkChatAccountConfig & {
214
+ baseUrl?: string;
215
+ websocketUrl?: string;
216
+ appKey?: string;
217
+ appSecret?: string;
218
+ allowInsecureTls?: boolean;
219
+ requestTimeout?: number;
220
+ accounts?: Record<string, WorkChatAccountConfig>;
221
+ /** Written on each successful binding; see triggerWorkChatChannelReload. */
222
+ channelConfigUpdatedAt?: string;
223
+ };
224
+
225
+ /** List accountIds from the index file (written at binding). */
226
+ export function listWorkChatAccountIds(_cfg: OpenClawConfig): string[] {
227
+ return listIndexedWorkChatAccountIds();
228
+ }
229
+
230
+ /** Resolve a workchat account by ID, merging config and stored ids. */
231
+ export function resolveWorkChatAccount(
232
+ cfg: OpenClawConfig,
233
+ accountId?: string | null,
234
+ ): ResolvedWorkChatAccount {
235
+ const raw = accountId?.trim();
236
+ if (!raw) {
237
+ throw new Error("workchat: accountId is required (no default account)");
238
+ }
239
+ const section = cfg.channels?.["openclaw-workchat"] as WorkChatSectionConfig | undefined;
240
+ const accountCfg: WorkChatAccountConfig = section?.accounts?.[raw] ?? section ?? {};
241
+
242
+ const accountData = loadWorkChatAccount(raw);
243
+ const appKey = section?.appKey?.trim() || undefined;
244
+ const appSecret = section?.appSecret?.trim() || undefined;
245
+
246
+ return {
247
+ accountId: raw,
248
+ baseUrl: section?.baseUrl?.trim() || DEFAULT_BASE_URL,
249
+ appKey,
250
+ appSecret,
251
+ agentId: String(accountCfg.agentId ?? "") || accountData?.agentId,
252
+ userId: String(accountCfg.userId ?? "") || accountData?.userId,
253
+ enabled: accountCfg.enabled !== false,
254
+ configured: Boolean(appKey && appSecret),
255
+ name: accountCfg.name?.trim() || undefined,
256
+ allowInsecureTls: section?.allowInsecureTls,
257
+ requestTimeout: section?.requestTimeout,
258
+ };
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Config reload trigger
263
+ // ---------------------------------------------------------------------------
264
+
265
+ /**
266
+ * Bump `channels.openclaw-workchat.channelConfigUpdatedAt` in openclaw.json on
267
+ * each successful binding so the gateway reloads config from disk.
268
+ */
269
+ export async function triggerWorkChatChannelReload(): Promise<void> {
270
+ try {
271
+ const { loadConfig, writeConfigFile } = await import("openclaw/plugin-sdk/config-runtime");
272
+ const cfg = loadConfig();
273
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
274
+ const existing = (channels["openclaw-workchat"] as Record<string, unknown> | undefined) ?? {};
275
+ const updated: OpenClawConfig = {
276
+ ...cfg,
277
+ channels: {
278
+ ...channels,
279
+ "openclaw-workchat": {
280
+ ...existing,
281
+ channelConfigUpdatedAt: new Date().toISOString(),
282
+ },
283
+ },
284
+ };
285
+ await writeConfigFile(updated);
286
+ logger.info("triggerWorkChatChannelReload: wrote channel config to openclaw.json");
287
+ } catch (err) {
288
+ logger.warn(`triggerWorkChatChannelReload: failed to update config: ${String(err)}`);
289
+ }
290
+ }