openclaw-openagent 1.0.12 → 1.0.14
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 +2 -0
- package/dist/src/config/config-schema.d.ts +9 -0
- package/dist/src/config/config-schema.js +6 -0
- package/dist/src/messaging/executor.d.ts +2 -3
- package/dist/src/messaging/executor.js +4 -31
- package/dist/src/plugin-ui/assets/openagent-override.js +124 -18
- package/dist/src/proxy/auth-proxy.js +5 -0
- package/dist/src/runtime/update-checker.d.ts +18 -0
- package/dist/src/runtime/update-checker.js +216 -0
- package/index.ts +3 -0
- package/package.json +1 -3
- package/src/app/channel-tools.ts +0 -249
- package/src/app/discovery-tools.ts +0 -273
- package/src/app/download-file-tool.ts +0 -133
- package/src/app/hooks.ts +0 -144
- package/src/app/index.ts +0 -84
- package/src/app/messaging-tools.ts +0 -79
- package/src/app/ops-tools.ts +0 -155
- package/src/app/remote-agent-tool.ts +0 -762
- package/src/app/types.ts +0 -68
- package/src/app/upload-file-tool.ts +0 -411
- package/src/app/verbose-preflight.ts +0 -190
- package/src/auth/config.ts +0 -266
- package/src/auth/credential-manager.ts +0 -146
- package/src/auth/index.ts +0 -24
- package/src/auth/verify.ts +0 -99
- package/src/channel.ts +0 -566
- package/src/compat.ts +0 -94
- package/src/config/config-schema.ts +0 -39
- package/src/messaging/aggregator.ts +0 -120
- package/src/messaging/collector.ts +0 -89
- package/src/messaging/executor.ts +0 -72
- package/src/messaging/inbound.ts +0 -150
- package/src/messaging/index.ts +0 -11
- package/src/messaging/mention-protocol.ts +0 -94
- package/src/messaging/process-c2c-request.ts +0 -564
- package/src/messaging/process-message.ts +0 -373
- package/src/messaging/scheduler.ts +0 -55
- package/src/messaging/types.ts +0 -38
- package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
- package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
- package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
- package/src/plugin-ui/adapters/oc-unknown.js +0 -48
- package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
- package/src/plugin-ui/assets/bg.png +0 -0
- package/src/plugin-ui/assets/icon.png +0 -0
- package/src/plugin-ui/assets/magic.svg +0 -5
- package/src/plugin-ui/assets/openagent-override.js +0 -11156
- package/src/plugin-ui/build.cjs +0 -463
- package/src/plugin-ui/index.ts +0 -18
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
- package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
- package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
- package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -833
- package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
- package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
- package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
- package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
- package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -256
- package/src/plugin-ui/modules/loader/bootstrap.js +0 -38
- package/src/plugin-ui/modules/loader/shared-state.js +0 -838
- package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
- package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
- package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
- package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
- package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
- package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
- package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
- package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
- package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
- package/src/plugin-ui/modules/remote-agent/styles.js +0 -896
- package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
- package/src/plugin-ui/postinstall-deploy.cjs +0 -52
- package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
- package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
- package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
- package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
- package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
- package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
- package/src/proxy/auth-proxy.ts +0 -409
- package/src/runtime/account.ts +0 -549
- package/src/runtime/index.ts +0 -7
- package/src/runtime/plugin-runtime.ts +0 -94
- package/src/runtime/registry.ts +0 -71
- package/src/sdk/CLASS_MAP.md +0 -143
- package/src/sdk/index.d.ts +0 -126
- package/src/sdk/index.js +0 -23990
- package/src/sdk/modules/cloud-search-module.js +0 -1117
- package/src/sdk/modules/follow-module.js +0 -1069
- package/src/sdk/modules/group-module.js +0 -7397
- package/src/sdk/modules/relationship-module.js +0 -2269
- package/src/sdk/modules/signaling-module.js +0 -1468
- package/src/sdk/modules/tim-upload-plugin.js +0 -730
- package/src/sdk/node-env/http-request.js +0 -90
- package/src/sdk/node-env/index.js +0 -57
- package/src/sdk/node-env/storage.js +0 -114
- package/src/sdk/package.json +0 -10
- package/src/sdk/tsconfig.json +0 -16
- package/src/state/pending-invocation-store.ts +0 -43
- package/src/state/store.ts +0 -756
- package/src/tim/c2c.ts +0 -451
- package/src/tim/channels.ts +0 -364
- package/src/tim/client.ts +0 -330
- package/src/tim/index.ts +0 -18
- package/src/tim/messages.ts +0 -166
- package/src/tim/sdk-logger-init.ts +0 -50
- package/src/tools.ts +0 -10
- package/src/transport/factory.ts +0 -95
- package/src/transport/oasn/index.ts +0 -17
- package/src/transport/oasn/oasn-agent-card.ts +0 -111
- package/src/transport/oasn/oasn-discovery.ts +0 -108
- package/src/transport/oasn/oasn-files.ts +0 -210
- package/src/transport/oasn/oasn-http.ts +0 -529
- package/src/transport/oasn/oasn-invocation.ts +0 -604
- package/src/transport/oasn/oasn-normalize.ts +0 -133
- package/src/transport/oasn/oasn-register.ts +0 -33
- package/src/transport/oasn/oasn-transport.ts +0 -318
- package/src/transport/oasn/oasn-types.ts +0 -404
- package/src/transport/tim/index.ts +0 -8
- package/src/transport/tim/tim-transport.ts +0 -515
- package/src/transport/types.ts +0 -538
- package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
- package/src/types/openclaw.d.ts +0 -97
- package/src/types/tencentcloud-chat.d.ts +0 -15
- package/src/util/http.ts +0 -113
- package/src/util/logger.ts +0 -131
- package/src/util/url-resolver.ts +0 -77
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { logger } from '../../util/logger.js';
|
|
4
|
-
import { findFileByPrefix, resolveOpenClawDistDir } from './locator.js';
|
|
5
|
-
import { restoreFile } from './backup.js';
|
|
6
|
-
|
|
7
|
-
export type RemovedRevertResult = 'none' | 'restored' | 'failed';
|
|
8
|
-
|
|
9
|
-
export type RemovedUIExtension = {
|
|
10
|
-
id: string;
|
|
11
|
-
/** Which shared file group this removed extension belongs to (if any). */
|
|
12
|
-
sharedFile?: string;
|
|
13
|
-
revert(controlUiDir: string): RemovedRevertResult;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function fileNeedsRemoval(filepath: string, id: string, fingerprints: string[]): boolean {
|
|
17
|
-
const content = readFileSync(filepath, 'utf-8');
|
|
18
|
-
return content.includes(`/*openagent:${id}`) || fingerprints.some(fp => content.includes(fp));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function restoreRemovedPatch(filepath: string | null, id: string, fingerprints: string[]): RemovedRevertResult {
|
|
22
|
-
if (!filepath) return 'none';
|
|
23
|
-
if (!fileNeedsRemoval(filepath, id, fingerprints)) return 'none';
|
|
24
|
-
|
|
25
|
-
const restored = restoreFile(filepath);
|
|
26
|
-
if (!restored) {
|
|
27
|
-
logger.warn(`[ui-ext] ${id}: removed extension is present but no backup exists; refusing to continue`);
|
|
28
|
-
return 'failed';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
logger.info(`[ui-ext] ${id}: removed extension reverted from backup`);
|
|
32
|
-
return 'restored';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const REMOVED_UI_EXTENSIONS: RemovedUIExtension[] = [
|
|
36
|
-
{
|
|
37
|
-
id: 'gateway-openagent-tool-payload',
|
|
38
|
-
revert(controlUiDir: string): RemovedRevertResult {
|
|
39
|
-
const distDir = resolveOpenClawDistDir(controlUiDir);
|
|
40
|
-
const gatewayPath = distDir ? findFileByPrefix(distDir, 'gateway-cli-') : null;
|
|
41
|
-
return restoreRemovedPatch(gatewayPath, this.id, [
|
|
42
|
-
'__openagentToolCallIds',
|
|
43
|
-
'__openagentKeepToolPayload',
|
|
44
|
-
'__openagentEffectiveToolVerbose',
|
|
45
|
-
]);
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'agent-event-openagent-tool-result',
|
|
50
|
-
revert(controlUiDir: string): RemovedRevertResult {
|
|
51
|
-
const distDir = resolveOpenClawDistDir(controlUiDir);
|
|
52
|
-
const agentBundlePath = distDir ? findFileByPrefix(distDir, 'auth-profiles-') : null;
|
|
53
|
-
return restoreRemovedPatch(agentBundlePath, this.id, [
|
|
54
|
-
'__openagentIsRemoteToolName',
|
|
55
|
-
]);
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'marked-expose',
|
|
60
|
-
sharedFile: 'index-bundle',
|
|
61
|
-
revert(controlUiDir: string): RemovedRevertResult {
|
|
62
|
-
const assetsDir = join(controlUiDir, 'assets');
|
|
63
|
-
const bundlePath = findFileByPrefix(assetsDir, 'index-');
|
|
64
|
-
return restoreRemovedPatch(bundlePath, this.id, [
|
|
65
|
-
'window.__openagentMarked=',
|
|
66
|
-
'__openagentMarked',
|
|
67
|
-
]);
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
];
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UIExtension 类型定义与注册表
|
|
3
|
-
*
|
|
4
|
-
* 每个 UIExtension 是一个独立的、可增删的扩展点。
|
|
5
|
-
* 框架只关心 id 和 version,具体操作由 apply/revert 内部处理。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface UIExtension {
|
|
9
|
-
/** 唯一标识(如 'override-script'、'html-script-tag') */
|
|
10
|
-
id: string;
|
|
11
|
-
|
|
12
|
-
/** 内容版本,改了 apply 逻辑就递增 */
|
|
13
|
-
version: number;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 共享文件标识(可选)。
|
|
17
|
-
*
|
|
18
|
-
* 多个扩展修改同一文件时(如 toolcard-render 和 toolcallid-propagate 都改 index-*.js),
|
|
19
|
-
* 设置相同的 sharedFile 值。reconciler 会把它们作为一组处理:
|
|
20
|
-
* 任何一个需要变更 → 恢复 backup → 重新 apply 全组中应保留的扩展。
|
|
21
|
-
*
|
|
22
|
-
* 不设置此字段的扩展独立 apply/revert,不受其他扩展影响。
|
|
23
|
-
*/
|
|
24
|
-
sharedFile?: string;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 每次启动都调用 apply(可选)。
|
|
28
|
-
*
|
|
29
|
-
* 默认行为:manifest version 匹配时跳过 apply。
|
|
30
|
-
* 设为 true 后:即使 version 不变也调用 apply,由 apply 内部判断是否需要操作。
|
|
31
|
-
* 适用于内容可能独立于 version 变化的扩展(如 override-script 的产物文件)。
|
|
32
|
-
*/
|
|
33
|
-
alwaysApply?: boolean;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 加载扩展。
|
|
37
|
-
* 接收 control-ui 目录路径,内部自行决定改什么文件、怎么改。
|
|
38
|
-
* 返回 true 表示成功,false 表示失败(不写入 manifest,下次重试)。
|
|
39
|
-
*/
|
|
40
|
-
apply: (controlUiDir: string, context?: UIExtensionContext) => boolean;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 卸载扩展。
|
|
44
|
-
* 还原 apply 所做的全部修改。
|
|
45
|
-
* 注意:共享文件的扩展不应单独调用 revert,由 reconciler 统一处理。
|
|
46
|
-
*/
|
|
47
|
-
revert: (controlUiDir: string) => void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface UIExtensionContext {
|
|
51
|
-
hostVersion?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* OpenAgent 标记前缀,注入的代码旁加此注释以标识修改来源。
|
|
56
|
-
* 格式:/*openagent:<extension-id>* /
|
|
57
|
-
*/
|
|
58
|
-
export const OPENAGENT_MARKER_PREFIX = '/*openagent:';
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 生成标记注释。
|
|
62
|
-
*/
|
|
63
|
-
export function makeMarker(extensionId: string): string {
|
|
64
|
-
return `/*openagent:${extensionId}*/`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 检查内容中是否包含指定扩展点的标记。
|
|
69
|
-
*/
|
|
70
|
-
export function hasMarker(content: string, extensionId: string): boolean {
|
|
71
|
-
return content.includes(makeMarker(extensionId));
|
|
72
|
-
}
|
package/src/proxy/auth-proxy.ts
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth API Proxy — Gateway-side reverse proxy for auth.ai-talk.live
|
|
3
|
-
*
|
|
4
|
-
* Eliminates CSP connect-src patching by proxying browser requests
|
|
5
|
-
* through a same-origin plugin HTTP route:
|
|
6
|
-
*
|
|
7
|
-
* Browser: fetch('/plugins/openagent/api/agents')
|
|
8
|
-
* → Gateway plugin-http stage
|
|
9
|
-
* → this handler
|
|
10
|
-
* → fetch('https://auth.ai-talk.live/api/agents')
|
|
11
|
-
* → response back to browser
|
|
12
|
-
*
|
|
13
|
-
* Registered via api.registerHttpRoute() in index.ts.
|
|
14
|
-
* See: docs/audit/022-A-loader-audit-v1.md §4.7–4.10
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
18
|
-
import fs from 'node:fs';
|
|
19
|
-
import nodePath from 'node:path';
|
|
20
|
-
import { logger } from '../util/logger.js';
|
|
21
|
-
import { registry } from '../runtime/registry.js';
|
|
22
|
-
import { isUnconfiguredOasnApiBase } from '../auth/config.js';
|
|
23
|
-
import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
|
|
24
|
-
|
|
25
|
-
/** TIM 路径默认上游(兼容旧版) */
|
|
26
|
-
const AUTH_UPSTREAM_TIM = 'https://auth.ai-talk.live';
|
|
27
|
-
const PROXY_TIMEOUT_MS = 15_000;
|
|
28
|
-
const MAX_BODY_BYTES = 64 * 1024; // 64KB — tagline API body is ~100 bytes
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 获取当前 OASN access API base URL,用于解析响应中的相对路径。
|
|
32
|
-
*/
|
|
33
|
-
function getOasnAccessBase(): string {
|
|
34
|
-
const rt = registry.getDefault();
|
|
35
|
-
if (rt?.transportType !== 'oasn') return '';
|
|
36
|
-
return (rt.config?.accessApiBase || rt.config?.apiBase || '').replace(/\/+$/, '');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 根据当前 active runtime 的 transport 类型决定上游。
|
|
41
|
-
*
|
|
42
|
-
* - transport='oasn':Discovery/onboarding 用 account.apiBase;
|
|
43
|
-
* Access-layer endpoint 用 account.accessApiBase(未配置时回落到 apiBase)
|
|
44
|
-
* - transport='tim' 或无 active runtime:fallback 到 AUTH_UPSTREAM_TIM
|
|
45
|
-
*
|
|
46
|
-
* 浏览器侧 Agent Book 通过 /plugins/openagent/api/* 统一访问,由此处分流。
|
|
47
|
-
*/
|
|
48
|
-
function pickUpstream(method: string, pathname: string): string | null {
|
|
49
|
-
const rt = registry.getDefault();
|
|
50
|
-
if (rt?.transportType === 'oasn') {
|
|
51
|
-
const apiBase = rt.config?.apiBase;
|
|
52
|
-
const accessApiBase = rt.config?.accessApiBase || apiBase;
|
|
53
|
-
const upstream = isAccessLayerProxyPath(method, pathname) ? accessApiBase : apiBase;
|
|
54
|
-
return isUnconfiguredOasnApiBase(upstream) ? null : upstream ?? null;
|
|
55
|
-
}
|
|
56
|
-
return AUTH_UPSTREAM_TIM;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Gateway base URL detection ──────────────────────────────────────────
|
|
60
|
-
// Captured from the first incoming HTTP request's Host header.
|
|
61
|
-
// Handles Docker port mapping: container port ≠ host port.
|
|
62
|
-
let _gatewayBaseUrl: string | undefined;
|
|
63
|
-
|
|
64
|
-
/** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
|
|
65
|
-
export function getGatewayBaseUrl(): string {
|
|
66
|
-
return _gatewayBaseUrl ?? 'http://localhost:18794';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Handler factory ─────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
export function createAuthProxyHandler() {
|
|
72
|
-
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
|
|
73
|
-
// Gateway strips the matched prefix; req.url is the path after prefix removal.
|
|
74
|
-
// E.g. /plugins/openagent/api/agents → req.url = /api/agents
|
|
75
|
-
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
76
|
-
logger.info(`[proxy] RECEIVED: ${req.method} ${req.url}`);
|
|
77
|
-
|
|
78
|
-
// Capture the gateway's external base URL from the browser's request.
|
|
79
|
-
// Reads standard reverse-proxy headers for correct protocol/host detection:
|
|
80
|
-
// - Local: http://localhost:18794
|
|
81
|
-
// - Docker: http://127.0.0.1:18794 (port mapped)
|
|
82
|
-
// - Nginx: https://oc6.openagent.club (X-Forwarded-Proto/Host)
|
|
83
|
-
if (!_gatewayBaseUrl && req.headers.host) {
|
|
84
|
-
const proto = (req.headers['x-forwarded-proto'] as string)?.split(',')[0]?.trim() || 'http';
|
|
85
|
-
const host = (req.headers['x-forwarded-host'] as string)?.split(',')[0]?.trim() || req.headers.host;
|
|
86
|
-
_gatewayBaseUrl = `${proto}://${host}`;
|
|
87
|
-
logger.info(`[proxy] Gateway base URL detected: ${_gatewayBaseUrl}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// req.url may or may not have the /plugins/openagent prefix stripped by Gateway
|
|
91
|
-
// (behavior varies across Gateway versions), so strip it defensively.
|
|
92
|
-
let targetPath = url.pathname;
|
|
93
|
-
if (targetPath.startsWith('/plugins/openagent')) {
|
|
94
|
-
targetPath = targetPath.slice('/plugins/openagent'.length);
|
|
95
|
-
}
|
|
96
|
-
// ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
|
|
97
|
-
// Files downloaded from remote agents are stored here. The media route must
|
|
98
|
-
// be detected after prefix normalization because Gateway versions disagree
|
|
99
|
-
// on whether /plugins/openagent has already been stripped.
|
|
100
|
-
if (targetPath.startsWith('/media/')) {
|
|
101
|
-
return serveLocalMedia(targetPath, res);
|
|
102
|
-
}
|
|
103
|
-
// v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
|
|
104
|
-
const method = req.method ?? 'GET';
|
|
105
|
-
const rt = registry.getDefault();
|
|
106
|
-
if (!rt && isOasnProxyPath(method, targetPath)) {
|
|
107
|
-
writeJson(res, 503, {
|
|
108
|
-
error: 'OASN account is not configured',
|
|
109
|
-
detail: 'Run openclaw-plugin-openagent-cli install to register this device before opening Agent Book.',
|
|
110
|
-
});
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
if (rt && rt.transportType !== 'oasn' && isOasnProxyPath(method, targetPath)) {
|
|
114
|
-
writeJson(res, 503, {
|
|
115
|
-
error: 'OASN transport is not active',
|
|
116
|
-
detail: 'Agent Book discovery requires an installed OASN account.',
|
|
117
|
-
});
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (rt?.transportType === 'oasn' && !isAllowedOasnProxyRequest(method, targetPath)) {
|
|
121
|
-
logger.warn(`[proxy] blocked OASN proxy path: ${method} ${targetPath}`);
|
|
122
|
-
writeJson(res, 403, { error: 'proxy path not allowed' });
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const upstreamBase = pickUpstream(method, targetPath);
|
|
127
|
-
if (!upstreamBase) {
|
|
128
|
-
writeJson(res, 503, { error: 'OASN api_base is not configured' });
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
const targetUrl = resolveOasnProxyTargetUrl(upstreamBase, targetPath, url.search);
|
|
132
|
-
|
|
133
|
-
logger.info(`[proxy] ${method} ${url.pathname} → ${targetUrl}`);
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
// Read request body for non-GET methods
|
|
137
|
-
let body: string | undefined;
|
|
138
|
-
if (method !== 'GET' && method !== 'HEAD') {
|
|
139
|
-
body = await readBody(req, MAX_BODY_BYTES);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Forward to upstream with timeout
|
|
143
|
-
const upstream = await fetch(targetUrl, {
|
|
144
|
-
method,
|
|
145
|
-
headers: buildUpstreamHeaders(req),
|
|
146
|
-
body,
|
|
147
|
-
signal: AbortSignal.timeout(PROXY_TIMEOUT_MS),
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Relay response (binary-safe for images)
|
|
151
|
-
res.statusCode = upstream.status;
|
|
152
|
-
const contentType = upstream.headers.get('content-type');
|
|
153
|
-
if (contentType) res.setHeader('Content-Type', contentType);
|
|
154
|
-
|
|
155
|
-
let buf = Buffer.from(await upstream.arrayBuffer());
|
|
156
|
-
|
|
157
|
-
// Resolve OASN-relative URLs in GET invocation responses so the
|
|
158
|
-
// frontend receives absolute URLs (launch_url, content_url, etc.).
|
|
159
|
-
if (method === 'GET' && /^\/api\/invocations\/[^/]+$/.test(targetPath) && buf.length > 0) {
|
|
160
|
-
try {
|
|
161
|
-
const isJson = contentType?.includes('json');
|
|
162
|
-
if (isJson) {
|
|
163
|
-
const oasnBase = getOasnAccessBase();
|
|
164
|
-
if (oasnBase) {
|
|
165
|
-
const text = buf.toString('utf-8');
|
|
166
|
-
const resolved = text.replace(
|
|
167
|
-
/"(launch_url|content_url)":"(\/[^"]+)"/g,
|
|
168
|
-
(_m: string, key: string, path: string) => {
|
|
169
|
-
try {
|
|
170
|
-
return `"${key}":"${new URL(path, oasnBase).toString()}"`;
|
|
171
|
-
} catch { return _m; }
|
|
172
|
-
},
|
|
173
|
-
);
|
|
174
|
-
buf = Buffer.from(resolved, 'utf-8');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch { /* best-effort; don't break proxy on parse errors */ }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
res.end(buf);
|
|
181
|
-
|
|
182
|
-
logger.info(`[proxy] ${method} ${url.pathname} → ${upstream.status} (${buf.length} bytes)`);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
const msg = (err as Error).message;
|
|
185
|
-
logger.warn(`[proxy] ${method} ${targetUrl} failed: ${msg}`);
|
|
186
|
-
|
|
187
|
-
if (!res.headersSent) {
|
|
188
|
-
writeJson(res, 502, { error: 'proxy upstream failed', detail: msg });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return true;
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function resolveOasnProxyTargetUrl(upstreamBase: string, targetPath: string, search = ''): string {
|
|
197
|
-
return resolveOasnRelativeUrl(upstreamBase, `${targetPath}${search}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── T4: Local media serve ────────────────────────────────────────────────
|
|
201
|
-
|
|
202
|
-
const MEDIA_DIR = '/tmp/openclaw/openagent-media';
|
|
203
|
-
|
|
204
|
-
/** MIME type lookup for common media extensions. */
|
|
205
|
-
const MIME_MAP: Record<string, string> = {
|
|
206
|
-
'.mp3': 'audio/mpeg',
|
|
207
|
-
'.wav': 'audio/wav',
|
|
208
|
-
'.ogg': 'audio/ogg',
|
|
209
|
-
'.m4a': 'audio/mp4',
|
|
210
|
-
'.mp4': 'video/mp4',
|
|
211
|
-
'.webm': 'video/webm',
|
|
212
|
-
'.png': 'image/png',
|
|
213
|
-
'.jpg': 'image/jpeg',
|
|
214
|
-
'.jpeg': 'image/jpeg',
|
|
215
|
-
'.gif': 'image/gif',
|
|
216
|
-
'.webp': 'image/webp',
|
|
217
|
-
'.pdf': 'application/pdf',
|
|
218
|
-
'.txt': 'text/plain',
|
|
219
|
-
'.md': 'text/markdown; charset=utf-8',
|
|
220
|
-
'.json': 'application/json; charset=utf-8',
|
|
221
|
-
'.zip': 'application/zip',
|
|
222
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
223
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
224
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Serve a file from the local openagent-media directory.
|
|
229
|
-
* Used by T4 remote file transfer — tool result references files as
|
|
230
|
-
* MEDIA:http://localhost:PORT/plugins/openagent/media/{filename}
|
|
231
|
-
*
|
|
232
|
-
* @returns true (request handled)
|
|
233
|
-
*/
|
|
234
|
-
function serveLocalMedia(pathname: string, res: ServerResponse): true {
|
|
235
|
-
// /media/{filename} → {filename} (prefix already stripped by Gateway)
|
|
236
|
-
const raw = pathname.replace('/media/', '');
|
|
237
|
-
const fileName = decodeURIComponent(raw);
|
|
238
|
-
|
|
239
|
-
// Security: block path traversal, null bytes, and directory components
|
|
240
|
-
if (!fileName || fileName.includes('..') || fileName.includes('/') || fileName.includes('\\') || fileName.includes('\0')) {
|
|
241
|
-
logger.warn(`[proxy/media] Blocked unsafe path: ${raw}`);
|
|
242
|
-
res.statusCode = 400;
|
|
243
|
-
res.end('Bad request');
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const filePath = nodePath.join(MEDIA_DIR, fileName);
|
|
248
|
-
|
|
249
|
-
// Double-check: resolved path must stay within MEDIA_DIR
|
|
250
|
-
if (!nodePath.resolve(filePath).startsWith(nodePath.resolve(MEDIA_DIR) + nodePath.sep) &&
|
|
251
|
-
nodePath.resolve(filePath) !== nodePath.resolve(MEDIA_DIR)) {
|
|
252
|
-
logger.warn(`[proxy/media] Path escape attempt: ${fileName} → ${filePath}`);
|
|
253
|
-
res.statusCode = 400;
|
|
254
|
-
res.end('Bad request');
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (!fs.existsSync(filePath)) {
|
|
259
|
-
logger.debug(`[proxy/media] File not found: ${filePath}`);
|
|
260
|
-
res.statusCode = 404;
|
|
261
|
-
res.end('Not found');
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
const ext = nodePath.extname(fileName).toLowerCase();
|
|
267
|
-
const contentType = MIME_MAP[ext] ?? 'application/octet-stream';
|
|
268
|
-
const stat = fs.statSync(filePath);
|
|
269
|
-
|
|
270
|
-
res.setHeader('Content-Type', contentType);
|
|
271
|
-
res.setHeader('Content-Length', stat.size);
|
|
272
|
-
if (shouldDownloadAsAttachment(contentType)) {
|
|
273
|
-
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
|
274
|
-
}
|
|
275
|
-
// Allow browser to cache media for 1 hour
|
|
276
|
-
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
277
|
-
|
|
278
|
-
const stream = fs.createReadStream(filePath);
|
|
279
|
-
stream.pipe(res);
|
|
280
|
-
stream.on('error', (err) => {
|
|
281
|
-
logger.error(`[proxy/media] Stream error: ${err.message} file=${fileName}`);
|
|
282
|
-
if (!res.headersSent) {
|
|
283
|
-
res.statusCode = 500;
|
|
284
|
-
res.end('Internal error');
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
logger.info(`[proxy/media] Serving ${fileName} (${contentType}, ${stat.size} bytes)`);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
logger.error(`[proxy/media] Error serving ${fileName}: ${(err as Error).message}`);
|
|
291
|
-
if (!res.headersSent) {
|
|
292
|
-
res.statusCode = 500;
|
|
293
|
-
res.end('Internal error');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function shouldDownloadAsAttachment(contentType: string): boolean {
|
|
301
|
-
return !contentType.startsWith('image/')
|
|
302
|
-
&& !contentType.startsWith('audio/')
|
|
303
|
-
&& !contentType.startsWith('video/')
|
|
304
|
-
&& !contentType.startsWith('text/plain');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Build headers for the upstream request.
|
|
311
|
-
* Forwards Content-Type and Authorization (needed by tagline API).
|
|
312
|
-
*
|
|
313
|
-
* OASN 模式:浏览器端没有 api_key,代理自动注入 Bearer token。
|
|
314
|
-
* TIM 模式:转发浏览器自带的 Authorization(如有)。
|
|
315
|
-
*/
|
|
316
|
-
function buildUpstreamHeaders(req: IncomingMessage): Record<string, string> {
|
|
317
|
-
const headers: Record<string, string> = {};
|
|
318
|
-
|
|
319
|
-
const ct = req.headers['content-type'];
|
|
320
|
-
if (ct) headers['Content-Type'] = ct;
|
|
321
|
-
|
|
322
|
-
const rt = registry.getDefault();
|
|
323
|
-
if (rt?.transportType === 'oasn') {
|
|
324
|
-
if (rt.config?.apiKey) headers['Authorization'] = `Bearer ${rt.config.apiKey}`;
|
|
325
|
-
return headers;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// TIM mode: keep forwarding browser Authorization for the legacy auth API.
|
|
329
|
-
const auth = req.headers['authorization'];
|
|
330
|
-
if (auth) {
|
|
331
|
-
headers['Authorization'] = auth;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return headers;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function isAllowedOasnProxyRequest(method: string, pathname: string): boolean {
|
|
338
|
-
const m = method.toUpperCase();
|
|
339
|
-
const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
340
|
-
const isRead = m === 'GET' || m === 'HEAD';
|
|
341
|
-
|
|
342
|
-
if (isRead && p === '/api/agent-card-categories') return true;
|
|
343
|
-
if (isRead && /^\/api\/agent-cards\/[^/]+$/.test(p)) return true;
|
|
344
|
-
if (m === 'POST' && /^\/api\/agent-cards\/search\/(by-query|by-category|by-tags)$/.test(p)) return true;
|
|
345
|
-
if (isRead && /^\/api\/agents\/[^/]+$/.test(p)) return true;
|
|
346
|
-
if (m === 'POST' && p === '/api/agents/claim') return true;
|
|
347
|
-
if (m === 'POST' && p === '/api/invocations') return true;
|
|
348
|
-
if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
|
|
349
|
-
if (m === 'POST' && p === '/api/files') return true;
|
|
350
|
-
if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
|
|
351
|
-
if (isRead && p.startsWith('/avatars/')) return true;
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function isAccessLayerProxyPath(method: string, pathname: string): boolean {
|
|
356
|
-
const m = method.toUpperCase();
|
|
357
|
-
const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
358
|
-
const isRead = m === 'GET' || m === 'HEAD';
|
|
359
|
-
if (m === 'POST' && p === '/api/invocations') return true;
|
|
360
|
-
if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
|
|
361
|
-
if (m === 'POST' && p === '/api/files') return true;
|
|
362
|
-
if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function isOasnProxyPath(method: string, pathname: string): boolean {
|
|
367
|
-
const m = method.toUpperCase();
|
|
368
|
-
const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
369
|
-
const isRead = m === 'GET' || m === 'HEAD';
|
|
370
|
-
if (isRead && p === '/api/agent-card-categories') return true;
|
|
371
|
-
if (isRead && /^\/api\/agent-cards\/[^/]+$/.test(p)) return true;
|
|
372
|
-
if (m === 'POST' && /^\/api\/agent-cards\/search\/(by-query|by-category|by-tags)$/.test(p)) return true;
|
|
373
|
-
if (isRead && /^\/api\/agents\/[^/]+$/.test(p)) return true;
|
|
374
|
-
if (m === 'POST' && p === '/api/agents/claim') return true;
|
|
375
|
-
if (m === 'POST' && p === '/api/invocations') return true;
|
|
376
|
-
if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
|
|
377
|
-
if (m === 'POST' && p === '/api/files') return true;
|
|
378
|
-
if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
|
|
379
|
-
return false;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function writeJson(res: ServerResponse, statusCode: number, body: Record<string, unknown>): void {
|
|
383
|
-
res.statusCode = statusCode;
|
|
384
|
-
res.setHeader('Content-Type', 'application/json');
|
|
385
|
-
res.end(JSON.stringify(body));
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Read the request body with a size limit.
|
|
390
|
-
*/
|
|
391
|
-
function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
const chunks: Buffer[] = [];
|
|
394
|
-
let total = 0;
|
|
395
|
-
|
|
396
|
-
req.on('data', (chunk: Buffer) => {
|
|
397
|
-
total += chunk.length;
|
|
398
|
-
if (total > maxBytes) {
|
|
399
|
-
req.destroy();
|
|
400
|
-
reject(new Error(`Request body exceeds ${maxBytes} bytes`));
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
chunks.push(chunk);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
407
|
-
req.on('error', reject);
|
|
408
|
-
});
|
|
409
|
-
}
|