@ynhcj/xiaoyi-channel 1.1.17 → 1.1.18
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/bot.js +1 -0
- package/dist/src/file-download.js +3 -6
- package/dist/src/file-upload.js +52 -5
- package/dist/src/outbound.js +2 -7
- package/dist/src/provider.js +34 -3
- package/dist/src/tools/upload-file-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +6 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -0
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -177,6 +177,7 @@ export async function handleXYMessage(params) {
|
|
|
177
177
|
const fileParts = extractFileParts(parsed.parts);
|
|
178
178
|
// Download files to local disk
|
|
179
179
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
180
|
+
console.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
180
181
|
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
181
182
|
// Resolve envelope format options (following feishu pattern)
|
|
182
183
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { logger } from "./utils/logger.js";
|
|
6
5
|
/**
|
|
7
6
|
* Download a file from URL to local path.
|
|
8
7
|
*/
|
|
9
8
|
export async function downloadFile(url, destPath) {
|
|
10
|
-
logger.debug(`Downloading file from ${url} to ${destPath}`);
|
|
11
9
|
const controller = new AbortController();
|
|
12
10
|
const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
|
|
13
11
|
try {
|
|
@@ -18,14 +16,13 @@ export async function downloadFile(url, destPath) {
|
|
|
18
16
|
const arrayBuffer = await response.arrayBuffer();
|
|
19
17
|
const buffer = Buffer.from(arrayBuffer);
|
|
20
18
|
await fs.writeFile(destPath, buffer);
|
|
21
|
-
logger.debug(`File downloaded successfully: ${destPath}`);
|
|
22
19
|
}
|
|
23
20
|
catch (error) {
|
|
24
21
|
if (error.name === 'AbortError') {
|
|
25
|
-
|
|
22
|
+
console.log(`Download timeout (30s) for ${url}`);
|
|
26
23
|
throw new Error(`Download timeout after 30 seconds`);
|
|
27
24
|
}
|
|
28
|
-
|
|
25
|
+
console.log(`Failed to download file from ${url}:`);
|
|
29
26
|
throw error;
|
|
30
27
|
}
|
|
31
28
|
finally {
|
|
@@ -54,7 +51,7 @@ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_chann
|
|
|
54
51
|
});
|
|
55
52
|
}
|
|
56
53
|
catch (error) {
|
|
57
|
-
|
|
54
|
+
console.log(`Failed to download file ${name}:`);
|
|
58
55
|
// Continue with other files
|
|
59
56
|
}
|
|
60
57
|
}
|
package/dist/src/file-upload.js
CHANGED
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
// OSMS file upload implementation
|
|
3
3
|
import fetch from "node-fetch";
|
|
4
4
|
import fs from "fs/promises";
|
|
5
|
+
import os from "os";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import { calculateSHA256 } from "./utils/crypto.js";
|
|
8
|
+
function isRemoteUrl(filePath) {
|
|
9
|
+
return filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
10
|
+
}
|
|
11
|
+
async function downloadToTempFile(url) {
|
|
12
|
+
console.log(`[XY File Upload] Downloading remote file: ${url}`);
|
|
13
|
+
const response = await fetch(url);
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(`Failed to download remote file: HTTP ${response.status}`);
|
|
16
|
+
}
|
|
17
|
+
const buffer = await response.buffer();
|
|
18
|
+
const urlFileName = path.basename(new URL(url).pathname) || "download";
|
|
19
|
+
const tempPath = path.join(os.tmpdir(), `xy-upload-${Date.now()}-${urlFileName}`);
|
|
20
|
+
await fs.writeFile(tempPath, buffer);
|
|
21
|
+
console.log(`[XY File Upload] Downloaded to temp file: ${tempPath}`);
|
|
22
|
+
return tempPath;
|
|
23
|
+
}
|
|
7
24
|
/**
|
|
8
25
|
* Service for uploading files to XY file storage.
|
|
9
26
|
* Implements three-phase upload: prepare → upload → complete.
|
|
@@ -23,10 +40,17 @@ export class XYFileUploadService {
|
|
|
23
40
|
*/
|
|
24
41
|
async uploadFile(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
25
42
|
console.log(`[XY File Upload] Starting file upload: ${filePath}`);
|
|
43
|
+
let localFilePath = filePath;
|
|
44
|
+
let isTempFile = false;
|
|
26
45
|
try {
|
|
46
|
+
// Handle remote URLs by downloading first
|
|
47
|
+
if (isRemoteUrl(filePath)) {
|
|
48
|
+
localFilePath = await downloadToTempFile(filePath);
|
|
49
|
+
isTempFile = true;
|
|
50
|
+
}
|
|
27
51
|
// Read file
|
|
28
|
-
const fileBuffer = await fs.readFile(
|
|
29
|
-
const fileName = path.basename(
|
|
52
|
+
const fileBuffer = await fs.readFile(localFilePath);
|
|
53
|
+
const fileName = path.basename(localFilePath);
|
|
30
54
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
31
55
|
const fileSize = fileBuffer.length;
|
|
32
56
|
// Phase 1: Prepare
|
|
@@ -96,7 +120,15 @@ export class XYFileUploadService {
|
|
|
96
120
|
}
|
|
97
121
|
catch (error) {
|
|
98
122
|
console.error(`[XY File Upload] File upload failed for ${filePath}:`, error);
|
|
99
|
-
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (isTempFile) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.unlink(localFilePath);
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
100
132
|
}
|
|
101
133
|
}
|
|
102
134
|
/**
|
|
@@ -104,10 +136,17 @@ export class XYFileUploadService {
|
|
|
104
136
|
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
105
137
|
*/
|
|
106
138
|
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
139
|
+
let localFilePath = filePath;
|
|
140
|
+
let isTempFile = false;
|
|
107
141
|
try {
|
|
142
|
+
// Handle remote URLs by downloading first
|
|
143
|
+
if (isRemoteUrl(filePath)) {
|
|
144
|
+
localFilePath = await downloadToTempFile(filePath);
|
|
145
|
+
isTempFile = true;
|
|
146
|
+
}
|
|
108
147
|
// Read file
|
|
109
|
-
const fileBuffer = await fs.readFile(
|
|
110
|
-
const fileName = path.basename(
|
|
148
|
+
const fileBuffer = await fs.readFile(localFilePath);
|
|
149
|
+
const fileName = path.basename(localFilePath);
|
|
111
150
|
const fileSha256 = calculateSHA256(fileBuffer);
|
|
112
151
|
const fileSize = fileBuffer.length;
|
|
113
152
|
// Phase 1: Prepare
|
|
@@ -186,6 +225,14 @@ export class XYFileUploadService {
|
|
|
186
225
|
console.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
|
|
187
226
|
throw error;
|
|
188
227
|
}
|
|
228
|
+
finally {
|
|
229
|
+
if (isTempFile) {
|
|
230
|
+
try {
|
|
231
|
+
await fs.unlink(localFilePath);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
189
236
|
}
|
|
190
237
|
/**
|
|
191
238
|
* Upload multiple files and return their file IDs.
|
package/dist/src/outbound.js
CHANGED
|
@@ -174,14 +174,9 @@ export const xyOutbound = {
|
|
|
174
174
|
}
|
|
175
175
|
// Upload file
|
|
176
176
|
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
177
|
-
// Check if fileId is empty
|
|
177
|
+
// Check if fileId is empty (should not happen if uploadFile throws on failure)
|
|
178
178
|
if (!fileId) {
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
channel: "xiaoyi-channel",
|
|
182
|
-
messageId: "",
|
|
183
|
-
chatId: to,
|
|
184
|
-
};
|
|
179
|
+
throw new Error(`File upload returned empty fileId for: ${mediaUrl}`);
|
|
185
180
|
}
|
|
186
181
|
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
187
182
|
fileId,
|
package/dist/src/provider.js
CHANGED
|
@@ -24,10 +24,10 @@ function encodeUid(uid) {
|
|
|
24
24
|
return createHash("sha256").update(uid).digest("hex").slice(0, 32);
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Get uid from
|
|
27
|
+
* Get uid from channel config (OpenClawConfig -> channels -> xiaoyi-channel -> uid).
|
|
28
28
|
*/
|
|
29
29
|
function getUidFromConfig(config) {
|
|
30
|
-
return config?.
|
|
30
|
+
return config?.channels?.["xiaoyi-channel"]?.uid;
|
|
31
31
|
}
|
|
32
32
|
export const xiaoyiProvider = {
|
|
33
33
|
id: "xiaoyiprovider",
|
|
@@ -101,6 +101,35 @@ export const xiaoyiProvider = {
|
|
|
101
101
|
if (context.systemPrompt) {
|
|
102
102
|
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
103
103
|
}
|
|
104
|
+
// 在发送给模型前,优化 systemPrompt 结构
|
|
105
|
+
if (context.systemPrompt) {
|
|
106
|
+
let sp = context.systemPrompt;
|
|
107
|
+
const beforeLen = sp.length;
|
|
108
|
+
// 删除 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
109
|
+
sp = sp.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
|
|
110
|
+
// (1) 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
|
|
111
|
+
const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
|
|
112
|
+
const part1 = skillsMatch ? skillsMatch[0] : '';
|
|
113
|
+
// (2) 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 ## /home/sandbox/.openclaw/workspace/TOOLS.md 之前的内容作为第二部分
|
|
114
|
+
const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?)(?=## \/home\/sandbox\/\.openclaw\/workspace\/TOOLS\.md)/);
|
|
115
|
+
const part2 = soulMatch ? soulMatch[1].trim() : '';
|
|
116
|
+
if (part1 || part2) {
|
|
117
|
+
// 从原始位置删除已提取的部分
|
|
118
|
+
if (skillsMatch)
|
|
119
|
+
sp = sp.replace(skillsMatch[0], '');
|
|
120
|
+
if (soulMatch)
|
|
121
|
+
sp = sp.replace(soulMatch[1], '');
|
|
122
|
+
// 清理多余空行
|
|
123
|
+
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
124
|
+
// (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
|
|
125
|
+
const combined = (part2 + '\n\n' + part1).trim();
|
|
126
|
+
if (combined && sp.includes('## Runtime')) {
|
|
127
|
+
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
131
|
+
context.systemPrompt = sp;
|
|
132
|
+
}
|
|
104
133
|
const stream = await underlying(model, context, {
|
|
105
134
|
...options,
|
|
106
135
|
headers: {
|
|
@@ -109,7 +138,9 @@ export const xiaoyiProvider = {
|
|
|
109
138
|
},
|
|
110
139
|
});
|
|
111
140
|
// 异步监听输出(不阻塞 stream 返回)
|
|
112
|
-
stream.result().then((
|
|
141
|
+
stream.result().then((result) => {
|
|
142
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
|
|
143
|
+
}, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
|
|
113
144
|
return stream;
|
|
114
145
|
};
|
|
115
146
|
},
|
|
@@ -18,10 +18,10 @@ export const uploadFileTool = {
|
|
|
18
18
|
label: "Upload File",
|
|
19
19
|
description: `工具能力描述:将手机本地文件上传并获取可公网访问的 URL。
|
|
20
20
|
|
|
21
|
-
前置工具调用:此工具使用前必须先调用 search_file 或者
|
|
21
|
+
前置工具调用:此工具使用前必须先调用 search_file 或者 query_collection 工具获取文件的 uri=
|
|
22
22
|
|
|
23
23
|
工具参数说明:
|
|
24
|
-
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者
|
|
24
|
+
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者query_collection返回结果中的uri),必须与search_file或者query_collection结果中对应的uri完全保持一致,不要自行修改。
|
|
25
25
|
b. fileInfos中的timeout字段是可选的,表示上传文件超时时间,单位是毫秒,默认是20000(20秒)。
|
|
26
26
|
c. fileInfos 是文件在手机本地的信息数组(从 search_file 工具响应中获取)。限制:每次最多支持传入 5 条文件信息。
|
|
27
27
|
|
|
@@ -28,6 +28,7 @@ export const xiaoyiAddCollectionTool = {
|
|
|
28
28
|
● dataType:必填字段,数据类型为string,功能描述是标识数据类型。HYPER_LINK标识网页,TEXT标识文本,IMAGE标识图片,FILE标识文件。
|
|
29
29
|
● title:非必填字段,数据类型为string,功能描述是标识文件类型数据的文件名称。适用于FILE类型。
|
|
30
30
|
说明:如果dataType为HYPER_LINK或TEXT,则content字段必填且不能为空;如果dataType为IMAGE或FILE,则uri字段必填且不能为空。当用户希望收藏海报、截图等图片类数据时,请将数据以图片IMAGE的形式存入到小艺帮记;当用户希望收藏电子书、笔记、报告、素材、文档、合同、协议、简历、证书、报表、日志、安装包、压缩包等描述的文件时,请将数据以文件FILE的形式存入到小艺帮记。
|
|
31
|
+
当你成功收藏这个数据到小艺帮记后,请在最后显示"已成功把数据添加到[小艺帮记](vassistant://voice/main?page=CollectionPage&jumpHomePageTab=myCollection)",
|
|
31
32
|
注意:
|
|
32
33
|
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
33
34
|
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
|
@@ -44,7 +45,11 @@ export const xiaoyiAddCollectionTool = {
|
|
|
44
45
|
},
|
|
45
46
|
uri: {
|
|
46
47
|
type: "string",
|
|
47
|
-
description:
|
|
48
|
+
description: `必填字段(IMAGE/FILE类型时)。图片或文件的地址链接。
|
|
49
|
+
uri具备三种严格的格式
|
|
50
|
+
(1) http或者https开头的公网链接,一般是用户通过联网搜索获取的文件地址,不允许自行编造,此类链接填入时必须确保真实
|
|
51
|
+
(2) file://开头的链接,此类链接必须来源于query_collection结果或者search_file或者search_photo_gallery的结果,不允许自行编造
|
|
52
|
+
(3) 本地路径例,如/tmp/xy_channel/xxx,此类路径是文件保存在当前openclaw运行环境的本地目录,可以直接填入,不要擅自拼接http://或者file://前缀`,
|
|
48
53
|
},
|
|
49
54
|
sourceAppBundleName: {
|
|
50
55
|
type: "string",
|
|
@@ -25,6 +25,7 @@ export const xiaoyiCollectionTool = {
|
|
|
25
25
|
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
26
26
|
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
|
27
27
|
c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
28
|
+
d. 如果用户希望获取文件,可以使用upload_file工具完成文件上传
|
|
28
29
|
|
|
29
30
|
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
|
|
30
31
|
`,
|