@workclaw/openclaw-workclaw 1.0.19 → 1.0.23

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.
Files changed (70) hide show
  1. package/README.md +363 -364
  2. package/api.ts +3 -0
  3. package/dist/index.js +2 -1
  4. package/dist/src/api/workspace.js +2 -0
  5. package/dist/src/channel.js +1 -0
  6. package/dist/src/gateway/agent-handlers.js +87 -87
  7. package/dist/src/gateway/config-writer.js +4 -14
  8. package/dist/src/gateway/cron-tasks-handler.d.ts +19 -0
  9. package/dist/src/gateway/cron-tasks-handler.js +188 -0
  10. package/dist/src/gateway/message-context.d.ts +8 -4
  11. package/dist/src/gateway/message-context.js +148 -138
  12. package/dist/src/gateway/message-dispatcher.d.ts +0 -1
  13. package/dist/src/gateway/message-dispatcher.js +383 -323
  14. package/dist/src/gateway/reconnect.js +4 -0
  15. package/dist/src/gateway/skills-handler.js +187 -148
  16. package/dist/src/gateway/workclaw-gateway.d.ts +1 -1
  17. package/dist/src/gateway/workclaw-gateway.js +47 -33
  18. package/dist/src/tools/openclaw-workclaw-cron/src/update/params.js +17 -17
  19. package/index.ts +325 -0
  20. package/package.json +45 -62
  21. package/setup-entry.ts +13 -0
  22. package/src/accounts.ts +360 -0
  23. package/src/api/accounts-api.ts +156 -0
  24. package/src/api/prompts-api.ts +122 -0
  25. package/src/api/session-api.ts +246 -0
  26. package/src/api/skills-api.ts +74 -0
  27. package/src/api/workspace.ts +45 -0
  28. package/src/channel.ts +226 -0
  29. package/src/config-schema.ts +60 -0
  30. package/src/connection/workclaw-client.ts +618 -0
  31. package/src/gateway/agent-handlers.ts +551 -0
  32. package/src/gateway/config-writer.ts +378 -0
  33. package/src/gateway/cron-tasks-handler.ts +230 -0
  34. package/src/gateway/message-context.ts +645 -0
  35. package/src/gateway/message-dispatcher.ts +688 -0
  36. package/src/gateway/reconnect.ts +260 -0
  37. package/src/gateway/skills-handler.ts +805 -0
  38. package/src/gateway/skills-list-handler.ts +332 -0
  39. package/src/gateway/tools-list-handler.ts +161 -0
  40. package/src/gateway/workclaw-gateway.ts +298 -0
  41. package/src/media/upload.ts +168 -0
  42. package/src/outbound/index.ts +191 -0
  43. package/src/outbound/workclaw-sender.ts +161 -0
  44. package/src/runtime.ts +520 -0
  45. package/src/secret-contract-api.ts +4 -0
  46. package/src/send.ts +1 -0
  47. package/src/setup-api.ts +3 -0
  48. package/src/setup-core.ts +25 -0
  49. package/src/setup-surface.ts +498 -0
  50. package/src/tools/openclaw-workclaw-cron/api/index.ts +326 -0
  51. package/src/tools/openclaw-workclaw-cron/index.ts +39 -0
  52. package/src/tools/openclaw-workclaw-cron/src/add/params.ts +177 -0
  53. package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +188 -0
  54. package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +100 -0
  55. package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +127 -0
  56. package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +100 -0
  57. package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +127 -0
  58. package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +148 -0
  59. package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +109 -0
  60. package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +127 -0
  61. package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -0
  62. package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +161 -0
  63. package/src/tools/openclaw-workclaw-cron/types/index.ts +55 -0
  64. package/src/tools/openclaw-workclaw-cron/utils/index.ts +141 -0
  65. package/src/tools/openclaw-workclaw-system/index.ts +17 -0
  66. package/src/tools/openclaw-workclaw-system/src/get/index.ts +77 -0
  67. package/src/tools/openclaw-workclaw-system/src/token/index.ts +93 -0
  68. package/src/types.ts +50 -0
  69. package/src/utils/content.ts +40 -0
  70. package/tsconfig.json +34 -0
