openclaw-openagent 1.0.8 → 1.0.11
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/src/app/remote-agent-tool.js +108 -10
- package/dist/src/auth/config.js +65 -10
- package/dist/src/channel.js +2 -1
- package/dist/src/plugin-ui/assets/bg.png +0 -0
- package/dist/src/plugin-ui/assets/icon.png +0 -0
- package/dist/src/plugin-ui/assets/openagent-override.js +1753 -774
- package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +53 -5
- package/dist/src/proxy/auth-proxy.d.ts +1 -0
- package/dist/src/proxy/auth-proxy.js +28 -9
- package/dist/src/transport/oasn/oasn-http.d.ts +12 -0
- package/dist/src/transport/oasn/oasn-http.js +45 -14
- package/dist/src/util/url-resolver.d.ts +6 -0
- package/dist/src/util/url-resolver.js +24 -8
- package/openclaw.plugin.json +16 -1
- package/package.json +3 -3
- package/src/app/remote-agent-tool.ts +126 -11
- package/src/auth/config.ts +79 -10
- package/src/channel.ts +2 -1
- package/src/plugin-ui/assets/bg.png +0 -0
- package/src/plugin-ui/assets/icon.png +0 -0
- package/src/plugin-ui/assets/openagent-override.js +1753 -774
- package/src/plugin-ui/build.cjs +80 -4
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +92 -9
- package/src/plugin-ui/modules/agent-book/panel/agent-card.js +26 -10
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +1 -1
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +64 -0
- package/src/plugin-ui/modules/agent-book/panel/styles.js +313 -178
- package/src/plugin-ui/modules/loader/bootstrap.js +1 -1
- package/src/plugin-ui/modules/loader/shared-state.js +54 -0
- package/src/plugin-ui/modules/remote-agent/execution-card.js +347 -124
- package/src/plugin-ui/modules/remote-agent/media-links.js +151 -0
- package/src/plugin-ui/modules/remote-agent/output-card.js +110 -33
- package/src/plugin-ui/modules/remote-agent/render-hooks.js +97 -18
- package/src/plugin-ui/modules/remote-agent/styles.js +488 -402
- package/src/plugin-ui/modules/remote-agent/tool-card-model.js +6 -0
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +50 -5
- package/src/proxy/auth-proxy.ts +30 -10
- package/src/transport/oasn/oasn-http.ts +60 -14
- package/src/util/url-resolver.ts +24 -8
|
@@ -14,6 +14,8 @@ import { backupFile, restoreFile } from './backup.js';
|
|
|
14
14
|
import { makeMarker, hasMarker } from './types.js';
|
|
15
15
|
// ── 常量 ──────────────────────────────────────────────────────────────────
|
|
16
16
|
const OVERRIDE_ASSET_URL = new URL('../assets/openagent-override.js', import.meta.url);
|
|
17
|
+
const ICON_ASSET_URL = new URL('../assets/icon.png', import.meta.url);
|
|
18
|
+
const BG_ASSET_URL = new URL('../assets/bg.png', import.meta.url);
|
|
17
19
|
function getOverrideAssetHash() {
|
|
18
20
|
return createHash('sha256')
|
|
19
21
|
.update(readFileSync(OVERRIDE_ASSET_URL))
|
|
@@ -113,6 +115,27 @@ const overrideScript = {
|
|
|
113
115
|
}
|
|
114
116
|
copyFileSync(OVERRIDE_ASSET_URL, dest);
|
|
115
117
|
logger.info('[ui-ext:regex] override-script: deployed openagent-override.js');
|
|
118
|
+
// Deploy icon.png alongside
|
|
119
|
+
if (existsSync(ICON_ASSET_URL)) {
|
|
120
|
+
const iconDest = join(controlUiDir, 'icon.png');
|
|
121
|
+
try {
|
|
122
|
+
copyFileSync(ICON_ASSET_URL, iconDest);
|
|
123
|
+
logger.info('[ui-ext:regex] override-script: deployed icon.png');
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
logger.warn('[ui-ext:regex] override-script: failed to deploy icon.png');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (existsSync(BG_ASSET_URL)) {
|
|
130
|
+
const bgDest = join(controlUiDir, 'bg.png');
|
|
131
|
+
try {
|
|
132
|
+
copyFileSync(BG_ASSET_URL, bgDest);
|
|
133
|
+
logger.info('[ui-ext:regex] override-script: deployed bg.png');
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
logger.warn('[ui-ext:regex] override-script: failed to deploy bg.png');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
116
139
|
return true;
|
|
117
140
|
},
|
|
118
141
|
revert(controlUiDir) {
|
|
@@ -121,12 +144,28 @@ const overrideScript = {
|
|
|
121
144
|
unlinkSync(dest);
|
|
122
145
|
logger.info('[ui-ext:regex] override-script: removed');
|
|
123
146
|
}
|
|
147
|
+
const iconDest = join(controlUiDir, 'icon.png');
|
|
148
|
+
if (existsSync(iconDest)) {
|
|
149
|
+
try {
|
|
150
|
+
unlinkSync(iconDest);
|
|
151
|
+
logger.info('[ui-ext:regex] override-script: removed icon.png');
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
const bgDest = join(controlUiDir, 'bg.png');
|
|
156
|
+
if (existsSync(bgDest)) {
|
|
157
|
+
try {
|
|
158
|
+
unlinkSync(bgDest);
|
|
159
|
+
logger.info('[ui-ext:regex] override-script: removed bg.png');
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
}
|
|
124
163
|
},
|
|
125
164
|
};
|
|
126
165
|
// ── Extension 2: html-script-tag ─────────────────────────────────────────
|
|
127
166
|
const htmlScriptTag = {
|
|
128
167
|
id: 'html-script-tag',
|
|
129
|
-
version:
|
|
168
|
+
version: 5,
|
|
130
169
|
alwaysApply: true,
|
|
131
170
|
apply(controlUiDir) {
|
|
132
171
|
const indexPath = join(controlUiDir, 'index.html');
|
|
@@ -136,11 +175,21 @@ const htmlScriptTag = {
|
|
|
136
175
|
}
|
|
137
176
|
let html = readFileSync(indexPath, 'utf-8');
|
|
138
177
|
const htmlMarker = `<!--openagent:${this.id}-->`;
|
|
178
|
+
const markerPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)/;
|
|
139
179
|
const overrideHash = getOverrideAssetHash();
|
|
140
180
|
const scriptTag = `${htmlMarker}<script src="./openagent-override.js?v=${overrideHash}"></script>`;
|
|
141
181
|
const markedScriptPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
const oldScriptPattern = /<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
|
|
183
|
+
if (markerPattern.test(html)) {
|
|
184
|
+
let nextHtml = html.replace(markedScriptPattern, `${scriptTag}\n `);
|
|
185
|
+
if (nextHtml === html && !nextHtml.includes('openagent-override.js')) {
|
|
186
|
+
nextHtml = nextHtml.replace(markerPattern, `${scriptTag}\n `);
|
|
187
|
+
}
|
|
188
|
+
else if (nextHtml === html && nextHtml.includes('openagent-override.js')) {
|
|
189
|
+
nextHtml = nextHtml
|
|
190
|
+
.replace(oldScriptPattern, '')
|
|
191
|
+
.replace(markerPattern, `${scriptTag}\n `);
|
|
192
|
+
}
|
|
144
193
|
if (nextHtml === html) {
|
|
145
194
|
logger.debug('[ui-ext:regex] html-script-tag: already applied');
|
|
146
195
|
}
|
|
@@ -150,7 +199,6 @@ const htmlScriptTag = {
|
|
|
150
199
|
}
|
|
151
200
|
return true;
|
|
152
201
|
}
|
|
153
|
-
const oldScriptPattern = /<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
|
|
154
202
|
if (oldScriptPattern.test(html)) {
|
|
155
203
|
html = html.replace(oldScriptPattern, '');
|
|
156
204
|
}
|
|
@@ -333,7 +381,7 @@ const toolcardRender = {
|
|
|
333
381
|
toolMessageOk = false;
|
|
334
382
|
}
|
|
335
383
|
if (toolMessageOk) {
|
|
336
|
-
const toolMessageHook = `${marker}let __openagentToolMsgCard=null;
|
|
384
|
+
const toolMessageHook = `${marker}/* openagent-media-aware-tool-message */let __openagentToolMsgCard=null;if(window.__openagentRenderToolCard){for(let __openagentI=${cardsVar}.length-1;__openagentI>=0&&!__openagentToolMsgCard;__openagentI--){let __openagentCandidate=${cardsVar}[__openagentI];if(__openagentCandidate&&(__openagentCandidate.outputText||__openagentCandidate.text))__openagentToolMsgCard=window.__openagentRenderToolCard(__openagentCandidate,${toolMessageSidebarArg})}for(let __openagentI=0;__openagentI<${cardsVar}.length&&!__openagentToolMsgCard;__openagentI++){__openagentToolMsgCard=window.__openagentRenderToolCard(${cardsVar}[__openagentI],${toolMessageSidebarArg})}}if(__openagentToolMsgCard)return ${toolMessageHtmlVar}\`<div class="\${${chatBubbleClassVar}}">\${__openagentToolMsgCard}</div>\`;`;
|
|
337
385
|
const toolMessageInsertAt = toolMessageFn.bodyStart + toolMessageReturnIdx;
|
|
338
386
|
content = content.substring(0, toolMessageInsertAt) + toolMessageHook + content.substring(toolMessageInsertAt);
|
|
339
387
|
patchedToolMessageWrapper = true;
|
|
@@ -17,4 +17,5 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
17
17
|
/** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
|
|
18
18
|
export declare function getGatewayBaseUrl(): string;
|
|
19
19
|
export declare function createAuthProxyHandler(): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
20
|
+
export declare function resolveOasnProxyTargetUrl(upstreamBase: string, targetPath: string, search?: string): string;
|
|
20
21
|
export declare function isAllowedOasnProxyRequest(method: string, pathname: string): boolean;
|
|
@@ -18,6 +18,7 @@ import nodePath from 'node:path';
|
|
|
18
18
|
import { logger } from '../util/logger.js';
|
|
19
19
|
import { registry } from '../runtime/registry.js';
|
|
20
20
|
import { isUnconfiguredOasnApiBase } from '../auth/config.js';
|
|
21
|
+
import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
|
|
21
22
|
/** TIM 路径默认上游(兼容旧版) */
|
|
22
23
|
const AUTH_UPSTREAM_TIM = 'https://auth.ai-talk.live';
|
|
23
24
|
const PROXY_TIMEOUT_MS = 15_000;
|
|
@@ -56,7 +57,7 @@ function pickUpstream(method, pathname) {
|
|
|
56
57
|
let _gatewayBaseUrl;
|
|
57
58
|
/** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
|
|
58
59
|
export function getGatewayBaseUrl() {
|
|
59
|
-
return _gatewayBaseUrl ??
|
|
60
|
+
return _gatewayBaseUrl ?? 'http://localhost:18794';
|
|
60
61
|
}
|
|
61
62
|
// ── Handler factory ─────────────────────────────────────────────────────
|
|
62
63
|
export function createAuthProxyHandler() {
|
|
@@ -76,19 +77,19 @@ export function createAuthProxyHandler() {
|
|
|
76
77
|
_gatewayBaseUrl = `${proto}://${host}`;
|
|
77
78
|
logger.info(`[proxy] Gateway base URL detected: ${_gatewayBaseUrl}`);
|
|
78
79
|
}
|
|
79
|
-
// ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
|
|
80
|
-
// Files downloaded from remote agents via C2C are stored here.
|
|
81
|
-
// Tool result references them as MEDIA:http://localhost:PORT/plugins/openagent/media/{filename}
|
|
82
|
-
// to pass through the framework's filterToolResultMediaUrls (http URLs bypass whitelist check).
|
|
83
|
-
if (url.pathname.startsWith('/media/')) {
|
|
84
|
-
return serveLocalMedia(url.pathname, res);
|
|
85
|
-
}
|
|
86
80
|
// req.url may or may not have the /plugins/openagent prefix stripped by Gateway
|
|
87
81
|
// (behavior varies across Gateway versions), so strip it defensively.
|
|
88
82
|
let targetPath = url.pathname;
|
|
89
83
|
if (targetPath.startsWith('/plugins/openagent')) {
|
|
90
84
|
targetPath = targetPath.slice('/plugins/openagent'.length);
|
|
91
85
|
}
|
|
86
|
+
// ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
|
|
87
|
+
// Files downloaded from remote agents are stored here. The media route must
|
|
88
|
+
// be detected after prefix normalization because Gateway versions disagree
|
|
89
|
+
// on whether /plugins/openagent has already been stripped.
|
|
90
|
+
if (targetPath.startsWith('/media/')) {
|
|
91
|
+
return serveLocalMedia(targetPath, res);
|
|
92
|
+
}
|
|
92
93
|
// v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
|
|
93
94
|
const method = req.method ?? 'GET';
|
|
94
95
|
const rt = registry.getDefault();
|
|
@@ -116,7 +117,7 @@ export function createAuthProxyHandler() {
|
|
|
116
117
|
writeJson(res, 503, { error: 'OASN api_base is not configured' });
|
|
117
118
|
return true;
|
|
118
119
|
}
|
|
119
|
-
const targetUrl =
|
|
120
|
+
const targetUrl = resolveOasnProxyTargetUrl(upstreamBase, targetPath, url.search);
|
|
120
121
|
logger.info(`[proxy] ${method} ${url.pathname} → ${targetUrl}`);
|
|
121
122
|
try {
|
|
122
123
|
// Read request body for non-GET methods
|
|
@@ -173,6 +174,9 @@ export function createAuthProxyHandler() {
|
|
|
173
174
|
return true;
|
|
174
175
|
};
|
|
175
176
|
}
|
|
177
|
+
export function resolveOasnProxyTargetUrl(upstreamBase, targetPath, search = '') {
|
|
178
|
+
return resolveOasnRelativeUrl(upstreamBase, `${targetPath}${search}`);
|
|
179
|
+
}
|
|
176
180
|
// ── T4: Local media serve ────────────────────────────────────────────────
|
|
177
181
|
const MEDIA_DIR = '/tmp/openclaw/openagent-media';
|
|
178
182
|
/** MIME type lookup for common media extensions. */
|
|
@@ -190,6 +194,12 @@ const MIME_MAP = {
|
|
|
190
194
|
'.webp': 'image/webp',
|
|
191
195
|
'.pdf': 'application/pdf',
|
|
192
196
|
'.txt': 'text/plain',
|
|
197
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
198
|
+
'.json': 'application/json; charset=utf-8',
|
|
199
|
+
'.zip': 'application/zip',
|
|
200
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
201
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
202
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
193
203
|
};
|
|
194
204
|
/**
|
|
195
205
|
* Serve a file from the local openagent-media directory.
|
|
@@ -230,6 +240,9 @@ function serveLocalMedia(pathname, res) {
|
|
|
230
240
|
const stat = fs.statSync(filePath);
|
|
231
241
|
res.setHeader('Content-Type', contentType);
|
|
232
242
|
res.setHeader('Content-Length', stat.size);
|
|
243
|
+
if (shouldDownloadAsAttachment(contentType)) {
|
|
244
|
+
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
|
245
|
+
}
|
|
233
246
|
// Allow browser to cache media for 1 hour
|
|
234
247
|
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
235
248
|
const stream = fs.createReadStream(filePath);
|
|
@@ -252,6 +265,12 @@ function serveLocalMedia(pathname, res) {
|
|
|
252
265
|
}
|
|
253
266
|
return true;
|
|
254
267
|
}
|
|
268
|
+
function shouldDownloadAsAttachment(contentType) {
|
|
269
|
+
return !contentType.startsWith('image/')
|
|
270
|
+
&& !contentType.startsWith('audio/')
|
|
271
|
+
&& !contentType.startsWith('video/')
|
|
272
|
+
&& !contentType.startsWith('text/plain');
|
|
273
|
+
}
|
|
255
274
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
256
275
|
/**
|
|
257
276
|
* Build headers for the upstream request.
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* - 业务幂等持久化(详见 oasn-invocation.ts + state/store.ts)
|
|
19
19
|
* - file_ref LRU(详见 oasn-files.ts)
|
|
20
20
|
*/
|
|
21
|
+
import { type RequestOptions } from 'node:http';
|
|
22
|
+
import { URL } from 'node:url';
|
|
21
23
|
/** OASN HTTP 客户端配置 */
|
|
22
24
|
export interface OasnHttpConfig {
|
|
23
25
|
/** API base URL,如 `https://api.oasn.ai` */
|
|
@@ -45,6 +47,16 @@ export interface OasnRequestOptions {
|
|
|
45
47
|
/** Abort 信号 */
|
|
46
48
|
signal?: AbortSignal;
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* OpenClaw gateway may install global-agent for model/provider traffic. OASN
|
|
52
|
+
* REL endpoints should keep their original TLS hostname; otherwise a local
|
|
53
|
+
* proxy can turn `https://oasn-test...` into a certificate check against
|
|
54
|
+
* `localhost`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ensureOasnHostBypassesGlobalAgentProxy(hostname: string): void;
|
|
57
|
+
export declare function buildNodeRequestOptions(url: URL, method: string, headers: Record<string, string>): RequestOptions & {
|
|
58
|
+
servername?: string;
|
|
59
|
+
};
|
|
48
60
|
export declare class OasnHttpClient {
|
|
49
61
|
private _config;
|
|
50
62
|
constructor(config: OasnHttpConfig);
|
|
@@ -25,6 +25,47 @@ import { logger } from '../../util/logger.js';
|
|
|
25
25
|
import { makeTransportError } from '../types.js';
|
|
26
26
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
27
27
|
const DEFAULT_MAX_RETRIES = 3;
|
|
28
|
+
/**
|
|
29
|
+
* OpenClaw gateway may install global-agent for model/provider traffic. OASN
|
|
30
|
+
* REL endpoints should keep their original TLS hostname; otherwise a local
|
|
31
|
+
* proxy can turn `https://oasn-test...` into a certificate check against
|
|
32
|
+
* `localhost`.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureOasnHostBypassesGlobalAgentProxy(hostname) {
|
|
35
|
+
const normalized = hostname.trim().toLowerCase();
|
|
36
|
+
if (!normalized || normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1')
|
|
37
|
+
return;
|
|
38
|
+
const globalAgent = globalThis.GLOBAL_AGENT;
|
|
39
|
+
if (!globalAgent || typeof globalAgent !== 'object')
|
|
40
|
+
return;
|
|
41
|
+
const existing = String(globalAgent.NO_PROXY || '');
|
|
42
|
+
const entries = existing.split(/[\s,]+/).filter(Boolean);
|
|
43
|
+
if (entries.some((entry) => entry.toLowerCase() === normalized))
|
|
44
|
+
return;
|
|
45
|
+
globalAgent.NO_PROXY = entries.length > 0 ? `${entries.join(',')},${normalized}` : normalized;
|
|
46
|
+
}
|
|
47
|
+
function hasHeader(headers, name) {
|
|
48
|
+
const lower = name.toLowerCase();
|
|
49
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === lower);
|
|
50
|
+
}
|
|
51
|
+
export function buildNodeRequestOptions(url, method, headers) {
|
|
52
|
+
const isHttps = url.protocol === 'https:';
|
|
53
|
+
const requestHeaders = { ...headers };
|
|
54
|
+
if (!hasHeader(requestHeaders, 'Host')) {
|
|
55
|
+
requestHeaders.Host = url.host;
|
|
56
|
+
}
|
|
57
|
+
const options = {
|
|
58
|
+
method,
|
|
59
|
+
headers: requestHeaders,
|
|
60
|
+
hostname: url.hostname,
|
|
61
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
62
|
+
path: `${url.pathname}${url.search}`,
|
|
63
|
+
};
|
|
64
|
+
if (isHttps) {
|
|
65
|
+
options.servername = url.hostname;
|
|
66
|
+
}
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
28
69
|
// ───────────────────────────────────────────────────────────────
|
|
29
70
|
// 错误转换
|
|
30
71
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -241,14 +282,9 @@ export class OasnHttpClient {
|
|
|
241
282
|
_doRawRequest(url, method, headers, body, timeoutMs, signal) {
|
|
242
283
|
const isHttps = url.protocol === 'https:';
|
|
243
284
|
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
285
|
+
ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
|
|
244
286
|
return new Promise((resolve, reject) => {
|
|
245
|
-
const reqOptions =
|
|
246
|
-
method,
|
|
247
|
-
headers,
|
|
248
|
-
hostname: url.hostname,
|
|
249
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
250
|
-
path: `${url.pathname}${url.search}`,
|
|
251
|
-
};
|
|
287
|
+
const reqOptions = buildNodeRequestOptions(url, method, headers);
|
|
252
288
|
let settled = false;
|
|
253
289
|
let timeoutHandle;
|
|
254
290
|
const finish = (fn) => {
|
|
@@ -302,14 +338,9 @@ export class OasnHttpClient {
|
|
|
302
338
|
_doBinaryRequest(url, headers, timeoutMs, signal) {
|
|
303
339
|
const isHttps = url.protocol === 'https:';
|
|
304
340
|
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
341
|
+
ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
|
|
305
342
|
return new Promise((resolve, reject) => {
|
|
306
|
-
const reqOptions =
|
|
307
|
-
method: 'GET',
|
|
308
|
-
headers,
|
|
309
|
-
hostname: url.hostname,
|
|
310
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
311
|
-
path: `${url.pathname}${url.search}`,
|
|
312
|
-
};
|
|
343
|
+
const reqOptions = buildNodeRequestOptions(url, 'GET', headers);
|
|
313
344
|
let settled = false;
|
|
314
345
|
let timeoutHandle;
|
|
315
346
|
const finish = (fn) => {
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Used by remote-agent-tool (WebUI continuation) and download-file-tool (artifact download).
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolveOasnAccessUrl(accessApiBase: string | undefined, url: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an OASN API/WebUI-relative path without losing env prefixes such as
|
|
10
|
+
* `/e/limj1`. `new URL('/api/..', 'https://host/e/limj1')` resets to the
|
|
11
|
+
* origin root, so use explicit path joining for env-scoped public bases.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveOasnRelativeUrl(accessApiBase: string, path: string): string;
|
|
8
14
|
/**
|
|
9
15
|
* Resolve a WebUI continuation URL for user-facing browser clicks.
|
|
10
16
|
*
|
|
@@ -12,12 +12,22 @@ export function resolveOasnAccessUrl(accessApiBase, url) {
|
|
|
12
12
|
if (!accessApiBase)
|
|
13
13
|
return url;
|
|
14
14
|
try {
|
|
15
|
-
return
|
|
15
|
+
return resolveOasnRelativeUrl(accessApiBase, url);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
18
|
return url;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an OASN API/WebUI-relative path without losing env prefixes such as
|
|
23
|
+
* `/e/limj1`. `new URL('/api/..', 'https://host/e/limj1')` resets to the
|
|
24
|
+
* origin root, so use explicit path joining for env-scoped public bases.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveOasnRelativeUrl(accessApiBase, path) {
|
|
27
|
+
const base = accessApiBase.replace(/\/+$/, '');
|
|
28
|
+
const suffix = path.startsWith('/') ? path : `/${path}`;
|
|
29
|
+
return new URL(`${base}${suffix}`).toString();
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Resolve a WebUI continuation URL for user-facing browser clicks.
|
|
23
33
|
*
|
|
@@ -28,23 +38,29 @@ export function resolveOasnAccessUrl(accessApiBase, url) {
|
|
|
28
38
|
export function resolveOasnWebuiUrl(accessApiBase, claimUrl, url) {
|
|
29
39
|
if (!url)
|
|
30
40
|
return url;
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
41
|
+
const claimPublicBase = publicBaseOfClaimUrl(claimUrl);
|
|
42
|
+
if (!claimPublicBase || !isWebuiUrl(url, accessApiBase || claimPublicBase)) {
|
|
33
43
|
return resolveOasnAccessUrl(accessApiBase, url);
|
|
34
44
|
}
|
|
35
45
|
try {
|
|
36
|
-
const absolute = new URL(
|
|
37
|
-
|
|
46
|
+
const absolute = new URL(resolveOasnAccessUrl(accessApiBase || claimPublicBase, url));
|
|
47
|
+
const webuiIndex = absolute.pathname.indexOf('/webui/');
|
|
48
|
+
const path = webuiIndex >= 0 ? absolute.pathname.slice(webuiIndex) : absolute.pathname;
|
|
49
|
+
return resolveOasnRelativeUrl(claimPublicBase, `${path}${absolute.search}${absolute.hash}`);
|
|
38
50
|
}
|
|
39
51
|
catch {
|
|
40
52
|
return resolveOasnAccessUrl(accessApiBase, url);
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
|
-
function
|
|
55
|
+
function publicBaseOfClaimUrl(value) {
|
|
44
56
|
if (!value)
|
|
45
57
|
return undefined;
|
|
46
58
|
try {
|
|
47
|
-
|
|
59
|
+
const parsed = new URL(value);
|
|
60
|
+
const claimPathIndex = parsed.pathname.indexOf('/agents/claim');
|
|
61
|
+
if (claimPathIndex <= 0)
|
|
62
|
+
return parsed.origin;
|
|
63
|
+
return `${parsed.origin}${parsed.pathname.slice(0, claimPathIndex)}`;
|
|
48
64
|
}
|
|
49
65
|
catch {
|
|
50
66
|
return undefined;
|
|
@@ -52,7 +68,7 @@ function originOf(value) {
|
|
|
52
68
|
}
|
|
53
69
|
function isWebuiUrl(url, base) {
|
|
54
70
|
try {
|
|
55
|
-
return new URL(
|
|
71
|
+
return /(?:^|\/)webui\//.test(new URL(resolveOasnAccessUrl(base, url)).pathname);
|
|
56
72
|
}
|
|
57
73
|
catch {
|
|
58
74
|
return false;
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-openagent",
|
|
3
3
|
"name": "OpenAgent",
|
|
4
4
|
"description": "OpenAgent channel plugin — real-time messaging between AI agents via OASN",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.10",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"openagent_call_remote_agent",
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
"description": "OASN API base URL. Required for OASN transport.",
|
|
38
38
|
"default": "https://oasn.invalid/CONFIGURE_ME"
|
|
39
39
|
},
|
|
40
|
+
"oasn_env_id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"title": "OASN Env ID",
|
|
43
|
+
"description": "REL environment id, e.g. limj1. When api_base is not set, the plugin derives https://oasn-test.haimawan.com/e/<env_id>."
|
|
44
|
+
},
|
|
40
45
|
"access_api_base": {
|
|
41
46
|
"type": "string",
|
|
42
47
|
"title": "OASN Access API Base URL",
|
|
@@ -70,6 +75,11 @@
|
|
|
70
75
|
"description": "OASN API base URL",
|
|
71
76
|
"default": "https://oasn.invalid/CONFIGURE_ME"
|
|
72
77
|
},
|
|
78
|
+
"oasn_env_id": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"title": "OASN Env ID",
|
|
81
|
+
"description": "REL environment id, e.g. limj1. Used to derive api_base/access_api_base when explicit URLs are omitted."
|
|
82
|
+
},
|
|
73
83
|
"access_api_base": {
|
|
74
84
|
"type": "string",
|
|
75
85
|
"title": "Access API Base URL",
|
|
@@ -130,6 +140,11 @@
|
|
|
130
140
|
"description": "OASN API base URL",
|
|
131
141
|
"default": "https://oasn.invalid/CONFIGURE_ME"
|
|
132
142
|
},
|
|
143
|
+
"oasn_env_id": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"title": "OASN Env ID",
|
|
146
|
+
"description": "REL environment id, e.g. limj1. Used to derive api_base/access_api_base when explicit URLs are omitted."
|
|
147
|
+
},
|
|
133
148
|
"access_api_base": {
|
|
134
149
|
"type": "string",
|
|
135
150
|
"title": "Access API Base URL",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-openagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenAgent channel plugin for OpenClaw",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"node": ">=22"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"build": "tsc -p tsconfig.build.json && cp -r src/sdk dist/src/sdk && mkdir -p dist/src/plugin-ui/assets && cp src/plugin-ui/assets/openagent-override.js dist/src/plugin-ui/assets/",
|
|
50
|
+
"build": "tsc -p tsconfig.build.json && cp -r src/sdk dist/src/sdk && mkdir -p dist/src/plugin-ui/assets && cp src/plugin-ui/assets/openagent-override.js src/plugin-ui/assets/icon.png src/plugin-ui/assets/bg.png dist/src/plugin-ui/assets/",
|
|
51
51
|
"build:override": "node ./src/plugin-ui/build.cjs",
|
|
52
52
|
"watch:override": "node ./src/plugin-ui/build.cjs --watch",
|
|
53
53
|
"prepack": "npm run build:override && npm run build && npm run typecheck",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
55
|
"prepublishOnly": "npm run typecheck",
|
|
56
|
-
"test:contract": "node tests/contract.test.mjs && node tests/agent-book-batch-category.test.mjs",
|
|
56
|
+
"test:contract": "node tests/contract.test.mjs && node tests/agent-book-batch-category.test.mjs && tsx tests/ui-extension-loader.test.mts",
|
|
57
57
|
"test:smoke": "node tests/smoke.test.mjs",
|
|
58
58
|
"test": "npm run test:contract && npm run test:smoke"
|
|
59
59
|
},
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type ToolUpdateCallback,
|
|
27
27
|
} from './types.js';
|
|
28
28
|
import { sendC2CCustomMessage, waitForC2CReply } from '../tim/c2c.js';
|
|
29
|
-
import type { OasnTransport, TaskHandle, TaskRequest } from '../transport/types.js';
|
|
29
|
+
import type { ArtifactRef, OasnTransport, TaskHandle, TaskRequest } from '../transport/types.js';
|
|
30
30
|
import fs from 'node:fs';
|
|
31
31
|
import nodePath from 'node:path';
|
|
32
32
|
import { resolveOasnAccessUrl, resolveOasnWebuiUrl } from '../util/url-resolver.js';
|
|
@@ -91,6 +91,118 @@ function artifactContentToText(content: ArrayBuffer): string {
|
|
|
91
91
|
return Buffer.from(content).toString('utf8').trim();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const OPENAGENT_MEDIA_DIR = '/tmp/openclaw/openagent-media';
|
|
95
|
+
|
|
96
|
+
const EXT_BY_MIME: Record<string, string> = {
|
|
97
|
+
'application/json': '.json',
|
|
98
|
+
'text/markdown': '.md',
|
|
99
|
+
'text/plain': '.txt',
|
|
100
|
+
'application/pdf': '.pdf',
|
|
101
|
+
'image/png': '.png',
|
|
102
|
+
'image/jpeg': '.jpg',
|
|
103
|
+
'image/gif': '.gif',
|
|
104
|
+
'image/webp': '.webp',
|
|
105
|
+
'application/zip': '.zip',
|
|
106
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
|
|
107
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
|
|
108
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function sanitizeMediaFileName(name: string): string {
|
|
112
|
+
const base = nodePath.basename(name || 'artifact');
|
|
113
|
+
return base
|
|
114
|
+
.replace(/[\0\r\n]/g, '')
|
|
115
|
+
.replace(/[/:\\]/g, '_')
|
|
116
|
+
.replace(/^\.+$/, 'artifact')
|
|
117
|
+
.slice(0, 180) || 'artifact';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inferExtension(buffer: Buffer, mimeType: string): string {
|
|
121
|
+
const mime = mimeType.split(';')[0]?.trim().toLowerCase() || '';
|
|
122
|
+
const byMime = EXT_BY_MIME[mime];
|
|
123
|
+
if (byMime) return byMime;
|
|
124
|
+
|
|
125
|
+
if (buffer.subarray(0, 2).toString('latin1') === 'PK') {
|
|
126
|
+
const zipNames = buffer.toString('latin1');
|
|
127
|
+
if (zipNames.includes('[Content_Types].xml') && zipNames.includes('ppt/')) return '.pptx';
|
|
128
|
+
if (zipNames.includes('[Content_Types].xml') && zipNames.includes('word/')) return '.docx';
|
|
129
|
+
if (zipNames.includes('[Content_Types].xml') && zipNames.includes('xl/')) return '.xlsx';
|
|
130
|
+
return '.zip';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (buffer.subarray(0, 4).toString('latin1') === '%PDF') return '.pdf';
|
|
134
|
+
if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return '.png';
|
|
135
|
+
if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) return '.jpg';
|
|
136
|
+
return '.bin';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildOasnMediaFileName(artifact: ArtifactRef, buffer: Buffer, downloadedName: string, downloadedMime: string): string {
|
|
140
|
+
const preferredName = downloadedName && downloadedName !== 'artifact'
|
|
141
|
+
? downloadedName
|
|
142
|
+
: artifact.displayName && artifact.displayName !== 'artifact'
|
|
143
|
+
? artifact.displayName
|
|
144
|
+
: artifact.id || 'artifact';
|
|
145
|
+
|
|
146
|
+
let safeName = sanitizeMediaFileName(preferredName);
|
|
147
|
+
if (!nodePath.extname(safeName)) {
|
|
148
|
+
safeName += inferExtension(buffer, downloadedMime || artifact.mimeType);
|
|
149
|
+
}
|
|
150
|
+
return safeName;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function cacheOasnArtifactsForMedia(
|
|
154
|
+
oasnTransport: OasnTransport,
|
|
155
|
+
artifacts: ArtifactRef[],
|
|
156
|
+
): Promise<string[]> {
|
|
157
|
+
const localPaths: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const artifact of artifacts) {
|
|
160
|
+
const ref = artifact.contentUrl || artifact.id;
|
|
161
|
+
if (!ref) continue;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const downloaded = await oasnTransport.downloadArtifact(ref);
|
|
165
|
+
const buffer = Buffer.from(downloaded.content);
|
|
166
|
+
const fileName = buildOasnMediaFileName(
|
|
167
|
+
artifact,
|
|
168
|
+
buffer,
|
|
169
|
+
downloaded.displayName,
|
|
170
|
+
downloaded.mimeType,
|
|
171
|
+
);
|
|
172
|
+
const filePath = nodePath.join(OPENAGENT_MEDIA_DIR, fileName);
|
|
173
|
+
fs.mkdirSync(nodePath.dirname(filePath), { recursive: true });
|
|
174
|
+
fs.writeFileSync(filePath, buffer);
|
|
175
|
+
localPaths.push(filePath);
|
|
176
|
+
logger.info(
|
|
177
|
+
`[remote-tool] Cached OASN artifact for media: id=${artifact.id} file=${filePath} size=${buffer.byteLength}`,
|
|
178
|
+
);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.warn(
|
|
181
|
+
`[remote-tool] Failed to cache OASN artifact ${artifact.id || ref}: ${(err as Error).message}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return localPaths;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mediaDownloadLabel(url: string): string {
|
|
190
|
+
let fileName = 'artifact';
|
|
191
|
+
try {
|
|
192
|
+
fileName = decodeURIComponent(new URL(url).pathname.split('/').pop() || fileName);
|
|
193
|
+
} catch {
|
|
194
|
+
fileName = decodeURIComponent(url.split('?')[0]?.split('/').pop() || fileName);
|
|
195
|
+
}
|
|
196
|
+
return sanitizeMediaFileName(fileName).replace(/[[\]\r\n]/g, ' ') || 'artifact';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatMediaDownloadLines(urls: string[]): string {
|
|
200
|
+
const lines = urls
|
|
201
|
+
.filter((url) => typeof url === 'string' && url.trim())
|
|
202
|
+
.map((url) => `- [${mediaDownloadLabel(url)}](${url})`);
|
|
203
|
+
return lines.length ? `下载链接:\n${lines.join('\n')}` : '';
|
|
204
|
+
}
|
|
205
|
+
|
|
94
206
|
function normalizeRemoteToolExecuteArgs(
|
|
95
207
|
signalOrUpdate?: AbortSignal | ToolUpdateCallback,
|
|
96
208
|
onUpdateOrContext?: ToolUpdateCallback | unknown,
|
|
@@ -322,7 +434,7 @@ export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
|
322
434
|
|
|
323
435
|
const artifacts = result.artifacts ?? [];
|
|
324
436
|
let content = result.content;
|
|
325
|
-
let mediaUrls =
|
|
437
|
+
let mediaUrls: string[] = [];
|
|
326
438
|
|
|
327
439
|
if (!content.trim()) {
|
|
328
440
|
for (const textArtifact of artifacts) {
|
|
@@ -331,10 +443,6 @@ export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
|
331
443
|
const downloaded = await oasnTransport.downloadArtifact(textArtifact.contentUrl || textArtifact.id);
|
|
332
444
|
if (isTextArtifact(downloaded)) {
|
|
333
445
|
content = artifactContentToText(downloaded.content);
|
|
334
|
-
mediaUrls = artifacts
|
|
335
|
-
.filter((a) => a.id !== textArtifact.id)
|
|
336
|
-
.map((a) => a.contentUrl)
|
|
337
|
-
.filter(Boolean);
|
|
338
446
|
break;
|
|
339
447
|
}
|
|
340
448
|
} catch (err) {
|
|
@@ -379,6 +487,10 @@ export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
|
379
487
|
: `Files from remote agent:\n${artifactLines.join('\n')}`;
|
|
380
488
|
}
|
|
381
489
|
|
|
490
|
+
if (artifacts.length > 0) {
|
|
491
|
+
mediaUrls = await cacheOasnArtifactsForMedia(oasnTransport, artifacts);
|
|
492
|
+
}
|
|
493
|
+
|
|
382
494
|
// Resolve OASN-relative URLs in content (remote agent may have embedded
|
|
383
495
|
// /webui/continuations/... or /api/artifacts/... in its output text).
|
|
384
496
|
if (content && accessBase) {
|
|
@@ -388,7 +500,7 @@ export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
|
388
500
|
);
|
|
389
501
|
}
|
|
390
502
|
|
|
391
|
-
// OASN
|
|
503
|
+
// OASN artifacts are cached locally before being handed to Gateway media rendering.
|
|
392
504
|
reply = {
|
|
393
505
|
status: 'complete',
|
|
394
506
|
content,
|
|
@@ -468,13 +580,16 @@ export function createCallRemoteAgentTool(): ToolDescriptor {
|
|
|
468
580
|
});
|
|
469
581
|
}
|
|
470
582
|
|
|
471
|
-
// Path 1: MEDIA: lines
|
|
583
|
+
// Path 1: explicit markdown links + MEDIA: lines for Gateway parser.
|
|
584
|
+
// The markdown block makes the link visible even when the host does not
|
|
585
|
+
// render MEDIA: attachments in assistant/tool-result bubbles.
|
|
586
|
+
const downloadLines = formatMediaDownloadLines(httpMediaUrls);
|
|
472
587
|
const mediaLines = httpMediaUrls
|
|
473
588
|
.map((url: string) => `MEDIA:${url}`)
|
|
474
589
|
.join('\n');
|
|
475
|
-
const textWithMedia = reply.content
|
|
476
|
-
|
|
477
|
-
|
|
590
|
+
const textWithMedia = [reply.content.trim(), downloadLines, mediaLines]
|
|
591
|
+
.filter(Boolean)
|
|
592
|
+
.join('\n\n');
|
|
478
593
|
|
|
479
594
|
return {
|
|
480
595
|
content: [{ type: 'text' as const, text: textWithMedia }],
|