lightclawbot 1.2.0-beta.2 → 1.2.3
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/config.js +1 -1
- package/dist/src/download-tool.js +3 -3
- package/dist/src/file-storage.js +11 -11
- package/dist/src/inbound.js +12 -11
- package/dist/src/socket/handlers.js +7 -7
- package/dist/src/streaming/stream-reply-sink.js +2 -2
- package/openclaw.plugin.json +77 -3
- package/package.json +1 -1
package/dist/src/config.js
CHANGED
|
@@ -32,7 +32,7 @@ export const WS_URL = `wss://${DOMAIN}`;
|
|
|
32
32
|
/** HTTP API 基础地址 */
|
|
33
33
|
export const API_BASE_URL = `https://${DOMAIN}`;
|
|
34
34
|
/** 文件存储相关配置 */
|
|
35
|
-
export const
|
|
35
|
+
export const SERVER_UPLOAD_BASE_URL = `https://${DOMAIN}`;
|
|
36
36
|
// ============================================================
|
|
37
37
|
// WebSocket 配置
|
|
38
38
|
// ============================================================
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import { getFileDownloadUrl,
|
|
12
|
+
import { getFileDownloadUrl, downloadFileFromServer, uploadFileToServer, } from "./file-storage.js";
|
|
13
13
|
import { formatFileSize } from "./media.js";
|
|
14
14
|
import { resolveEffectiveApiKey } from "./config.js";
|
|
15
15
|
// ============================================================
|
|
@@ -79,7 +79,7 @@ export function registerDownloadTool(api) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
case "download_to_local": {
|
|
82
|
-
const result = await
|
|
82
|
+
const result = await downloadFileFromServer(filePath, { apiKey });
|
|
83
83
|
const targetDir = localDir || process.cwd();
|
|
84
84
|
// 确保目录存在
|
|
85
85
|
if (!fs.existsSync(targetDir)) {
|
|
@@ -107,7 +107,7 @@ export function registerDownloadTool(api) {
|
|
|
107
107
|
content: [{ type: "text", text: `Error: local file not found: ${filePath}` }],
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
const uploadResult = await
|
|
110
|
+
const uploadResult = await uploadFileToServer(filePath, { apiKey });
|
|
111
111
|
const fileName = path.basename(filePath);
|
|
112
112
|
return {
|
|
113
113
|
content: [{
|
package/dist/src/file-storage.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { guessMimeByExt } from "./media.js";
|
|
13
|
-
import {
|
|
13
|
+
import { SERVER_UPLOAD_BASE_URL, API_PATH_UPLOAD, API_PATH_DOWNLOAD, UPLOAD_TIMEOUT, } from "./config.js";
|
|
14
14
|
import { buildAuthHeaders } from './utils/index.js';
|
|
15
15
|
// ============================================================
|
|
16
16
|
// 核心方法
|
|
@@ -23,7 +23,7 @@ import { buildAuthHeaders } from './utils/index.js';
|
|
|
23
23
|
* @param customFileName - 自定义文件名(可选,默认使用原文件名)
|
|
24
24
|
* @returns 上传结果,包含公网 URL
|
|
25
25
|
*/
|
|
26
|
-
export async function
|
|
26
|
+
export async function uploadFileToServer(localPath, config = {}, customFileName) {
|
|
27
27
|
// 验证文件存在
|
|
28
28
|
if (!fs.existsSync(localPath)) {
|
|
29
29
|
throw new Error(`File not found: ${localPath}`);
|
|
@@ -48,7 +48,7 @@ export async function uploadFileToCos(localPath, config = {}, customFileName) {
|
|
|
48
48
|
const controller = new AbortController();
|
|
49
49
|
const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT);
|
|
50
50
|
try {
|
|
51
|
-
const response = await fetch(`${
|
|
51
|
+
const response = await fetch(`${SERVER_UPLOAD_BASE_URL}${API_PATH_UPLOAD}`, {
|
|
52
52
|
method: "POST",
|
|
53
53
|
body: formData,
|
|
54
54
|
headers,
|
|
@@ -60,7 +60,7 @@ export async function uploadFileToCos(localPath, config = {}, customFileName) {
|
|
|
60
60
|
}
|
|
61
61
|
const result = (await response.json());
|
|
62
62
|
if (result.code === 0 && result.data?.uploaded) {
|
|
63
|
-
return { url: `${
|
|
63
|
+
return { url: `${SERVER_UPLOAD_BASE_URL}${API_PATH_DOWNLOAD}?filePath=${filePath}`, filePath, isUploaded: true };
|
|
64
64
|
}
|
|
65
65
|
throw new Error(`Upload failed other (HTTP ${response.status}): ${result.data}`);
|
|
66
66
|
}
|
|
@@ -77,7 +77,7 @@ export async function uploadFileToCos(localPath, config = {}, customFileName) {
|
|
|
77
77
|
* @param config - 文件存储配置
|
|
78
78
|
* @returns 上传结果,包含公网 URL
|
|
79
79
|
*/
|
|
80
|
-
export async function
|
|
80
|
+
export async function uploadBufferToServer(buffer, fileName, mimeType, config = {}) {
|
|
81
81
|
const filePath = `${Date.now()}/${fileName}`;
|
|
82
82
|
const { Blob: NodeBlob } = await import("node:buffer");
|
|
83
83
|
const blob = new NodeBlob([new Uint8Array(buffer)], { type: mimeType });
|
|
@@ -88,7 +88,7 @@ export async function uploadBufferToCos(buffer, fileName, mimeType, config = {})
|
|
|
88
88
|
const controller = new AbortController();
|
|
89
89
|
const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT);
|
|
90
90
|
try {
|
|
91
|
-
const response = await fetch(`${
|
|
91
|
+
const response = await fetch(`${SERVER_UPLOAD_BASE_URL}${API_PATH_UPLOAD}`, {
|
|
92
92
|
method: "POST",
|
|
93
93
|
body: formData,
|
|
94
94
|
headers,
|
|
@@ -100,7 +100,7 @@ export async function uploadBufferToCos(buffer, fileName, mimeType, config = {})
|
|
|
100
100
|
}
|
|
101
101
|
const result = (await response.json());
|
|
102
102
|
if (result.code === 0 && result.data?.uploaded) {
|
|
103
|
-
return { url: `${
|
|
103
|
+
return { url: `${SERVER_UPLOAD_BASE_URL}${API_PATH_DOWNLOAD}?filePath=${filePath}`, filePath, isUploaded: true };
|
|
104
104
|
}
|
|
105
105
|
throw new Error(`Upload failed (HTTP ${response.status}): ${result.data}`);
|
|
106
106
|
}
|
|
@@ -116,7 +116,7 @@ export async function uploadBufferToCos(buffer, fileName, mimeType, config = {})
|
|
|
116
116
|
* @returns 公网下载 URL
|
|
117
117
|
*/
|
|
118
118
|
export function getFileDownloadUrl(filePath) {
|
|
119
|
-
return `${
|
|
119
|
+
return `${SERVER_UPLOAD_BASE_URL}${API_PATH_DOWNLOAD}?filePath=${encodeURIComponent(filePath)}`;
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* 从云端存储下载文件内容
|
|
@@ -125,8 +125,8 @@ export function getFileDownloadUrl(filePath) {
|
|
|
125
125
|
* @param config - 文件存储配置
|
|
126
126
|
* @returns 下载结果,包含 Buffer 和文件信息
|
|
127
127
|
*/
|
|
128
|
-
export async function
|
|
129
|
-
const url = filePath.startsWith("http") ? filePath : `${
|
|
128
|
+
export async function downloadFileFromServer(filePath, config = {}) {
|
|
129
|
+
const url = filePath.startsWith("http") ? filePath : `${SERVER_UPLOAD_BASE_URL}${API_PATH_DOWNLOAD}?filePath=${encodeURIComponent(filePath)}`;
|
|
130
130
|
const headers = buildAuthHeaders(config.apiKey ?? "");
|
|
131
131
|
const controller = new AbortController();
|
|
132
132
|
const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT);
|
|
@@ -154,6 +154,6 @@ export async function downloadFileFromCos(filePath, config = {}) {
|
|
|
154
154
|
* @returns 公网可访问的下载 URL
|
|
155
155
|
*/
|
|
156
156
|
export async function uploadAndGetPublicUrl(localPath, config = {}) {
|
|
157
|
-
const result = await
|
|
157
|
+
const result = await uploadFileToServer(localPath, config);
|
|
158
158
|
return result.url || '';
|
|
159
159
|
}
|
package/dist/src/inbound.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* - /stop 及自然语言 abort 由 tryFastAbortFromMessage 统一处理并递归 kill subagent。
|
|
9
9
|
*/
|
|
10
10
|
import { emitSignal } from './utils/common.js';
|
|
11
|
-
import { CHANNEL_KEY, MEDIA_MAX_BYTES, resolveEffectiveApiKey, setSessionApiKey, DEFAULT_AGENT_ID } from './config.js';
|
|
11
|
+
import { CHANNEL_KEY, MEDIA_MAX_BYTES, resolveEffectiveApiKey, setSessionApiKey, DEFAULT_AGENT_ID, LOCALFILE_SCHEME } from './config.js';
|
|
12
12
|
import { getLightclawRuntime } from './runtime.js';
|
|
13
13
|
import { createChannelReplyPipeline } from 'openclaw/plugin-sdk/channel-reply-pipeline';
|
|
14
14
|
import { generateMsgId } from './dedup.js';
|
|
15
15
|
import { parseDataUrl, formatFileSize } from './media.js';
|
|
16
|
-
import {
|
|
16
|
+
import { uploadFileToServer, downloadFileFromServer, getFileDownloadUrl } from './file-storage.js';
|
|
17
17
|
import { createStreamReplyConfig } from './streaming/index.js';
|
|
18
18
|
export function createInboundHandler(account, emitter, log) {
|
|
19
19
|
return async (msg) => {
|
|
@@ -92,7 +92,7 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
92
92
|
try {
|
|
93
93
|
let buffer;
|
|
94
94
|
let mimeType;
|
|
95
|
-
let
|
|
95
|
+
let uploadPublicUrl;
|
|
96
96
|
const parsed = file.bytes ? parseDataUrl(file.bytes) : null;
|
|
97
97
|
if (parsed) {
|
|
98
98
|
buffer = parsed.buffer;
|
|
@@ -100,10 +100,10 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
100
100
|
}
|
|
101
101
|
else if (file.uri) {
|
|
102
102
|
log?.info(`[${CHANNEL_KEY}] File has URI, downloading from cloud: ${file.uri}`);
|
|
103
|
-
const downloaded = await
|
|
103
|
+
const downloaded = await downloadFileFromServer(file.uri, { apiKey: effectiveApiKey });
|
|
104
104
|
buffer = downloaded.buffer;
|
|
105
105
|
mimeType = file.mimeType;
|
|
106
|
-
|
|
106
|
+
uploadPublicUrl = file.uri.startsWith('http') ? file.uri : getFileDownloadUrl(file.uri);
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
109
|
// 来源 3:既无 bytes 也无 uri,无法处理,跳过此文件
|
|
@@ -114,19 +114,20 @@ export function createInboundHandler(account, emitter, log) {
|
|
|
114
114
|
const saved = await pluginRuntime.channel.media.saveMediaBuffer(buffer, mimeType, 'inbound', MEDIA_MAX_BYTES, file.name);
|
|
115
115
|
localMediaPaths.push(saved.path);
|
|
116
116
|
localMediaTypes.push(mimeType);
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const localPath = `${LOCALFILE_SCHEME}${saved.path}`;
|
|
118
|
+
if (uploadPublicUrl) {
|
|
119
|
+
publicMediaUrls.push(localPath);
|
|
119
120
|
}
|
|
120
121
|
else {
|
|
121
|
-
// data URL
|
|
122
|
+
// data URL 来源:用户自行上传到服务器
|
|
122
123
|
try {
|
|
123
|
-
const uploadResult = await
|
|
124
|
-
publicMediaUrls.push(
|
|
124
|
+
const uploadResult = await uploadFileToServer(saved.path, { apiKey: effectiveApiKey });
|
|
125
|
+
publicMediaUrls.push(localPath);
|
|
125
126
|
log?.info(`[${CHANNEL_KEY}] Uploaded inbound file: ${saved.path} → ${uploadResult.url}`);
|
|
126
127
|
}
|
|
127
128
|
catch (uploadErr) {
|
|
128
129
|
log?.warn(`[${CHANNEL_KEY}] Upload failed, falling back to local path: ${uploadErr}`);
|
|
129
|
-
publicMediaUrls.push(
|
|
130
|
+
publicMediaUrls.push(localPath);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
const attachmentUrl = publicMediaUrls[publicMediaUrls.length - 1];
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* 所有出站 socket.emit 通过 ReliableEmitter 实现 ACK 确认 + 自动重试,
|
|
13
13
|
* 保证消息在网络抖动时不丢失。
|
|
14
14
|
*/
|
|
15
|
-
import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, EVENT_HISTORY_REQUEST, EVENT_HISTORY_RESPONSE, DEFAULT_HISTORY_LIMIT, DEFAULT_AGENT_ID, EVENT_SESSIONS_REQUEST, EVENT_SESSIONS_RESPONSE, KIND_FILE_DOWNLOAD, FILE_DOWNLOAD_STATUS
|
|
15
|
+
import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, EVENT_HISTORY_REQUEST, EVENT_HISTORY_RESPONSE, DEFAULT_HISTORY_LIMIT, DEFAULT_AGENT_ID, EVENT_SESSIONS_REQUEST, EVENT_SESSIONS_RESPONSE, KIND_FILE_DOWNLOAD, FILE_DOWNLOAD_STATUS } from '../config.js';
|
|
16
16
|
import { isDuplicate, debounceHistoryRequest, generateMsgId } from '../dedup.js';
|
|
17
17
|
import { getLightclawRuntime } from '../runtime.js';
|
|
18
18
|
import { readSessionHistoryWithCron, listSessions } from '../history/index.js';
|
|
19
|
-
import {
|
|
19
|
+
import { uploadFileToServer } from '../file-storage.js';
|
|
20
20
|
import { guessMimeByExt } from '../media.js';
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import * as path from 'node:path';
|
|
@@ -51,7 +51,7 @@ export function bindSocketHandlers(socket, deps) {
|
|
|
51
51
|
if (isDuplicate(data.msgId))
|
|
52
52
|
return;
|
|
53
53
|
onEvent?.();
|
|
54
|
-
void handleFileDownloadReq(data, botClientId, reliableEmitter, log);
|
|
54
|
+
void handleFileDownloadReq(data, botClientId, reliableEmitter, account, log);
|
|
55
55
|
}
|
|
56
56
|
// 其他 status(ready/url/error)是本端自己发出的下行消息,理论上不会从前端回流;
|
|
57
57
|
// 即便误触发,此处直接 return 不做处理,避免进入 AI 处理队列
|
|
@@ -199,14 +199,14 @@ export function bindSocketHandlers(socket, deps) {
|
|
|
199
199
|
// 1. 从 data.extra.transferData 解析 transferId / localPath
|
|
200
200
|
// 2. 基础校验:必须是绝对路径,文件必须存在
|
|
201
201
|
// 3. 先回告 download_ready 帧(transferId + 文件元数据),前端据此更新下载按钮状态
|
|
202
|
-
// 4. 通过
|
|
202
|
+
// 4. 通过 uploadFileToServer 将本机文件上传到 ai-server,拿到下载 URL
|
|
203
203
|
// 5. 回告 download_url 帧(transferId + url),前端触发浏览器原生下载
|
|
204
204
|
// 6. 任一阶段失败 → 回告 download_error 帧(transferId + error message)
|
|
205
205
|
//
|
|
206
206
|
// 备注:不走目录白名单校验,因为 localPath 来自 AI 工具返回的 localfile:// 链接,
|
|
207
207
|
// AI 只会返回自己刚写盘的文件;上传到 ai-server 后会经过一轮内容审核兜底。
|
|
208
208
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
209
|
-
async function handleFileDownloadReq(data, botClientId, reliableEmitter, log) {
|
|
209
|
+
async function handleFileDownloadReq(data, botClientId, reliableEmitter, account, log) {
|
|
210
210
|
const transferData = data.extra?.transferData;
|
|
211
211
|
const transferId = transferData?.transferId;
|
|
212
212
|
const localPath = transferData?.localPath;
|
|
@@ -268,8 +268,8 @@ async function handleFileDownloadReq(data, botClientId, reliableEmitter, log) {
|
|
|
268
268
|
// 上传本机文件到 ai-server(含审核),成功后把 URL 回告前端
|
|
269
269
|
try {
|
|
270
270
|
// 通过 senderId 解析真实 apiKey,用于 /drive/save 鉴权
|
|
271
|
-
const apiKey =
|
|
272
|
-
const uploadResult = await
|
|
271
|
+
const apiKey = account.apiKey;
|
|
272
|
+
const uploadResult = await uploadFileToServer(resolvedPath, { apiKey });
|
|
273
273
|
if (!uploadResult.isUploaded || !uploadResult.url) {
|
|
274
274
|
sendError('Upload to ai-server failed');
|
|
275
275
|
return;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* counts / *Count 计数器,供 markComplete 推断 NO_REPLY 原因
|
|
14
14
|
*/
|
|
15
15
|
import { CHANNEL_KEY } from "../config.js";
|
|
16
|
-
import {
|
|
16
|
+
import { uploadFileToServer } from "../file-storage.js";
|
|
17
17
|
import { mediaUrlsToFiles } from "../media.js";
|
|
18
18
|
import { createDeltaTrackerState, toStreamDeltaText } from "./delta-tracker.js";
|
|
19
19
|
import { emitSignal } from "../utils/common.js";
|
|
@@ -87,7 +87,7 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
|
|
|
87
87
|
if (localPath.startsWith("/") || localPath.match(/^[A-Za-z]:\\/)) {
|
|
88
88
|
const { existsSync } = await import("node:fs");
|
|
89
89
|
if (existsSync(localPath)) {
|
|
90
|
-
const result = await
|
|
90
|
+
const result = await uploadFileToServer(localPath, storageConfig);
|
|
91
91
|
publicUrls.push(result.url || "");
|
|
92
92
|
log?.info(`[${CHANNEL_KEY}] [stream] Uploaded to COS: ${localPath} → ${result.url}`);
|
|
93
93
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,10 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "lightclawbot",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"name": "LightClawBot",
|
|
4
|
+
"description": "Connect OpenClaw to your LightClawBot via WebSocket long-connection",
|
|
5
|
+
"kind": "channel",
|
|
6
|
+
"channels": [
|
|
7
|
+
"lightclawbot"
|
|
8
|
+
],
|
|
9
|
+
"skills": [
|
|
10
|
+
"./skills"
|
|
11
|
+
],
|
|
5
12
|
"configSchema": {
|
|
6
13
|
"type": "object",
|
|
7
14
|
"additionalProperties": false,
|
|
8
15
|
"properties": {}
|
|
16
|
+
},
|
|
17
|
+
"channelConfigs": {
|
|
18
|
+
"lightclawbot": {
|
|
19
|
+
"label": "LightClawBot",
|
|
20
|
+
"description": "Connect OpenClaw to your LightClawBot via WebSocket long-connection",
|
|
21
|
+
"schema": {
|
|
22
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": true,
|
|
25
|
+
"properties": {
|
|
26
|
+
"enabled": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"description": "Whether this channel is enabled."
|
|
29
|
+
},
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Human-readable account/channel name."
|
|
33
|
+
},
|
|
34
|
+
"dmPolicy": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["open", "allowlist", "disabled"],
|
|
37
|
+
"description": "Policy that controls who can DM the bot."
|
|
38
|
+
},
|
|
39
|
+
"allowFrom": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "type": "string" },
|
|
42
|
+
"description": "Allowlist of user IDs ('*' means everyone)."
|
|
43
|
+
},
|
|
44
|
+
"systemPrompt": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Custom system prompt applied to this account."
|
|
47
|
+
},
|
|
48
|
+
"defaultAccount": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Default account id to use when none is specified."
|
|
51
|
+
},
|
|
52
|
+
"accounts": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"description": "Per-account configuration keyed by account id (typically the uin).",
|
|
55
|
+
"additionalProperties": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"enabled": { "type": "boolean" },
|
|
60
|
+
"name": { "type": "string" },
|
|
61
|
+
"apiKey": { "type": "string" },
|
|
62
|
+
"apiBaseUrl": { "type": "string" },
|
|
63
|
+
"dmPolicy": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": ["open", "allowlist", "disabled"]
|
|
66
|
+
},
|
|
67
|
+
"allowFrom": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": { "type": "string" }
|
|
70
|
+
},
|
|
71
|
+
"systemPrompt": { "type": "string" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"contracts": {
|
|
80
|
+
"tools": [
|
|
81
|
+
"lightclaw_upload_file"
|
|
82
|
+
]
|
|
9
83
|
}
|
|
10
|
-
}
|
|
84
|
+
}
|