@2en/clawly-plugins 1.22.2 → 1.23.0
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/config-setup.ts +383 -0
- package/gateway/offline-push.test.ts +14 -3
- package/gateway/offline-push.ts +2 -2
- package/index.ts +2 -4
- package/model-gateway-setup.ts +22 -20
- package/openclaw.plugin.json +11 -2
- package/package.json +2 -2
- package/session-setup.ts +0 -61
package/config-setup.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On plugin init, patches openclaw.json to set all business config that was
|
|
3
|
+
* previously written by provision's buildConfig().
|
|
4
|
+
*
|
|
5
|
+
* Domain helpers read the pluginConfig, apply enforce or set-if-missing
|
|
6
|
+
* semantics, and write once if anything changed. This runs before
|
|
7
|
+
* setupModelGateway so agents.defaults.model is available for the model
|
|
8
|
+
* provider setup.
|
|
9
|
+
*
|
|
10
|
+
* Backward compatibility: old sprites have pluginConfig with only 4 fields
|
|
11
|
+
* (skill/model gateway credentials). Helpers check for the new fields and
|
|
12
|
+
* skip gracefully when absent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
|
|
17
|
+
import type {PluginApi} from './index'
|
|
18
|
+
import {
|
|
19
|
+
PROVIDER_NAME,
|
|
20
|
+
patchModelGateway,
|
|
21
|
+
readOpenclawConfig,
|
|
22
|
+
resolveStateDir,
|
|
23
|
+
writeOpenclawConfig,
|
|
24
|
+
} from './model-gateway-setup'
|
|
25
|
+
|
|
26
|
+
export interface ConfigPluginConfig {
|
|
27
|
+
agentId?: string
|
|
28
|
+
agentName?: string
|
|
29
|
+
workspaceDir?: string
|
|
30
|
+
defaultModel?: string
|
|
31
|
+
defaultImageModel?: string
|
|
32
|
+
sessionMainKey?: string
|
|
33
|
+
elevenlabsApiKey?: string
|
|
34
|
+
elevenlabsVoiceId?: string
|
|
35
|
+
braveSearchApiKey?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toPC(api: PluginApi): ConfigPluginConfig {
|
|
39
|
+
return (api.pluginConfig ?? {}) as ConfigPluginConfig
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Domain helpers — each returns true when config was mutated
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function patchAgent(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
|
|
47
|
+
if (!pc.agentId) return false
|
|
48
|
+
|
|
49
|
+
let dirty = false
|
|
50
|
+
const agents = (config.agents ?? {}) as Record<string, unknown>
|
|
51
|
+
const defaults = (agents.defaults ?? {}) as Record<string, unknown>
|
|
52
|
+
|
|
53
|
+
// agents.list: find-or-create entry by id, then enforce our fields only
|
|
54
|
+
const list = (Array.isArray(agents.list) ? agents.list : []) as Record<string, unknown>[]
|
|
55
|
+
let entry = list.find((e) => e.id === pc.agentId)
|
|
56
|
+
if (!entry) {
|
|
57
|
+
entry = {id: pc.agentId}
|
|
58
|
+
list.push(entry)
|
|
59
|
+
dirty = true
|
|
60
|
+
}
|
|
61
|
+
const agentName = pc.agentName ?? 'Clawly'
|
|
62
|
+
if (entry.name !== agentName) {
|
|
63
|
+
entry.name = agentName
|
|
64
|
+
dirty = true
|
|
65
|
+
}
|
|
66
|
+
if (entry.default !== true) {
|
|
67
|
+
entry.default = true
|
|
68
|
+
dirty = true
|
|
69
|
+
}
|
|
70
|
+
const workspace = pc.workspaceDir ?? ''
|
|
71
|
+
if (entry.workspace !== workspace) {
|
|
72
|
+
entry.workspace = workspace
|
|
73
|
+
dirty = true
|
|
74
|
+
}
|
|
75
|
+
if (agents.list !== list) {
|
|
76
|
+
agents.list = list
|
|
77
|
+
dirty = true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// model: set-if-missing
|
|
81
|
+
if (pc.defaultModel && !defaults.model) {
|
|
82
|
+
defaults.model = {primary: `${PROVIDER_NAME}/${pc.defaultModel}`}
|
|
83
|
+
dirty = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// imageModel: set-if-missing
|
|
87
|
+
if (pc.defaultImageModel && !defaults.imageModel) {
|
|
88
|
+
defaults.imageModel = {
|
|
89
|
+
primary: `${PROVIDER_NAME}/${pc.defaultImageModel}`,
|
|
90
|
+
fallbacks: [],
|
|
91
|
+
}
|
|
92
|
+
dirty = true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// sandbox.mode: enforce (only set the field we care about)
|
|
96
|
+
const sandbox = (defaults.sandbox ?? {}) as Record<string, unknown>
|
|
97
|
+
if (sandbox.mode !== 'off') {
|
|
98
|
+
sandbox.mode = 'off'
|
|
99
|
+
defaults.sandbox = sandbox
|
|
100
|
+
dirty = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// verboseDefault: set-if-missing
|
|
104
|
+
if (defaults.verboseDefault === undefined) {
|
|
105
|
+
defaults.verboseDefault = 'on'
|
|
106
|
+
dirty = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (dirty) {
|
|
110
|
+
agents.defaults = defaults
|
|
111
|
+
config.agents = agents
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return dirty
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function patchGateway(config: Record<string, unknown>): boolean {
|
|
118
|
+
let dirty = false
|
|
119
|
+
const gateway = (config.gateway ?? {}) as Record<string, unknown>
|
|
120
|
+
|
|
121
|
+
if (gateway.mode !== 'local') {
|
|
122
|
+
gateway.mode = 'local'
|
|
123
|
+
dirty = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// http.endpoints.chatCompletions.enabled: enforce (only set the field we care about)
|
|
127
|
+
const http = (gateway.http ?? {}) as Record<string, unknown>
|
|
128
|
+
const endpoints = (http.endpoints ?? {}) as Record<string, unknown>
|
|
129
|
+
const chatCompletions = (endpoints.chatCompletions ?? {}) as Record<string, unknown>
|
|
130
|
+
if (chatCompletions.enabled !== true) {
|
|
131
|
+
chatCompletions.enabled = true
|
|
132
|
+
endpoints.chatCompletions = chatCompletions
|
|
133
|
+
http.endpoints = endpoints
|
|
134
|
+
gateway.http = http
|
|
135
|
+
dirty = true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const desiredProxies = ['0.0.0.0/0']
|
|
139
|
+
if (JSON.stringify(gateway.trustedProxies) !== JSON.stringify(desiredProxies)) {
|
|
140
|
+
gateway.trustedProxies = desiredProxies
|
|
141
|
+
dirty = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (dirty) config.gateway = gateway
|
|
145
|
+
return dirty
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function patchBrowser(config: Record<string, unknown>): boolean {
|
|
149
|
+
let dirty = false
|
|
150
|
+
|
|
151
|
+
// browser: enforce individual fields only
|
|
152
|
+
const browser = (config.browser ?? {}) as Record<string, unknown>
|
|
153
|
+
if (browser.headless !== true) {
|
|
154
|
+
browser.headless = true
|
|
155
|
+
dirty = true
|
|
156
|
+
}
|
|
157
|
+
if (browser.noSandbox !== true) {
|
|
158
|
+
browser.noSandbox = true
|
|
159
|
+
dirty = true
|
|
160
|
+
}
|
|
161
|
+
if (browser.attachOnly !== false) {
|
|
162
|
+
browser.attachOnly = false
|
|
163
|
+
dirty = true
|
|
164
|
+
}
|
|
165
|
+
if (dirty) config.browser = browser
|
|
166
|
+
|
|
167
|
+
// commands: enforce individual fields only
|
|
168
|
+
const commands = (config.commands ?? {}) as Record<string, unknown>
|
|
169
|
+
if (commands.restart !== true) {
|
|
170
|
+
commands.restart = true
|
|
171
|
+
config.commands = commands
|
|
172
|
+
dirty = true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return dirty
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function patchSessionMainKey(
|
|
179
|
+
config: Record<string, unknown>,
|
|
180
|
+
pc: ConfigPluginConfig,
|
|
181
|
+
): boolean {
|
|
182
|
+
if (!pc.sessionMainKey) return false
|
|
183
|
+
|
|
184
|
+
const session = (config.session ?? {}) as Record<string, unknown>
|
|
185
|
+
if (session.mainKey === pc.sessionMainKey) return false
|
|
186
|
+
|
|
187
|
+
session.mainKey = pc.sessionMainKey
|
|
188
|
+
config.session = session
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function patchTts(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
|
|
193
|
+
if (!pc.elevenlabsApiKey) return false
|
|
194
|
+
|
|
195
|
+
let dirty = false
|
|
196
|
+
const messages = (config.messages ?? {}) as Record<string, unknown>
|
|
197
|
+
const tts = (messages.tts ?? {}) as Record<string, unknown>
|
|
198
|
+
const elevenlabs = (tts.elevenlabs ?? {}) as Record<string, unknown>
|
|
199
|
+
|
|
200
|
+
// Credentials: enforce
|
|
201
|
+
if (elevenlabs.apiKey !== pc.elevenlabsApiKey) {
|
|
202
|
+
elevenlabs.apiKey = pc.elevenlabsApiKey
|
|
203
|
+
dirty = true
|
|
204
|
+
}
|
|
205
|
+
if (elevenlabs.baseUrl !== 'https://api.elevenlabs.io') {
|
|
206
|
+
elevenlabs.baseUrl = 'https://api.elevenlabs.io'
|
|
207
|
+
dirty = true
|
|
208
|
+
}
|
|
209
|
+
const voiceId = pc.elevenlabsVoiceId ?? ''
|
|
210
|
+
if (elevenlabs.voiceId !== voiceId) {
|
|
211
|
+
elevenlabs.voiceId = voiceId
|
|
212
|
+
dirty = true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Tuning: set-if-missing
|
|
216
|
+
if (tts.auto === undefined) {
|
|
217
|
+
tts.auto = 'tagged'
|
|
218
|
+
dirty = true
|
|
219
|
+
}
|
|
220
|
+
if (tts.mode === undefined) {
|
|
221
|
+
tts.mode = 'final'
|
|
222
|
+
dirty = true
|
|
223
|
+
}
|
|
224
|
+
if (tts.provider === undefined) {
|
|
225
|
+
tts.provider = 'elevenlabs'
|
|
226
|
+
dirty = true
|
|
227
|
+
}
|
|
228
|
+
if (tts.summaryModel === undefined && pc.defaultModel) {
|
|
229
|
+
tts.summaryModel = `${PROVIDER_NAME}/${pc.defaultModel}`
|
|
230
|
+
dirty = true
|
|
231
|
+
}
|
|
232
|
+
if (tts.modelOverrides === undefined) {
|
|
233
|
+
tts.modelOverrides = {enabled: true}
|
|
234
|
+
dirty = true
|
|
235
|
+
}
|
|
236
|
+
if (tts.maxTextLength === undefined) {
|
|
237
|
+
tts.maxTextLength = 4000
|
|
238
|
+
dirty = true
|
|
239
|
+
}
|
|
240
|
+
if (tts.timeoutMs === undefined) {
|
|
241
|
+
tts.timeoutMs = 30000
|
|
242
|
+
dirty = true
|
|
243
|
+
}
|
|
244
|
+
if (tts.prefsPath === undefined) {
|
|
245
|
+
tts.prefsPath = '~/.openclaw/settings/tts.json'
|
|
246
|
+
dirty = true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// elevenlabs tuning: set-if-missing
|
|
250
|
+
if (elevenlabs.modelId === undefined) {
|
|
251
|
+
elevenlabs.modelId = 'eleven_multilingual_v2'
|
|
252
|
+
dirty = true
|
|
253
|
+
}
|
|
254
|
+
if (elevenlabs.seed === undefined) {
|
|
255
|
+
elevenlabs.seed = 42
|
|
256
|
+
dirty = true
|
|
257
|
+
}
|
|
258
|
+
if (elevenlabs.applyTextNormalization === undefined) {
|
|
259
|
+
elevenlabs.applyTextNormalization = 'auto'
|
|
260
|
+
dirty = true
|
|
261
|
+
}
|
|
262
|
+
if (elevenlabs.languageCode === undefined) {
|
|
263
|
+
elevenlabs.languageCode = 'en'
|
|
264
|
+
dirty = true
|
|
265
|
+
}
|
|
266
|
+
if (elevenlabs.voiceSettings === undefined) {
|
|
267
|
+
elevenlabs.voiceSettings = {
|
|
268
|
+
stability: 0.5,
|
|
269
|
+
similarityBoost: 0.75,
|
|
270
|
+
style: 0.0,
|
|
271
|
+
useSpeakerBoost: true,
|
|
272
|
+
speed: 1.0,
|
|
273
|
+
}
|
|
274
|
+
dirty = true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (dirty) {
|
|
278
|
+
tts.elevenlabs = elevenlabs
|
|
279
|
+
messages.tts = tts
|
|
280
|
+
config.messages = messages
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return dirty
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function patchWebSearch(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
|
|
287
|
+
if (!pc.braveSearchApiKey) return false
|
|
288
|
+
|
|
289
|
+
let dirty = false
|
|
290
|
+
const tools = (config.tools ?? {}) as Record<string, unknown>
|
|
291
|
+
const web = (tools.web ?? {}) as Record<string, unknown>
|
|
292
|
+
const search = (web.search ?? {}) as Record<string, unknown>
|
|
293
|
+
|
|
294
|
+
// Credentials: enforce
|
|
295
|
+
if (search.provider !== 'brave') {
|
|
296
|
+
search.provider = 'brave'
|
|
297
|
+
dirty = true
|
|
298
|
+
}
|
|
299
|
+
if (search.apiKey !== pc.braveSearchApiKey) {
|
|
300
|
+
search.apiKey = pc.braveSearchApiKey
|
|
301
|
+
dirty = true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Tuning: set-if-missing
|
|
305
|
+
if (search.maxResults === undefined) {
|
|
306
|
+
search.maxResults = 5
|
|
307
|
+
dirty = true
|
|
308
|
+
}
|
|
309
|
+
if (search.timeoutSeconds === undefined) {
|
|
310
|
+
search.timeoutSeconds = 30
|
|
311
|
+
dirty = true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (dirty) {
|
|
315
|
+
web.search = search
|
|
316
|
+
tools.web = web
|
|
317
|
+
config.tools = tools
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return dirty
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function patchSession(config: Record<string, unknown>): boolean {
|
|
324
|
+
let dirty = false
|
|
325
|
+
|
|
326
|
+
// session.dmScope: enforce
|
|
327
|
+
const session = (config.session ?? {}) as Record<string, unknown>
|
|
328
|
+
if (session.dmScope !== 'per-channel-peer') {
|
|
329
|
+
session.dmScope = 'per-channel-peer'
|
|
330
|
+
config.session = session
|
|
331
|
+
dirty = true
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// tools.sessions.visibility: enforce
|
|
335
|
+
const tools = (config.tools ?? {}) as Record<string, unknown>
|
|
336
|
+
const sessions = (tools.sessions ?? {}) as Record<string, unknown>
|
|
337
|
+
if (sessions.visibility !== 'agent') {
|
|
338
|
+
sessions.visibility = 'agent'
|
|
339
|
+
tools.sessions = sessions
|
|
340
|
+
config.tools = tools
|
|
341
|
+
dirty = true
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return dirty
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Entry point — single read, patch all, single write
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
export function setupConfig(api: PluginApi): void {
|
|
352
|
+
const stateDir = resolveStateDir(api)
|
|
353
|
+
if (!stateDir) {
|
|
354
|
+
api.logger.warn('Cannot resolve state dir — config setup skipped.')
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const configPath = path.join(stateDir, 'openclaw.json')
|
|
359
|
+
const config = readOpenclawConfig(configPath)
|
|
360
|
+
const pc = toPC(api)
|
|
361
|
+
|
|
362
|
+
let dirty = false
|
|
363
|
+
dirty = patchAgent(config, pc) || dirty
|
|
364
|
+
dirty = patchGateway(config) || dirty
|
|
365
|
+
dirty = patchBrowser(config) || dirty
|
|
366
|
+
dirty = patchSessionMainKey(config, pc) || dirty
|
|
367
|
+
dirty = patchSession(config) || dirty
|
|
368
|
+
dirty = patchTts(config, pc) || dirty
|
|
369
|
+
dirty = patchWebSearch(config, pc) || dirty
|
|
370
|
+
dirty = patchModelGateway(config, api) || dirty
|
|
371
|
+
|
|
372
|
+
if (!dirty) {
|
|
373
|
+
api.logger.info('Config setup: no changes needed.')
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
writeOpenclawConfig(configPath, config)
|
|
379
|
+
api.logger.info('Config setup: patched openclaw.json.')
|
|
380
|
+
} catch (err) {
|
|
381
|
+
api.logger.error(`Config setup failed: ${(err as Error).message}`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -279,9 +279,20 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
279
279
|
expect(shouldSkipPushForMessage('All good. HEARTBEAT_OK')).toBe('heartbeat ack')
|
|
280
280
|
})
|
|
281
281
|
|
|
282
|
-
test('
|
|
283
|
-
const
|
|
284
|
-
|
|
282
|
+
test('skips verbose heartbeat ack ending with HEARTBEAT_OK', () => {
|
|
283
|
+
const verbose =
|
|
284
|
+
'The user said hello recently. Looking at HEARTBEAT.md checklist: nothing needs attention. HEARTBEAT_OK'
|
|
285
|
+
expect(shouldSkipPushForMessage(verbose)).toBe('heartbeat ack')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('does not skip message mentioning HEARTBEAT_OK mid-text', () => {
|
|
289
|
+
expect(
|
|
290
|
+
shouldSkipPushForMessage('HEARTBEAT_OK is a status code I output after each check.'),
|
|
291
|
+
).toBeNull()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('skips heartbeat ack with non-ASCII trailing punctuation', () => {
|
|
295
|
+
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT_OK。')).toBe('heartbeat ack')
|
|
285
296
|
})
|
|
286
297
|
|
|
287
298
|
test('skips system prompt leak', () => {
|
package/gateway/offline-push.ts
CHANGED
|
@@ -79,8 +79,8 @@ export function shouldSkipPushForMessage(text: string): string | null {
|
|
|
79
79
|
// Agent sentinel "nothing to say" — mobile hides as "silentReply"
|
|
80
80
|
if (trimmed === 'NO_REPLY') return 'silent reply'
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
if (
|
|
82
|
+
// Heartbeat acknowledgment (HEARTBEAT_OK as ending sentinel) — mobile hides as "heartbeatAck"
|
|
83
|
+
if (/HEARTBEAT_OK[\p{P}\s]*$/u.test(text)) return 'heartbeat ack'
|
|
84
84
|
|
|
85
85
|
// Agent echoed system prompt metadata — mobile hides as "systemPromptLeak"
|
|
86
86
|
if (text.includes('Conversation info (untrusted metadata)')) return 'system prompt leak'
|
package/index.ts
CHANGED
|
@@ -32,13 +32,12 @@
|
|
|
32
32
|
import {registerAutoPair} from './auto-pair'
|
|
33
33
|
import {registerCalendar} from './calendar'
|
|
34
34
|
import {registerCommands} from './command'
|
|
35
|
+
import {setupConfig} from './config-setup'
|
|
35
36
|
import {registerCronHook} from './cron-hook'
|
|
36
37
|
import {registerEmail} from './email'
|
|
37
38
|
import {registerGateway} from './gateway'
|
|
38
39
|
import {getGatewayConfig} from './gateway-fetch'
|
|
39
|
-
import {setupModelGateway} from './model-gateway-setup'
|
|
40
40
|
import {registerOutboundHook, registerOutboundHttpRoute, registerOutboundMethods} from './outbound'
|
|
41
|
-
import {setupSession} from './session-setup'
|
|
42
41
|
import {registerSkillCommandRestore} from './skill-command-restore'
|
|
43
42
|
import {registerTools} from './tools'
|
|
44
43
|
|
|
@@ -184,10 +183,9 @@ export default {
|
|
|
184
183
|
registerCommands(api)
|
|
185
184
|
registerTools(api)
|
|
186
185
|
registerCronHook(api)
|
|
186
|
+
setupConfig(api)
|
|
187
187
|
registerGateway(api)
|
|
188
188
|
registerAutoPair(api)
|
|
189
|
-
setupModelGateway(api)
|
|
190
|
-
setupSession(api)
|
|
191
189
|
|
|
192
190
|
// Email & calendar (optional — requires skillGatewayBaseUrl + skillGatewayToken in config)
|
|
193
191
|
const gw = getGatewayConfig(api)
|
package/model-gateway-setup.ts
CHANGED
|
@@ -83,16 +83,7 @@ export function writeOpenclawConfig(configPath: string, config: Record<string, u
|
|
|
83
83
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function
|
|
87
|
-
const stateDir = resolveStateDir(api)
|
|
88
|
-
if (!stateDir) {
|
|
89
|
-
api.logger.warn('Cannot resolve state dir — model gateway setup skipped.')
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const configPath = path.join(stateDir, 'openclaw.json')
|
|
94
|
-
const config = readOpenclawConfig(configPath)
|
|
95
|
-
|
|
86
|
+
export function patchModelGateway(config: Record<string, unknown>, api: PluginApi): boolean {
|
|
96
87
|
// If provider already exists, check if extra models or aliases need updating.
|
|
97
88
|
// This runs before the credentials check because provisioned sprites have
|
|
98
89
|
// credentials in openclaw.json directly, not in pluginConfig.
|
|
@@ -150,16 +141,11 @@ export function setupModelGateway(api: PluginApi): void {
|
|
|
150
141
|
defaults.models = existingAliases
|
|
151
142
|
agents.defaults = defaults
|
|
152
143
|
config.agents = agents
|
|
153
|
-
|
|
154
|
-
writeOpenclawConfig(configPath, config)
|
|
155
|
-
api.logger.info(`Model gateway updated: appended missing extra models/aliases.`)
|
|
156
|
-
} catch (err) {
|
|
157
|
-
api.logger.error(`Failed to update model gateway: ${(err as Error).message}`)
|
|
158
|
-
}
|
|
144
|
+
api.logger.info(`Model gateway updated: appended missing extra models/aliases.`)
|
|
159
145
|
} else {
|
|
160
146
|
api.logger.info('Model gateway provider already configured.')
|
|
161
147
|
}
|
|
162
|
-
return
|
|
148
|
+
return dirty
|
|
163
149
|
}
|
|
164
150
|
|
|
165
151
|
// No existing provider — need pluginConfig credentials to create one
|
|
@@ -170,7 +156,7 @@ export function setupModelGateway(api: PluginApi): void {
|
|
|
170
156
|
|
|
171
157
|
if (!baseUrl || !token) {
|
|
172
158
|
api.logger.info('Model gateway not configured (missing baseUrl or token), skipping.')
|
|
173
|
-
return
|
|
159
|
+
return false
|
|
174
160
|
}
|
|
175
161
|
|
|
176
162
|
// Derive model IDs from agents.defaults
|
|
@@ -187,7 +173,7 @@ export function setupModelGateway(api: PluginApi): void {
|
|
|
187
173
|
|
|
188
174
|
if (!defaultModel) {
|
|
189
175
|
api.logger.warn('No default model found in agents.defaults — model gateway setup skipped.')
|
|
190
|
-
return
|
|
176
|
+
return false
|
|
191
177
|
}
|
|
192
178
|
|
|
193
179
|
const defaultModels =
|
|
@@ -233,9 +219,25 @@ export function setupModelGateway(api: PluginApi): void {
|
|
|
233
219
|
agents.defaults = defaults
|
|
234
220
|
config.agents = agents
|
|
235
221
|
|
|
222
|
+
api.logger.info(`Model gateway provider configured: ${baseUrl} with ${models.length} model(s).`)
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Standalone wrapper — reads config, patches, writes. Used by tests. */
|
|
227
|
+
export function setupModelGateway(api: PluginApi): void {
|
|
228
|
+
const stateDir = resolveStateDir(api)
|
|
229
|
+
if (!stateDir) {
|
|
230
|
+
api.logger.warn('Cannot resolve state dir — model gateway setup skipped.')
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const configPath = path.join(stateDir, 'openclaw.json')
|
|
235
|
+
const config = readOpenclawConfig(configPath)
|
|
236
|
+
|
|
237
|
+
if (!patchModelGateway(config, api)) return
|
|
238
|
+
|
|
236
239
|
try {
|
|
237
240
|
writeOpenclawConfig(configPath, config)
|
|
238
|
-
api.logger.info(`Model gateway provider configured: ${baseUrl} with ${models.length} model(s).`)
|
|
239
241
|
} catch (err) {
|
|
240
242
|
api.logger.error(`Failed to setup model gateway: ${(err as Error).message}`)
|
|
241
243
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"configSchema": {
|
|
40
40
|
"type": "object",
|
|
41
|
-
"additionalProperties":
|
|
41
|
+
"additionalProperties": true,
|
|
42
42
|
"properties": {
|
|
43
43
|
"memoryDir": { "type": "string", "minLength": 1 },
|
|
44
44
|
"bin": { "type": "string", "minLength": 1 },
|
|
@@ -49,7 +49,16 @@
|
|
|
49
49
|
"skillGatewayBaseUrl": { "type": "string" },
|
|
50
50
|
"skillGatewayToken": { "type": "string" },
|
|
51
51
|
"modelGatewayBaseUrl": { "type": "string" },
|
|
52
|
-
"modelGatewayToken": { "type": "string" }
|
|
52
|
+
"modelGatewayToken": { "type": "string" },
|
|
53
|
+
"agentId": { "type": "string" },
|
|
54
|
+
"agentName": { "type": "string" },
|
|
55
|
+
"workspaceDir": { "type": "string" },
|
|
56
|
+
"defaultModel": { "type": "string" },
|
|
57
|
+
"defaultImageModel": { "type": "string" },
|
|
58
|
+
"sessionMainKey": { "type": "string" },
|
|
59
|
+
"elevenlabsApiKey": { "type": "string" },
|
|
60
|
+
"elevenlabsVoiceId": { "type": "string" },
|
|
61
|
+
"braveSearchApiKey": { "type": "string" }
|
|
53
62
|
},
|
|
54
63
|
"required": []
|
|
55
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"index.ts",
|
|
20
20
|
"auto-pair.ts",
|
|
21
21
|
"calendar.ts",
|
|
22
|
+
"config-setup.ts",
|
|
22
23
|
"cron-hook.ts",
|
|
23
24
|
"email.ts",
|
|
24
25
|
"gateway-fetch.ts",
|
|
25
26
|
"outbound.ts",
|
|
26
27
|
"model-gateway-setup.ts",
|
|
27
|
-
"session-setup.ts",
|
|
28
28
|
"skill-command-restore.ts",
|
|
29
29
|
"openclaw.plugin.json"
|
|
30
30
|
],
|
package/session-setup.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* On plugin init, patches openclaw.json to:
|
|
3
|
-
* 1. Set `session.dmScope` to "per-channel-peer" — isolates Telegram (and
|
|
4
|
-
* other channel) DMs from the Clawly mobile session.
|
|
5
|
-
* 2. Set `tools.sessions.visibility` to "agent" — lets the agent read
|
|
6
|
-
* conversation history from other sessions under the same agent (e.g.
|
|
7
|
-
* reference recent Telegram chats from the mobile session).
|
|
8
|
-
*
|
|
9
|
-
* Runs synchronously during plugin registration, same pattern as
|
|
10
|
-
* model-gateway-setup.ts.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
|
|
15
|
-
import type {PluginApi} from './index'
|
|
16
|
-
import {readOpenclawConfig, resolveStateDir, writeOpenclawConfig} from './model-gateway-setup'
|
|
17
|
-
|
|
18
|
-
const DESIRED_DM_SCOPE = 'per-channel-peer'
|
|
19
|
-
const DESIRED_SESSION_VISIBILITY = 'agent'
|
|
20
|
-
|
|
21
|
-
export function setupSession(api: PluginApi): void {
|
|
22
|
-
const stateDir = resolveStateDir(api)
|
|
23
|
-
if (!stateDir) {
|
|
24
|
-
api.logger.warn('Cannot resolve state dir — session setup skipped.')
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const configPath = path.join(stateDir, 'openclaw.json')
|
|
29
|
-
const config = readOpenclawConfig(configPath)
|
|
30
|
-
|
|
31
|
-
let dirty = false
|
|
32
|
-
|
|
33
|
-
// 1. dmScope
|
|
34
|
-
const session = ((config.session as Record<string, unknown>) ?? {}) as Record<string, unknown>
|
|
35
|
-
if (session.dmScope !== DESIRED_DM_SCOPE) {
|
|
36
|
-
session.dmScope = DESIRED_DM_SCOPE
|
|
37
|
-
config.session = session
|
|
38
|
-
dirty = true
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 2. tools.sessions.visibility
|
|
42
|
-
const tools = ((config.tools as Record<string, unknown>) ?? {}) as Record<string, unknown>
|
|
43
|
-
const sessions = ((tools.sessions as Record<string, unknown>) ?? {}) as Record<string, unknown>
|
|
44
|
-
if (sessions.visibility !== DESIRED_SESSION_VISIBILITY) {
|
|
45
|
-
sessions.visibility = DESIRED_SESSION_VISIBILITY
|
|
46
|
-
tools.sessions = sessions
|
|
47
|
-
config.tools = tools
|
|
48
|
-
dirty = true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!dirty) return
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
writeOpenclawConfig(configPath, config)
|
|
55
|
-
api.logger.info(
|
|
56
|
-
`Session: dmScope="${DESIRED_DM_SCOPE}", tools.sessions.visibility="${DESIRED_SESSION_VISIBILITY}".`,
|
|
57
|
-
)
|
|
58
|
-
} catch (err) {
|
|
59
|
-
api.logger.error(`Failed to update session config: ${(err as Error).message}`)
|
|
60
|
-
}
|
|
61
|
-
}
|