@@ -0,0 +1,360 @@
1
+ import type { OpenClawConfig } from 'openclaw/plugin-sdk'
2
+ import type {
3
+ ResolvedWorkclawAccount,
4
+ WorkclawAccountConfig,
5
+ WorkclawConfig,
6
+ } from './types.js'
7
+ import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id'
8
+ import { getWorkclawConnectionConfig, getWorkclawLogger } from './runtime.js'
9
+
10
+ // =============================================================================
11
+ // In-memory lookup cache for (userId, agentId) → workspace-N accountId
12
+ // Problem: OpenClaw doesn't support dashes in account IDs, agentId can be negative
13
+ // Solution: workspace-N naming + in-memory Map for O(1) lookups
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Normalize account ID by trimming whitespace
18
+ */
19
+ export function normalizeAccountId(accountId: string | null | undefined): string {
20
+ return String(accountId ?? '').trim()
21
+ }
22
+
23
+ /**
24
+ * Key format for the lookup map: "userId|agentId"
25
+ */
26
+ function makeLookupKey(userId: string, agentId: string): string {
27
+ return `${userId}|${agentId}`
28
+ }
29
+
30
+ /**
31
+ * In-memory map: lookupKey -> accountId
32
+ */
33
+ const lookupCache = new Map<string, string>()
34
+
35
+ /**
36
+ * In-memory map: accountId -> { userId, agentId }
37
+ */
38
+ const accountIndex = new Map<string, { userId: string, agentId: string }>()
39
+
40
+ /**
41
+ * Next workspace number to allocate
42
+ */
43
+ let nextWorkspaceNum = 1
44
+
45
+ /**
46
+ * Build the lookup cache from existing accounts in cfg.
47
+ * Called once at gateway startup.
48
+ */
49
+ export function buildAccountMap(cfg: OpenClawConfig): void {
50
+ lookupCache.clear()
51
+ accountIndex.clear()
52
+ nextWorkspaceNum = 1
53
+
54
+ const _WorkclawConfig = (cfg.channels?.['openclaw-workclaw'] || {}) as WorkclawConfig
55
+
56
+ const accounts = _WorkclawConfig?.accounts
57
+ if (!accounts || typeof accounts !== 'object')
58
+ return
59
+
60
+ for (const [accountId, accountCfg] of Object.entries(accounts)) {
61
+ if (!accountCfg)
62
+ continue
63
+ const userId = accountCfg.userId || _WorkclawConfig.userId
64
+ const agentId = accountCfg.agentId
65
+ if (!agentId)
66
+ continue
67
+
68
+ // userId may not be present for config-defined accounts; only build
69
+ // lookup if userId is available (dynamically created accounts have it)
70
+ if (userId) {
71
+ const key = makeLookupKey(String(userId), String(agentId))
72
+ lookupCache.set(key, accountId)
73
+ }
74
+ accountIndex.set(accountId, { userId: String(userId ?? ''), agentId: String(agentId) })
75
+
76
+ if (accountId.startsWith('workspace-')) {
77
+ const num = Number.parseInt(accountId.slice(accountId.lastIndexOf('-') + 1), 10)
78
+ if (!Number.isNaN(num) && num >= nextWorkspaceNum) {
79
+ nextWorkspaceNum = num + 1
80
+ }
81
+ }
82
+ }
83
+
84
+ // 默认账户允许直接使用顶层 userId/agentId,不要求必须存在 accounts.default
85
+ if (!accountIndex.has(DEFAULT_ACCOUNT_ID) && _WorkclawConfig.userId && _WorkclawConfig.agentId) {
86
+ const userId = String(_WorkclawConfig.userId)
87
+ const agentId = String(_WorkclawConfig.agentId)
88
+ const key = makeLookupKey(userId, agentId)
89
+ lookupCache.set(key, DEFAULT_ACCOUNT_ID)
90
+ accountIndex.set(DEFAULT_ACCOUNT_ID, { userId, agentId })
91
+ }
92
+
93
+ getWorkclawLogger().info(`[AccountManager] Built account map with ${lookupCache.size} entries, nextWorkspaceNum=${nextWorkspaceNum}`)
94
+ }
95
+
96
+ /**
97
+ * Resolve accountId by userId + agentId.
98
+ * First checks cache, then falls back to iteration.
99
+ */
100
+ export function resolveAccountByUserIdAndAgentId(
101
+ cfg: OpenClawConfig,
102
+ userId: string,
103
+ agentId: string,
104
+ ): string | null {
105
+ const key = makeLookupKey(userId, agentId)
106
+
107
+ if (lookupCache.has(key)) {
108
+ return lookupCache.get(key)!
109
+ }
110
+
111
+ const _WorkclawConfig = (cfg.channels?.['openclaw-workclaw'] || {}) as WorkclawConfig
112
+ const accounts = _WorkclawConfig?.accounts
113
+ if (accounts && typeof accounts === 'object') {
114
+ for (const [accountId, accountCfg] of Object.entries(accounts)) {
115
+ if (!accountCfg)
116
+ continue
117
+ const accountUserId = accountCfg.userId || _WorkclawConfig.userId
118
+ if (String(accountUserId) === userId && String(accountCfg.agentId) === agentId) {
119
+ lookupCache.set(key, accountId)
120
+ accountIndex.set(accountId, { userId, agentId })
121
+ return accountId
122
+ }
123
+ }
124
+ }
125
+
126
+ // 兼容默认账户直接写在顶层配置中的场景
127
+ if (
128
+ String(_WorkclawConfig.userId ?? '') === userId
129
+ && String(_WorkclawConfig.agentId ?? '') === agentId
130
+ ) {
131
+ lookupCache.set(key, DEFAULT_ACCOUNT_ID)
132
+ accountIndex.set(DEFAULT_ACCOUNT_ID, { userId, agentId })
133
+ return DEFAULT_ACCOUNT_ID
134
+ }
135
+
136
+ return null
137
+ }
138
+
139
+ /**
140
+ * Allocate a new workspace-N accountId and register it in the map.
141
+ */
142
+ export function allocateWorkerAccountId(
143
+ _cfg: OpenClawConfig,
144
+ userId: string,
145
+ agentId: string,
146
+ ): string {
147
+ const accountId = `workspace-${nextWorkspaceNum}`
148
+ nextWorkspaceNum++
149
+
150
+ const key = makeLookupKey(userId, agentId)
151
+ lookupCache.set(key, accountId)
152
+ accountIndex.set(accountId, { userId, agentId })
153
+
154
+ getWorkclawLogger().info(`[AccountManager] Allocated accountId=${accountId} for userId=${userId} agentId=${agentId}`)
155
+ return accountId
156
+ }
157
+
158
+ /**
159
+ * Refresh the cache when an account's userId or agentId changes.
160
+ */
161
+ export function refreshAccountCache(cfg: OpenClawConfig): void {
162
+ buildAccountMap(cfg)
163
+ }
164
+
165
+ // =============================================================================
166
+ // Account config resolution (pure functions, no in-memory state)
167
+ // =============================================================================
168
+
169
+ /**
170
+ * List all configured account IDs from the accounts field.
171
+ */
172
+ function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
173
+ const accounts = (cfg.channels?.['openclaw-workclaw'] as WorkclawConfig)?.accounts
174
+ if (!accounts || typeof accounts !== 'object') {
175
+ return []
176
+ }
177
+ return Object.keys(accounts).filter(Boolean)
178
+ }
179
+
180
+ /**
181
+ * Check if plugin-level workclaw config exists (appKey + appSecret)
182
+ */
183
+ function hasPluginLevelWorkClawConfig(cfg: OpenClawConfig): boolean {
184
+ const workclawCfg = cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
185
+ const key = typeof workclawCfg?.appKey === 'string' ? workclawCfg.appKey.trim() : ''
186
+ const secret = typeof workclawCfg?.appSecret === 'string' ? workclawCfg.appSecret.trim() : ''
187
+ return Boolean(key && secret)
188
+ }
189
+
190
+ /**
191
+ * List all account IDs.
192
+ * If no accounts are configured but plugin has workclaw config, returns [DEFAULT_ACCOUNT_ID].
193
+ */
194
+ export function listWorkclawAccountIds(cfg: OpenClawConfig): string[] {
195
+ const ids = listConfiguredAccountIds(cfg)
196
+ if (ids.length === 0) {
197
+ // 如果没有配置账户,但插件级别有 workclaw 配置,返回默认账户
198
+ if (hasPluginLevelWorkClawConfig(cfg)) {
199
+ return [DEFAULT_ACCOUNT_ID]
200
+ }
201
+ // Backward compatibility: no accounts configured, use default
202
+ return [DEFAULT_ACCOUNT_ID]
203
+ }
204
+ return [...ids].toSorted((a, b) => a.localeCompare(b))
205
+ }
206
+
207
+ /**
208
+ * Resolve the default account ID.
209
+ */
210
+ export function resolveDefaultWorkclawAccountId(cfg: OpenClawConfig): string {
211
+ const ids = listWorkclawAccountIds(cfg)
212
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
213
+ return DEFAULT_ACCOUNT_ID
214
+ }
215
+ return ids[0] ?? DEFAULT_ACCOUNT_ID
216
+ }
217
+
218
+ /**
219
+ * Get the raw account-specific config.
220
+ */
221
+ function resolveAccountConfig(
222
+ cfg: OpenClawConfig,
223
+ accountId: string,
224
+ ): WorkclawAccountConfig | undefined {
225
+ const accounts = (cfg.channels?.['openclaw-workclaw'] as WorkclawConfig)?.accounts
226
+ if (!accounts || typeof accounts !== 'object') {
227
+ return undefined
228
+ }
229
+ return accounts[accountId]
230
+ }
231
+
232
+ function hasWorkClawConfig(config: {
233
+ appKey?: string
234
+ appSecret?: string
235
+ agentId?: string | number
236
+ }): boolean {
237
+ const key = typeof config.appKey === 'string' ? config.appKey.trim() : ''
238
+ const secret
239
+ = typeof config.appSecret === 'string' ? config.appSecret.trim() : ''
240
+ // 只要有 appKey 和 appSecret 就认为配置了 workclaw(agentId 可以动态添加)
241
+ return Boolean(key && secret)
242
+ }
243
+
244
+ export function isWorkclawAccountConfigured(config: {
245
+ appKey?: string
246
+ appSecret?: string
247
+ agentId?: string | number
248
+ }): boolean {
249
+ return hasWorkClawConfig(config)
250
+ }
251
+
252
+ /**
253
+ * Merge top-level config with account-specific config.
254
+ * Account-specific fields override top-level fields.
255
+ */
256
+ function mergeWorkclawAccountConfig(cfg: OpenClawConfig, accountId: string): WorkclawConfig {
257
+ const workclawCfg = cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
258
+
259
+ // Extract base config (exclude accounts field to avoid recursion)
260
+ const { accounts: _ignored, ...base } = workclawCfg ?? {}
261
+
262
+ // Get account-specific overrides
263
+ const account = resolveAccountConfig(cfg, accountId) ?? {}
264
+
265
+ // Merge: account config overrides base config
266
+ return { ...base, ...account } as WorkclawConfig
267
+ }
268
+
269
+ /**
270
+ * Resolve a complete account with merged config.
271
+ */
272
+ export function resolveWorkclawAccount(params: {
273
+ cfg: OpenClawConfig
274
+ accountId?: string | null
275
+ }): ResolvedWorkclawAccount {
276
+ const accountId = normalizeAccountId(params.accountId)
277
+ const workclawCfg = params.cfg.channels?.['openclaw-workclaw'] as WorkclawConfig | undefined
278
+
279
+ // Base enabled state (top-level)
280
+ const baseEnabled = workclawCfg?.enabled !== false
281
+
282
+ // Merge configs
283
+ const merged = mergeWorkclawAccountConfig(params.cfg, accountId)
284
+
285
+ // Account-level enabled state
286
+ const accountEnabled = merged.enabled !== false
287
+ const enabled = baseEnabled && accountEnabled
288
+
289
+ return {
290
+ accountId,
291
+ enabled,
292
+ configured: isWorkclawAccountConfigured(merged),
293
+ name: (merged as WorkclawAccountConfig).name?.trim() || undefined,
294
+ config: merged,
295
+ }
296
+ }
297
+
298
+ /**
299
+ * List all enabled and configured accounts.
300
+ */
301
+ export function listEnabledWorkclawAccounts(cfg: OpenClawConfig): ResolvedWorkclawAccount[] {
302
+ return listConfiguredAccountIds(cfg)
303
+ .map(accountId => resolveWorkclawAccount({ cfg, accountId }))
304
+ .filter(account => account.enabled && account.configured)
305
+ }
306
+
307
+ /**
308
+ * Resolve account and apply cached connection config if available.
309
+ * This ensures that appKey/appSecret etc. use the stable values captured at connection time,
310
+ * not potentially mutated values from cfg at resolution time.
311
+ */
312
+ export function resolveWorkclawAccountWithCache(params: {
313
+ cfg: OpenClawConfig
314
+ accountId?: string | null
315
+ }): ResolvedWorkclawAccount {
316
+ const account = resolveWorkclawAccount(params)
317
+ const cachedConfig = getWorkclawConnectionConfig(account.accountId)
318
+
319
+ if (cachedConfig) {
320
+ // Apply cached values to ensure stability
321
+ account.config.appKey = cachedConfig.appKey || account.config.appKey
322
+ account.config.appSecret = cachedConfig.appSecret || account.config.appSecret
323
+ if (cachedConfig.baseUrl)
324
+ account.config.baseUrl = cachedConfig.baseUrl
325
+ if (cachedConfig.websocketUrl !== undefined)
326
+ account.config.websocketUrl = cachedConfig.websocketUrl
327
+ if (cachedConfig.localIp !== undefined)
328
+ account.config.localIp = cachedConfig.localIp
329
+ if (cachedConfig.allowInsecureTls !== undefined)
330
+ account.config.allowInsecureTls = cachedConfig.allowInsecureTls
331
+ if (cachedConfig.requestTimeout !== undefined)
332
+ account.config.requestTimeout = cachedConfig.requestTimeout
333
+ }
334
+
335
+ return account
336
+ }
337
+
338
+ /**
339
+ * Inspect and probe workclaw credentials.
340
+ * Returns a function that can be called to test the connection.
341
+ */
342
+ export function inspectWorkclawCredentials(workclawCfg?: WorkclawConfig): (() => Promise<{ ok: boolean, botName?: string }>) | null {
343
+ const appKey = typeof workclawCfg?.appKey === 'string' ? workclawCfg.appKey.trim() : ''
344
+ const appSecret = typeof workclawCfg?.appSecret === 'string' ? workclawCfg.appSecret.trim() : ''
345
+
346
+ if (!appKey || !appSecret) {
347
+ return null
348
+ }
349
+
350
+ return async () => {
351
+ try {
352
+ // 这里可以实现凭证验证逻辑
353
+ // 暂时返回成功,因为实际验证需要连接工作线程
354
+ return { ok: true }
355
+ }
356
+ catch {
357
+ return { ok: false }
358
+ }
359
+ }
360
+ }
@@ -0,0 +1,156 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
+ import { normalizeAccountId } from '../accounts.js'
3
+
4
+ function sendJson(res: any, statusCode: number, payload: unknown) {
5
+ res.statusCode = statusCode
6
+ res.setHeader('Content-Type', 'application/json')
7
+ res.end(JSON.stringify(payload))
8
+ }
9
+
10
+ async function readRequestBody(req: any): Promise<string> {
11
+ const chunks: Buffer[] = []
12
+ await new Promise<void>((resolve, reject) => {
13
+ req.on('data', (chunk: any) => {
14
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
15
+ })
16
+ req.on('end', () => resolve())
17
+ req.on('error', (err: unknown) => reject(err))
18
+ })
19
+ return Buffer.concat(chunks).toString('utf-8')
20
+ }
21
+
22
+ async function loadRuntimeConfig(api: OpenClawPluginApi): Promise<any> {
23
+ const runtimeConfig = (api.runtime as any)?.config
24
+ if (runtimeConfig?.loadConfig) {
25
+ return runtimeConfig.loadConfig()
26
+ }
27
+ return api.config
28
+ }
29
+
30
+ async function writeRuntimeConfig(api: OpenClawPluginApi, config: any): Promise<void> {
31
+ const runtimeConfig = (api.runtime as any)?.config
32
+ if (runtimeConfig?.writeConfigFile) {
33
+ await runtimeConfig.writeConfigFile(config)
34
+ return
35
+ }
36
+ throw new Error('Config write not supported')
37
+ }
38
+
39
+ function getAccountMap(config: any): Record<string, any> {
40
+ const channels = config?.channels && typeof config.channels === 'object' ? config.channels : {}
41
+ const openclawWorkclaw
42
+ = channels['openclaw-workclaw'] && typeof channels['openclaw-workclaw'] === 'object' ? channels['openclaw-workclaw'] : {}
43
+ const accounts
44
+ = openclawWorkclaw.accounts && typeof openclawWorkclaw.accounts === 'object' ? openclawWorkclaw.accounts : {}
45
+ return { ...accounts }
46
+ }
47
+
48
+ function withAccounts(config: any, accounts: Record<string, any>): any {
49
+ const channels = config?.channels && typeof config.channels === 'object' ? config.channels : {}
50
+ const openclawWorkclaw
51
+ = channels['openclaw-workclaw'] && typeof channels['openclaw-workclaw'] === 'object' ? channels['openclaw-workclaw'] : {}
52
+ return {
53
+ ...config,
54
+ channels: {
55
+ ...channels,
56
+ 'openclaw-workclaw': {
57
+ ...openclawWorkclaw,
58
+ accounts,
59
+ },
60
+ },
61
+ }
62
+ }
63
+
64
+ export function createAccountsApiHandler(api: OpenClawPluginApi) {
65
+ return async (req: any, res: any) => {
66
+ const method = String(req.method ?? 'GET').toUpperCase()
67
+ const url = new URL(req.url ?? '', 'http://localhost')
68
+ const idRaw = url.searchParams.get('id') ?? url.searchParams.get('accountId')
69
+ const accountId = idRaw ? normalizeAccountId(idRaw) : null
70
+
71
+ if (method === 'GET') {
72
+ const cfg = await loadRuntimeConfig(api)
73
+ const accounts = getAccountMap(cfg)
74
+
75
+ if (accountId) {
76
+ const account = accounts[accountId]
77
+ if (!account) {
78
+ sendJson(res, 404, { ok: false, error: 'Not Found' })
79
+ return
80
+ }
81
+ sendJson(res, 200, { ok: true, accountId, config: account })
82
+ return
83
+ }
84
+
85
+ const entries = Object.entries(accounts).map(([id, config]) => ({
86
+ accountId: id,
87
+ config,
88
+ }))
89
+ sendJson(res, 200, { ok: true, accounts: entries })
90
+ return
91
+ }
92
+
93
+ if (method === 'POST' || method === 'PUT') {
94
+ const raw = await readRequestBody(req)
95
+ let input: any = {}
96
+ try {
97
+ input = raw ? JSON.parse(raw) : {}
98
+ }
99
+ catch {
100
+ sendJson(res, 400, { ok: false, error: 'Invalid JSON' })
101
+ return
102
+ }
103
+
104
+ const inputId = input?.accountId ?? input?.id
105
+ const normalizedId = inputId ? normalizeAccountId(String(inputId)) : ''
106
+ const patch = input?.config
107
+
108
+ if (!normalizedId || !patch || typeof patch !== 'object') {
109
+ sendJson(res, 400, { ok: false, error: 'Missing accountId or config' })
110
+ return
111
+ }
112
+
113
+ const cfg = await loadRuntimeConfig(api)
114
+ const accounts = getAccountMap(cfg)
115
+
116
+ if (method === 'POST' && accounts[normalizedId]) {
117
+ sendJson(res, 409, { ok: false, error: 'Account already exists' })
118
+ return
119
+ }
120
+
121
+ if (method === 'PUT' && !accounts[normalizedId]) {
122
+ sendJson(res, 404, { ok: false, error: 'Not Found' })
123
+ return
124
+ }
125
+
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
131
+ }
132
+
133
+ if (method === 'DELETE') {
134
+ if (!accountId) {
135
+ sendJson(res, 400, { ok: false, error: 'Missing accountId' })
136
+ return
137
+ }
138
+
139
+ const cfg = await loadRuntimeConfig(api)
140
+ const accounts = getAccountMap(cfg)
141
+
142
+ if (!accounts[accountId]) {
143
+ sendJson(res, 404, { ok: false, error: 'Not Found' })
144
+ return
145
+ }
146
+
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
152
+ }
153
+
154
+ sendJson(res, 405, { ok: false, error: 'Method Not Allowed' })
155
+ }
156
+ }
@@ -0,0 +1,122 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
+ import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ import { resolveWorkspaceDir } from './workspace.js'
6
+
7
+ const promptNames = ['SOUL.md', 'USER.md'] as const
8
+
9
+ function normalizePromptName(raw: string | null | undefined): (typeof promptNames)[number] | null {
10
+ const name = String(raw ?? '').trim().toUpperCase()
11
+ if (name === 'SOUL' || name === 'SOUL.MD')
12
+ return 'SOUL.md'
13
+ if (name === 'USER' || name === 'USER.MD')
14
+ return 'USER.md'
15
+ return null
16
+ }
17
+
18
+ function resolvePromptPath(api: OpenClawPluginApi, name: (typeof promptNames)[number]): string {
19
+ return path.join(resolveWorkspaceDir(api), name)
20
+ }
21
+
22
+ function sendJson(res: any, statusCode: number, payload: unknown) {
23
+ res.statusCode = statusCode
24
+ res.setHeader('Content-Type', 'application/json')
25
+ res.end(JSON.stringify(payload))
26
+ }
27
+
28
+ export function createPromptsApiHandler(api: OpenClawPluginApi) {
29
+ return async (req: any, res: any) => {
30
+ const method = String(req.method ?? 'GET').toUpperCase()
31
+ const url = new URL(req.url ?? '', 'http://localhost')
32
+ const name = normalizePromptName(url.searchParams.get('name'))
33
+
34
+ if (method === 'GET') {
35
+ if (name) {
36
+ const filePath = resolvePromptPath(api, name)
37
+ try {
38
+ const content = await readFile(filePath, 'utf-8')
39
+ sendJson(res, 200, { ok: true, name, content })
40
+ return
41
+ }
42
+ catch (error) {
43
+ sendJson(res, 404, { ok: false, error: 'Not Found' })
44
+ return
45
+ }
46
+ }
47
+
48
+ const entries = await Promise.all(
49
+ promptNames.map(async (promptName) => {
50
+ const filePath = resolvePromptPath(api, promptName)
51
+ try {
52
+ const info = await stat(filePath)
53
+ return { name: promptName, exists: true, bytes: info.size }
54
+ }
55
+ catch {
56
+ return { name: promptName, exists: false, bytes: 0 }
57
+ }
58
+ }),
59
+ )
60
+ sendJson(res, 200, { ok: true, entries })
61
+ return
62
+ }
63
+
64
+ if (method === 'PUT' || method === 'POST') {
65
+ const raw = await readRequestBody(req)
66
+ let input: any = {}
67
+ try {
68
+ input = raw ? JSON.parse(raw) : {}
69
+ }
70
+ catch {
71
+ sendJson(res, 400, { ok: false, error: 'Invalid JSON' })
72
+ return
73
+ }
74
+
75
+ const targetName = normalizePromptName(input?.name)
76
+ const content = typeof input?.content === 'string' ? input.content : null
77
+
78
+ if (!targetName || content === null) {
79
+ sendJson(res, 400, { ok: false, error: 'Missing name or content' })
80
+ return
81
+ }
82
+
83
+ const workspaceDir = resolveWorkspaceDir(api)
84
+ await mkdir(workspaceDir, { recursive: true })
85
+ await writeFile(resolvePromptPath(api, targetName), content, { encoding: 'utf-8' })
86
+ sendJson(res, 200, { ok: true, name: targetName })
87
+ return
88
+ }
89
+
90
+ if (method === 'DELETE') {
91
+ if (!name) {
92
+ sendJson(res, 400, { ok: false, error: 'Missing name' })
93
+ return
94
+ }
95
+
96
+ const filePath = resolvePromptPath(api, name)
97
+ try {
98
+ await unlink(filePath)
99
+ sendJson(res, 200, { ok: true, name, deleted: true })
100
+ return
101
+ }
102
+ catch {
103
+ sendJson(res, 200, { ok: true, name, deleted: false })
104
+ return
105
+ }
106
+ }
107
+
108
+ sendJson(res, 405, { ok: false, error: 'Method Not Allowed' })
109
+ }
110
+ }
111
+
112
+ async function readRequestBody(req: any): Promise<string> {
113
+ const chunks: Buffer[] = []
114
+ await new Promise<void>((resolve, reject) => {
115
+ req.on('data', (chunk: any) => {
116
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
117
+ })
118
+ req.on('end', () => resolve())
119
+ req.on('error', (err: unknown) => reject(err))
120
+ })
121
+ return Buffer.concat(chunks).toString('utf-8')
122
+ }