@workclaw/openclaw-workclaw 1.0.17 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/index.ts +210 -210
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -4
- package/setup-entry.ts +6 -0
- package/skills/openclaw-workclaw-cron/SKILL.md +45 -28
- package/src/accounts.ts +62 -37
- package/src/api/accounts-api.ts +88 -89
- package/src/api/prompts-api.ts +70 -77
- package/src/api/session-api.ts +99 -108
- package/src/api/skills-api.ts +35 -37
- package/src/api/workspace.ts +27 -29
- package/src/channel.ts +200 -202
- package/src/config-schema.ts +9 -9
- package/src/connection/workclaw-client.ts +554 -567
- package/src/gateway/agent-handlers.ts +392 -426
- package/src/gateway/config-writer.ts +228 -243
- package/src/gateway/message-context.ts +534 -362
- package/src/gateway/message-dispatcher.ts +529 -489
- package/src/gateway/reconnect.ts +217 -113
- package/src/gateway/skills-handler.ts +408 -472
- package/src/gateway/skills-list-handler.ts +9 -9
- package/src/gateway/tools-list-handler.ts +70 -72
- package/src/gateway/workclaw-gateway.ts +328 -486
- package/src/media/upload.ts +83 -94
- package/src/outbound/index.ts +57 -55
- package/src/outbound/workclaw-sender.ts +134 -133
- package/src/runtime.ts +291 -194
- package/src/send.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/api/index.ts +6 -6
- package/src/tools/openclaw-workclaw-cron/src/add/params.ts +20 -19
- package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -197
- package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +4 -4
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +2 -2
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +4 -4
- package/src/types.ts +38 -40
- package/src/utils/content.ts +16 -21
- package/tests/accounts.test.ts +285 -0
- package/tests/message-context.test.ts +313 -0
- package/tests/reconnect.test.ts +257 -0
- package/tests/workclaw-client.test.ts +112 -0
- package/tsconfig.json +8 -5
- package/vitest.config.ts +8 -0
|
@@ -15,13 +15,21 @@ always: true
|
|
|
15
15
|
|
|
16
16
|
你没有内存或后台线程,口头承诺"到时候提醒"是无效的——只有调用工具才能真正注册定时任务。
|
|
17
17
|
|
|
18
|
-
**🔒
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
18
|
+
**🔒 时间格式强制规则(核心!)**:
|
|
19
|
+
|
|
20
|
+
1. **所有时间都必须转换为 cron 表达式**
|
|
21
|
+
- 相对时间(如 `5m`、`1h`、`1h30m`、`2d`)会自动转换为 cron 表达式
|
|
22
|
+
- 你只需要传入用户说的原始时间,工具会自动转换
|
|
23
|
+
- 示例:`"5分钟后提醒"` → 传入 `time: "5m"` → 工具转换为 cron 表达式
|
|
24
|
+
|
|
25
|
+
2. **严禁使用 `--at` 参数**
|
|
26
|
+
- ❌ 禁止将 cron 表达式转换为 `--at` 绝对时间参数
|
|
27
|
+
- ✅ 必须使用 `--cron` 参数传递 cron 表达式
|
|
28
|
+
- ❌ 禁止使用任何绝对时间格式(如 ISO 时间戳)
|
|
29
|
+
|
|
30
|
+
3. **禁止使用其他定时方式**
|
|
31
|
+
- ❌ 禁止使用 `--at`、`--every`、`--deliver` 等参数
|
|
32
|
+
- ✅ 只能使用 `--cron` 参数
|
|
25
33
|
|
|
26
34
|
***
|
|
27
35
|
|
|
@@ -68,24 +76,26 @@ always: true
|
|
|
68
76
|
|
|
69
77
|
| 参数 | 说明 | 示例 |
|
|
70
78
|
|------|------|------|
|
|
71
|
-
| `time` |
|
|
79
|
+
| `time` | ⏰ **时间(核心!)**,支持相对时间和 cron 表达式,工具会自动统一转换为 cron 表达式 | `"5m"` / `"0 8 * * *"` |
|
|
72
80
|
| `name` | 任务名称 | `"喝水提醒"` |
|
|
73
81
|
| `message` | 提醒内容 | `"该喝水了"` |
|
|
74
82
|
|
|
75
|
-
### ⏰
|
|
83
|
+
### ⏰ 时间格式说明
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
- `5m` - 5分钟后
|
|
79
|
-
- `1h` - 1小时后
|
|
80
|
-
- `1h30m` - 1.5小时后
|
|
81
|
-
- `2d` - 2天后
|
|
85
|
+
**⚠️ 重要:所有时间都会被自动转换为 cron 表达式**
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
**支持的输入格式**:
|
|
88
|
+
| 输入格式 | 示例 | 说明 |
|
|
89
|
+
|---------|------|------|
|
|
90
|
+
| 相对时间 | `5m` | 5分钟后 |
|
|
91
|
+
| 相对时间 | `1h` | 1小时后 |
|
|
92
|
+
| 相对时间 | `1h30m` | 1.5小时后 |
|
|
93
|
+
| 相对时间 | `2d` | 2天后 |
|
|
94
|
+
| cron 表达式 | `0 8 * * *` | 每天早上8点 |
|
|
95
|
+
| cron 表达式 | `0 22 * * *` | 每天晚上10点 |
|
|
96
|
+
| cron 表达式 | `0 9 * * 1-5` | 工作日早上9点 |
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
- `0 8 * * *` - 每天早上8点
|
|
87
|
-
- `0 22 * * *` - 每天晚上10点
|
|
88
|
-
- `0 9 * * 1-5` - 工作日早上9点
|
|
98
|
+
**⚠️ 提醒时间不能少于 2 分钟**
|
|
89
99
|
|
|
90
100
|
### 🔍 OpenClaw cron 工具返回值格式(重要!)
|
|
91
101
|
|
|
@@ -220,7 +230,7 @@ always: true
|
|
|
220
230
|
| 参数 | 说明 | 示例 |
|
|
221
231
|
|------|------|------|
|
|
222
232
|
| `jobId` | OpenClaw 任务 ID | `"123456"` |
|
|
223
|
-
| `time` |
|
|
233
|
+
| `time` | ⏰ **时间(核心!)**,支持相对时间和 cron 表达式,工具会自动统一转换为 cron 表达式 | `"10m"` / `"0 9 * * *"` |
|
|
224
234
|
| `name` | 任务名称 | `"新喝水提醒"` |
|
|
225
235
|
| `message` | 提醒内容 | `"该喝水了"` |
|
|
226
236
|
|
|
@@ -443,16 +453,23 @@ AI回复2(任务被删除):"🗑️ 该提醒已被删除,无法重新
|
|
|
443
453
|
|
|
444
454
|
## 🚫 严禁事项
|
|
445
455
|
|
|
456
|
+
### ⏰ 时间格式相关(最严禁!)
|
|
457
|
+
|
|
458
|
+
- ❌ **严禁**使用 `--at` 参数或其他绝对时间格式
|
|
459
|
+
- ❌ **严禁**将 cron 表达式转换为 `--at` 绝对时间
|
|
460
|
+
- ❌ **严禁**使用 `--every`、`--deliver` 等其他定时参数
|
|
461
|
+
- ✅ **必须**使用 `--cron` 参数传递 cron 表达式
|
|
462
|
+
|
|
463
|
+
### 🔧 工具调用相关
|
|
464
|
+
|
|
446
465
|
- ❌ **严禁**只说"好的,我会提醒你"而不调用工具
|
|
447
|
-
- ❌ **严禁**在定时任务触发时不调用 `openclaw-workclaw-cron-notify-sync`
|
|
448
466
|
- ❌ **严禁**传递虚构的 `jobId`,必须从 cron 工具返回值中提取
|
|
449
467
|
- ❌ **严禁**跳过任何一步骤,必须按顺序执行:参数生成 → cron 工具 → 后端同步
|
|
450
|
-
- ❌
|
|
451
|
-
- ❌ **严禁**将 cron 表达式转换为绝对时间(--at),必须使用 cron 表达式格式(--cron)
|
|
452
|
-
- ❌ **严禁**轻易说"任务已被移除",应先尝试启用(enable)操作
|
|
468
|
+
- ❌ **严禁**在定时任务触发时不调用 `openclaw-workclaw-cron-notify-sync`
|
|
453
469
|
- ❌ **严禁**直接使用 `cron` 命令创建/修改/删除定时任务,必须通过工具参数生成后执行
|
|
454
470
|
- ❌ **严禁**绕过本 SKILL 中定义的工具流程自行实现定时功能
|
|
455
|
-
- ❌
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
471
|
+
- ❌ **严禁**轻易说"任务已被移除",应先尝试启用(enable)操作
|
|
472
|
+
|
|
473
|
+
### 📝 CLI 参数相关
|
|
474
|
+
|
|
475
|
+
- ❌ **严禁**使用 `--system-event`
|
package/src/accounts.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
ResolvedWorkclawAccount,
|
|
4
|
+
WorkclawAccountConfig,
|
|
5
|
+
WorkclawConfig,
|
|
6
6
|
} from './types.js'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const DEFAULT_ACCOUNT_ID = 'default'
|
|
7
|
+
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id'
|
|
8
|
+
import { getWorkclawLogger, getWorkclawConnectionConfig } from './runtime.js'
|
|
10
9
|
|
|
11
10
|
// =============================================================================
|
|
12
11
|
// In-memory lookup cache for (userId, agentId) → workspace-N accountId
|
|
@@ -52,16 +51,16 @@ export function buildAccountMap(cfg: OpenClawConfig): void {
|
|
|
52
51
|
accountIndex.clear()
|
|
53
52
|
nextWorkspaceNum = 1
|
|
54
53
|
|
|
55
|
-
const
|
|
54
|
+
const _WorkclawConfig = (cfg.channels?.['openclaw-workclaw'] || {}) as WorkclawConfig
|
|
56
55
|
|
|
57
|
-
const accounts =
|
|
56
|
+
const accounts = _WorkclawConfig?.accounts
|
|
58
57
|
if (!accounts || typeof accounts !== 'object')
|
|
59
58
|
return
|
|
60
59
|
|
|
61
60
|
for (const [accountId, accountCfg] of Object.entries(accounts)) {
|
|
62
61
|
if (!accountCfg)
|
|
63
62
|
continue
|
|
64
|
-
const userId = accountCfg.userId ||
|
|
63
|
+
const userId = accountCfg.userId || _WorkclawConfig.userId
|
|
65
64
|
const agentId = accountCfg.agentId
|
|
66
65
|
if (!agentId)
|
|
67
66
|
continue
|
|
@@ -82,7 +81,7 @@ export function buildAccountMap(cfg: OpenClawConfig): void {
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
getWorkclawLogger().info(`[AccountManager] Built account map with ${lookupCache.size} entries, nextWorkspaceNum=${nextWorkspaceNum}`)
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
/**
|
|
@@ -100,13 +99,13 @@ export function resolveAccountByUserIdAndAgentId(
|
|
|
100
99
|
return lookupCache.get(key)!
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
const
|
|
104
|
-
const accounts =
|
|
102
|
+
const _WorkclawConfig = (cfg.channels?.['openclaw-workclaw'] || {}) as WorkclawConfig
|
|
103
|
+
const accounts = _WorkclawConfig?.accounts
|
|
105
104
|
if (accounts && typeof accounts === 'object') {
|
|
106
105
|
for (const [accountId, accountCfg] of Object.entries(accounts)) {
|
|
107
106
|
if (!accountCfg)
|
|
108
107
|
continue
|
|
109
|
-
const accountUserId = accountCfg.userId ||
|
|
108
|
+
const accountUserId = accountCfg.userId || _WorkclawConfig.userId
|
|
110
109
|
if (String(accountUserId) === userId && String(accountCfg.agentId) === agentId) {
|
|
111
110
|
lookupCache.set(key, accountId)
|
|
112
111
|
accountIndex.set(accountId, { userId, agentId })
|
|
@@ -133,7 +132,7 @@ export function allocateWorkerAccountId(
|
|
|
133
132
|
lookupCache.set(key, accountId)
|
|
134
133
|
accountIndex.set(accountId, { userId, agentId })
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
getWorkclawLogger().info(`[AccountManager] Allocated accountId=${accountId} for userId=${userId} agentId=${agentId}`)
|
|
137
136
|
return accountId
|
|
138
137
|
}
|
|
139
138
|
|
|
@@ -152,7 +151,7 @@ export function refreshAccountCache(cfg: OpenClawConfig): void {
|
|
|
152
151
|
* List all configured account IDs from the accounts field.
|
|
153
152
|
*/
|
|
154
153
|
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
|
155
|
-
const accounts = (cfg.channels?.['openclaw-workclaw'] as
|
|
154
|
+
const accounts = (cfg.channels?.['openclaw-workclaw'] as WorkclawConfig)?.accounts
|
|
156
155
|
if (!accounts || typeof accounts !== 'object') {
|
|
157
156
|
return []
|
|
158
157
|
}
|
|
@@ -163,9 +162,9 @@ function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
|
|
163
162
|
* Check if plugin-level workclaw config exists (appKey + appSecret)
|
|
164
163
|
*/
|
|
165
164
|
function hasPluginLevelWorkClawConfig(cfg: OpenClawConfig): boolean {
|
|
166
|
-
const
|
|
167
|
-
const key = typeof
|
|
168
|
-
const secret = typeof
|
|
165
|
+
const workclawCfg = cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
|
|
166
|
+
const key = typeof workclawCfg?.appKey === 'string' ? workclawCfg.appKey.trim() : ''
|
|
167
|
+
const secret = typeof workclawCfg?.appSecret === 'string' ? workclawCfg.appSecret.trim() : ''
|
|
169
168
|
return Boolean(key && secret)
|
|
170
169
|
}
|
|
171
170
|
|
|
@@ -173,7 +172,7 @@ function hasPluginLevelWorkClawConfig(cfg: OpenClawConfig): boolean {
|
|
|
173
172
|
* List all account IDs.
|
|
174
173
|
* If no accounts are configured but plugin has workclaw config, returns [DEFAULT_ACCOUNT_ID].
|
|
175
174
|
*/
|
|
176
|
-
export function
|
|
175
|
+
export function listWorkclawAccountIds(cfg: OpenClawConfig): string[] {
|
|
177
176
|
const ids = listConfiguredAccountIds(cfg)
|
|
178
177
|
if (ids.length === 0) {
|
|
179
178
|
// 如果没有配置账户,但插件级别有 workclaw 配置,返回默认账户
|
|
@@ -189,8 +188,8 @@ export function listOpenclawWorkclawAccountIds(cfg: OpenClawConfig): string[] {
|
|
|
189
188
|
/**
|
|
190
189
|
* Resolve the default account ID.
|
|
191
190
|
*/
|
|
192
|
-
export function
|
|
193
|
-
const ids =
|
|
191
|
+
export function resolveDefaultWorkclawAccountId(cfg: OpenClawConfig): string {
|
|
192
|
+
const ids = listWorkclawAccountIds(cfg)
|
|
194
193
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
195
194
|
return DEFAULT_ACCOUNT_ID
|
|
196
195
|
}
|
|
@@ -203,8 +202,8 @@ export function resolveDefaultOpenclawWorkclawAccountId(cfg: OpenClawConfig): st
|
|
|
203
202
|
function resolveAccountConfig(
|
|
204
203
|
cfg: OpenClawConfig,
|
|
205
204
|
accountId: string,
|
|
206
|
-
):
|
|
207
|
-
const accounts = (cfg.channels?.['openclaw-workclaw'] as
|
|
205
|
+
): WorkclawAccountConfig | undefined {
|
|
206
|
+
const accounts = (cfg.channels?.['openclaw-workclaw'] as WorkclawConfig)?.accounts
|
|
208
207
|
if (!accounts || typeof accounts !== 'object') {
|
|
209
208
|
return undefined
|
|
210
209
|
}
|
|
@@ -223,7 +222,7 @@ function hasWorkClawConfig(config: {
|
|
|
223
222
|
return Boolean(key && secret)
|
|
224
223
|
}
|
|
225
224
|
|
|
226
|
-
export function
|
|
225
|
+
export function isWorkclawAccountConfigured(config: {
|
|
227
226
|
appKey?: string
|
|
228
227
|
appSecret?: string
|
|
229
228
|
agentId?: string | number
|
|
@@ -235,34 +234,34 @@ export function isOpenclawWorkclawAccountConfigured(config: {
|
|
|
235
234
|
* Merge top-level config with account-specific config.
|
|
236
235
|
* Account-specific fields override top-level fields.
|
|
237
236
|
*/
|
|
238
|
-
function
|
|
239
|
-
const
|
|
237
|
+
function mergeWorkclawAccountConfig(cfg: OpenClawConfig, accountId: string): WorkclawConfig {
|
|
238
|
+
const workclawCfg = cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
|
|
240
239
|
|
|
241
240
|
// Extract base config (exclude accounts field to avoid recursion)
|
|
242
|
-
const { accounts: _ignored, ...base } =
|
|
241
|
+
const { accounts: _ignored, ...base } = workclawCfg ?? {}
|
|
243
242
|
|
|
244
243
|
// Get account-specific overrides
|
|
245
244
|
const account = resolveAccountConfig(cfg, accountId) ?? {}
|
|
246
245
|
|
|
247
246
|
// Merge: account config overrides base config
|
|
248
|
-
return { ...base, ...account } as
|
|
247
|
+
return { ...base, ...account } as WorkclawConfig
|
|
249
248
|
}
|
|
250
249
|
|
|
251
250
|
/**
|
|
252
251
|
* Resolve a complete account with merged config.
|
|
253
252
|
*/
|
|
254
|
-
export function
|
|
253
|
+
export function resolveWorkclawAccount(params: {
|
|
255
254
|
cfg: OpenClawConfig
|
|
256
255
|
accountId?: string | null
|
|
257
|
-
}):
|
|
256
|
+
}): ResolvedWorkclawAccount {
|
|
258
257
|
const accountId = normalizeAccountId(params.accountId)
|
|
259
|
-
const
|
|
258
|
+
const workclawCfg = params.cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
|
|
260
259
|
|
|
261
260
|
// Base enabled state (top-level)
|
|
262
|
-
const baseEnabled =
|
|
261
|
+
const baseEnabled = workclawCfg?.enabled !== false
|
|
263
262
|
|
|
264
263
|
// Merge configs
|
|
265
|
-
const merged =
|
|
264
|
+
const merged = mergeWorkclawAccountConfig(params.cfg, accountId)
|
|
266
265
|
|
|
267
266
|
// Account-level enabled state
|
|
268
267
|
const accountEnabled = merged.enabled !== false
|
|
@@ -271,8 +270,8 @@ export function resolveOpenclawWorkclawAccount(params: {
|
|
|
271
270
|
return {
|
|
272
271
|
accountId,
|
|
273
272
|
enabled,
|
|
274
|
-
configured:
|
|
275
|
-
name: (merged as
|
|
273
|
+
configured: isWorkclawAccountConfigured(merged),
|
|
274
|
+
name: (merged as WorkclawAccountConfig).name?.trim() || undefined,
|
|
276
275
|
config: merged,
|
|
277
276
|
}
|
|
278
277
|
}
|
|
@@ -280,8 +279,34 @@ export function resolveOpenclawWorkclawAccount(params: {
|
|
|
280
279
|
/**
|
|
281
280
|
* List all enabled and configured accounts.
|
|
282
281
|
*/
|
|
283
|
-
export function
|
|
282
|
+
export function listEnabledWorkclawAccounts(cfg: OpenClawConfig): ResolvedWorkclawAccount[] {
|
|
284
283
|
return listConfiguredAccountIds(cfg)
|
|
285
|
-
.map(accountId =>
|
|
284
|
+
.map(accountId => resolveWorkclawAccount({ cfg, accountId }))
|
|
286
285
|
.filter(account => account.enabled && account.configured)
|
|
287
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolve account and apply cached connection config if available.
|
|
290
|
+
* This ensures that appKey/appSecret etc. use the stable values captured at connection time,
|
|
291
|
+
* not potentially mutated values from cfg at resolution time.
|
|
292
|
+
*/
|
|
293
|
+
export function resolveWorkclawAccountWithCache(params: {
|
|
294
|
+
cfg: OpenClawConfig
|
|
295
|
+
accountId?: string | null
|
|
296
|
+
}): ResolvedWorkclawAccount {
|
|
297
|
+
const account = resolveWorkclawAccount(params);
|
|
298
|
+
const cachedConfig = getWorkclawConnectionConfig(account.accountId);
|
|
299
|
+
|
|
300
|
+
if (cachedConfig) {
|
|
301
|
+
// Apply cached values to ensure stability
|
|
302
|
+
account.config.appKey = cachedConfig.appKey || account.config.appKey;
|
|
303
|
+
account.config.appSecret = cachedConfig.appSecret || account.config.appSecret;
|
|
304
|
+
if (cachedConfig.baseUrl) account.config.baseUrl = cachedConfig.baseUrl;
|
|
305
|
+
if (cachedConfig.websocketUrl !== undefined) account.config.websocketUrl = cachedConfig.websocketUrl;
|
|
306
|
+
if (cachedConfig.localIp !== undefined) account.config.localIp = cachedConfig.localIp;
|
|
307
|
+
if (cachedConfig.allowInsecureTls !== undefined) account.config.allowInsecureTls = cachedConfig.allowInsecureTls;
|
|
308
|
+
if (cachedConfig.requestTimeout !== undefined) account.config.requestTimeout = cachedConfig.requestTimeout;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return account;
|
|
312
|
+
}
|
package/src/api/accounts-api.ts
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function sendJson(res: any, statusCode: number, payload: unknown)
|
|
6
|
-
res.statusCode = statusCode
|
|
7
|
-
res.setHeader(
|
|
8
|
-
res.end(JSON.stringify(payload))
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { normalizeAccountId } from "../accounts.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function sendJson(res: any, statusCode: number, payload: unknown) {
|
|
6
|
+
res.statusCode = statusCode;
|
|
7
|
+
res.setHeader("Content-Type", "application/json");
|
|
8
|
+
res.end(JSON.stringify(payload));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
async function readRequestBody(req: any): Promise<string> {
|
|
12
|
-
const chunks: Buffer[] = []
|
|
12
|
+
const chunks: Buffer[] = [];
|
|
13
13
|
await new Promise<void>((resolve, reject) => {
|
|
14
|
-
req.on(
|
|
15
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
16
|
-
})
|
|
17
|
-
req.on(
|
|
18
|
-
req.on(
|
|
19
|
-
})
|
|
20
|
-
return Buffer.concat(chunks).toString(
|
|
14
|
+
req.on("data", (chunk: any) => {
|
|
15
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
16
|
+
});
|
|
17
|
+
req.on("end", () => resolve());
|
|
18
|
+
req.on("error", (err: unknown) => reject(err));
|
|
19
|
+
});
|
|
20
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async function loadRuntimeConfig(api: OpenClawPluginApi): Promise<any> {
|
|
24
|
-
const runtimeConfig = (api.runtime as any)?.config
|
|
24
|
+
const runtimeConfig = (api.runtime as any)?.config;
|
|
25
25
|
if (runtimeConfig?.loadConfig) {
|
|
26
|
-
return runtimeConfig.loadConfig()
|
|
26
|
+
return runtimeConfig.loadConfig();
|
|
27
27
|
}
|
|
28
|
-
return api.config
|
|
28
|
+
return api.config;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function writeRuntimeConfig(api: OpenClawPluginApi, config: any): Promise<void> {
|
|
32
|
-
const runtimeConfig = (api.runtime as any)?.config
|
|
32
|
+
const runtimeConfig = (api.runtime as any)?.config;
|
|
33
33
|
if (runtimeConfig?.writeConfigFile) {
|
|
34
|
-
await runtimeConfig.writeConfigFile(config)
|
|
35
|
-
return
|
|
34
|
+
await runtimeConfig.writeConfigFile(config);
|
|
35
|
+
return;
|
|
36
36
|
}
|
|
37
|
-
throw new Error(
|
|
37
|
+
throw new Error("Config write not supported");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function getAccountMap(config: any): Record<string, any> {
|
|
41
|
-
const channels = config?.channels && typeof config.channels ===
|
|
42
|
-
const openclawWorkclaw
|
|
43
|
-
|
|
44
|
-
const accounts
|
|
45
|
-
|
|
46
|
-
return { ...accounts }
|
|
41
|
+
const channels = config?.channels && typeof config.channels === "object" ? config.channels : {};
|
|
42
|
+
const openclawWorkclaw =
|
|
43
|
+
channels['openclaw-workclaw'] && typeof channels['openclaw-workclaw'] === "object" ? channels['openclaw-workclaw'] : {};
|
|
44
|
+
const accounts =
|
|
45
|
+
openclawWorkclaw.accounts && typeof openclawWorkclaw.accounts === "object" ? openclawWorkclaw.accounts : {};
|
|
46
|
+
return { ...accounts };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function withAccounts(config: any, accounts: Record<string, any>): any {
|
|
50
|
-
const channels = config?.channels && typeof config.channels ===
|
|
51
|
-
const openclawWorkclaw
|
|
52
|
-
|
|
50
|
+
const channels = config?.channels && typeof config.channels === "object" ? config.channels : {};
|
|
51
|
+
const openclawWorkclaw =
|
|
52
|
+
channels['openclaw-workclaw'] && typeof channels['openclaw-workclaw'] === "object" ? channels['openclaw-workclaw'] : {};
|
|
53
53
|
return {
|
|
54
54
|
...config,
|
|
55
55
|
channels: {
|
|
@@ -59,99 +59,98 @@ function withAccounts(config: any, accounts: Record<string, any>): any {
|
|
|
59
59
|
accounts,
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
}
|
|
62
|
+
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function createAccountsApiHandler(api: OpenClawPluginApi) {
|
|
66
66
|
return async (req: any, res: any) => {
|
|
67
|
-
const method = String(req.method ??
|
|
68
|
-
const url = new URL(req.url ??
|
|
69
|
-
const idRaw = url.searchParams.get(
|
|
70
|
-
const accountId = idRaw ? normalizeAccountId(idRaw) : null
|
|
67
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
68
|
+
const url = new URL(req.url ?? "", "http://localhost");
|
|
69
|
+
const idRaw = url.searchParams.get("id") ?? url.searchParams.get("accountId");
|
|
70
|
+
const accountId = idRaw ? normalizeAccountId(idRaw) : null;
|
|
71
71
|
|
|
72
|
-
if (method ===
|
|
73
|
-
const cfg = await loadRuntimeConfig(api)
|
|
74
|
-
const accounts = getAccountMap(cfg)
|
|
72
|
+
if (method === "GET") {
|
|
73
|
+
const cfg = await loadRuntimeConfig(api);
|
|
74
|
+
const accounts = getAccountMap(cfg);
|
|
75
75
|
|
|
76
76
|
if (accountId) {
|
|
77
|
-
const account = accounts[accountId]
|
|
77
|
+
const account = accounts[accountId];
|
|
78
78
|
if (!account) {
|
|
79
|
-
sendJson(res, 404, { ok: false, error:
|
|
80
|
-
return
|
|
79
|
+
sendJson(res, 404, { ok: false, error: "Not Found" });
|
|
80
|
+
return;
|
|
81
81
|
}
|
|
82
|
-
sendJson(res, 200, { ok: true, accountId, config: account })
|
|
83
|
-
return
|
|
82
|
+
sendJson(res, 200, { ok: true, accountId, config: account });
|
|
83
|
+
return;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const entries = Object.entries(accounts).map(([id, config]) => ({
|
|
87
87
|
accountId: id,
|
|
88
88
|
config,
|
|
89
|
-
}))
|
|
90
|
-
sendJson(res, 200, { ok: true, accounts: entries })
|
|
91
|
-
return
|
|
89
|
+
}));
|
|
90
|
+
sendJson(res, 200, { ok: true, accounts: entries });
|
|
91
|
+
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
if (method ===
|
|
95
|
-
const raw = await readRequestBody(req)
|
|
96
|
-
let input: any = {}
|
|
94
|
+
if (method === "POST" || method === "PUT") {
|
|
95
|
+
const raw = await readRequestBody(req);
|
|
96
|
+
let input: any = {};
|
|
97
97
|
try {
|
|
98
|
-
input = raw ? JSON.parse(raw) : {}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
98
|
+
input = raw ? JSON.parse(raw) : {};
|
|
99
|
+
} catch {
|
|
100
|
+
sendJson(res, 400, { ok: false, error: "Invalid JSON" });
|
|
101
|
+
return;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
const inputId = input?.accountId ?? input?.id
|
|
106
|
-
const normalizedId = inputId ? normalizeAccountId(String(inputId)) :
|
|
107
|
-
const patch = input?.config
|
|
104
|
+
const inputId = input?.accountId ?? input?.id;
|
|
105
|
+
const normalizedId = inputId ? normalizeAccountId(String(inputId)) : "";
|
|
106
|
+
const patch = input?.config;
|
|
108
107
|
|
|
109
|
-
if (!normalizedId || !patch || typeof patch !==
|
|
110
|
-
sendJson(res, 400, { ok: false, error:
|
|
111
|
-
return
|
|
108
|
+
if (!normalizedId || !patch || typeof patch !== "object") {
|
|
109
|
+
sendJson(res, 400, { ok: false, error: "Missing accountId or config" });
|
|
110
|
+
return;
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
const cfg = await loadRuntimeConfig(api)
|
|
115
|
-
const accounts = getAccountMap(cfg)
|
|
113
|
+
const cfg = await loadRuntimeConfig(api);
|
|
114
|
+
const accounts = getAccountMap(cfg);
|
|
116
115
|
|
|
117
|
-
if (method ===
|
|
118
|
-
sendJson(res, 409, { ok: false, error:
|
|
119
|
-
return
|
|
116
|
+
if (method === "POST" && accounts[normalizedId]) {
|
|
117
|
+
sendJson(res, 409, { ok: false, error: "Account already exists" });
|
|
118
|
+
return;
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
if (method ===
|
|
123
|
-
sendJson(res, 404, { ok: false, error:
|
|
124
|
-
return
|
|
121
|
+
if (method === "PUT" && !accounts[normalizedId]) {
|
|
122
|
+
sendJson(res, 404, { ok: false, error: "Not Found" });
|
|
123
|
+
return;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
const next = method ===
|
|
128
|
-
const updated = withAccounts(cfg, { ...accounts, [normalizedId]: next })
|
|
129
|
-
await writeRuntimeConfig(api, updated)
|
|
130
|
-
sendJson(res, 200, { ok: true, accountId: normalizedId })
|
|
131
|
-
return
|
|
126
|
+
const next = method === "PUT" ? { ...accounts[normalizedId], ...patch } : patch;
|
|
127
|
+
const updated = withAccounts(cfg, { ...accounts, [normalizedId]: next });
|
|
128
|
+
await writeRuntimeConfig(api, updated);
|
|
129
|
+
sendJson(res, 200, { ok: true, accountId: normalizedId });
|
|
130
|
+
return;
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
if (method ===
|
|
133
|
+
if (method === "DELETE") {
|
|
135
134
|
if (!accountId) {
|
|
136
|
-
sendJson(res, 400, { ok: false, error:
|
|
137
|
-
return
|
|
135
|
+
sendJson(res, 400, { ok: false, error: "Missing accountId" });
|
|
136
|
+
return;
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
const cfg = await loadRuntimeConfig(api)
|
|
141
|
-
const accounts = getAccountMap(cfg)
|
|
139
|
+
const cfg = await loadRuntimeConfig(api);
|
|
140
|
+
const accounts = getAccountMap(cfg);
|
|
142
141
|
|
|
143
142
|
if (!accounts[accountId]) {
|
|
144
|
-
sendJson(res, 404, { ok: false, error:
|
|
145
|
-
return
|
|
143
|
+
sendJson(res, 404, { ok: false, error: "Not Found" });
|
|
144
|
+
return;
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
const { [accountId]: _ignored, ...rest } = accounts
|
|
149
|
-
const updated = withAccounts(cfg, rest)
|
|
150
|
-
await writeRuntimeConfig(api, updated)
|
|
151
|
-
sendJson(res, 200, { ok: true, accountId, deleted: true })
|
|
152
|
-
return
|
|
147
|
+
const { [accountId]: _ignored, ...rest } = accounts;
|
|
148
|
+
const updated = withAccounts(cfg, rest);
|
|
149
|
+
await writeRuntimeConfig(api, updated);
|
|
150
|
+
sendJson(res, 200, { ok: true, accountId, deleted: true });
|
|
151
|
+
return;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
sendJson(res, 405, { ok: false, error:
|
|
156
|
-
}
|
|
154
|
+
sendJson(res, 405, { ok: false, error: "Method Not Allowed" });
|
|
155
|
+
};
|
|
157
156
|
}
|