openclaw-openagent 1.0.12 → 1.0.13

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/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { registerTools, registerHooks } from './src/app/index.js';
14
14
  import { installOpenagentVerbosePreflight } from './src/app/verbose-preflight.js';
15
15
  // guard.ts removed — unhandled rejections fixed at source in ws-frame.ts send()
16
16
  import { setOpenagentPluginRuntime } from './src/runtime/plugin-runtime.js';
17
+ import { startOpenagentUpdateChecker } from './src/runtime/update-checker.js';
17
18
  import { injectControlUiOverride } from './src/plugin-ui/index.js';
18
19
  import { createAuthProxyHandler } from './src/proxy/auth-proxy.js';
19
20
  import { logger } from './src/util/logger.js';
@@ -104,6 +105,7 @@ export default {
104
105
  injectControlUiOverride(api.runtime?.version).catch(err => {
105
106
  logger.warn(`[entry] Control UI injection failed: ${err.message}`);
106
107
  });
108
+ startOpenagentUpdateChecker();
107
109
  logger.info('[entry] Tools + hooks registered (full mode)');
108
110
  _fullModeRegistered = true;
109
111
  },
@@ -31,6 +31,15 @@ export declare const OpenagentConfigSchema: z.ZodObject<{
31
31
  }>>>;
32
32
  api_base: z.ZodOptional<z.ZodString>;
33
33
  access_api_base: z.ZodOptional<z.ZodString>;
34
+ update_policy: z.ZodOptional<z.ZodEnum<{
35
+ auto: "auto";
36
+ off: "off";
37
+ notify: "notify";
38
+ }>>;
39
+ update_check: z.ZodOptional<z.ZodBoolean>;
40
+ auto_update: z.ZodOptional<z.ZodBoolean>;
41
+ update_channel: z.ZodOptional<z.ZodString>;
42
+ update_dist_tag: z.ZodOptional<z.ZodString>;
34
43
  accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
35
44
  name: z.ZodOptional<z.ZodString>;
36
45
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -13,6 +13,7 @@ import { z } from 'zod';
13
13
  // Zod config schema
14
14
  // ---------------------------------------------------------------------------
15
15
  const transportSchema = z.enum(['oasn', 'tim']).default('oasn');
