openclaw-openagent 1.0.11 → 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.
Files changed (65) hide show
  1. package/dist/index.js +3 -1
  2. package/dist/src/app/remote-agent-tool.js +110 -14
  3. package/dist/src/app/types.d.ts +2 -2
  4. package/dist/src/config/config-schema.d.ts +9 -0
  5. package/dist/src/config/config-schema.js +6 -0
  6. package/dist/src/plugin-ui/adapters/adapters/oc-2026-04.js +103 -0
  7. package/dist/src/plugin-ui/adapters/adapters/oc-2026-05.js +125 -0
  8. package/dist/src/plugin-ui/adapters/adapters/oc-2026-06.js +125 -0
  9. package/dist/src/plugin-ui/adapters/adapters/oc-unknown.js +48 -0
  10. package/dist/src/plugin-ui/adapters/oc-2026-04.js +103 -0
  11. package/dist/src/plugin-ui/adapters/oc-2026-05.js +125 -0
  12. package/dist/src/plugin-ui/adapters/oc-2026-06.js +125 -0
  13. package/dist/src/plugin-ui/adapters/oc-unknown.js +48 -0
  14. package/dist/src/plugin-ui/assets/openagent-override.js +1129 -273
  15. package/dist/src/plugin-ui/index.d.ts +1 -1
  16. package/dist/src/plugin-ui/index.js +2 -2
  17. package/dist/src/plugin-ui/ui-extension-loader/index.d.ts +2 -1
  18. package/dist/src/plugin-ui/ui-extension-loader/index.js +5 -5
  19. package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +75 -8
  20. package/dist/src/plugin-ui/ui-extension-loader/types.d.ts +4 -1
  21. package/dist/src/proxy/auth-proxy.js +5 -0
  22. package/dist/src/runtime/update-checker.d.ts +18 -0
  23. package/dist/src/runtime/update-checker.js +253 -0
  24. package/dist/src/state/store.d.ts +21 -0
  25. package/dist/src/state/store.js +54 -0
  26. package/dist/src/transport/oasn/oasn-invocation.d.ts +3 -0
  27. package/dist/src/transport/oasn/oasn-invocation.js +28 -12
  28. package/dist/src/transport/oasn/oasn-types.d.ts +8 -4
  29. package/index.ts +4 -1
  30. package/package.json +4 -3
  31. package/src/app/remote-agent-tool.ts +131 -16
  32. package/src/app/types.ts +2 -2
  33. package/src/config/config-schema.ts +6 -0
  34. package/src/plugin-ui/adapters/oc-2026-04.js +103 -0
  35. package/src/plugin-ui/adapters/oc-2026-05.js +125 -0
  36. package/src/plugin-ui/adapters/oc-2026-06.js +125 -0
  37. package/src/plugin-ui/adapters/oc-unknown.js +48 -0
  38. package/src/plugin-ui/assets/openagent-override.js +1129 -273
  39. package/src/plugin-ui/build.cjs +249 -38
  40. package/src/plugin-ui/index.ts +2 -2
  41. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +102 -12
  42. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +105 -3
  43. package/src/plugin-ui/modules/agent-book/panel/styles.js +54 -48
  44. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +4 -2
  45. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +4 -1
  46. package/src/plugin-ui/modules/agent-book/scanner.js +17 -5
  47. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +12 -1
  48. package/src/plugin-ui/modules/loader/bootstrap.js +95 -0
  49. package/src/plugin-ui/modules/loader/shared-state.js +244 -20
  50. package/src/plugin-ui/modules/remote-agent/execution-card.js +54 -16
  51. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +5 -23
  52. package/src/plugin-ui/modules/remote-agent/output-card.js +13 -7
  53. package/src/plugin-ui/modules/remote-agent/render-hooks.js +53 -41
  54. package/src/plugin-ui/modules/remote-agent/styles.js +238 -89
  55. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +72 -4
  56. package/src/plugin-ui/postinstall-deploy.cjs +52 -0
  57. package/src/plugin-ui/ui-extension-loader/index.ts +6 -6
  58. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +81 -9
  59. package/src/plugin-ui/ui-extension-loader/types.ts +5 -1
  60. package/src/proxy/auth-proxy.ts +5 -0
  61. package/src/runtime/update-checker.ts +286 -0
  62. package/src/state/store.ts +80 -0
  63. package/src/transport/oasn/oasn-invocation.ts +47 -12
  64. package/src/transport/oasn/oasn-types.ts +6 -2
  65. package/src/types/openclaw-plugin-sdk-media-store.d.ts +9 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Best-effort deploy for npm install/upgrade.
