@qihoo/tuitui-openclaw-channel 1.0.23 → 1.0.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
package/src/filespace.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { CHANNEL_ID } from "./const";
2
+ import { tuituiRobotApi, downloadUrl, parseChannelIdBySessionKey, tuituiRobotUpload} from "./robot_api"
1
3
 
2
4
  export interface FileSpaceItem {
3
5
  filename: string;
@@ -42,4 +44,181 @@ export function flattenFileSpaceList(list: any[]): FileSpaceItem[] {
42
44
  url: node.file_url,
43
45
  filesize: node.file_size,
44
46
  }));
45
- }
47
+ }
48
+
49
+
50
+
51
+ async function getChannelInfoBySessionKey(account: any, sessionKey: string) {
52
+ const channel_id = parseChannelIdBySessionKey(sessionKey);
53
+ if(!channel_id) {
54
+ return null;
55
+ }
56
+
57
+ const payload = {channel_id: channel_id};
58
+ const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
59
+ return body?.datas?.info;
60
+ }
61
+
62
+ export async function file_space_list(
63
+ account: any,
64
+ sessionKey: string
65
+ ): Promise<FileSpaceItem[]> {
66
+
67
+ const channelInfo = await getChannelInfoBySessionKey(account, sessionKey);
68
+ if(!channelInfo){
69
+ return [];
70
+ }
71
+
72
+ const payload = {
73
+ space_id: channelInfo.team_id,
74
+ space_type: "2",
75
+ };
76
+
77
+ const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
78
+
79
+ const list = data?.datas?.list;
80
+ const flat_list = flattenFileSpaceList(list);
81
+
82
+ console.log(`[${CHANNEL_ID}] file_space_list `, flat_list);
83
+ return flat_list
84
+ }
85
+
86
+ /**
87
+ * 确保目录路径存在,自动创建缺失的文件夹,返回最终目录的 parent_id
88
+ * @param account - TuiTui 账号
89
+ * @param spaceId - 空间 ID
90
+ * @param folderPath - 文件夹路径,如 "/projects/2024/q1"
91
+ * @returns 最终文件夹的 parent_id(用于添加文件)
92
+ */
93
+ async function ensureFolderPath(
94
+ account: any,
95
+ spaceId: string,
96
+ folderPath: string
97
+ ): Promise<string> {
98
+ // 标准化路径
99
+ const normalizedPath = folderPath.startsWith('/') ? folderPath : '/' + folderPath;
100
+
101
+ // 如果是根目录,返回空字符串
102
+ if (normalizedPath === '/') {
103
+ return "";
104
+ }
105
+
106
+ // 分割路径层级
107
+ const pathParts = normalizedPath.split('/').filter(part => part.trim());
108
+ if (pathParts.length === 0) {
109
+ return "";
110
+ }
111
+
112
+ // 一次性获取所有节点
113
+ const listPayload = {
114
+ space_id: spaceId,
115
+ space_type: "2",
116
+ };
117
+ const listData = await tuituiRobotApi(account, '/file_space/node/list', listPayload);
118
+ const allNodes = listData?.datas?.list || [];
119
+
120
+ // 构建父目录ID映射,便于查找
121
+ const parentIdToNodes = new Map<string, any[]>();
122
+ allNodes.forEach(node => {
123
+ const parentId = node.parent_id || "";
124
+ if (!parentIdToNodes.has(parentId)) {
125
+ parentIdToNodes.set(parentId, []);
126
+ }
127
+ parentIdToNodes.get(parentId)!.push(node);
128
+ });
129
+
130
+ // 逐级查找或创建文件夹
131
+ let currentParentId = ""; // 从根目录开始
132
+
133
+ for (const folderName of pathParts) {
134
+ // 在当前层级查找文件夹
135
+ const siblings = parentIdToNodes.get(currentParentId) || [];
136
+ let folderNode = siblings.find(node =>
137
+ node.node_type === '1' && // 文件夹
138
+ node.name === folderName
139
+ );
140
+
141
+ // 如果不存在,创建新文件夹
142
+ if (!folderNode) {
143
+ const createFolderPayload = {
144
+ space_id: spaceId,
145
+ space_type: "2",
146
+ node_type: "1", // 文件夹
147
+ name: folderName,
148
+ parent_id: currentParentId,
149
+ };
150
+
151
+ const createResult = await tuituiRobotApi(account, '/file_space/node/add', createFolderPayload);
152
+ folderNode = {
153
+ node_id: createResult?.datas?.node_id || createResult?.node_id,
154
+ name: folderName,
155
+ node_type: '1',
156
+ parent_id: currentParentId,
157
+ };
158
+
159
+ if (!folderNode.node_id) {
160
+ throw new Error(`[${CHANNEL_ID}] Failed to create folder: ${folderName}`);
161
+ }
162
+
163
+ // 更新缓存,避免后续重复创建
164
+ if (!parentIdToNodes.has(currentParentId)) {
165
+ parentIdToNodes.set(currentParentId, []);
166
+ }
167
+ parentIdToNodes.get(currentParentId)!.push(folderNode);
168
+ }
169
+
170
+ currentParentId = folderNode.node_id;
171
+ }
172
+
173
+ return currentParentId;
174
+ }
175
+
176
+ export async function file_space_add(
177
+ account: any,
178
+ sessionKey: string,
179
+ cloud_filepath: string,
180
+ url_or_localpath: string,
181
+ ): Promise<any> {
182
+ const channelInfo = await getChannelInfoBySessionKey(account, sessionKey);
183
+ if (!channelInfo) {
184
+ throw new Error(`私聊、群聊会话不支持共享空间。仅团队频道支持。`);
185
+ }
186
+
187
+ // 1. 解析云路径,分离目录和文件名
188
+ const normalizedPath = cloud_filepath.startsWith('/')
189
+ ? cloud_filepath
190
+ : '/' + cloud_filepath;
191
+
192
+ const pathParts = normalizedPath.split('/').filter(part => part);
193
+ const filename = pathParts.length > 0 ? pathParts[pathParts.length - 1] : 'unnamed';
194
+ const folderPath = pathParts.length > 0
195
+ ? '/' + pathParts.slice(0, -1).join('/')
196
+ : '';
197
+
198
+ // 2. 确保目标目录存在,获取 parent_id
199
+ const parentId = await ensureFolderPath(account, channelInfo.team_id, folderPath);
200
+
201
+ // 3. 上传文件获取 fid
202
+ const isImage = /^data:image\//i.test(url_or_localpath) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(url_or_localpath);
203
+ const mediaType = isImage ? 'image' : 'file';
204
+ const uploadResult = await tuituiRobotUpload(url_or_localpath, account, mediaType);
205
+ const { fid, filesize } = uploadResult;
206
+
207
+ console.log(`[${CHANNEL_ID}] file_space_add: uploading ${normalizedPath}, parentId=${parentId}`);
208
+
209
+ // 4. 在指定目录中添加文件
210
+ const filePayload = {
211
+ space_id: channelInfo.team_id,
212
+ space_type: "2",
213
+ node_type: "2", // 文件
214
+ name: filename,
215
+ fid: fid,
216
+ file_size: filesize?.toString() || "0",
217
+ parent_id: parentId,
218
+ };
219
+
220
+ const body = await tuituiRobotApi(account, '/file_space/node/add', filePayload);
221
+ console.log(`[${CHANNEL_ID}] file_space_add: uploaded ${normalizedPath}`);
222
+
223
+ return body;
224
+ }
package/src/outbound.ts CHANGED
@@ -1,12 +1,9 @@
1
- import { readFileSync, existsSync, statSync } from 'node:fs';
2
- import { basename } from 'node:path';
1
+
3
2
  import { CHANNEL_ID } from "./const";
