@2en/clawly-plugins 1.33.1 → 1.34.0-beta.1
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/clawly-config-defaults.json5 +2 -2
- package/config-setup.ts +39 -1
- package/cron-history.ts +151 -0
- package/gateway/config-cron-history.ts +57 -0
- package/gateway/index.ts +2 -0
- package/gateway/info.ts +5 -1
- package/http/info.ts +28 -0
- package/http/version.ts +29 -0
- package/index.ts +8 -0
- package/media-understanding.ts +81 -28
- package/package.json +2 -1
- package/gateway/memory-browser.md +0 -55
- package/gateway/plugin-version-management.md +0 -190
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
input: ["text", "image"],
|
|
40
40
|
contextWindow: 1000000,
|
|
41
41
|
maxTokens: 128000,
|
|
42
|
-
|
|
42
|
+
api: "anthropic-messages",
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
id: "anthropic/claude-opus-4.6",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
input: ["text", "image"],
|
|
48
48
|
contextWindow: 1000000,
|
|
49
49
|
maxTokens: 128000,
|
|
50
|
-
|
|
50
|
+
api: "anthropic-messages",
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
id: "openai/gpt-5.4",
|
package/config-setup.ts
CHANGED
|
@@ -237,7 +237,7 @@ export function patchBrowser(config: OpenClawConfig): boolean {
|
|
|
237
237
|
// patchHeartbeat — migrated to clawly-config-defaults.json5
|
|
238
238
|
|
|
239
239
|
/** Fields enforced by patchModelDefinitions on every gateway restart. */
|
|
240
|
-
const MODEL_ENFORCED_KEYS = ['contextWindow', 'maxTokens', 'input'] as const
|
|
240
|
+
const MODEL_ENFORCED_KEYS = ['contextWindow', 'maxTokens', 'input', 'api'] as const
|
|
241
241
|
|
|
242
242
|
/** Shallow equality — only supports primitives and arrays of primitives. */
|
|
243
243
|
function valuesEqual(a: unknown, b: unknown): boolean {
|
|
@@ -593,6 +593,43 @@ export function patchPluginsAllow(config: OpenClawConfig): boolean {
|
|
|
593
593
|
return true
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Ensure the bootstrap-extra-files internal hook is configured so the agent
|
|
598
|
+
* loads system-managed workspace files from .clawly/ alongside user files.
|
|
599
|
+
*/
|
|
600
|
+
export function patchBootstrapExtraFiles(config: OpenClawConfig): boolean {
|
|
601
|
+
const hooks = (config.hooks ?? {}) as Record<string, unknown>
|
|
602
|
+
const internal = asObj(hooks.internal)
|
|
603
|
+
const entries = asObj(internal.entries)
|
|
604
|
+
|
|
605
|
+
// Must match systemWorkspacePaths from @clawly/workspace-files.
|
|
606
|
+
// Hardcoded because this plugin runs on sprites without access to that package.
|
|
607
|
+
const EXPECTED_PATHS = ['.clawly/AGENTS.md', '.clawly/TOOLS.md']
|
|
608
|
+
|
|
609
|
+
const existing = entries['bootstrap-extra-files'] as
|
|
610
|
+
| {enabled?: boolean; paths?: string[]}
|
|
611
|
+
| undefined
|
|
612
|
+
if (
|
|
613
|
+
internal.enabled === true &&
|
|
614
|
+
existing?.enabled === true &&
|
|
615
|
+
Array.isArray(existing.paths) &&
|
|
616
|
+
existing.paths.length === EXPECTED_PATHS.length &&
|
|
617
|
+
EXPECTED_PATHS.every((p) => existing.paths!.includes(p))
|
|
618
|
+
) {
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
entries['bootstrap-extra-files'] = {
|
|
623
|
+
enabled: true,
|
|
624
|
+
paths: EXPECTED_PATHS,
|
|
625
|
+
}
|
|
626
|
+
internal.enabled = true
|
|
627
|
+
internal.entries = entries
|
|
628
|
+
hooks.internal = internal
|
|
629
|
+
config.hooks = hooks as OpenClawConfig['hooks']
|
|
630
|
+
return true
|
|
631
|
+
}
|
|
632
|
+
|
|
596
633
|
function reconcileRuntimeConfig(
|
|
597
634
|
api: PluginApi,
|
|
598
635
|
config: OpenClawConfig,
|
|
@@ -618,6 +655,7 @@ function reconcileRuntimeConfig(
|
|
|
618
655
|
dirty = patchGateway(config) || dirty
|
|
619
656
|
dirty = patchBrowser(config) || dirty
|
|
620
657
|
dirty = patchSession(config) || dirty
|
|
658
|
+
dirty = patchBootstrapExtraFiles(config) || dirty
|
|
621
659
|
|
|
622
660
|
const defaults = loadDefaults(stateDir)
|
|
623
661
|
if (defaults) {
|
package/cron-history.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron history injection — before_agent_start hook that prepends past run
|
|
3
|
+
* summaries to cron job prompts, preventing the LLM from repeating the same
|
|
4
|
+
* output across isolated sessions.
|
|
5
|
+
*
|
|
6
|
+
* Feature-flagged via agents.defaults.cronHistoryInjection in openclaw.json
|
|
7
|
+
* (default: off). Toggled from the mobile app's system settings.
|
|
8
|
+
*
|
|
9
|
+
* Data source: the existing cron run log JSONL at
|
|
10
|
+
* {cronStoreDir}/runs/{jobId}.jsonl — populated by OpenClaw after each run
|
|
11
|
+
* with a `summary` field (last agent output, truncated to 2000 chars).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
|
|
17
|
+
import {autoUpdateJobId} from './internal/hooks/auto-update'
|
|
18
|
+
import type {PluginApi} from './types'
|
|
19
|
+
import type {OpenClawConfig} from './types/openclaw'
|
|
20
|
+
|
|
21
|
+
const HISTORY_DEPTH = 10
|
|
22
|
+
const SUMMARY_MAX_CHARS = 200
|
|
23
|
+
|
|
24
|
+
/** Extract the first sentence or first line, truncated to maxChars. */
|
|
25
|
+
function truncateSummary(summary: string, maxChars: number): string {
|
|
26
|
+
// Take first line
|
|
27
|
+
const firstLine = summary.split('\n')[0]?.trim() ?? summary.trim()
|
|
28
|
+
if (firstLine.length <= maxChars) return firstLine
|
|
29
|
+
return `${firstLine.slice(0, maxChars)}…`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Parse job ID from the cron prompt prefix: [cron:{jobId} {jobName}] */
|
|
33
|
+
function parseJobIdFromPrompt(prompt: string): string | null {
|
|
34
|
+
const match = prompt.match(/\[cron:([0-9a-f-]+)\s/)
|
|
35
|
+
return match?.[1] ?? null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve the cron run log path for a job. */
|
|
39
|
+
function resolveRunLogPath(cronStore: string | undefined, jobId: string): string {
|
|
40
|
+
const storePath =
|
|
41
|
+
cronStore || path.join(process.env.HOME || '/home/claw', '.openclaw', 'cron', 'cron.json')
|
|
42
|
+
return path.join(path.dirname(storePath), 'runs', `${jobId}.jsonl`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RunLogEntry {
|
|
46
|
+
ts: number
|
|
47
|
+
status?: string
|
|
48
|
+
summary?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Read last N successful run summaries from the JSONL run log. */
|
|
52
|
+
async function readRecentSummaries(
|
|
53
|
+
logPath: string,
|
|
54
|
+
limit: number,
|
|
55
|
+
): Promise<Array<{ts: number; summary: string}>> {
|
|
56
|
+
let raw: string
|
|
57
|
+
try {
|
|
58
|
+
raw = await fs.readFile(logPath, 'utf-8')
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
if (!raw.trim()) return []
|
|
63
|
+
|
|
64
|
+
const lines = raw.split('\n')
|
|
65
|
+
const results: Array<{ts: number; summary: string}> = []
|
|
66
|
+
|
|
67
|
+
// Read from the end (most recent first)
|
|
68
|
+
for (let i = lines.length - 1; i >= 0 && results.length < limit; i--) {
|
|
69
|
+
const line = lines[i]?.trim()
|
|
70
|
+
if (!line) continue
|
|
71
|
+
try {
|
|
72
|
+
const entry = JSON.parse(line) as Partial<RunLogEntry>
|
|
73
|
+
if (entry.status !== 'ok') continue
|
|
74
|
+
const summary = entry.summary?.trim()
|
|
75
|
+
if (!summary) continue
|
|
76
|
+
if (typeof entry.ts !== 'number') continue
|
|
77
|
+
results.push({ts: entry.ts, summary})
|
|
78
|
+
} catch {
|
|
79
|
+
// skip malformed lines
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return results.reverse()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatDate(ts: number): string {
|
|
87
|
+
const d = new Date(ts)
|
|
88
|
+
const y = d.getFullYear()
|
|
89
|
+
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
90
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
91
|
+
return `${y}-${m}-${day}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerCronHistory(api: PluginApi) {
|
|
95
|
+
api.on(
|
|
96
|
+
'before_agent_start',
|
|
97
|
+
async (
|
|
98
|
+
event: {prompt: string; messages?: unknown[]},
|
|
99
|
+
ctx?: {agentId?: string; sessionKey?: string; sessionId?: string},
|
|
100
|
+
) => {
|
|
101
|
+
// Feature flag check — read fresh config each time (loadConfig has ~200ms cache)
|
|
102
|
+
try {
|
|
103
|
+
const config = api.runtime.config.loadConfig() as OpenClawConfig
|
|
104
|
+
const defaults = (config.agents as Record<string, unknown> | undefined)?.defaults as
|
|
105
|
+
| Record<string, unknown>
|
|
106
|
+
| undefined
|
|
107
|
+
if (defaults?.cronHistoryInjection !== true) return
|
|
108
|
+
} catch {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Only fire for cron sessions
|
|
113
|
+
const sessionKey = ctx?.sessionKey
|
|
114
|
+
if (!sessionKey?.includes(':cron:')) return
|
|
115
|
+
|
|
116
|
+
// Extract job ID from the prompt prefix
|
|
117
|
+
const jobId = parseJobIdFromPrompt(event.prompt)
|
|
118
|
+
if (!jobId) return
|
|
119
|
+
|
|
120
|
+
// Skip auto-update job
|
|
121
|
+
if (autoUpdateJobId && jobId === autoUpdateJobId) return
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const logPath = resolveRunLogPath(
|
|
125
|
+
(api.config.cron as Record<string, unknown> | undefined)?.store as string | undefined,
|
|
126
|
+
jobId,
|
|
127
|
+
)
|
|
128
|
+
const summaries = await readRecentSummaries(logPath, HISTORY_DEPTH)
|
|
129
|
+
if (summaries.length === 0) return
|
|
130
|
+
|
|
131
|
+
const lines = summaries.map(
|
|
132
|
+
(s, i) =>
|
|
133
|
+
`${i + 1}. (${formatDate(s.ts)}) ${truncateSummary(s.summary, SUMMARY_MAX_CHARS)}`,
|
|
134
|
+
)
|
|
135
|
+
const prependContext = [
|
|
136
|
+
`[For context: summaries of your previous ${summaries.length} ${summaries.length === 1 ? 'output' : 'outputs'} for this task. Use this to avoid repetition where appropriate.]`,
|
|
137
|
+
...lines,
|
|
138
|
+
].join('\n')
|
|
139
|
+
|
|
140
|
+
api.logger.info(`cron-history: injected ${summaries.length} summaries for job ${jobId}`)
|
|
141
|
+
|
|
142
|
+
return {prependContext}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
145
|
+
api.logger.warn(`cron-history: failed to read history for job ${jobId} — ${msg}`)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
api.logger.info('cron-history: registered before_agent_start hook')
|
|
151
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron history injection toggle RPC: writes agents.defaults.cronHistoryInjection
|
|
3
|
+
* to openclaw.json without triggering a gateway restart.
|
|
4
|
+
*
|
|
5
|
+
* When enabled, the before_agent_start hook in cron-history.ts injects past run
|
|
6
|
+
* summaries into every cron job's prompt to prevent repeated outputs.
|
|
7
|
+
*
|
|
8
|
+
* Methods:
|
|
9
|
+
* - clawly.config.setCronHistory({ enabled }) → { changed, enabled }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {PluginApi} from '../types'
|
|
13
|
+
import type {OpenClawConfig} from '../types/openclaw'
|
|
14
|
+
|
|
15
|
+
export function registerConfigCronHistory(api: PluginApi) {
|
|
16
|
+
api.registerGatewayMethod('clawly.config.setCronHistory', async ({params, respond}) => {
|
|
17
|
+
const enabled = params.enabled === true
|
|
18
|
+
|
|
19
|
+
let config: OpenClawConfig
|
|
20
|
+
try {
|
|
21
|
+
config = {...(api.runtime.config.loadConfig() as OpenClawConfig)}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
24
|
+
respond(true, {changed: false, enabled, error: `Load failed: ${msg}`})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currentDefaults = (config.agents as Record<string, unknown> | undefined)?.defaults as
|
|
29
|
+
| Record<string, unknown>
|
|
30
|
+
| undefined
|
|
31
|
+
|
|
32
|
+
if (currentDefaults?.cronHistoryInjection === enabled) {
|
|
33
|
+
respond(true, {changed: false, enabled})
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Shallow-copy nested objects to avoid polluting the loadConfig() cache
|
|
38
|
+
// if writeConfigFile fails below.
|
|
39
|
+
const agents = {...((config.agents ?? {}) as Record<string, unknown>)}
|
|
40
|
+
const defaults = {...((agents.defaults ?? {}) as Record<string, unknown>)}
|
|
41
|
+
defaults.cronHistoryInjection = enabled
|
|
42
|
+
agents.defaults = defaults
|
|
43
|
+
config.agents = agents
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await api.runtime.config.writeConfigFile(config)
|
|
47
|
+
api.logger.info(`config-cron-history: set cronHistoryInjection to ${enabled}`)
|
|
48
|
+
respond(true, {changed: true, enabled})
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
51
|
+
api.logger.error(`config-cron-history: write failed — ${msg}`)
|
|
52
|
+
respond(true, {changed: false, enabled, error: `Write failed: ${msg}`})
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
api.logger.info('config-cron-history: registered clawly.config.setCronHistory')
|
|
57
|
+
}
|
package/gateway/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {registerCalendarNative} from './calendar-native'
|
|
|
4
4
|
import {registerAnalytics} from './analytics'
|
|
5
5
|
import {registerAudit} from './audit'
|
|
6
6
|
import {registerClawhub2gateway} from './clawhub2gateway'
|
|
7
|
+
import {registerConfigCronHistory} from './config-cron-history'
|
|
7
8
|
import {registerConfigModel} from './config-model'
|
|
8
9
|
import {registerConfigRepair} from './config-repair'
|
|
9
10
|
import {registerConfigTimezone} from './config-timezone'
|
|
@@ -63,6 +64,7 @@ export function registerGateway(api: PluginApi) {
|
|
|
63
64
|
registerCronTelemetry(api)
|
|
64
65
|
registerMessageLog(api)
|
|
65
66
|
registerAnalytics(api)
|
|
67
|
+
registerConfigCronHistory(api)
|
|
66
68
|
registerConfigModel(api)
|
|
67
69
|
registerConfigRepair(api)
|
|
68
70
|
registerConfigTimezone(api)
|
package/gateway/info.ts
CHANGED
|
@@ -11,8 +11,12 @@ import type {PluginApi} from '../types'
|
|
|
11
11
|
// @ts-expect-error — JSON import
|
|
12
12
|
import pkg from '../package.json'
|
|
13
13
|
|
|
14
|
+
export function getInfoPayload(): {version: string} {
|
|
15
|
+
return {version: pkg.version}
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export function registerInfo(api: PluginApi) {
|
|
15
19
|
api.registerGatewayMethod('clawly.info', async ({respond}) => {
|
|
16
|
-
respond(true,
|
|
20
|
+
respond(true, getInfoPayload())
|
|
17
21
|
})
|
|
18
22
|
}
|
package/http/info.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route for plugin package info.
|
|
3
|
+
*
|
|
4
|
+
* Route: GET /clawly/info → { version: string }
|
|
5
|
+
* Auth: plugin (HMAC access token or gateway Bearer token)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {IncomingMessage, ServerResponse} from 'node:http'
|
|
9
|
+
|
|
10
|
+
import {getInfoPayload} from '../gateway/info'
|
|
11
|
+
import {guardHttpAuth, handleCors, sendJson} from '../lib/httpAuth'
|
|
12
|
+
import type {PluginApi} from '../types'
|
|
13
|
+
|
|
14
|
+
export function registerInfoHttpRoute(api: PluginApi) {
|
|
15
|
+
api.registerHttpRoute({
|
|
16
|
+
path: '/clawly/info',
|
|
17
|
+
auth: 'plugin',
|
|
18
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
19
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
|
|
20
|
+
if (handleCors(req, res)) return
|
|
21
|
+
if (!guardHttpAuth(api, req, res, url)) return
|
|
22
|
+
|
|
23
|
+
sendJson(res, 200, getInfoPayload())
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
api.logger.info('http: registered /clawly/info route')
|
|
28
|
+
}
|
package/http/version.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route for OpenClaw server version.
|
|
3
|
+
*
|
|
4
|
+
* Route: GET /clawly/version → { version: string }
|
|
5
|
+
* Auth: plugin (HMAC access token or gateway Bearer token)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {IncomingMessage, ServerResponse} from 'node:http'
|
|
9
|
+
|
|
10
|
+
import {getOpenClawEnvPromise} from '../lib/resolveOpenClawEnv'
|
|
11
|
+
import {guardHttpAuth, handleCors, sendJson} from '../lib/httpAuth'
|
|
12
|
+
import type {PluginApi} from '../types'
|
|
13
|
+
|
|
14
|
+
export function registerVersionHttpRoute(api: PluginApi) {
|
|
15
|
+
api.registerHttpRoute({
|
|
16
|
+
path: '/clawly/version',
|
|
17
|
+
auth: 'plugin',
|
|
18
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
19
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
|
|
20
|
+
if (handleCors(req, res)) return
|
|
21
|
+
if (!guardHttpAuth(api, req, res, url)) return
|
|
22
|
+
|
|
23
|
+
const env = await getOpenClawEnvPromise()
|
|
24
|
+
sendJson(res, 200, {version: env.version})
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
api.logger.info('http: registered /clawly/version route')
|
|
29
|
+
}
|
package/index.ts
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
*
|
|
27
27
|
* HTTP routes:
|
|
28
28
|
* - GET /clawly/file/outbound — serve files (hash lookup first, then direct path with allowlist)
|
|
29
|
+
* - GET /clawly/version — OpenClaw server version
|
|
30
|
+
* - GET /clawly/info — plugin package version
|
|
29
31
|
*
|
|
30
32
|
* Hooks:
|
|
31
33
|
* - before_message_write — restores original /skill command in user messages (undoes gateway rewrite)
|
|
@@ -46,6 +48,7 @@ import {registerAutoUpdate} from './internal/hooks/auto-update'
|
|
|
46
48
|
import {registerCalendar} from './calendar'
|
|
47
49
|
import {registerCommands} from './command'
|
|
48
50
|
import {setupConfig} from './config-setup'
|
|
51
|
+
import {registerCronHistory} from './cron-history'
|
|
49
52
|
import {registerCronHook} from './cron-hook'
|
|
50
53
|
import {registerEmail} from './email'
|
|
51
54
|
import {registerGateway} from './gateway'
|
|
@@ -55,6 +58,8 @@ import {
|
|
|
55
58
|
registerOutboundHttpRoute,
|
|
56
59
|
registerOutboundMethods,
|
|
57
60
|
} from './http/file/outbound'
|
|
61
|
+
import {registerInfoHttpRoute} from './http/info'
|
|
62
|
+
import {registerVersionHttpRoute} from './http/version'
|
|
58
63
|
import {registerMediaUnderstanding} from './media-understanding'
|
|
59
64
|
import {registerSkillCommandRestore} from './skill-command-restore'
|
|
60
65
|
import {registerTools} from './tools'
|
|
@@ -77,9 +82,12 @@ export default {
|
|
|
77
82
|
registerOutboundHook(api)
|
|
78
83
|
registerOutboundMethods(api)
|
|
79
84
|
registerOutboundHttpRoute(api)
|
|
85
|
+
registerVersionHttpRoute(api)
|
|
86
|
+
registerInfoHttpRoute(api)
|
|
80
87
|
registerCommands(api)
|
|
81
88
|
registerTools(api)
|
|
82
89
|
registerCronHook(api)
|
|
90
|
+
registerCronHistory(api)
|
|
83
91
|
setupConfig(api)
|
|
84
92
|
registerGateway(api)
|
|
85
93
|
registerAutoPair(api)
|
package/media-understanding.ts
CHANGED
|
@@ -1,44 +1,97 @@
|
|
|
1
1
|
import {PROVIDER_NAME} from './model-gateway-setup'
|
|
2
|
+
import {resolveGatewayCredentials, type GatewayCredentials} from './resolve-gateway-credentials'
|
|
2
3
|
import type {PluginApi} from './types'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
async function chatCompletion(
|
|
6
|
+
creds: GatewayCredentials,
|
|
7
|
+
content: Array<Record<string, unknown>>,
|
|
8
|
+
opts: {model: string; timeoutMs: number; maxTokens: number},
|
|
9
|
+
): Promise<{text: string; model?: string}> {
|
|
10
|
+
const controller = new AbortController()
|
|
11
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${creds.baseUrl}/chat/completions`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
Authorization: `Bearer ${creds.token}`,
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
model: opts.model,
|
|
22
|
+
messages: [{role: 'user', content}],
|
|
23
|
+
max_tokens: opts.maxTokens,
|
|
24
|
+
}),
|
|
25
|
+
signal: controller.signal,
|
|
22
26
|
})
|
|
27
|
+
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const errText = await res.text().catch(() => '')
|
|
30
|
+
throw new Error(`Model gateway returned ${res.status}: ${errText}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = (await res.json()) as {
|
|
34
|
+
choices?: {message?: {content?: string}}[]
|
|
35
|
+
model?: string
|
|
36
|
+
}
|
|
37
|
+
const text = data.choices?.[0]?.message?.content ?? ''
|
|
38
|
+
return {text, model: data.model ?? opts.model}
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timeout)
|
|
23
41
|
}
|
|
24
|
-
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function imageToContentPart(buffer: Buffer, mime?: string): Record<string, unknown> {
|
|
45
|
+
const base64 = buffer.toString('base64')
|
|
46
|
+
return {type: 'image_url', image_url: {url: `data:${mime || 'image/jpeg'};base64,${base64}`}}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function describeImageViaGateway(
|
|
50
|
+
creds: GatewayCredentials,
|
|
51
|
+
params: Record<string, unknown>,
|
|
52
|
+
): Promise<{text: string; model?: string}> {
|
|
53
|
+
const content = [
|
|
54
|
+
{type: 'text', text: (params.prompt as string) || 'Describe the image.'},
|
|
55
|
+
imageToContentPart(params.buffer as Buffer, params.mime as string | undefined),
|
|
56
|
+
]
|
|
57
|
+
return chatCompletion(creds, content, {
|
|
58
|
+
model: params.model as string,
|
|
59
|
+
timeoutMs: (params.timeoutMs as number) || 30_000,
|
|
60
|
+
maxTokens: (params.maxTokens as number) || 512,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function describeImagesViaGateway(
|
|
65
|
+
creds: GatewayCredentials,
|
|
66
|
+
params: Record<string, unknown>,
|
|
67
|
+
): Promise<{text: string; model?: string}> {
|
|
68
|
+
const images = params.images as Array<{buffer: Buffer; fileName?: string; mime?: string}>
|
|
69
|
+
const content: Array<Record<string, unknown>> = [
|
|
70
|
+
{type: 'text', text: (params.prompt as string) || 'Describe the images.'},
|
|
71
|
+
...images.map((img) => imageToContentPart(img.buffer, img.mime)),
|
|
72
|
+
]
|
|
73
|
+
return chatCompletion(creds, content, {
|
|
74
|
+
model: params.model as string,
|
|
75
|
+
timeoutMs: (params.timeoutMs as number) || 30_000,
|
|
76
|
+
maxTokens: (params.maxTokens as number) || 512,
|
|
77
|
+
})
|
|
25
78
|
}
|
|
26
79
|
|
|
27
80
|
export function registerMediaUnderstanding(api: PluginApi): void {
|
|
28
81
|
if (!api.registerMediaUnderstandingProvider) return
|
|
29
82
|
|
|
83
|
+
const creds = resolveGatewayCredentials(api)
|
|
84
|
+
if (!creds) {
|
|
85
|
+
api.logger.warn('media-understanding: model gateway credentials not available, skipping')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
30
89
|
api.registerMediaUnderstandingProvider({
|
|
31
90
|
id: PROVIDER_NAME,
|
|
32
91
|
capabilities: ['image'],
|
|
33
|
-
describeImage:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
describeImages: async (...args: unknown[]) => {
|
|
39
|
-
const sdk = await loadSdk(api.logger)
|
|
40
|
-
if (!sdk) return undefined
|
|
41
|
-
return sdk.describeImagesWithModel(...args)
|
|
42
|
-
},
|
|
92
|
+
describeImage: (...args: unknown[]) =>
|
|
93
|
+
describeImageViaGateway(creds, args[0] as Record<string, unknown>),
|
|
94
|
+
describeImages: (...args: unknown[]) =>
|
|
95
|
+
describeImagesViaGateway(creds, args[0] as Record<string, unknown>),
|
|
43
96
|
})
|
|
44
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0-beta.1",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"auto-pair.ts",
|
|
31
31
|
"calendar.ts",
|
|
32
32
|
"config-setup.ts",
|
|
33
|
+
"cron-history.ts",
|
|
33
34
|
"cron-hook.ts",
|
|
34
35
|
"email.ts",
|
|
35
36
|
"gateway-fetch.ts",
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# memory-browser
|
|
2
|
-
|
|
3
|
-
OpenClaw plugin that exposes the **memory directory** (all `.md` files) as **Gateway WebSocket RPC methods**.
|
|
4
|
-
|
|
5
|
-
## What you get
|
|
6
|
-
|
|
7
|
-
Gateway methods (call via WS RPC):
|
|
8
|
-
|
|
9
|
-
- **`memory-browser.list`** — List all `.md` files in the memory directory (recursive). Returns `{ files: string[] }` (relative paths).
|
|
10
|
-
- **`memory-browser.get`** — Return the content of a single `.md` file. Params: `{ path: string }` (relative path, e.g. `"notes/foo.md"`). Returns `{ path: string, content: string }`.
|
|
11
|
-
|
|
12
|
-
## Memory directory
|
|
13
|
-
|
|
14
|
-
The memory directory is resolved in this order:
|
|
15
|
-
|
|
16
|
-
1. Plugin config `memoryDir` (absolute path)
|
|
17
|
-
2. `<OPENCLAW_STATE_DIR>/memory` (or `<CLAWDBOT_STATE_DIR>/memory`)
|
|
18
|
-
3. `<process.cwd()>/memory`
|
|
19
|
-
|
|
20
|
-
## Install on a gateway host
|
|
21
|
-
|
|
22
|
-
1. Put this plugin on the gateway host and make it discoverable, e.g.:
|
|
23
|
-
- Workspace: `<workspace>/.openclaw/extensions/memory-browser/` with `index.ts` + `openclaw.plugin.json`
|
|
24
|
-
- Or set `plugins.load.paths` to the plugin folder
|
|
25
|
-
|
|
26
|
-
2. Enable the plugin in `openclaw.json`:
|
|
27
|
-
|
|
28
|
-
```json5
|
|
29
|
-
{
|
|
30
|
-
"plugins": {
|
|
31
|
-
"enabled": true,
|
|
32
|
-
"entries": {
|
|
33
|
-
"memory-browser": { "enabled": true, "config": {} }
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Optional config: `config.memoryDir` to override the memory directory path.
|
|
40
|
-
|
|
41
|
-
## Call examples (RPC params)
|
|
42
|
-
|
|
43
|
-
List all `.md` files:
|
|
44
|
-
|
|
45
|
-
```json
|
|
46
|
-
{ "method": "memory-browser.list", "params": {} }
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Get one file:
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{ "method": "memory-browser.get", "params": { "path": "notes/meeting-2024.md" } }
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`path` must be a relative path to a `.md` file inside the memory directory; directory traversal (`..`) is rejected.
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# 插件版本管理
|
|
2
|
-
|
|
3
|
-
## 产品需求
|
|
4
|
-
|
|
5
|
-
### 背景
|
|
6
|
-
|
|
7
|
-
OpenClaw 插件以 npm 包形式安装在 gateway 的 `extensions/` 目录下。用户需要一种方式来查询已安装插件的版本、检测是否有更新、并执行更新操作——无需手动 SSH 到服务器执行 CLI 命令。
|
|
8
|
-
|
|
9
|
-
### 目标
|
|
10
|
-
|
|
11
|
-
- 通过 Gateway RPC 远程查询任意插件的已安装版本和 npm 最新版本
|
|
12
|
-
- 支持多种更新策略(常规更新、全新安装、强制重装)
|
|
13
|
-
- 更新后可选自动重启 gateway,使新版本立即生效
|
|
14
|
-
|
|
15
|
-
### 功能需求
|
|
16
|
-
|
|
17
|
-
1. **版本查询** — 给定 `pluginId` 和 `npmPkgName`,返回已安装版本、npm 最新版本、所有可用版本列表,以及是否有更新。
|
|
18
|
-
2. **插件更新** — 给定 `pluginId`、`npmPkgName` 和更新策略,执行安装或更新操作。支持指定目标版本和更新后自动重启。
|
|
19
|
-
3. **强制重装** — `force` 策略会备份插件配置、删除扩展目录、全新安装、再恢复配置,解决损坏安装或缓存问题。
|
|
20
|
-
|
|
21
|
-
### 约束
|
|
22
|
-
|
|
23
|
-
- 仅通过 Gateway WebSocket RPC 调用,不暴露 HTTP 端点。
|
|
24
|
-
- 版本比较仅支持标准 semver,预发布版本(如 `1.0.0-beta.1`)不会被标记为可用更新。
|
|
25
|
-
- npm registry 查询结果缓存 5 分钟(LRU,最多 5 个包),避免频繁请求。
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## 技术设计
|
|
30
|
-
|
|
31
|
-
### 概览
|
|
32
|
-
|
|
33
|
-
插件版本管理通过 `gateway/plugins.ts` 注册两个 Gateway RPC 方法,分别负责版本查询和更新执行。
|
|
34
|
-
|
|
35
|
-
### 数据流
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
Mobile / Agent
|
|
39
|
-
│
|
|
40
|
-
├─ clawly.plugins.version({ pluginId, npmPkgName })
|
|
41
|
-
│ ├─ 读取 extensions/<pluginId>/package.json → 已安装版本
|
|
42
|
-
│ ├─ npm view <npmPkgName> --json → 最新版本 + 所有版本
|
|
43
|
-
│ └─ isUpdateAvailable(current, latest) → 是否有更新
|
|
44
|
-
│
|
|
45
|
-
└─ clawly.plugins.update({ pluginId, npmPkgName, strategy, targetVersion?, restart? })
|
|
46
|
-
├─ strategy=install → openclaw plugins install <pkg>
|
|
47
|
-
├─ strategy=update → openclaw plugins update <pluginId>
|
|
48
|
-
└─ strategy=force → 备份配置 → 删除目录 → 安装 → 恢复配置
|
|
49
|
-
└─ restart=true → openclaw gateway restart
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Gateway RPC 方法
|
|
53
|
-
|
|
54
|
-
| 方法 | 参数 | 返回 |
|
|
55
|
-
|---|---|---|
|
|
56
|
-
| `clawly.plugins.version` | `{ pluginId, npmPkgName }` | `VersionResult` |
|
|
57
|
-
| `clawly.plugins.update` | `{ pluginId, npmPkgName, strategy, targetVersion?, restart? }` | `UpdateResult` |
|
|
58
|
-
|
|
59
|
-
### 关键类型
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
// clawly.plugins.version 返回
|
|
63
|
-
interface VersionResult {
|
|
64
|
-
pluginVersion: string | null // 已安装版本(来自 package.json)
|
|
65
|
-
npmPackageVersion: string | null // 同上(兼容字段)
|
|
66
|
-
latestNpmVersion: string | null // npm registry 最新稳定版
|
|
67
|
-
allNpmVersions: string[] // npm registry 所有已发布版本
|
|
68
|
-
updateAvailable: boolean // 是否有可用更新
|
|
69
|
-
error?: string // 查询错误(如 npm 不可达)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// clawly.plugins.update 返回
|
|
73
|
-
interface UpdateResult {
|
|
74
|
-
ok: boolean // 操作是否成功
|
|
75
|
-
strategy: string // 实际使用的策略
|
|
76
|
-
output?: string // CLI 输出
|
|
77
|
-
restarted?: boolean // 是否已重启 gateway
|
|
78
|
-
error?: string // 失败时的错误信息
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 更新策略
|
|
83
|
-
|
|
84
|
-
| 策略 | 行为 | 适用场景 |
|
|
85
|
-
|---|---|---|
|
|
86
|
-
| `install` | `openclaw plugins install <pkg>[@version]` | 首次安装或指定版本安装 |
|
|
87
|
-
| `update` | `openclaw plugins update <pluginId>` | 常规更新到最新版 |
|
|
88
|
-
| `force` | 备份配置 → 删除扩展目录 → 全新安装 → 恢复配置 | 安装损坏、缓存问题、降级 |
|
|
89
|
-
|
|
90
|
-
### `force` 策略详细流程
|
|
91
|
-
|
|
92
|
-
1. 读取 `openclaw.json` 中 `plugins.entries.<pluginId>` 的用户配置并保存
|
|
93
|
-
2. 从 `openclaw.json` 删除该插件条目(确保干净安装)
|
|
94
|
-
3. 删除 `extensions/<pluginId>/` 目录
|
|
95
|
-
4. 执行 `openclaw plugins install <pkg>[@version]`
|
|
96
|
-
5. 将保存的用户配置合并回 `openclaw.json`
|
|
97
|
-
|
|
98
|
-
### 版本检测逻辑
|
|
99
|
-
|
|
100
|
-
已安装版本从 `<stateDir>/extensions/<pluginId>/package.json` 读取。npm 最新版本通过 `npm view <pkg> --json` 获取。版本比较使用内置 `isUpdateAvailable(current, latest)`:
|
|
101
|
-
|
|
102
|
-
- 仅比较标准 semver(`major.minor.patch`)
|
|
103
|
-
- `latest` 为预发布版本时返回 `false`(不推荐更新到预发布版)
|
|
104
|
-
- `latest > current` 时返回 `true`
|
|
105
|
-
|
|
106
|
-
### 缓存
|
|
107
|
-
|
|
108
|
-
npm registry 查询结果通过 `LruCache` 缓存:
|
|
109
|
-
|
|
110
|
-
- **容量**: 5 个包
|
|
111
|
-
- **TTL**: 5 分钟
|
|
112
|
-
- 更新操作成功后自动失效对应包的缓存
|
|
113
|
-
|
|
114
|
-
### 状态目录解析
|
|
115
|
-
|
|
116
|
-
`stateDir` 按以下优先级解析:
|
|
117
|
-
|
|
118
|
-
1. `api.runtime.state.resolveStateDir(process.env)`(插件 API 提供)
|
|
119
|
-
2. `OPENCLAW_STATE_DIR` 环境变量
|
|
120
|
-
3. 空字符串(回退,版本查询将返回 `null`)
|
|
121
|
-
|
|
122
|
-
### 调用示例
|
|
123
|
-
|
|
124
|
-
查询版本:
|
|
125
|
-
|
|
126
|
-
```json
|
|
127
|
-
{
|
|
128
|
-
"method": "clawly.plugins.version",
|
|
129
|
-
"params": {
|
|
130
|
-
"pluginId": "clawly-plugins",
|
|
131
|
-
"npmPkgName": "@AISomething/clawly-plugins"
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
常规更新:
|
|
137
|
-
|
|
138
|
-
```json
|
|
139
|
-
{
|
|
140
|
-
"method": "clawly.plugins.update",
|
|
141
|
-
"params": {
|
|
142
|
-
"pluginId": "clawly-plugins",
|
|
143
|
-
"npmPkgName": "@AISomething/clawly-plugins",
|
|
144
|
-
"strategy": "update"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
强制重装并重启 gateway:
|
|
150
|
-
|
|
151
|
-
```json
|
|
152
|
-
{
|
|
153
|
-
"method": "clawly.plugins.update",
|
|
154
|
-
"params": {
|
|
155
|
-
"pluginId": "clawly-plugins",
|
|
156
|
-
"npmPkgName": "@AISomething/clawly-plugins",
|
|
157
|
-
"strategy": "force",
|
|
158
|
-
"restart": true
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
安装指定版本:
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{
|
|
167
|
-
"method": "clawly.plugins.update",
|
|
168
|
-
"params": {
|
|
169
|
-
"pluginId": "clawly-plugins",
|
|
170
|
-
"npmPkgName": "@AISomething/clawly-plugins",
|
|
171
|
-
"strategy": "install",
|
|
172
|
-
"targetVersion": "1.2.3"
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 错误处理
|
|
178
|
-
|
|
179
|
-
- 参数缺失返回 `{ code: "invalid_params" }` 错误。
|
|
180
|
-
- 无效 strategy 返回 `{ code: "invalid_params" }` 错误。
|
|
181
|
-
- npm 查询失败时,版本查询仍返回成功,`error` 字段包含错误信息。
|
|
182
|
-
- 更新操作失败时返回 `{ ok: false, error: "..." }`。
|
|
183
|
-
- `force` 策略在恢复配置失败时仅打印警告日志,不影响整体操作结果。
|
|
184
|
-
|
|
185
|
-
### 不在范围内
|
|
186
|
-
|
|
187
|
-
- 批量更新多个插件。
|
|
188
|
-
- 插件安装/卸载(使用 `clawhub2gateway` 或 CLI)。
|
|
189
|
-
- 自动定时检查更新。
|
|
190
|
-
- 版本回滚历史记录。
|