4
+ *
5
+ * The browser cache strategy depends on Control UI index.html carrying the
6
+ * latest openagent-override.js?v=<hash>. Running the override builder here
7
+ * updates reachable OpenClaw Control UI directories immediately after the npm
8
+ * package changes, without making npm install fail if OpenClaw is not present.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { spawnSync } = require('child_process');
14
+
15
+ function log(message) {
16
+ console.log('[openagent postinstall] ' + message);
17
+ }
18
+
19
+ function main() {
20
+ if (process.env.OPENAGENT_SKIP_POSTINSTALL_DEPLOY === '1') {
21
+ log('skipped by OPENAGENT_SKIP_POSTINSTALL_DEPLOY=1');
22
+ return;
23
+ }
24
+
25
+ const buildScript = path.join(__dirname, 'build.cjs');
26
+ if (!fs.existsSync(buildScript)) {
27
+ log('build script not found, skipped');
28
+ return;
29
+ }
30
+
31
+ const result = spawnSync(process.execPath, [buildScript], {
32
+ cwd: path.join(__dirname, '..', '..'),
33
+ env: {
34
+ ...process.env,
35
+ OPENAGENT_POSTINSTALL_DEPLOY: '1',
36
+ },
37
+ stdio: 'inherit',
38
+ });
39
+
40
+ if (result.error) {
41
+ log('deploy skipped: ' + result.error.message);
42
+ return;
43
+ }
44
+ if (result.status !== 0) {
45
+ log('deploy exited with code ' + result.status + ' (ignored)');
46
+ return;
47
+ }
48
+
49
+ log('deployed latest Control UI override where reachable');
50
+ }
51
+
52
+ main();
@@ -17,7 +17,7 @@ import { restoreFile } from './backup.js';
17
17
  import { UI_EXTENSIONS } from './registry-regex.js';
18
18
  import { REMOVED_UI_EXTENSIONS } from './removed-extensions.js';
19
19
  import type { ManifestData } from './manifest.js';
20
- import type { UIExtension } from './types.js';
20
+ import type { UIExtension, UIExtensionContext } from './types.js';
21
21
  import type { RemovedUIExtension, RemovedRevertResult } from './removed-extensions.js';
22
22
 
23
23
  // ── 注入策略:正则模式链(无需 .map,21 版本全仿真验证通过)──────────────────