4
- import { flattenFileSpaceList, FileSpaceItem } from "./filespace";
5
- import { tuituiRobotApi, downloadUrl } from "./robot_api"
3
+ import { tuituiRobotApi, parseChannelIdBySessionKey, tuituiRobotUpload } from "./robot_api"
6
4
 
7
5
  import type {
8
6
  TuiTuiMessageData,
9
- TuiTuiMediaUploadResponse,
10
7
  TuiTuiSingleEmojiReactionTarget,
11
8
  TuiTuiGroupEmojiReactionTarget,
12
9
  TuiTuiOutboundTextMessage,
@@ -32,120 +29,11 @@ export function guessChatType(chatId: string): ChatType {
32
29
  return CHAT_TYPE_DIRECT;
33
30
  }
34
31
 
35
- const mimeTypes: Record<string, string> = {
36
- jpg: 'image/jpeg',
37
- jpeg: 'image/jpeg',
38
- png: 'image/png',
39
- gif: 'image/gif',
40
- webp: 'image/webp',
41
- bmp: 'image/bmp',
42
- svg: 'image/svg+xml',
43
- pdf: 'application/pdf',
44
- txt: 'text/plain',
45
- json: 'application/json',
46
- mp3: 'audio/mpeg',
47
- mp4: 'video/mp4',
48
- };
49
- /** Get MIME type from file extension. */
50
- export function getMimeType(filename: string): string {
51
- const fns = filename.split('.');
52
- const ext = fns.length > 1 ? fns[fns.length - 1].toLowerCase() : '';
53
- return mimeTypes[ext] || 'application/octet-stream';
54
- }
55
32
 
56
33
  export async function postTuituiMsg(account: any, json: any, auditCtx: string): Promise<any> {
57
34
  await tuituiRobotApi(account, '/message/custom/send', json);
58
35
  }
59
36
 
60
- interface tuituiUploadResult {
61
- fid: string;
62
- filename: string;
63
- }
64
-
65
- /**
66
- * Upload file to TuiTui and get media_id.
67
- * Supports:
68
- * - HTTP/HTTPS URLs (downloads then uploads)
69
- * - Local file paths (reads then uploads)
70
- * - Base64 data URLs (decodes then uploads)
71
- *
72
- * @param fileSrc - The URL, file path, or data URL of the media
73
- * @param account - TuiTui app account id and secret
74
- * @param type - Media type: 'image' or "file" (auto-detected if not specified)
75
- * @returns The media_id from TuiTui
76
- */
77
- export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'image' | 'file'): Promise<tuituiUploadResult> {
78
- let fileBuffer: ArrayBuffer;
79
- let contentType: string;
80
- let filename: string;
81
-
82
-
83
- // Check if it's a Base64 data URL
84
- if (/^data\:/.test(fileSrc)) {
85
- console.log("uploadFileToTuiTui: Base64 data");
86
-
87
- const matches = fileSrc.match(/^data:([^;,]*)(;base64)?,(.*)$/);
88
- if (!matches) {
89
- throw new Error(`[${CHANNEL_ID}] Invalid data URL format`);
90
- }
91
-
92
- contentType = matches[1] || 'application/octet-stream';
93
- const isBase64 = !!matches[2];
94
- const data = matches[3];
95
-
96
- if (isBase64) {
97
- // Use atob if available; fallback to Buffer for Node environments
98
- const binaryStr = typeof atob === 'function' ? atob(data) : Buffer.from(data, 'base64').toString('binary');
99
- const bytes = new Uint8Array(binaryStr.length);
100
- for (let i = 0; i < binaryStr.length; i++) {
101
- bytes[i] = binaryStr.charCodeAt(i);
102
- }
103
- fileBuffer = bytes.buffer;
104
- } else {
105
- fileBuffer = new TextEncoder().encode(decodeURIComponent(data)).buffer;
106
- }
107
-
108
- const ext = contentType.split("/")[1] || "bin";
109
- filename = `media_${Date.now()}.${ext}`;
110
- }
111
- // HTTP/HTTPS URL
112
- else if (/^https?\:/.test(fileSrc)) {
113
- console.log("uploadFileToTuiTui: url", fileSrc);
114
- const { buffer, filename: downloadedFilename, contentType: downloadedContentType } = await downloadUrl(fileSrc);
115
- fileBuffer = buffer;
116
- filename = downloadedFilename;
117
- contentType = downloadedContentType;
118
- }
119
- // Check if it's a local file path
120
- else {
121
- console.log("uploadFileToTuiTui: local", fileSrc);
122
- if (!existsSync(fileSrc)) {
123
- throw new Error(`[${CHANNEL_ID}] Local file not found: ${fileSrc}`);
124
- }
125
-
126
- const stats = statSync(fileSrc);
127
- const maxSize = 10 * 1024 * 1024;
128
- if (stats.size > maxSize) {
129
- throw new Error(
130
- `[${CHANNEL_ID}] File too large: ${fileSrc} (${(stats.size / 1024 / 1024).toFixed(2)}MB > 10MB limit)`,
131
- );
132
- }
133
-
134
- const localFileBuffer = readFileSync(fileSrc);
135
- const { byteOffset, byteLength } = localFileBuffer;
136
- fileBuffer = localFileBuffer.buffer.slice(byteOffset, byteOffset + byteLength);
137
-
138
- filename = basename(fileSrc);
139
- contentType = getMimeType(filename);
140
- }
141
-
142
- const body = new FormData();
143
- body.append('media', new Blob([fileBuffer], { type: contentType }), filename);
144
-
145
- const result: TuiTuiMediaUploadResponse = await tuituiRobotApi(account, '/media/upload', body);
146
- return {fid: result.media_id||"", filename};
147
- }
148
-
149
37
  export async function tuituiEmojiReaction(
150
38
  account: any,
151
39
  target: string,
@@ -331,7 +219,7 @@ export async function sendMediaMsg(
331
219
  const isImage = /^data:image\//i.test(mediaUrl) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(mediaUrl);
332
220
  const mediaType = isImage ? 'image' : 'file';
333
221
 
334
- const uploadResult = await uploadFileToTuiTui(mediaUrl, account, mediaType);
222
+ const uploadResult = await tuituiRobotUpload(mediaUrl, account, mediaType);
335
223
  const {fid, filename} = uploadResult;
336
224
 
337
225
  const targets = getTargets(chatId, chatType);
@@ -546,55 +434,12 @@ export async function getPostChainByChatId(
546
434
  return await getPostChain(account, team_id, channel_id, parent_id||"");
547
435
  }
548
436
 
549
- export async function file_space_list(
550
- account: any,
551
- spaceId: string = "",
552
- spaceType: string = "2"
553
- ): Promise<FileSpaceItem[]> {
554
- const guessType = guessChatType(spaceId);
555
- let space_final_id = "";
556
- if(guessType == CHAT_TYPE_CHANNEL){
557
- const { team_id, channel_id, parent_id } = teamsParseChatId(spaceId);
558
- space_final_id = team_id;
559
- } else {
560
- space_final_id = spaceId;
561
- }
562
-
563
- const payload = {
564
- space_id: space_final_id,
565
- space_type: spaceType,
566
- };
567
-
568
- const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
569
-
570
- const list = data?.datas?.list;
571
- const flat_list = flattenFileSpaceList(list);
572
-
573
- console.log(`[${CHANNEL_ID}] file_space_list `, flat_list);
574
- return flat_list
575
- }
576
-
577
- function parseSessionKey(str: string): string {
578
- // 检查是否包含必须的格式 "tuitui:channel:"
579
- if (!str.includes('tuitui:channel:')) {
580
- return "";
581
- }
582
-
583
- const parts = str.split(':');
584
- const channelIndex = parts.findIndex(part => part === 'channel');
585
-
586
- if (channelIndex !== -1 && parts[channelIndex + 1]) {
587
- return parts[channelIndex + 1];
588
- }
589
-
590
- return "";
591
- }
592
437
 
593
438
  // TODO: 支持群公告
594
439
  export async function get_announcement(account: any, id: any, id_is_session: boolean = true): Promise<any> {
595
440
  let channel_id = id;
596
441
  if(id_is_session) {
597
- channel_id = parseSessionKey(id);
442
+ channel_id = parseChannelIdBySessionKey(id);
598
443
  if(!channel_id) {
599
444
  return "";
600
445
  }
package/src/robot_api.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFileSync, existsSync, statSync } from 'node:fs';
2
+ import { basename } from 'node:path';
1
3
  import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk/tlon';
2
4
  import { CHANNEL_ID } from "./const";
3
5
  import { TUITUI_SSRF_POLICY, getTuituiApiHost } from "./env"
@@ -121,3 +123,146 @@ export async function downloadUrl(url: string): Promise<{ buffer: ArrayBuffer; f
121
123
  await release();
122
124
  }
123
125
  }
126
+
127
+
128
+
129
+ const mimeTypes: Record<string, string> = {
130
+ jpg: 'image/jpeg',
131
+ jpeg: 'image/jpeg',
132
+ png: 'image/png',
133
+ gif: 'image/gif',
134
+ webp: 'image/webp',
135
+ bmp: 'image/bmp',
136
+ svg: 'image/svg+xml',
137
+ pdf: 'application/pdf',
138
+ txt: 'text/plain',
139
+ json: 'application/json',
140
+ mp3: 'audio/mpeg',
141
+ mp4: 'video/mp4',
142
+ };
143
+ /** Get MIME type from file extension. */
144
+ export function getMimeType(filename: string): string {
145
+ const fns = filename.split('.');
146
+ const ext = fns.length > 1 ? fns[fns.length - 1].toLowerCase() : '';
147
+ return mimeTypes[ext] || 'application/octet-stream';
148
+ }
149
+
150
+ interface tuituiUploadResult {
151
+ fid: string;
152
+ filename: string;
153
+ filesize: number;
154
+ }
155
+
156
+ export interface TuiTuiMediaUploadResponse {
157
+ errcode: number;
158
+ errmsg: string;
159
+ filename?: string;
160
+ media_id?: string;
161
+ }
162
+
163
+
164
+
165
+ /**
166
+ * Upload file to TuiTui and get media_id.
167
+ * Supports:
168
+ * - HTTP/HTTPS URLs (downloads then uploads)
169
+ * - Local file paths (reads then uploads)
170
+ * - Base64 data URLs (decodes then uploads)
171
+ *
172
+ * @param fileSrc - The URL, file path, or data URL of the media
173
+ * @param account - TuiTui app account id and secret
174
+ * @param type - Media type: 'image' or "file" (auto-detected if not specified)
175
+ * @returns The media_id from TuiTui
176
+ */
177
+ export async function tuituiRobotUpload(fileSrc: string, account: any, type: 'image' | 'file'): Promise<tuituiUploadResult> {
178
+ let fileBuffer: ArrayBuffer;
179
+ let contentType: string;
180
+ let filename: string;
181
+ let filesize = 0;
182
+
183
+
184
+ // Check if it's a Base64 data URL
185
+ if (/^data\:/.test(fileSrc)) {
186
+ console.log("tuituiRobotUpload: Base64 data");
187
+
188
+ const matches = fileSrc.match(/^data:([^;,]*)(;base64)?,(.*)$/);
189
+ if (!matches) {
190
+ throw new Error(`[${CHANNEL_ID}] Invalid data URL format`);
191
+ }
192
+
193
+ contentType = matches[1] || 'application/octet-stream';
194
+ const isBase64 = !!matches[2];
195
+ const data = matches[3];
196
+
197
+ if (isBase64) {
198
+ // Use atob if available; fallback to Buffer for Node environments
199
+ const binaryStr = typeof atob === 'function' ? atob(data) : Buffer.from(data, 'base64').toString('binary');
200
+ const bytes = new Uint8Array(binaryStr.length);
201
+ for (let i = 0; i < binaryStr.length; i++) {
202
+ bytes[i] = binaryStr.charCodeAt(i);
203
+ }
204
+ fileBuffer = bytes.buffer;
205
+ } else {
206
+ fileBuffer = new TextEncoder().encode(decodeURIComponent(data)).buffer;
207
+ }
208
+
209
+ const ext = contentType.split("/")[1] || "bin";
210
+ filename = `media_${Date.now()}.${ext}`;
211
+ }
212
+ // HTTP/HTTPS URL
213
+ else if (/^https?\:/.test(fileSrc)) {
214
+ console.log("tuituiRobotUpload: url", fileSrc);
215
+ const { buffer, filename: downloadedFilename, contentType: downloadedContentType } = await downloadUrl(fileSrc);
216
+ fileBuffer = buffer;
217
+ filename = downloadedFilename;
218
+ contentType = downloadedContentType;
219
+ }
220
+ // Check if it's a local file path
221
+ else {
222
+ console.log("tuituiRobotUpload: local", fileSrc);
223
+ if (!existsSync(fileSrc)) {
224
+ throw new Error(`[${CHANNEL_ID}] Local file not found: ${fileSrc}`);
225
+ }
226
+
227
+ const stats = statSync(fileSrc);
228
+ const maxSize = 10 * 1024 * 1024;
229
+ if (stats.size > maxSize) {
230
+ throw new Error(
231
+ `[${CHANNEL_ID}] File too large: ${fileSrc} (${(stats.size / 1024 / 1024).toFixed(2)}MB > 10MB limit)`,
232
+ );
233
+ }
234
+
235
+ const localFileBuffer = readFileSync(fileSrc);
236
+ const { byteOffset, byteLength } = localFileBuffer;
237
+ fileBuffer = localFileBuffer.buffer.slice(byteOffset, byteOffset + byteLength);
238
+
239
+ filename = basename(fileSrc);
240
+ contentType = getMimeType(filename);
241
+ }
242
+
243
+ filesize = fileBuffer.byteLength;
244
+
245
+ const body = new FormData();
246
+ body.append('media', new Blob([fileBuffer], { type: contentType }), filename);
247
+
248
+ const result: TuiTuiMediaUploadResponse = await tuituiRobotApi(account, '/media/upload', body);
249
+ return {fid: result.media_id||"", filename, filesize};
250
+ }
251
+
252
+
253
+
254
+ export function parseChannelIdBySessionKey(str: string): string {
255
+ // 检查是否包含必须的格式 "tuitui:channel:"
256
+ if (!str.includes('tuitui:channel:')) {
257
+ return "";
258
+ }
259
+
260
+ const parts = str.split(':');
261
+ const channelIndex = parts.findIndex(part => part === 'channel');
262
+
263
+ if (channelIndex !== -1 && parts[channelIndex + 1]) {
264
+ return parts[channelIndex + 1];
265
+ }
266
+
267
+ return "";
268
+ }
package/src/tools.ts CHANGED
@@ -5,7 +5,7 @@ import { resolveAccount } from "./accounts"
5
5
  import { Type } from "@sinclair/typebox";
6
6
 
7
7
  import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, getChatRecord, getPostChainByChatId, sendTextMsg, teamsBuildChatId, get_announcement} from "./outbound"
8
- import {file_space_list} from "./outbound"
8
+ import {file_space_list, file_space_add} from "./filespace"
9
9
 
10
10
  function tool_errmsg(str:string) {
11
11
  const ret = `error: ${str}`
@@ -92,9 +92,11 @@ const tuitui_get_announcement_factory = (ctx: OpenClawPluginToolContext) => {
92
92
  label: "tuitui_get_announcement",
93
93
  description: "获取推推(tuitui) 当前会话的公告信息\n\n",
94
94
  parameters: Type.Object({
95
+ // 参数不能为空,对模型兼容有问题,即使不用也要传一个
96
+ reason: Type.String({ description: "调用本工具的原因" }),
95
97
  }),
96
98
  execute: async (_toolCallId: any, params: any) => {
97
- console.log(`tuitui_get_announcement(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
99
+ console.log(`tuitui_get_announcement(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`);
98
100
  const account = resolveAccount(ctx.config, ctx.agentAccountId);
99
101
  if(!account || !account.enabled || !account.appId || !account.appSecret) {
100
102
  return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
@@ -107,21 +109,42 @@ const tuitui_get_announcement_factory = (ctx: OpenClawPluginToolContext) => {
107
109
 
108
110
 
109
111
 
110
- const tuitui_file_space_factory = (ctx: OpenClawPluginToolContext) => {
112
+ const tuitui_file_space_list_factory = (ctx: OpenClawPluginToolContext) => {
111
113
  return {
112
114
  name: "tuitui_file_space_list",
113
115
  label: "tuitui_file_space_list",
114
- description: "推推(tuitui) 团队频道的共享文件(共享空间)的文件列表\n\n",
116
+ description: "推推(tuitui) 在当前Session的共享文件(共享空间)文件列表\n\n",
115
117
  parameters: Type.Object({
116
- space_id: Type.String({ description: "空间ID。请传入 GroupChannel 或者 GroupId 中非空的值" }),
118
+ // 参数不能为空,对模型兼容有问题,即使不用也要传一个
119
+ reason: Type.String({ description: "调用本工具的原因" }),
117
120
  }),
118
121
  execute: async (_toolCallId: any, params: any) => {
119
- console.log(`tuitui_file_space_list(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
122
+ console.log(`tuitui_file_space_list(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`);
120
123
  const account = resolveAccount(ctx.config, ctx.agentAccountId);
121
124
  if(!account || !account.enabled || !account.appId || !account.appSecret) {
122
125
  return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
123
126
  }
124
- return await file_space_list(account, params?.space_id);
127
+ return await file_space_list(account, ctx.sessionKey||"");
128
+ },
129
+ };
130
+ };
131
+
132
+ const tuitui_file_space_add_factory = (ctx: OpenClawPluginToolContext) => {
133
+ return {
134
+ name: "tuitui_file_space_add",
135
+ label: "tuitui_file_space_add",
136
+ description: "推推(tuitui) 团队频道的共享文件(共享空间)添加文件\n\n",
137
+ parameters: Type.Object({
138
+ cloud_filepath: Type.String({ description: "云端文件名路径,父目录不存在会自动创建。不含目录代表放根目录示例 1.png ,也支持带目录写法例如 /image/1.png" }),
139
+ url_or_localpath: Type.String({ description: "支持两种格式:以http开头的文件url;或者本地全路径文件名" }),
140
+ }),
141
+ execute: async (_toolCallId: any, params: any) => {
142
+ console.log(`tuitui_file_space_add(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
143
+ const account = resolveAccount(ctx.config, ctx.agentAccountId);
144
+ if(!account || !account.enabled || !account.appId || !account.appSecret) {
145
+ return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
146
+ }
147
+ return await file_space_add(account, ctx.sessionKey||"", params?.cloud_filepath, params?.url_or_localpath);
125
148
  },
126
149
  };
127
150
  };
@@ -136,6 +159,7 @@ export function registerTuituiTools(api: OpenClawPluginApi) {
136
159
  api.registerTool(tuitui_im_get_messages_factory);
137
160
  api.registerTool(tuitui_send_channel_post_factory);
138
161
  api.registerTool(tuitui_get_announcement_factory);
139
- api.registerTool(tuitui_file_space_factory);
162
+ api.registerTool(tuitui_file_space_list_factory);
163
+ api.registerTool(tuitui_file_space_add_factory);
140
164
  api.logger.info?.(`tuitui: Registered tool`);
141
165
  }
package/src/types.ts CHANGED
@@ -128,15 +128,6 @@ export interface TuiTuiOutboundPageMessage {
128
128
  page: TuiTuiOutboundPageMessagePage;
129
129
  }
130
130
 
131
- export interface TuiTuiMediaUploadResponse {
132
- errcode: number;
133
- errmsg: string;
134
- filename?: string;
135
- media_id?: string;
136
- }
137
-
138
-
139
-
140
131
  export interface TuiTuiSingleEmojiReactionTarget {
141
132
  user?: string;
142
133
  msgid?: string;