16
+ const updatePolicySchema = z.enum(['notify', 'auto', 'off']).optional();
16
17
  const openagentAccountSchema = z.object({
17
18
  name: z.string().optional(),
18
19
  enabled: z.boolean().optional(),
@@ -30,5 +31,10 @@ export const OpenagentConfigSchema = openagentAccountSchema.extend({
30
31
  transport: transportSchema.optional(),
31
32
  api_base: z.string().optional(),
32
33
  access_api_base: z.string().optional(),
34
+ update_policy: updatePolicySchema,
35
+ update_check: z.boolean().optional(),
36
+ auto_update: z.boolean().optional(),
37
+ update_channel: z.string().optional(),
38
+ update_dist_tag: z.string().optional(),
33
39
  accounts: z.record(z.string(), openagentAccountSchema).optional(),
34
40
  });
@@ -2,7 +2,7 @@
2
2
  * OpenAgent UI Override — Auto-generated, do not edit directly!
3
3
  *
4
4
  * Source: src/plugin-ui/modules/
5
- * Built: 2026-07-06T03:23:32.989Z
5
+ * Built: 2026-07-06T09:05:33.996Z
6
6
  *
7
7
  * To modify, edit the source modules and run:
8
8
  * npm run build:override
@@ -52,7 +52,7 @@ const CL = {
52
52
  hostVersion: '__OPENCLAW_HOST_VERSION__',
53
53
 
54
54
  // ── Override 构建标识(构建时注入,用于缓存自检)──
55
- buildId: "65bf8968e270",
55
+ buildId: "4207d7dc1f64",
56
56
 
57
57
  // ── 配置 ──
58
58
  OPENAGENT_API: 'https://api.openagent.club',
@@ -2118,6 +2118,13 @@ function injectStyles() {
2118
2118
  padding: 14px;
2119
2119
  font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
2120
2120
  box-sizing: border-box;
2121
+ scrollbar-width: none;
2122
+ -ms-overflow-style: none;
2123
+ }
2124
+ .openagent-agent-panel::-webkit-scrollbar {
2125
+ width: 0;
2126
+ height: 0;
2127
+ display: none;
2121
2128
  }
2122
2129
  .openagent-agent-panel.visible {
2123
2130
  display: flex;
@@ -2357,21 +2364,13 @@ function injectStyles() {
2357
2364
  flex-direction: column;
2358
2365
  min-height: 0;
2359
2366
  max-height: min(56vh, 451px);
2360
- scrollbar-width: thin;
2361
- scrollbar-color: #ddd transparent;
2367
+ scrollbar-width: none;
2368
+ -ms-overflow-style: none;
2362
2369
  }
2363
2370
  .openagent-agent-list::-webkit-scrollbar {
2364
- width: 4px;
2365
- }
2366
- .openagent-agent-list::-webkit-scrollbar-track {
2367
- background: transparent;
2368
- }
2369
- .openagent-agent-list::-webkit-scrollbar-thumb {
2370
- background: #ddd;
2371
- border-radius: 2px;
2372
- }
2373
- .openagent-agent-list::-webkit-scrollbar-thumb:hover {
2374
- background: #bbb;
2371
+ width: 0;
2372
+ height: 0;
2373
+ display: none;
2375
2374
  }
2376
2375
 
2377
2376
  .openagent-agent-section {
@@ -4923,7 +4922,13 @@ margin: 0 8px; position: relative; top: -1px;
4923
4922
  .trip-img-card {
4924
4923
  display: flex; flex-direction: row; gap: 6px;
4925
4924
  overflow-x: auto; padding: 8px 0; margin-left: 20px;
4926
- scrollbar-width: thin;
4925
+ scrollbar-width: none;
4926
+ -ms-overflow-style: none;
4927
+ }
4928
+ .trip-img-card::-webkit-scrollbar {
4929
+ width: 0;
4930
+ height: 0;
4931
+ display: none;
4927
4932
  }
4928
4933
  .trip-img-item {
4929
4934
  position: relative; width: 191px; height: 100px; border-radius: 8px;
@@ -6983,6 +6988,14 @@ function _clScan() {
6983
6988
  ' overflow-x: auto;',
6984
6989
  ' white-space: pre-wrap;',
6985
6990
  ' word-break: break-all;',
6991
+ ' scrollbar-width: none;',
6992
+ ' -ms-overflow-style: none;',
6993
+ '}',
6994
+ '.cl-output-card > .cl-thought-chain-content pre::-webkit-scrollbar,',
6995
+ '.cl-output-card > .cl-thought-chain-content code::-webkit-scrollbar {',
6996
+ ' width: 0;',
6997
+ ' height: 0;',
6998
+ ' display: none;',
6986
6999
  '}',
6987
7000
  // V2 output card:统一 border/padding 与 execution card 一致
6988
7001
  '.cl-exec-v2.cl-output-card {',
@@ -7225,8 +7238,7 @@ function _clScan() {
7225
7238
  '.cl-exec-v2 .cl-exec-agent-avatar-v2 {',
7226
7239
  ' width: 68px !important;',
7227
7240
  ' height: 68px !important;',
7228
- ' border-radius: 50% !important;',
7229
- ' object-fit: cover;',
7241
+ ' object-fit: contain;',
7230
7242
  ' flex-shrink: 0;',
7231
7243
  '}',
7232
7244
 
@@ -11142,9 +11154,104 @@ function _startClObserver() {
11142
11154
  _clObserverTarget = document.querySelector('openclaw-app') || document.body;
11143
11155
  _clObserver.observe(_clObserverTarget, { childList: true, subtree: true });
11144
11156
  setTimeout(_debouncedClScan, 2000);
11157
+ setTimeout(_clCheckPluginUpdateStatus, 5000);
11145
11158
  /* muted */ void 0;
11146
11159
  }
11147
11160
 
11161
+ function _clCheckPluginUpdateStatus() {
11162
+ fetch('/plugins/openagent/update-status', { cache: 'no-store' })
11163
+ .then(function(res) {
11164
+ if (!res.ok) return null;
11165
+ return res.json();
11166
+ })
11167
+ .then(function(status) {
11168
+ if (!status || !status.updateAvailable) return;
11169
+ _clShowPluginUpdateNotice(status);
11170
+ })
11171
+ .catch(function() {});
11172
+ }
11173
+
11174
+ function _clShowPluginUpdateNotice(status) {
11175
+ var latestVersion = String(status.latestVersion || '').trim();
11176
+ var currentVersion = String(status.currentVersion || '').trim();
11177
+ var dismissKey = 'openagent-update-dismissed-' + latestVersion;
11178
+ try {
11179
+ if (latestVersion && localStorage.getItem(dismissKey) === '1') return;
11180
+ } catch (e) {}
11181
+
11182
+ var existing = document.getElementById('openagent-update-notice');
11183
+ if (existing) existing.remove();
11184
+
11185
+ var notice = document.createElement('div');
11186
+ notice.id = 'openagent-update-notice';
11187
+ notice.style.cssText = [
11188
+ 'position:fixed',
11189
+ 'right:18px',
11190
+ 'bottom:18px',
11191
+ 'z-index:2147483647',
11192
+ 'max-width:360px',
11193
+ 'padding:14px 16px',
11194
+ 'border:1px solid rgba(0,0,0,0.08)',
11195
+ 'border-radius:12px',
11196
+ 'background:#fff',
11197
+ 'box-shadow:0 12px 32px rgba(15,23,42,0.16)',
11198
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
11199
+ 'color:#1f2937',
11200
+ 'font-size:13px',
11201
+ 'line-height:1.5'
11202
+ ].join(';');
11203
+
11204
+ var title = document.createElement('div');
11205
+ title.textContent = 'OpenAgent 有新版本 ' + latestVersion;
11206
+ title.style.cssText = 'font-weight:600;font-size:14px;margin-right:22px;margin-bottom:4px;';
11207
+ notice.appendChild(title);
11208
+
11209
+ var body = document.createElement('div');
11210
+ var command = String(status.installCommand || 'npx -y openclaw-openagent-cli update');
11211
+ body.textContent = currentVersion ? ('当前版本 ' + currentVersion + ',运行命令完成更新。') : '运行命令完成更新。';
11212
+ body.style.cssText = 'color:#4b5563;margin-bottom:10px;';
11213
+ notice.appendChild(body);
11214
+
11215
+ var code = document.createElement('code');
11216
+ code.textContent = command;
11217
+ code.style.cssText = 'display:block;white-space:normal;word-break:break-all;background:#f6f7f9;border-radius:8px;padding:8px 10px;color:#111827;';
11218
+ notice.appendChild(code);
11219
+
11220
+ var actions = document.createElement('div');
11221
+ actions.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;margin-top:10px;';
11222
+ var copy = document.createElement('button');
11223
+ copy.type = 'button';
11224
+ copy.textContent = '复制命令';
11225
+ copy.style.cssText = 'border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;color:#374151;';
11226
+ copy.onclick = function() {
11227
+ try {
11228
+ navigator.clipboard.writeText(command);
11229
+ copy.textContent = '已复制';
11230
+ } catch (e) {}
11231
+ };
11232
+ actions.appendChild(copy);
11233
+
11234
+ var close = document.createElement('button');
11235
+ close.type = 'button';
11236
+ close.textContent = '关闭';
11237
+ close.style.cssText = 'border:0;background:#111827;color:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;';
11238
+ close.onclick = function() {
11239
+ try {
11240
+ if (latestVersion) localStorage.setItem(dismissKey, '1');
11241
+ } catch (e) {}
11242
+ notice.remove();
11243
+ };
11244
+ actions.appendChild(close);
11245
+ notice.appendChild(actions);
11246
+
11247
+ if (status.policy === 'auto' && status.autoUpdateStarted) {
11248
+ body.textContent = '正在自动更新,OpenClaw Gateway 可能会自动重启。';
11249
+ actions.remove();
11250
+ }
11251
+
11252
+ document.body.appendChild(notice);
11253
+ }
11254
+
11148
11255
  if (document.readyState === 'loading') {
11149
11256
  document.addEventListener('DOMContentLoaded', _startClObserver);
11150
11257
  } else {
@@ -17,6 +17,7 @@ import fs from 'node:fs';
17
17
  import nodePath from 'node:path';
18
18
  import { logger } from '../util/logger.js';
19
19
  import { registry } from '../runtime/registry.js';
20
+ import { getOpenagentUpdateStatus } from '../runtime/update-checker.js';
20
21
  import { isUnconfiguredOasnApiBase } from '../auth/config.js';
21
22
  import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
22
23
  /** TIM 路径默认上游(兼容旧版) */
@@ -90,6 +91,10 @@ export function createAuthProxyHandler() {
90
91
  if (targetPath.startsWith('/media/')) {
91
92
  return serveLocalMedia(targetPath, res);
92
93
  }
94
+ if (targetPath === '/update-status') {
95
+ writeJson(res, 200, getOpenagentUpdateStatus());
96
+ return true;
97
+ }
93
98
  // v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
94
99
  const method = req.method ?? 'GET';
95
100
  const rt = registry.getDefault();
@@ -0,0 +1,18 @@
1
+ export type OpenagentUpdatePolicy = 'notify' | 'auto' | 'off';
2
+ export type OpenagentUpdateStatus = {
3
+ checked: boolean;
4
+ checking: boolean;
5
+ currentVersion: string;
6
+ latestVersion: string;
7
+ distTag: string;
8
+ updateAvailable: boolean;
9
+ policy: OpenagentUpdatePolicy;
10
+ installCommand: string;
11
+ autoUpdateStarted: boolean;
12
+ lastCheckedAt?: string;
13
+ error?: string;
14
+ };
15
+ export declare function getOpenagentUpdateStatus(): OpenagentUpdateStatus;
16
+ export declare function startOpenagentUpdateChecker(): void;
17
+ export declare function runOpenagentUpdateCheck(): Promise<OpenagentUpdateStatus>;
18
+ export declare function compareVersions(a: string, b: string): number;
@@ -0,0 +1,253 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { logger } from '../util/logger.js';
7
+ const PLUGIN_PACKAGE = 'openclaw-openagent';
8
+ const CLI_PACKAGE = 'openclaw-openagent-cli';
9
+ const DEFAULT_DIST_TAG = 'latest';
10
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
11
+ const CHECK_DELAY_MS = 3_000;
12
+ let checkerStarted = false;
13
+ const updateStatus = {
14
+ checked: false,
15
+ checking: false,
16
+ currentVersion: '',
17
+ latestVersion: '',
18
+ distTag: DEFAULT_DIST_TAG,
19
+ updateAvailable: false,
20
+ policy: 'notify',
21
+ installCommand: `npx -y ${CLI_PACKAGE}`,
22
+ autoUpdateStarted: false,
23
+ };
24
+ export function getOpenagentUpdateStatus() {
25
+ return { ...updateStatus };
26
+ }
27
+ export function startOpenagentUpdateChecker() {
28
+ if (checkerStarted)
29
+ return;
30
+ checkerStarted = true;
31
+ setTimeout(() => {
32
+ void runOpenagentUpdateCheck().catch((err) => {
33
+ updateStatus.checked = true;
34
+ updateStatus.checking = false;
35
+ updateStatus.error = err.message;
36
+ updateStatus.lastCheckedAt = new Date().toISOString();
37
+ logger.warn(`[update-check] failed: ${err.message}`);
38
+ });
39
+ }, CHECK_DELAY_MS).unref?.();
40
+ }
41
+ export async function runOpenagentUpdateCheck() {
42
+ const config = resolveUpdateConfig();
43
+ updateStatus.policy = config.policy;
44
+ updateStatus.distTag = config.distTag;
45
+ updateStatus.installCommand = buildInstallCommand(config);
46
+ updateStatus.currentVersion = resolveCurrentVersion();
47
+ if (config.policy === 'off') {
48
+ updateStatus.checked = true;
49
+ updateStatus.checking = false;
50
+ updateStatus.updateAvailable = false;
51
+ updateStatus.lastCheckedAt = new Date().toISOString();
52
+ logger.info('[update-check] disabled by policy');
53
+ return getOpenagentUpdateStatus();
54
+ }
55
+ updateStatus.checking = true;
56
+ updateStatus.error = undefined;
57
+ try {
58
+ const latestVersion = await fetchDistTagVersion(config.distTag);
59
+ updateStatus.latestVersion = latestVersion;
60
+ updateStatus.updateAvailable = compareVersions(latestVersion, updateStatus.currentVersion) > 0;
61
+ updateStatus.checked = true;
62
+ updateStatus.checking = false;
63
+ updateStatus.lastCheckedAt = new Date().toISOString();
64
+ if (!updateStatus.updateAvailable) {
65
+ logger.info(`[update-check] up to date: ${PLUGIN_PACKAGE}@${updateStatus.currentVersion}`);
66
+ return getOpenagentUpdateStatus();
67
+ }
68
+ logger.warn(`[update-check] new ${PLUGIN_PACKAGE} version available: `
69
+ + `${updateStatus.currentVersion} -> ${latestVersion}. Run: ${updateStatus.installCommand}`);
70
+ if (config.policy === 'auto') {
71
+ startAutoUpdate(config);
72
+ }
73
+ return getOpenagentUpdateStatus();
74
+ }
75
+ catch (err) {
76
+ updateStatus.checked = true;
77
+ updateStatus.checking = false;
78
+ updateStatus.error = err.message;
79
+ updateStatus.lastCheckedAt = new Date().toISOString();
80
+ throw err;
81
+ }
82
+ }
83
+ function resolveUpdateConfig() {
84
+ const fileConfig = readOpenagentConfig();
85
+ const envPolicy = normalizePolicy(process.env.OPENAGENT_UPDATE_POLICY);
86
+ const autoUpdateEnv = readBool(process.env.OPENAGENT_AUTO_UPDATE ?? process.env.OPENAGENT_PLUGIN_AUTO_UPDATE);
87
+ const checkEnabledEnv = readBool(process.env.OPENAGENT_UPDATE_CHECK);
88
+ const configPolicy = normalizePolicy(readString(fileConfig, 'update_policy', 'updatePolicy'));
89
+ const configAutoUpdate = readBool(readString(fileConfig, 'auto_update', 'autoUpdate'));
90
+ const configUpdateCheck = readBool(readString(fileConfig, 'update_check', 'updateCheck'));
91
+ let policy = configPolicy || 'notify';
92
+ if (configAutoUpdate === true)
93
+ policy = 'auto';
94
+ if (configUpdateCheck === false)
95
+ policy = 'off';
96
+ if (envPolicy)
97
+ policy = envPolicy;
98
+ if (autoUpdateEnv === true)
99
+ policy = 'auto';
100
+ if (checkEnabledEnv === false)
101
+ policy = 'off';
102
+ const distTag = normalizeDistTag(process.env.OPENAGENT_UPDATE_DIST_TAG
103
+ || readString(fileConfig, 'update_dist_tag', 'updateDistTag')
104
+ || process.env.OPENAGENT_PLUGIN_DIST_TAG
105
+ || DEFAULT_DIST_TAG);
106
+ const channel = normalizeDistTag(process.env.OPENAGENT_UPDATE_CHANNEL
107
+ || readString(fileConfig, 'update_channel', 'updateChannel')
108
+ || process.env.OPENAGENT_PLUGIN_CHANNEL
109
+ || '');
110
+ return {
111
+ policy,
112
+ distTag: channel || distTag,
113
+ channel,
114
+ };
115
+ }
116
+ function startAutoUpdate(config) {
117
+ if (updateStatus.autoUpdateStarted)
118
+ return;
119
+ updateStatus.autoUpdateStarted = true;
120
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
121
+ const args = ['-y', CLI_PACKAGE, 'update'];
122
+ if (config.channel) {
123
+ args.push('--channel', config.channel);
124
+ }
125
+ else if (config.distTag && config.distTag !== DEFAULT_DIST_TAG) {
126
+ args.push('--dist-tag', config.distTag);
127
+ }
128
+ logger.warn(`[update-check] auto update started: ${npxBin} ${args.join(' ')}`);
129
+ const child = spawn(npxBin, args, {
130
+ detached: true,
131
+ stdio: 'ignore',
132
+ env: process.env,
133
+ });
134
+ child.unref();
135
+ }
136
+ function buildInstallCommand(config) {
137
+ const parts = ['npx', '-y', CLI_PACKAGE, 'update'];
138
+ if (config.channel) {
139
+ parts.push('--channel', config.channel);
140
+ }
141
+ else if (config.distTag && config.distTag !== DEFAULT_DIST_TAG) {
142
+ parts.push('--dist-tag', config.distTag);
143
+ }
144
+ return parts.join(' ');
145
+ }
146
+ async function fetchDistTagVersion(distTag) {
147
+ if (process.env.OPENAGENT_UPDATE_TEST_VERSION) {
148
+ return process.env.OPENAGENT_UPDATE_TEST_VERSION;
149
+ }
150
+ const registry = (process.env.OPENAGENT_UPDATE_REGISTRY || DEFAULT_REGISTRY).replace(/\/+$/, '');
151
+ const url = `${registry}/${encodeURIComponent(PLUGIN_PACKAGE)}/${encodeURIComponent(distTag)}`;
152
+ const res = await fetch(url, {
153
+ headers: { Accept: 'application/json' },
154
+ signal: AbortSignal.timeout(5_000),
155
+ });
156
+ if (!res.ok) {
157
+ throw new Error(`npm registry ${res.status} for ${PLUGIN_PACKAGE}@${distTag}`);
158
+ }
159
+ const data = await res.json();
160
+ const version = typeof data.version === 'string' ? data.version : '';
161
+ if (!version)
162
+ throw new Error(`npm registry response missing version for ${PLUGIN_PACKAGE}@${distTag}`);
163
+ return version;
164
+ }
165
+ function resolveCurrentVersion() {
166
+ const packageJson = findPackageJson();
167
+ if (!packageJson)
168
+ return '0.0.0';
169
+ try {
170
+ const data = JSON.parse(fs.readFileSync(packageJson, 'utf8'));
171
+ return typeof data.version === 'string' ? data.version : '0.0.0';
172
+ }
173
+ catch {
174
+ return '0.0.0';
175
+ }
176
+ }
177
+ function findPackageJson() {
178
+ let dir = path.dirname(fileURLToPath(import.meta.url));
179
+ for (let i = 0; i < 8; i += 1) {
180
+ const candidate = path.join(dir, 'package.json');
181
+ try {
182
+ const data = JSON.parse(fs.readFileSync(candidate, 'utf8'));
183
+ if (data.name === PLUGIN_PACKAGE)
184
+ return candidate;
185
+ }
186
+ catch { }
187
+ const parent = path.dirname(dir);
188
+ if (parent === dir)
189
+ break;
190
+ dir = parent;
191
+ }
192
+ return null;
193
+ }
194
+ function readOpenagentConfig() {
195
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
196
+ const configPath = path.join(stateDir, 'openclaw.json');
197
+ try {
198
+ const root = JSON.parse(fs.readFileSync(configPath, 'utf8'));
199
+ return root.channels?.openagent || {};
200
+ }
201
+ catch {
202
+ return {};
203
+ }
204
+ }
205
+ function readString(obj, ...keys) {
206
+ for (const key of keys) {
207
+ const value = obj[key];
208
+ if (typeof value === 'string' && value.trim())
209
+ return value.trim();
210
+ if (typeof value === 'boolean')
211
+ return value ? 'true' : 'false';
212
+ }
213
+ return '';
214
+ }
215
+ function readBool(value) {
216
+ if (typeof value === 'boolean')
217
+ return value;
218
+ const text = typeof value === 'string' ? value.trim().toLowerCase() : '';
219
+ if (!text)
220
+ return null;
221
+ if (['1', 'true', 'yes', 'on', 'auto'].includes(text))
222
+ return true;
223
+ if (['0', 'false', 'no', 'off', 'notify'].includes(text))
224
+ return false;
225
+ return null;
226
+ }
227
+ function normalizePolicy(value) {
228
+ const text = typeof value === 'string' ? value.trim().toLowerCase() : '';
229
+ if (text === 'auto' || text === 'notify' || text === 'off')
230
+ return text;
231
+ return '';
232
+ }
233
+ function normalizeDistTag(value) {
234
+ const text = typeof value === 'string' ? value.trim() : '';
235
+ if (!text)
236
+ return '';
237
+ return /^[A-Za-z0-9._-]+$/.test(text) ? text : '';
238
+ }
239
+ export function compareVersions(a, b) {
240
+ const pa = parseVersion(a);
241
+ const pb = parseVersion(b);
242
+ for (let i = 0; i < 3; i += 1) {
243
+ if (pa[i] !== pb[i])
244
+ return pa[i] - pb[i];
245
+ }
246
+ return 0;
247
+ }
248
+ function parseVersion(version) {
249
+ const match = String(version || '').match(/(\d+)\.(\d+)\.(\d+)/);
250
+ if (!match)
251
+ return [0, 0, 0];
252
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
253
+ }
package/index.ts CHANGED
@@ -17,6 +17,7 @@ import { registerTools, registerHooks } from './src/app/index.js';
17
17
  import { installOpenagentVerbosePreflight } from './src/app/verbose-preflight.js';
18
18
  // guard.ts removed — unhandled rejections fixed at source in ws-frame.ts send()
19
19
  import { setOpenagentPluginRuntime } from './src/runtime/plugin-runtime.js';
20
+ import { startOpenagentUpdateChecker } from './src/runtime/update-checker.js';
20
21
  import { injectControlUiOverride } from './src/plugin-ui/index.js';
21
22
  import { createAuthProxyHandler } from './src/proxy/auth-proxy.js';
22
23
  import { logger } from './src/util/logger.js';
@@ -122,6 +123,8 @@ export default {
122
123
  logger.warn(`[entry] Control UI injection failed: ${(err as Error).message}`);
123
124
  });
124
125
 
126
+ startOpenagentUpdateChecker();
127
+
125
128
  logger.info('[entry] Tools + hooks registered (full mode)');
126
129
  _fullModeRegistered = true;
127
130
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-openagent",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "type": "module",
5
5
  "description": "OpenAgent channel plugin for OpenClaw",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@ import { z } from 'zod';
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
18
  const transportSchema = z.enum(['oasn', 'tim']).default('oasn');
19
+ const updatePolicySchema = z.enum(['notify', 'auto', 'off']).optional();
19
20
 
20
21
  const openagentAccountSchema = z.object({
21
22
  name: z.string().optional(),
@@ -35,5 +36,10 @@ export const OpenagentConfigSchema = openagentAccountSchema.extend({
35
36
  transport: transportSchema.optional(),
36
37
  api_base: z.string().optional(),
37
38
  access_api_base: z.string().optional(),
39
+ update_policy: updatePolicySchema,
40
+ update_check: z.boolean().optional(),
41
+ auto_update: z.boolean().optional(),
42
+ update_channel: z.string().optional(),
43
+ update_dist_tag: z.string().optional(),
38
44
  accounts: z.record(z.string(), openagentAccountSchema).optional(),
39
45
  });
@@ -2,7 +2,7 @@
2
2
  * OpenAgent UI Override — Auto-generated, do not edit directly!
3
3
  *
4
4
  * Source: src/plugin-ui/modules/
5
- * Built: 2026-07-06T03:23:32.989Z
5
+ * Built: 2026-07-06T09:05:33.996Z
6
6
  *
7
7
  * To modify, edit the source modules and run:
8
8
  * npm run build:override
@@ -52,7 +52,7 @@ const CL = {
52
52
  hostVersion: '__OPENCLAW_HOST_VERSION__',
53
53
 
54
54
  // ── Override 构建标识(构建时注入,用于缓存自检)──
55
- buildId: "65bf8968e270",
55
+ buildId: "4207d7dc1f64",
56
56
 
57
57
  // ── 配置 ──
58
58
  OPENAGENT_API: 'https://api.openagent.club',
@@ -2118,6 +2118,13 @@ function injectStyles() {
2118
2118
  padding: 14px;
2119
2119
  font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
2120
2120
  box-sizing: border-box;
2121
+ scrollbar-width: none;
2122
+ -ms-overflow-style: none;
2123
+ }
2124
+ .openagent-agent-panel::-webkit-scrollbar {
2125
+ width: 0;
2126
+ height: 0;
2127
+ display: none;
2121
2128
  }
2122
2129
  .openagent-agent-panel.visible {
2123
2130
  display: flex;
@@ -2357,21 +2364,13 @@ function injectStyles() {
2357
2364
  flex-direction: column;
2358
2365
  min-height: 0;
2359
2366
  max-height: min(56vh, 451px);
2360
- scrollbar-width: thin;
2361
- scrollbar-color: #ddd transparent;
2367
+ scrollbar-width: none;
2368
+ -ms-overflow-style: none;
2362
2369
  }
2363
2370
  .openagent-agent-list::-webkit-scrollbar {
2364
- width: 4px;
2365
- }
2366
- .openagent-agent-list::-webkit-scrollbar-track {
2367
- background: transparent;
2368
- }
2369
- .openagent-agent-list::-webkit-scrollbar-thumb {
2370
- background: #ddd;
2371
- border-radius: 2px;
2372
- }
2373
- .openagent-agent-list::-webkit-scrollbar-thumb:hover {
2374
- background: #bbb;
2371
+ width: 0;
2372
+ height: 0;
2373
+ display: none;
2375
2374
  }
2376
2375
 
2377
2376
  .openagent-agent-section {
@@ -4923,7 +4922,13 @@ margin: 0 8px; position: relative; top: -1px;
4923
4922
  .trip-img-card {
4924
4923
  display: flex; flex-direction: row; gap: 6px;
4925
4924
  overflow-x: auto; padding: 8px 0; margin-left: 20px;
4926
- scrollbar-width: thin;
4925
+ scrollbar-width: none;
4926
+ -ms-overflow-style: none;
4927
+ }
4928
+ .trip-img-card::-webkit-scrollbar {
4929
+ width: 0;
4930
+ height: 0;
4931
+ display: none;
4927
4932
  }
4928
4933
  .trip-img-item {
4929
4934
  position: relative; width: 191px; height: 100px; border-radius: 8px;
@@ -6983,6 +6988,14 @@ function _clScan() {
6983
6988
  ' overflow-x: auto;',
6984
6989
  ' white-space: pre-wrap;',
6985
6990
  ' word-break: break-all;',
6991
+ ' scrollbar-width: none;',
6992
+ ' -ms-overflow-style: none;',
6993
+ '}',
6994
+ '.cl-output-card > .cl-thought-chain-content pre::-webkit-scrollbar,',
6995
+ '.cl-output-card > .cl-thought-chain-content code::-webkit-scrollbar {',
6996
+ ' width: 0;',
6997
+ ' height: 0;',
6998
+ ' display: none;',
6986
6999
  '}',
6987
7000
  // V2 output card:统一 border/padding 与 execution card 一致
6988
7001
  '.cl-exec-v2.cl-output-card {',
@@ -7225,8 +7238,7 @@ function _clScan() {
7225
7238
  '.cl-exec-v2 .cl-exec-agent-avatar-v2 {',
7226
7239
  ' width: 68px !important;',
7227
7240
  ' height: 68px !important;',
7228
- ' border-radius: 50% !important;',
7229
- ' object-fit: cover;',
7241
+ ' object-fit: contain;',
7230
7242
  ' flex-shrink: 0;',
7231
7243
  '}',
7232
7244
 
@@ -11142,9 +11154,104 @@ function _startClObserver() {
11142
11154
  _clObserverTarget = document.querySelector('openclaw-app') || document.body;
11143
11155
  _clObserver.observe(_clObserverTarget, { childList: true, subtree: true });
11144
11156
  setTimeout(_debouncedClScan, 2000);
11157
+ setTimeout(_clCheckPluginUpdateStatus, 5000);
11145
11158
  /* muted */ void 0;
11146
11159
  }
11147
11160
 
11161
+ function _clCheckPluginUpdateStatus() {
11162
+ fetch('/plugins/openagent/update-status', { cache: 'no-store' })
11163
+ .then(function(res) {
11164
+ if (!res.ok) return null;
11165
+ return res.json();
11166
+ })
11167
+ .then(function(status) {
11168
+ if (!status || !status.updateAvailable) return;
11169
+ _clShowPluginUpdateNotice(status);
11170
+ })
11171
+ .catch(function() {});
11172
+ }
11173
+
11174
+ function _clShowPluginUpdateNotice(status) {
11175
+ var latestVersion = String(status.latestVersion || '').trim();
11176
+ var currentVersion = String(status.currentVersion || '').trim();
11177
+ var dismissKey = 'openagent-update-dismissed-' + latestVersion;
11178
+ try {
11179
+ if (latestVersion && localStorage.getItem(dismissKey) === '1') return;
11180
+ } catch (e) {}
11181
+
11182
+ var existing = document.getElementById('openagent-update-notice');
11183
+ if (existing) existing.remove();
11184
+
11185
+ var notice = document.createElement('div');
11186
+ notice.id = 'openagent-update-notice';
11187
+ notice.style.cssText = [
11188
+ 'position:fixed',
11189
+ 'right:18px',
11190
+ 'bottom:18px',
11191
+ 'z-index:2147483647',
11192
+ 'max-width:360px',
11193
+ 'padding:14px 16px',
11194
+ 'border:1px solid rgba(0,0,0,0.08)',
11195
+ 'border-radius:12px',
11196
+ 'background:#fff',
11197
+ 'box-shadow:0 12px 32px rgba(15,23,42,0.16)',
11198
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
11199
+ 'color:#1f2937',
11200
+ 'font-size:13px',
11201
+ 'line-height:1.5'
11202
+ ].join(';');
11203
+
11204
+ var title = document.createElement('div');
11205
+ title.textContent = 'OpenAgent 有新版本 ' + latestVersion;
11206
+ title.style.cssText = 'font-weight:600;font-size:14px;margin-right:22px;margin-bottom:4px;';
11207
+ notice.appendChild(title);
11208
+
11209
+ var body = document.createElement('div');
11210
+ var command = String(status.installCommand || 'npx -y openclaw-openagent-cli update');
11211
+ body.textContent = currentVersion ? ('当前版本 ' + currentVersion + ',运行命令完成更新。') : '运行命令完成更新。';
11212
+ body.style.cssText = 'color:#4b5563;margin-bottom:10px;';
11213
+ notice.appendChild(body);
11214
+
11215
+ var code = document.createElement('code');
11216
+ code.textContent = command;
11217
+ code.style.cssText = 'display:block;white-space:normal;word-break:break-all;background:#f6f7f9;border-radius:8px;padding:8px 10px;color:#111827;';
11218
+ notice.appendChild(code);
11219
+
11220
+ var actions = document.createElement('div');
11221
+ actions.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;margin-top:10px;';
11222
+ var copy = document.createElement('button');
11223
+ copy.type = 'button';
11224
+ copy.textContent = '复制命令';
11225
+ copy.style.cssText = 'border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;color:#374151;';
11226
+ copy.onclick = function() {
11227
+ try {
11228
+ navigator.clipboard.writeText(command);
11229
+ copy.textContent = '已复制';
11230
+ } catch (e) {}
11231
+ };
11232
+ actions.appendChild(copy);
11233
+
11234
+ var close = document.createElement('button');
11235
+ close.type = 'button';
11236
+ close.textContent = '关闭';
11237
+ close.style.cssText = 'border:0;background:#111827;color:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;';
11238
+ close.onclick = function() {
11239
+ try {
11240
+ if (latestVersion) localStorage.setItem(dismissKey, '1');
11241
+ } catch (e) {}
11242
+ notice.remove();
11243
+ };
11244
+ actions.appendChild(close);
11245
+ notice.appendChild(actions);
11246
+
11247
+ if (status.policy === 'auto' && status.autoUpdateStarted) {
11248
+ body.textContent = '正在自动更新,OpenClaw Gateway 可能会自动重启。';
11249
+ actions.remove();
11250
+ }
11251
+
11252
+ document.body.appendChild(notice);
11253
+ }
11254
+
11148
11255
  if (document.readyState === 'loading') {
11149
11256
  document.addEventListener('DOMContentLoaded', _startClObserver);
11150
11257
  } else {
@@ -84,6 +84,13 @@ function injectStyles() {
84
84
  padding: 14px;
85
85
  font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
86
86
  box-sizing: border-box;
87
+ scrollbar-width: none;
88
+ -ms-overflow-style: none;
89
+ }
90
+ .openagent-agent-panel::-webkit-scrollbar {
91
+ width: 0;
92
+ height: 0;
93
+ display: none;
87
94
  }
88
95
  .openagent-agent-panel.visible {
89
96
  display: flex;
@@ -323,21 +330,13 @@ function injectStyles() {
323
330
  flex-direction: column;
324
331
  min-height: 0;
325
332
  max-height: min(56vh, 451px);
326
- scrollbar-width: thin;
327
- scrollbar-color: #ddd transparent;
333
+ scrollbar-width: none;
334
+ -ms-overflow-style: none;
328
335
  }
329
336
  .openagent-agent-list::-webkit-scrollbar {
330
- width: 4px;
331
- }
332
- .openagent-agent-list::-webkit-scrollbar-track {
333
- background: transparent;
334
- }
335
- .openagent-agent-list::-webkit-scrollbar-thumb {
336
- background: #ddd;
337
- border-radius: 2px;
338
- }
339
- .openagent-agent-list::-webkit-scrollbar-thumb:hover {
340
- background: #bbb;
337
+ width: 0;
338
+ height: 0;
339
+ display: none;
341
340
  }
342
341
 
343
342
  .openagent-agent-section {
@@ -79,7 +79,13 @@ margin: 0 8px; position: relative; top: -1px;
79
79
  .trip-img-card {
80
80
  display: flex; flex-direction: row; gap: 6px;
81
81
  overflow-x: auto; padding: 8px 0; margin-left: 20px;
82
- scrollbar-width: thin;
82
+ scrollbar-width: none;
83
+ -ms-overflow-style: none;
84
+ }
85
+ .trip-img-card::-webkit-scrollbar {
86
+ width: 0;
87
+ height: 0;
88
+ display: none;
83
89
  }
84
90
  .trip-img-item {
85
91
  position: relative; width: 191px; height: 100px; border-radius: 8px;
@@ -26,9 +26,104 @@ function _startClObserver() {
26
26
  _clObserverTarget = document.querySelector('openclaw-app') || document.body;
27
27
  _clObserver.observe(_clObserverTarget, { childList: true, subtree: true });
28
28
  setTimeout(_debouncedClScan, 2000);
29
+ setTimeout(_clCheckPluginUpdateStatus, 5000);
29
30
  /* muted */ void 0;
30
31
  }
31
32
 
33
+ function _clCheckPluginUpdateStatus() {
34
+ fetch('/plugins/openagent/update-status', { cache: 'no-store' })
35
+ .then(function(res) {
36
+ if (!res.ok) return null;
37
+ return res.json();
38
+ })
39
+ .then(function(status) {
40
+ if (!status || !status.updateAvailable) return;
41
+ _clShowPluginUpdateNotice(status);
42
+ })
43
+ .catch(function() {});
44
+ }
45
+
46
+ function _clShowPluginUpdateNotice(status) {
47
+ var latestVersion = String(status.latestVersion || '').trim();
48
+ var currentVersion = String(status.currentVersion || '').trim();
49
+ var dismissKey = 'openagent-update-dismissed-' + latestVersion;
50
+ try {
51
+ if (latestVersion && localStorage.getItem(dismissKey) === '1') return;
52
+ } catch (e) {}
53
+
54
+ var existing = document.getElementById('openagent-update-notice');
55
+ if (existing) existing.remove();
56
+
57
+ var notice = document.createElement('div');
58
+ notice.id = 'openagent-update-notice';
59
+ notice.style.cssText = [
60
+ 'position:fixed',
61
+ 'right:18px',
62
+ 'bottom:18px',
63
+ 'z-index:2147483647',
64
+ 'max-width:360px',
65
+ 'padding:14px 16px',
66
+ 'border:1px solid rgba(0,0,0,0.08)',
67
+ 'border-radius:12px',
68
+ 'background:#fff',
69
+ 'box-shadow:0 12px 32px rgba(15,23,42,0.16)',
70
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
71
+ 'color:#1f2937',
72
+ 'font-size:13px',
73
+ 'line-height:1.5'
74
+ ].join(';');
75
+
76
+ var title = document.createElement('div');
77
+ title.textContent = 'OpenAgent 有新版本 ' + latestVersion;
78
+ title.style.cssText = 'font-weight:600;font-size:14px;margin-right:22px;margin-bottom:4px;';
79
+ notice.appendChild(title);
80
+
81
+ var body = document.createElement('div');
82
+ var command = String(status.installCommand || 'npx -y openclaw-openagent-cli update');
83
+ body.textContent = currentVersion ? ('当前版本 ' + currentVersion + ',运行命令完成更新。') : '运行命令完成更新。';
84
+ body.style.cssText = 'color:#4b5563;margin-bottom:10px;';
85
+ notice.appendChild(body);
86
+
87
+ var code = document.createElement('code');
88
+ code.textContent = command;
89
+ code.style.cssText = 'display:block;white-space:normal;word-break:break-all;background:#f6f7f9;border-radius:8px;padding:8px 10px;color:#111827;';
90
+ notice.appendChild(code);
91
+
92
+ var actions = document.createElement('div');
93
+ actions.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;margin-top:10px;';
94
+ var copy = document.createElement('button');
95
+ copy.type = 'button';
96
+ copy.textContent = '复制命令';
97
+ copy.style.cssText = 'border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;color:#374151;';
98
+ copy.onclick = function() {
99
+ try {
100
+ navigator.clipboard.writeText(command);
101
+ copy.textContent = '已复制';
102
+ } catch (e) {}
103
+ };
104
+ actions.appendChild(copy);
105
+
106
+ var close = document.createElement('button');
107
+ close.type = 'button';
108
+ close.textContent = '关闭';
109
+ close.style.cssText = 'border:0;background:#111827;color:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;';
110
+ close.onclick = function() {
111
+ try {
112
+ if (latestVersion) localStorage.setItem(dismissKey, '1');
113
+ } catch (e) {}
114
+ notice.remove();
115
+ };
116
+ actions.appendChild(close);
117
+ notice.appendChild(actions);
118
+
119
+ if (status.policy === 'auto' && status.autoUpdateStarted) {
120
+ body.textContent = '正在自动更新,OpenClaw Gateway 可能会自动重启。';
121
+ actions.remove();
122
+ }
123
+
124
+ document.body.appendChild(notice);
125
+ }
126
+
32
127
  if (document.readyState === 'loading') {
33
128
  document.addEventListener('DOMContentLoaded', _startClObserver);
34
129
  } else {
@@ -441,6 +441,14 @@
441
441
  ' overflow-x: auto;',
442
442
  ' white-space: pre-wrap;',
443
443
  ' word-break: break-all;',
444
+ ' scrollbar-width: none;',
445
+ ' -ms-overflow-style: none;',
446
+ '}',
447
+ '.cl-output-card > .cl-thought-chain-content pre::-webkit-scrollbar,',
448
+ '.cl-output-card > .cl-thought-chain-content code::-webkit-scrollbar {',
449
+ ' width: 0;',
450
+ ' height: 0;',
451
+ ' display: none;',
444
452
  '}',
445
453
  // V2 output card:统一 border/padding 与 execution card 一致
446
454
  '.cl-exec-v2.cl-output-card {',
@@ -683,8 +691,7 @@
683
691
  '.cl-exec-v2 .cl-exec-agent-avatar-v2 {',
684
692
  ' width: 68px !important;',
685
693
  ' height: 68px !important;',
686
- ' border-radius: 50% !important;',
687
- ' object-fit: cover;',
694
+ ' object-fit: contain;',
688
695
  ' flex-shrink: 0;',
689
696
  '}',
690
697
 
@@ -19,6 +19,7 @@ import fs from 'node:fs';
19
19
  import nodePath from 'node:path';
20
20
  import { logger } from '../util/logger.js';
21
21
  import { registry } from '../runtime/registry.js';
22
+ import { getOpenagentUpdateStatus } from '../runtime/update-checker.js';
22
23
  import { isUnconfiguredOasnApiBase } from '../auth/config.js';
23
24
  import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
24
25
 
@@ -100,6 +101,10 @@ export function createAuthProxyHandler() {
100
101
  if (targetPath.startsWith('/media/')) {
101
102
  return serveLocalMedia(targetPath, res);
102
103
  }
104
+ if (targetPath === '/update-status') {
105
+ writeJson(res, 200, getOpenagentUpdateStatus() as unknown as Record<string, unknown>);
106
+ return true;
107
+ }
103
108
  // v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
104
109
  const method = req.method ?? 'GET';
105
110
  const rt = registry.getDefault();
@@ -0,0 +1,286 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { logger } from '../util/logger.js';
8
+
9
+ const PLUGIN_PACKAGE = 'openclaw-openagent';
10
+ const CLI_PACKAGE = 'openclaw-openagent-cli';
11
+ const DEFAULT_DIST_TAG = 'latest';
12
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
13
+ const CHECK_DELAY_MS = 3_000;
14
+
15
+ export type OpenagentUpdatePolicy = 'notify' | 'auto' | 'off';
16
+
17
+ export type OpenagentUpdateStatus = {
18
+ checked: boolean;
19
+ checking: boolean;
20
+ currentVersion: string;
21
+ latestVersion: string;
22
+ distTag: string;
23
+ updateAvailable: boolean;
24
+ policy: OpenagentUpdatePolicy;
25
+ installCommand: string;
26
+ autoUpdateStarted: boolean;
27
+ lastCheckedAt?: string;
28
+ error?: string;
29
+ };
30
+
31
+ type UpdateConfig = {
32
+ policy: OpenagentUpdatePolicy;
33
+ distTag: string;
34
+ channel: string;
35
+ };
36
+
37
+ let checkerStarted = false;
38
+
39
+ const updateStatus: OpenagentUpdateStatus = {
40
+ checked: false,
41
+ checking: false,
42
+ currentVersion: '',
43
+ latestVersion: '',
44
+ distTag: DEFAULT_DIST_TAG,
45
+ updateAvailable: false,
46
+ policy: 'notify',
47
+ installCommand: `npx -y ${CLI_PACKAGE}`,
48
+ autoUpdateStarted: false,
49
+ };
50
+
51
+ export function getOpenagentUpdateStatus(): OpenagentUpdateStatus {
52
+ return { ...updateStatus };
53
+ }
54
+
55
+ export function startOpenagentUpdateChecker(): void {
56
+ if (checkerStarted) return;
57
+ checkerStarted = true;
58
+ setTimeout(() => {
59
+ void runOpenagentUpdateCheck().catch((err) => {
60
+ updateStatus.checked = true;
61
+ updateStatus.checking = false;
62
+ updateStatus.error = (err as Error).message;
63
+ updateStatus.lastCheckedAt = new Date().toISOString();
64
+ logger.warn(`[update-check] failed: ${(err as Error).message}`);
65
+ });
66
+ }, CHECK_DELAY_MS).unref?.();
67
+ }
68
+
69
+ export async function runOpenagentUpdateCheck(): Promise<OpenagentUpdateStatus> {
70
+ const config = resolveUpdateConfig();
71
+ updateStatus.policy = config.policy;
72
+ updateStatus.distTag = config.distTag;
73
+ updateStatus.installCommand = buildInstallCommand(config);
74
+ updateStatus.currentVersion = resolveCurrentVersion();
75
+
76
+ if (config.policy === 'off') {
77
+ updateStatus.checked = true;
78
+ updateStatus.checking = false;
79
+ updateStatus.updateAvailable = false;
80
+ updateStatus.lastCheckedAt = new Date().toISOString();
81
+ logger.info('[update-check] disabled by policy');
82
+ return getOpenagentUpdateStatus();
83
+ }
84
+
85
+ updateStatus.checking = true;
86
+ updateStatus.error = undefined;
87
+ try {
88
+ const latestVersion = await fetchDistTagVersion(config.distTag);
89
+ updateStatus.latestVersion = latestVersion;
90
+ updateStatus.updateAvailable = compareVersions(latestVersion, updateStatus.currentVersion) > 0;
91
+ updateStatus.checked = true;
92
+ updateStatus.checking = false;
93
+ updateStatus.lastCheckedAt = new Date().toISOString();
94
+
95
+ if (!updateStatus.updateAvailable) {
96
+ logger.info(`[update-check] up to date: ${PLUGIN_PACKAGE}@${updateStatus.currentVersion}`);
97
+ return getOpenagentUpdateStatus();
98
+ }
99
+
100
+ logger.warn(
101
+ `[update-check] new ${PLUGIN_PACKAGE} version available: `
102
+ + `${updateStatus.currentVersion} -> ${latestVersion}. Run: ${updateStatus.installCommand}`,
103
+ );
104
+
105
+ if (config.policy === 'auto') {
106
+ startAutoUpdate(config);
107
+ }
108
+ return getOpenagentUpdateStatus();
109
+ } catch (err) {
110
+ updateStatus.checked = true;
111
+ updateStatus.checking = false;
112
+ updateStatus.error = (err as Error).message;
113
+ updateStatus.lastCheckedAt = new Date().toISOString();
114
+ throw err;
115
+ }
116
+ }
117
+
118
+ function resolveUpdateConfig(): UpdateConfig {
119
+ const fileConfig = readOpenagentConfig();
120
+ const envPolicy = normalizePolicy(process.env.OPENAGENT_UPDATE_POLICY);
121
+ const autoUpdateEnv = readBool(process.env.OPENAGENT_AUTO_UPDATE ?? process.env.OPENAGENT_PLUGIN_AUTO_UPDATE);
122
+ const checkEnabledEnv = readBool(process.env.OPENAGENT_UPDATE_CHECK);
123
+ const configPolicy = normalizePolicy(readString(fileConfig, 'update_policy', 'updatePolicy'));
124
+ const configAutoUpdate = readBool(readString(fileConfig, 'auto_update', 'autoUpdate'));
125
+ const configUpdateCheck = readBool(readString(fileConfig, 'update_check', 'updateCheck'));
126
+
127
+ let policy: OpenagentUpdatePolicy = configPolicy || 'notify';
128
+ if (configAutoUpdate === true) policy = 'auto';
129
+ if (configUpdateCheck === false) policy = 'off';
130
+ if (envPolicy) policy = envPolicy;
131
+ if (autoUpdateEnv === true) policy = 'auto';
132
+ if (checkEnabledEnv === false) policy = 'off';
133
+
134
+ const distTag = normalizeDistTag(
135
+ process.env.OPENAGENT_UPDATE_DIST_TAG
136
+ || readString(fileConfig, 'update_dist_tag', 'updateDistTag')
137
+ || process.env.OPENAGENT_PLUGIN_DIST_TAG
138
+ || DEFAULT_DIST_TAG,
139
+ );
140
+ const channel = normalizeDistTag(
141
+ process.env.OPENAGENT_UPDATE_CHANNEL
142
+ || readString(fileConfig, 'update_channel', 'updateChannel')
143
+ || process.env.OPENAGENT_PLUGIN_CHANNEL
144
+ || '',
145
+ );
146
+
147
+ return {
148
+ policy,
149
+ distTag: channel || distTag,
150
+ channel,
151
+ };
152
+ }
153
+
154
+ function startAutoUpdate(config: UpdateConfig): void {
155
+ if (updateStatus.autoUpdateStarted) return;
156
+ updateStatus.autoUpdateStarted = true;
157
+
158
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
159
+ const args = ['-y', CLI_PACKAGE, 'update'];
160
+ if (config.channel) {
161
+ args.push('--channel', config.channel);
162
+ } else if (config.distTag && config.distTag !== DEFAULT_DIST_TAG) {
163
+ args.push('--dist-tag', config.distTag);
164
+ }
165
+
166
+ logger.warn(`[update-check] auto update started: ${npxBin} ${args.join(' ')}`);
167
+ const child = spawn(npxBin, args, {
168
+ detached: true,
169
+ stdio: 'ignore',
170
+ env: process.env,
171
+ });
172
+ child.unref();
173
+ }
174
+
175
+ function buildInstallCommand(config: UpdateConfig): string {
176
+ const parts = ['npx', '-y', CLI_PACKAGE, 'update'];
177
+ if (config.channel) {
178
+ parts.push('--channel', config.channel);
179
+ } else if (config.distTag && config.distTag !== DEFAULT_DIST_TAG) {
180
+ parts.push('--dist-tag', config.distTag);
181
+ }
182
+ return parts.join(' ');
183
+ }
184
+
185
+ async function fetchDistTagVersion(distTag: string): Promise<string> {
186
+ if (process.env.OPENAGENT_UPDATE_TEST_VERSION) {
187
+ return process.env.OPENAGENT_UPDATE_TEST_VERSION;
188
+ }
189
+ const registry = (process.env.OPENAGENT_UPDATE_REGISTRY || DEFAULT_REGISTRY).replace(/\/+$/, '');
190
+ const url = `${registry}/${encodeURIComponent(PLUGIN_PACKAGE)}/${encodeURIComponent(distTag)}`;
191
+ const res = await fetch(url, {
192
+ headers: { Accept: 'application/json' },
193
+ signal: AbortSignal.timeout(5_000),
194
+ });
195
+ if (!res.ok) {
196
+ throw new Error(`npm registry ${res.status} for ${PLUGIN_PACKAGE}@${distTag}`);
197
+ }
198
+ const data = await res.json() as { version?: unknown };
199
+ const version = typeof data.version === 'string' ? data.version : '';
200
+ if (!version) throw new Error(`npm registry response missing version for ${PLUGIN_PACKAGE}@${distTag}`);
201
+ return version;
202
+ }
203
+
204
+ function resolveCurrentVersion(): string {
205
+ const packageJson = findPackageJson();
206
+ if (!packageJson) return '0.0.0';
207
+ try {
208
+ const data = JSON.parse(fs.readFileSync(packageJson, 'utf8')) as { version?: unknown };
209
+ return typeof data.version === 'string' ? data.version : '0.0.0';
210
+ } catch {
211
+ return '0.0.0';
212
+ }
213
+ }
214
+
215
+ function findPackageJson(): string | null {
216
+ let dir = path.dirname(fileURLToPath(import.meta.url));
217
+ for (let i = 0; i < 8; i += 1) {
218
+ const candidate = path.join(dir, 'package.json');
219
+ try {
220
+ const data = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { name?: unknown };
221
+ if (data.name === PLUGIN_PACKAGE) return candidate;
222
+ } catch {}
223
+ const parent = path.dirname(dir);
224
+ if (parent === dir) break;
225
+ dir = parent;
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function readOpenagentConfig(): Record<string, unknown> {
231
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
232
+ const configPath = path.join(stateDir, 'openclaw.json');
233
+ try {
234
+ const root = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
235
+ channels?: { openagent?: Record<string, unknown> };
236
+ };
237
+ return root.channels?.openagent || {};
238
+ } catch {
239
+ return {};
240
+ }
241
+ }
242
+
243
+ function readString(obj: Record<string, unknown>, ...keys: string[]): string {
244
+ for (const key of keys) {
245
+ const value = obj[key];
246
+ if (typeof value === 'string' && value.trim()) return value.trim();
247
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
248
+ }
249
+ return '';
250
+ }
251
+
252
+ function readBool(value: unknown): boolean | null {
253
+ if (typeof value === 'boolean') return value;
254
+ const text = typeof value === 'string' ? value.trim().toLowerCase() : '';
255
+ if (!text) return null;
256
+ if (['1', 'true', 'yes', 'on', 'auto'].includes(text)) return true;
257
+ if (['0', 'false', 'no', 'off', 'notify'].includes(text)) return false;
258
+ return null;
259
+ }
260
+
261
+ function normalizePolicy(value: unknown): OpenagentUpdatePolicy | '' {
262
+ const text = typeof value === 'string' ? value.trim().toLowerCase() : '';
263
+ if (text === 'auto' || text === 'notify' || text === 'off') return text;
264
+ return '';
265
+ }
266
+
267
+ function normalizeDistTag(value: unknown): string {
268
+ const text = typeof value === 'string' ? value.trim() : '';
269
+ if (!text) return '';
270
+ return /^[A-Za-z0-9._-]+$/.test(text) ? text : '';
271
+ }
272
+
273
+ export function compareVersions(a: string, b: string): number {
274
+ const pa = parseVersion(a);
275
+ const pb = parseVersion(b);
276
+ for (let i = 0; i < 3; i += 1) {
277
+ if (pa[i] !== pb[i]) return pa[i] - pb[i];
278
+ }
279
+ return 0;
280
+ }
281
+
282
+ function parseVersion(version: string): [number, number, number] {
283
+ const match = String(version || '').match(/(\d+)\.(\d+)\.(\d+)/);
284
+ if (!match) return [0, 0, 0];
285
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
286
+ }