@@ -53,7 +53,7 @@ function needsChange(
53
53
  * 4. 共享文件扩展按组处理:任何一个变更 → 恢复 backup → 重新 apply 全组
54
54
  * 5. 写入新 manifest
55
55
  */
56
- export async function loadUIExtensions(): Promise<void> {
56
+ export async function loadUIExtensions(context: UIExtensionContext = {}): Promise<void> {
57
57
  const controlUiDir = resolveControlUiDir();
58
58
 
59
59
  if (!controlUiDir) {
@@ -134,7 +134,7 @@ export async function loadUIExtensions(): Promise<void> {
134
134
  }
135
135
 
136
136
  logger.debug(`[ui-ext] ${ext.id}: apply v${ext.version}`);
137
- if (safeApply(ext, controlUiDir)) {
137
+ if (safeApply(ext, controlUiDir, context)) {
138
138
  newExtensions[ext.id] = { version: ext.version };
139
139
  applied++;
140
140
  }
@@ -200,7 +200,7 @@ export async function loadUIExtensions(): Promise<void> {
200
200
  // 重新 apply 全组中所有应保留的扩展
201
201
  for (const ext of group) {
202
202
  logger.debug(`[ui-ext] ${ext.id}: apply v${ext.version} (group rebuild)`);
203
- if (safeApply(ext, controlUiDir)) {
203
+ if (safeApply(ext, controlUiDir, context)) {
204
204
  newExtensions[ext.id] = { version: ext.version };
205
205
  applied++;
206
206
  }
@@ -249,9 +249,9 @@ export async function unloadUIExtensions(): Promise<void> {
249
249
 
250
250
  // ── Helpers ──────────────────────────────────────────────────────────────
251
251
 
252
- function safeApply(ext: UIExtension, controlUiDir: string): boolean {
252
+ function safeApply(ext: UIExtension, controlUiDir: string, context: UIExtensionContext): boolean {
253
253
  try {
254
- return ext.apply(controlUiDir);
254
+ return ext.apply(controlUiDir, context);
255
255
  } catch (err) {
256
256
  logger.error(`[ui-ext] Failed to apply '${ext.id}': ${(err as Error).message}`);
257
257
  return false;
@@ -13,17 +13,87 @@ import { logger } from '../../util/logger.js';
13
13
  import { findFileByPrefix } from './locator.js';
14
14
  import { backupFile, restoreFile } from './backup.js';
15
15
  import { makeMarker, hasMarker } from './types.js';
16
- import type { UIExtension } from './types.js';
16
+ import type { UIExtension, UIExtensionContext } from './types.js';
17
17
 
18
18
  // ── 常量 ──────────────────────────────────────────────────────────────────
19
19
 
20
20
  const OVERRIDE_ASSET_URL = new URL('../assets/openagent-override.js', import.meta.url);
21
21
  const ICON_ASSET_URL = new URL('../assets/icon.png', import.meta.url);
22
22
  const BG_ASSET_URL = new URL('../assets/bg.png', import.meta.url);
23
+ const HOST_ADAPTER_URLS: Record<string, URL> = {
24
+ 'oc-2026-04': new URL('../adapters/oc-2026-04.js', import.meta.url),
25
+ 'oc-2026-05': new URL('../adapters/oc-2026-05.js', import.meta.url),
26
+ 'oc-2026-06': new URL('../adapters/oc-2026-06.js', import.meta.url),
27
+ 'oc-unknown': new URL('../adapters/oc-unknown.js', import.meta.url),
28
+ };
29
+ const HOST_ADAPTER_START = '/*__OPENAGENT_HOST_ADAPTER_START__*/';
30
+ const HOST_ADAPTER_END = '/*__OPENAGENT_HOST_ADAPTER_END__*/';
31
+ const HOST_ADAPTER_BLOCK_RE = /\/\*__OPENAGENT_HOST_ADAPTER_START__\*\/[\s\S]*?\/\*__OPENAGENT_HOST_ADAPTER_END__\*\//;
32
+ const BUILD_ID_TOKEN = "buildId: '__OPENAGENT_BUILD_ID__'";
33
+ const BUILD_ID_ASSIGNMENT_RE = /buildId:\s*(?:"[^"]*"|'[^']*')/;
34
+
35
+ function normalizeHostVersion(hostVersion: string | undefined): string {
36
+ const value = String(hostVersion || '').trim();
37
+ if (!value || value === 'unknown') return '';
38
+ const match = value.match(/20\d{2}\.\d{1,2}\.\d{1,2}(?:-[A-Za-z0-9._-]+)?/);
39
+ return match ? match[0] : '';
40
+ }
41
+
42
+ function resolveHostAdapterId(hostVersion: string | undefined): string {
43
+ const version = normalizeHostVersion(hostVersion);
44
+ const parts = version.split('.');
45
+ const year = Number.parseInt(parts[0], 10);
46
+ const month = Number.parseInt(parts[1], 10);
47
+ if (year === 2026 && month === 4) return 'oc-2026-04';
48
+ if (year === 2026 && month === 5) return 'oc-2026-05';
49
+ if (year === 2026 && month === 6) return 'oc-2026-06';
50
+ return 'oc-unknown';
51
+ }
52
+
53
+ function getHostAdapterBlock(hostVersion: string | undefined): string {
54
+ let adapterId = resolveHostAdapterId(hostVersion);
55
+ let adapterUrl = HOST_ADAPTER_URLS[adapterId];
56
+ if (!adapterUrl || !existsSync(adapterUrl)) {
57
+ adapterId = 'oc-unknown';
58
+ adapterUrl = HOST_ADAPTER_URLS[adapterId];
59
+ }
60
+ const adapterSource = readFileSync(adapterUrl, 'utf-8').trimEnd();
61
+ return [
62
+ HOST_ADAPTER_START,
63
+ `// HOST ADAPTER: ${adapterId}`,
64
+ adapterSource,
65
+ HOST_ADAPTER_END,
66
+ ].join('\n');
67
+ }
68
+
69
+ function injectHostAdapter(source: string, hostVersion: string | undefined): string {
70
+ const block = getHostAdapterBlock(hostVersion);
71
+ return HOST_ADAPTER_BLOCK_RE.test(source)
72
+ ? source.replace(HOST_ADAPTER_BLOCK_RE, block)
73
+ : source;
74
+ }
23
75
 
24
- function getOverrideAssetHash(): string {
76
+ function injectBuildId(source: string): string {
77
+ const normalized = source.replace(BUILD_ID_ASSIGNMENT_RE, BUILD_ID_TOKEN);
78
+ const buildId = createHash('sha256').update(normalized).digest('hex').slice(0, 12);
79
+ return normalized.replace(BUILD_ID_TOKEN, `buildId: ${JSON.stringify(buildId)}`);
80
+ }
81
+
82
+ function getOverrideAssetContent(context?: UIExtensionContext): string {
83
+ let source = readFileSync(OVERRIDE_ASSET_URL, 'utf-8');
84
+ const hostVersion = normalizeHostVersion(context?.hostVersion);
85
+ if (hostVersion) {
86
+ source = source.replace(
87
+ "hostVersion: '__OPENCLAW_HOST_VERSION__'",
88
+ `hostVersion: ${JSON.stringify(hostVersion)}`,
89
+ );
90
+ }
91
+ return injectBuildId(injectHostAdapter(source, hostVersion));
92
+ }
93
+
94
+ function getOverrideAssetHash(context?: UIExtensionContext): string {
25
95
  return createHash('sha256')
26
- .update(readFileSync(OVERRIDE_ASSET_URL))
96
+ .update(getOverrideAssetContent(context))
27
97
  .digest('hex')
28
98
  .slice(0, 12);
29
99
  }
@@ -109,21 +179,21 @@ const overrideScript: UIExtension = {
109
179
  version: 1,
110
180
  alwaysApply: true,
111
181
 
112
- apply(controlUiDir: string): boolean {
182
+ apply(controlUiDir: string, context?: UIExtensionContext): boolean {
113
183
  if (!existsSync(OVERRIDE_ASSET_URL)) {
114
184
  logger.warn('[ui-ext:regex] override-script: asset missing');
115
185
  return false;
116
186
  }
117
187
  const dest = join(controlUiDir, 'openagent-override.js');
188
+ const srcContent = getOverrideAssetContent(context);
118
189
  if (existsSync(dest)) {
119
- const srcContent = readFileSync(OVERRIDE_ASSET_URL, 'utf-8');
120
190
  const destContent = readFileSync(dest, 'utf-8');
121
191
  if (srcContent === destContent) {
122
192
  logger.debug('[ui-ext:regex] override-script: content unchanged, skip');
123
193
  return true;
124
194
  }
125
195
  }
126
- copyFileSync(OVERRIDE_ASSET_URL, dest);
196
+ writeFileSync(dest, srcContent, 'utf-8');
127
197
  logger.info('[ui-ext:regex] override-script: deployed openagent-override.js');
128
198
 
129
199
  // Deploy icon.png alongside
@@ -179,7 +249,7 @@ const htmlScriptTag: UIExtension = {
179
249
  version: 5,
180
250
  alwaysApply: true,
181
251
 
182
- apply(controlUiDir: string): boolean {
252
+ apply(controlUiDir: string, context?: UIExtensionContext): boolean {
183
253
  const indexPath = join(controlUiDir, 'index.html');
184
254
  if (!existsSync(indexPath)) {
185
255
  logger.warn('[ui-ext:regex] html-script-tag: index.html not found');
@@ -188,8 +258,10 @@ const htmlScriptTag: UIExtension = {
188
258
  let html = readFileSync(indexPath, 'utf-8');
189
259
  const htmlMarker = `<!--openagent:${this.id}-->`;
190
260
  const markerPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)/;
191
- const overrideHash = getOverrideAssetHash();
192
- const scriptTag = `${htmlMarker}<script src="./openagent-override.js?v=${overrideHash}"></script>`;
261
+ const overrideHash = getOverrideAssetHash(context);
262
+ const hostVersion = normalizeHostVersion(context?.hostVersion);
263
+ const hostVersionAttr = hostVersion ? ` data-openagent-host-version="${hostVersion}"` : '';
264
+ const scriptTag = `${htmlMarker}<script src="./openagent-override.js?v=${overrideHash}"${hostVersionAttr}></script>`;
193
265
  const markedScriptPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
194
266
  const oldScriptPattern = /<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
195
267
 
@@ -37,7 +37,7 @@ export interface UIExtension {
37
37
  * 接收 control-ui 目录路径,内部自行决定改什么文件、怎么改。
38
38
  * 返回 true 表示成功,false 表示失败(不写入 manifest,下次重试)。
39
39
  */
40
- apply: (controlUiDir: string) => boolean;
40
+ apply: (controlUiDir: string, context?: UIExtensionContext) => boolean;
41
41
 
42
42
  /**
43
43
  * 卸载扩展。
@@ -47,6 +47,10 @@ export interface UIExtension {
47
47
  revert: (controlUiDir: string) => void;
48
48
  }
49
49
 
50
+ export interface UIExtensionContext {
51
+ hostVersion?: string;
52
+ }
53
+
50
54
  /**
51
55
  * OpenAgent 标记前缀,注入的代码旁加此注释以标识修改来源。
52
56
  * 格式:/*openagent:<extension-id>* /
@@ -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
+ }
@@ -67,6 +67,22 @@ export interface PendingInvocationRecord {
67
67
  updated_at: number;
68
68
  }
69
69
 
70
+ /**
71
+ * 远程 ServiceAgent 会话续传记录。
72
+ *
73
+ * 一个 OpenClaw 本地会话里,同一个远程 Agent 的第一次 invocation 会返回
74
+ * session_id;后续 mode='session' 的调用应带回这个 session_id,才能保留
75
+ * 远端上下文。account_id + openclaw_session_key + target_agent_id 共同隔离,
76
+ * 避免不同账号、不同本地聊天、不同远程 Agent 之间串 session。
77
+ */
78
+ export interface RemoteAgentSessionRecord {
79
+ openclaw_session_key: string;
80
+ target_agent_id: string;
81
+ session_id: string;
82
+ created_at: number;
83
+ updated_at: number;
84
+ }
85
+
70
86
  interface SqlJsDatabase {
71
87
  run(sql: string, params?: unknown[]): void;
72
88
  prepare(sql: string): SqlJsStatement;
@@ -291,6 +307,21 @@ export class StateStore {
291
307
  CREATE INDEX IF NOT EXISTS idx_pending_invocations_invocation
292
308
  ON pending_invocations (account_id, invocation_id)
293
309
  `);
310
+ this.db.run(`
311
+ CREATE TABLE IF NOT EXISTS remote_agent_sessions (
312
+ account_id TEXT NOT NULL,
313
+ openclaw_session_key TEXT NOT NULL,
314
+ target_agent_id TEXT NOT NULL,
315
+ session_id TEXT NOT NULL,
316
+ created_at INTEGER NOT NULL DEFAULT 0,
317
+ updated_at INTEGER NOT NULL DEFAULT 0,
318
+ PRIMARY KEY (account_id, openclaw_session_key, target_agent_id)
319
+ )
320
+ `);
321
+ this.db.run(`
322
+ CREATE INDEX IF NOT EXISTS idx_remote_agent_sessions_updated
323
+ ON remote_agent_sessions (account_id, updated_at)
324
+ `);
294
325
  }
295
326
 
296
327
  // ── Write to disk ──
@@ -533,6 +564,55 @@ export class StateStore {
533
564
  }
534
565
  }
535
566
 
567
+ // ═════════════════════════════════════════════════════════════════
568
+ // OASN remote_agent_sessions CRUD
569
+ //
570
+ // 保存远端 ServiceAgent 返回的 session_id,用于同一 OpenClaw 会话内后续
571
+ // mode='session' 调用续传。run 模式不读写这张表。
572
+ // ═════════════════════════════════════════════════════════════════
573
+
574
+ getRemoteAgentSession(openclawSessionKey: string, targetAgentId: string): string | null {
575
+ if (!this.db) return null;
576
+ const scopeKey = openclawSessionKey.trim();
577
+ const agentId = targetAgentId.trim();
578
+ if (!scopeKey || !agentId) return null;
579
+
580
+ const stmt = this.db.prepare(
581
+ `SELECT session_id
582
+ FROM remote_agent_sessions
583
+ WHERE account_id = ? AND openclaw_session_key = ? AND target_agent_id = ?`,
584
+ );
585
+ stmt.bind([this.accountId, scopeKey, agentId]);
586
+ const row = stmt.step() ? stmt.getAsObject() : null;
587
+ stmt.free();
588
+ const sessionId = row?.['session_id'];
589
+ return typeof sessionId === 'string' && sessionId.trim() ? sessionId : null;
590
+ }
591
+
592
+ upsertRemoteAgentSession(record: {
593
+ openclaw_session_key: string;
594
+ target_agent_id: string;
595
+ session_id: string;
596
+ }): void {
597
+ if (!this.db) return;
598
+ const scopeKey = record.openclaw_session_key.trim();
599
+ const agentId = record.target_agent_id.trim();
600
+ const sessionId = record.session_id.trim();
601
+ if (!scopeKey || !agentId || !sessionId) return;
602
+
603
+ const now = Date.now();
604
+ this.db.run(
605
+ `INSERT INTO remote_agent_sessions
606
+ (account_id, openclaw_session_key, target_agent_id, session_id, created_at, updated_at)
607
+ VALUES (?, ?, ?, ?, ?, ?)
608
+ ON CONFLICT(account_id, openclaw_session_key, target_agent_id) DO UPDATE SET
609
+ session_id = excluded.session_id,
610
+ updated_at = excluded.updated_at`,
611
+ [this.accountId, scopeKey, agentId, sessionId, now, now],
612
+ );
613
+ this.persist();
614
+ }
615
+
536
616
  // ═════════════════════════════════════════════════════════════════
537
617
  // §11.1 OASN pending_invocations CRUD(OasnInvocation.PendingInvocationStore 适配)
538
618
  //