@qihoo/tuitui-openclaw-channel 1.0.21 → 1.0.23
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 +1 -1
- package/src/channel.ts +0 -5
- package/src/deduplicator.ts +121 -0
- package/src/inbound.ts +22 -8
- package/src/outbound.ts +92 -252
- package/src/robot_api.ts +123 -0
- package/src/tools.ts +24 -5
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from
|
|
|
8
8
|
import { CHANNEL_ID, CHANNEL_NAME } from "./const";
|
|
9
9
|
import { handleInboundMessage } from './inbound';
|
|
10
10
|
import {
|
|
11
|
-
checkAccount,
|
|
12
11
|
sendTextMsg,
|
|
13
12
|
sendPageMsg,
|
|
14
13
|
sendMediaMsg,
|
|
@@ -189,7 +188,6 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
189
188
|
|
|
190
189
|
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }: any) => {
|
|
191
190
|
const account = resolveAccount(cfg, accountId);
|
|
192
|
-
checkAccount(account);
|
|
193
191
|
|
|
194
192
|
const chatId = String(to || '').trim();
|
|
195
193
|
const chatType = guessChatType(chatId);
|
|
@@ -202,8 +200,6 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
202
200
|
|
|
203
201
|
sendCustom: async ({ cfg, to, payload, accountId, account, chatType, groupId }: any) => {
|
|
204
202
|
account = account || resolveAccount(cfg, accountId);
|
|
205
|
-
checkAccount(account, 'send custom message');
|
|
206
|
-
|
|
207
203
|
// If it's a page message, we need to construct it
|
|
208
204
|
if (payload?.msgtype !== 'page') {
|
|
209
205
|
throw new Error(`[${CHANNEL_ID}] unsupported custom message type: ${payload?.msgtype}`);
|
|
@@ -217,7 +213,6 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
217
213
|
|
|
218
214
|
sendMedia: async ({ cfg, to, mediaUrl, accountId, account }: any) => {
|
|
219
215
|
account = account || resolveAccount(cfg, accountId);
|
|
220
|
-
checkAccount(account, 'send media');
|
|
221
216
|
|
|
222
217
|
const chatId = String(to || '').trim();
|
|
223
218
|
// Determine if this is a group message based on 'to' being all digits (group) or not (direct)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export class StringDeduplicator {
|
|
2
|
+
private items = new Map<string, number>(); // 存储元素和其首次插入时间(秒级时间戳)
|
|
3
|
+
private readonly maxSize: number;
|
|
4
|
+
private readonly expireTime: number | null; // 过期时间(秒),null 表示永不过期
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 字符串去重器
|
|
8
|
+
* @param maxSize - 最大缓存元素数量,默认1000
|
|
9
|
+
* @param expireTime - 过期时间(秒),默认null(永不过期)。例如:300表示5分钟
|
|
10
|
+
*/
|
|
11
|
+
constructor(maxSize: number = 1000, expireTime: number | null = null) {
|
|
12
|
+
if (maxSize <= 0) {
|
|
13
|
+
throw new Error(`[StringDeduplicator] maxSize must be positive, got ${maxSize}`);
|
|
14
|
+
}
|
|
15
|
+
if (expireTime !== null && expireTime <= 0) {
|
|
16
|
+
throw new Error(`[StringDeduplicator] expireTime must be positive or null, got ${expireTime}`);
|
|
17
|
+
}
|
|
18
|
+
this.maxSize = maxSize;
|
|
19
|
+
this.expireTime = expireTime;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 检查并记录字符串
|
|
24
|
+
* @returns true 表示在有效时间内首次出现(可处理),false 表示重复或已过期
|
|
25
|
+
*/
|
|
26
|
+
checkAndRecord(item: string): boolean {
|
|
27
|
+
if (item === undefined || item === null) {
|
|
28
|
+
throw new Error('[StringDeduplicator] item cannot be null or undefined');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const now = Date.now() / 1000; // 转换为秒级时间戳
|
|
32
|
+
|
|
33
|
+
// 检查是否已存在
|
|
34
|
+
const firstSeenTime = this.items.get(item);
|
|
35
|
+
if (firstSeenTime !== undefined) {
|
|
36
|
+
// 如果设置了过期时间,检查是否过期
|
|
37
|
+
if (this.expireTime !== null && now - firstSeenTime >= this.expireTime) {
|
|
38
|
+
// 已过期,移除旧记录,重新添加
|
|
39
|
+
this.items.delete(item);
|
|
40
|
+
} else if (this.expireTime === null || now - firstSeenTime < this.expireTime) {
|
|
41
|
+
// 未过期(或永不过期模式),重复
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 添加新记录
|
|
47
|
+
this.items.set(item, now);
|
|
48
|
+
this.enforceSizeLimit();
|
|
49
|
+
this.cleanupExpired();
|
|
50
|
+
return true; // 首次出现或已过期后重新出现
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 清理过期的条目
|
|
55
|
+
*/
|
|
56
|
+
private cleanupExpired(): void {
|
|
57
|
+
if (this.expireTime === null) return;
|
|
58
|
+
|
|
59
|
+
const now = Date.now() / 1000;
|
|
60
|
+
for (const [item, timestamp] of this.items.entries()) {
|
|
61
|
+
if (now - timestamp >= this.expireTime) {
|
|
62
|
+
this.items.delete(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 确保不超过最大大小,删除最老的条目
|
|
69
|
+
*/
|
|
70
|
+
private enforceSizeLimit(): void {
|
|
71
|
+
if (this.items.size <= this.maxSize) return;
|
|
72
|
+
|
|
73
|
+
// 因为每次 checkAndRecord 最多插入1个元素,所以只需删除1个最老的
|
|
74
|
+
const iterator = this.items.entries();
|
|
75
|
+
const oldestEntry = iterator.next().value;
|
|
76
|
+
if (oldestEntry) {
|
|
77
|
+
const [oldestItem] = oldestEntry;
|
|
78
|
+
this.items.delete(oldestItem);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 可选:手动清理
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.items.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 可选:检查是否存在(不记录)
|
|
88
|
+
has(item: string): boolean {
|
|
89
|
+
const firstSeenTime = this.items.get(item);
|
|
90
|
+
if (firstSeenTime === undefined) return false;
|
|
91
|
+
// 如果设置了过期时间,检查是否过期
|
|
92
|
+
if (this.expireTime !== null) {
|
|
93
|
+
return (Date.now() / 1000) - firstSeenTime < this.expireTime;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 可选:获取当前大小
|
|
99
|
+
size(): number {
|
|
100
|
+
// 懒清理:每10次size调用执行一次完整清理
|
|
101
|
+
if (Math.random() < 0.1) {
|
|
102
|
+
this.cleanupExpired();
|
|
103
|
+
}
|
|
104
|
+
return this.items.size;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 可选:删除指定项
|
|
108
|
+
delete(item: string): boolean {
|
|
109
|
+
return this.items.delete(item);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 可选:获取过期时间(秒)
|
|
113
|
+
getExpireTime(): number | null {
|
|
114
|
+
return this.expireTime;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 可选:获取最大大小
|
|
118
|
+
getMaxSize(): number {
|
|
119
|
+
return this.maxSize;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -20,13 +20,15 @@ import {
|
|
|
20
20
|
sendMediaMsg,
|
|
21
21
|
teamsBuildChatId,
|
|
22
22
|
teamsParseChatId,
|
|
23
|
+
get_announcement,
|
|
23
24
|
} from "./outbound";
|
|
24
25
|
import { parseChatMessageBody } from './inbound_body_parse';
|
|
25
26
|
import { parseAllowFroms } from './utils';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
import { addUnmentionedHistory, popUnmentionedHistories } from "./histories";
|
|
28
|
+
import { StringDeduplicator } from "./deduplicator"
|
|
29
|
+
|
|
30
|
+
// 会话上下文注入排重
|
|
31
|
+
let _session_ctx_injected = new StringDeduplicator(1000, 3600);
|
|
30
32
|
|
|
31
33
|
/** 子函数共享的可变上下文,子函数直接修改字段,外层读取结果 */
|
|
32
34
|
interface ChatPayload {
|
|
@@ -352,9 +354,17 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
|
|
|
352
354
|
}
|
|
353
355
|
}
|
|
354
356
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
if(_session_ctx_injected.checkAndRecord(accountId + "_" + payload.chatId)) {
|
|
358
|
+
// 注入上下文
|
|
359
|
+
// 解决机器人不知道自己是谁的问题
|
|
360
|
+
if(payload.botName) {
|
|
361
|
+
payload.text += `\n[备忘]\n你在当前session中的名字叫: ${payload.botName}\n 如果有人@这个名字,就是在命令你`;
|
|
362
|
+
}
|
|
363
|
+
// 公告
|
|
364
|
+
const annnouncement = await get_announcement(account, channel_id, false);
|
|
365
|
+
if(annnouncement) {
|
|
366
|
+
payload.text += `\n[当前公告内容如下--有需要时可参考]\n${annnouncement}`;
|
|
367
|
+
}
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
|
|
@@ -451,7 +461,11 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
|
|
|
451
461
|
|
|
452
462
|
if (account.emojiReaction) {
|
|
453
463
|
// 因为回复较慢,先回复一个表情
|
|
454
|
-
|
|
464
|
+
try{
|
|
465
|
+
await tuituiEmojiReaction(account, payload.chatId, payload.chatType, payload.msgId, '收到');
|
|
466
|
+
} catch (err) {
|
|
467
|
+
// 出错不影响后续逻辑
|
|
468
|
+
}
|
|
455
469
|
}
|
|
456
470
|
|
|
457
471
|
// 路由判断
|
package/src/outbound.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
-
import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk/tlon';
|
|
4
3
|
import { CHANNEL_ID } from "./const";
|
|
5
|
-
import { TUITUI_SSRF_POLICY, getTuituiApiHost } from "./env"
|
|
6
4
|
import { flattenFileSpaceList, FileSpaceItem } from "./filespace";
|
|
5
|
+
import { tuituiRobotApi, downloadUrl } from "./robot_api"
|
|
7
6
|
|
|
8
7
|
import type {
|
|
9
8
|
TuiTuiMessageData,
|
|
@@ -33,12 +32,6 @@ export function guessChatType(chatId: string): ChatType {
|
|
|
33
32
|
return CHAT_TYPE_DIRECT;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export function addTuituiParams2Url(urlStr: string, params: any) {
|
|
37
|
-
const url = new URL(getTuituiApiHost() + urlStr);
|
|
38
|
-
for (let k in params) url.searchParams.set(k, params[k]);
|
|
39
|
-
return url.toString();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
const mimeTypes: Record<string, string> = {
|
|
43
36
|
jpg: 'image/jpeg',
|
|
44
37
|
jpeg: 'image/jpeg',
|
|
@@ -60,61 +53,10 @@ export function getMimeType(filename: string): string {
|
|
|
60
53
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
function _fetch(opts: any): Promise<any> {
|
|
64
|
-
return fetchWithSsrFGuard({
|
|
65
|
-
//url: fileSrc,
|
|
66
|
-
policy: TUITUI_SSRF_POLICY,
|
|
67
|
-
//auditCtx: "tuitui.media.download",
|
|
68
|
-
...opts,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
function _fetchJson(url: string, json: any, auditCtx: string): Promise<any> {
|
|
72
|
-
return _fetch({
|
|
73
|
-
url,
|
|
74
|
-
init: {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: { 'Content-Type': 'application/json' },
|
|
77
|
-
body: JSON.stringify(json),
|
|
78
|
-
},
|
|
79
|
-
auditCtx,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
56
|
export async function postTuituiMsg(account: any, json: any, auditCtx: string): Promise<any> {
|
|
83
|
-
|
|
84
|
-
const { response, release } = await _fetchJson(
|
|
85
|
-
addTuituiParams2Url('/message/custom/send', { appid, secret }),
|
|
86
|
-
json,
|
|
87
|
-
auditCtx,
|
|
88
|
-
);
|
|
89
|
-
try {
|
|
90
|
-
const bodyText = await response.text();
|
|
91
|
-
const parsed = JSON.parse(bodyText);
|
|
92
|
-
|
|
93
|
-
console.debug(`[${CHANNEL_ID}] ${auditCtx} postTuituiMsg response status=${response.status} ok=${response.ok} body=${bodyText || '<empty>'}`);
|
|
94
|
-
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
throw new Error(`postTuituiMsg Failed (response.ok unexpected): ${response.status} ${response.statusText}; body=${bodyText || '<empty>'}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (Number(parsed?.errcode) !== 0) {
|
|
100
|
-
throw new Error(`postTuituiMsg Failed (errcode unexpected): errcode=${parsed.errcode} errmsg=${parsed.errmsg ?? 'Unknown error'}`);
|
|
101
|
-
}
|
|
102
|
-
} catch(err) {
|
|
103
|
-
console.error(`[${CHANNEL_ID}] ${auditCtx} postTuituiMsg error:`, err, `\njson: ${JSON.stringify(json)}`);
|
|
104
|
-
// 必须抛出错误,否则上层无法知道失败了(channel机器人问答、agent调用tool的场景)
|
|
105
|
-
throw err;
|
|
106
|
-
} finally {
|
|
107
|
-
await release();
|
|
108
|
-
}
|
|
57
|
+
await tuituiRobotApi(account, '/message/custom/send', json);
|
|
109
58
|
}
|
|
110
59
|
|
|
111
|
-
export function checkAccount(account: any, ctxTips: string = 'send text') {
|
|
112
|
-
if (!account || !account.appId || !account.appSecret) {
|
|
113
|
-
throw new Error(`[${CHANNEL_ID}] appId and appSecret are required for ${ctxTips}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
60
|
interface tuituiUploadResult {
|
|
119
61
|
fid: string;
|
|
120
62
|
filename: string;
|
|
@@ -133,15 +75,15 @@ interface tuituiUploadResult {
|
|
|
133
75
|
* @returns The media_id from TuiTui
|
|
134
76
|
*/
|
|
135
77
|
export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'image' | 'file'): Promise<tuituiUploadResult> {
|
|
136
|
-
checkAccount(account, 'uploadFileToTuiTui');
|
|
137
|
-
|
|
138
|
-
const { appId: appid, appSecret: secret } = account;
|
|
139
78
|
let fileBuffer: ArrayBuffer;
|
|
140
79
|
let contentType: string;
|
|
141
80
|
let filename: string;
|
|
142
81
|
|
|
82
|
+
|
|
143
83
|
// Check if it's a Base64 data URL
|
|
144
84
|
if (/^data\:/.test(fileSrc)) {
|
|
85
|
+
console.log("uploadFileToTuiTui: Base64 data");
|
|
86
|
+
|
|
145
87
|
const matches = fileSrc.match(/^data:([^;,]*)(;base64)?,(.*)$/);
|
|
146
88
|
if (!matches) {
|
|
147
89
|
throw new Error(`[${CHANNEL_ID}] Invalid data URL format`);
|
|
@@ -168,33 +110,15 @@ export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'i
|
|
|
168
110
|
}
|
|
169
111
|
// HTTP/HTTPS URL
|
|
170
112
|
else if (/^https?\:/.test(fileSrc)) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
fileBuffer = await response.arrayBuffer();
|
|
178
|
-
contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
179
|
-
|
|
180
|
-
filename = 'media';
|
|
181
|
-
const urlPath = new URL(fileSrc).pathname;
|
|
182
|
-
const pathParts = urlPath.split('/');
|
|
183
|
-
const lastPart = pathParts[pathParts.length - 1];
|
|
184
|
-
if (lastPart) filename = lastPart;
|
|
185
|
-
const contentDisposition = response.headers.get('content-disposition');
|
|
186
|
-
if (contentDisposition) {
|
|
187
|
-
const match = contentDisposition.match(/filename\*?=(?:UTF-8''|")?([^";\r\n]+)"?/i);
|
|
188
|
-
if (match) {
|
|
189
|
-
filename = decodeURIComponent(match[1]);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
} finally {
|
|
193
|
-
await release();
|
|
194
|
-
}
|
|
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;
|
|
195
118
|
}
|
|
196
119
|
// Check if it's a local file path
|
|
197
120
|
else {
|
|
121
|
+
console.log("uploadFileToTuiTui: local", fileSrc);
|
|
198
122
|
if (!existsSync(fileSrc)) {
|
|
199
123
|
throw new Error(`[${CHANNEL_ID}] Local file not found: ${fileSrc}`);
|
|
200
124
|
}
|
|
@@ -218,27 +142,8 @@ export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'i
|
|
|
218
142
|
const body = new FormData();
|
|
219
143
|
body.append('media', new Blob([fileBuffer], { type: contentType }), filename);
|
|
220
144
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
init: { method: "POST", body },
|
|
224
|
-
auditCtx: "tuitui.media.upload",
|
|
225
|
-
});
|
|
226
|
-
try {
|
|
227
|
-
if (!response.ok) {
|
|
228
|
-
throw new Error(`[${CHANNEL_ID}] Failed to upload file to TuiTui: HTTP ${response.status}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const result: TuiTuiMediaUploadResponse = await response.json();
|
|
232
|
-
if (result.errcode !== 0 || !result.media_id) {
|
|
233
|
-
throw new Error(`[${CHANNEL_ID}] file upload failed: ${result.errmsg || "Unknown error"}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {fid: result.media_id, filename};
|
|
237
|
-
} catch(err) {
|
|
238
|
-
throw err;
|
|
239
|
-
} finally {
|
|
240
|
-
await release();
|
|
241
|
-
}
|
|
145
|
+
const result: TuiTuiMediaUploadResponse = await tuituiRobotApi(account, '/media/upload', body);
|
|
146
|
+
return {fid: result.media_id||"", filename};
|
|
242
147
|
}
|
|
243
148
|
|
|
244
149
|
export async function tuituiEmojiReaction(
|
|
@@ -260,21 +165,7 @@ export async function tuituiEmojiReaction(
|
|
|
260
165
|
payload.toteams = [{ ...teamsParseChatId(target), parent_id: '', post_id: msgid }] as TuiTuiTeamsTarget[];
|
|
261
166
|
}
|
|
262
167
|
|
|
263
|
-
|
|
264
|
-
const toTarget = (payload.togroups || payload.tousers || payload.toteams)[0];
|
|
265
|
-
const _logTxt =`[${CHANNEL_ID}] emoji_reaction "${emoji}"`;
|
|
266
|
-
console.log(`${_logTxt} request`, toTarget);
|
|
267
|
-
const sendUrl = addTuituiParams2Url('/message/custom/modify', { appid, secret });
|
|
268
|
-
const { response, release } = await _fetchJson(sendUrl, payload, 'tuitui.emoji_reaction');
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const body = JSON.parse(await response.text().catch(() => "{}"));
|
|
272
|
-
console.log(`${_logTxt} response errcode=${body.errcode} errmsg=${body.errmsg}`, toTarget);
|
|
273
|
-
} catch (err) {
|
|
274
|
-
console.error(`${_logTxt} Caught exception:`, err)
|
|
275
|
-
} finally {
|
|
276
|
-
await release();
|
|
277
|
-
}
|
|
168
|
+
await tuituiRobotApi(account, '/message/custom/modify', payload);
|
|
278
169
|
}
|
|
279
170
|
|
|
280
171
|
export function teamsBuildChatId(team_id: string, channel_id:string, thread_id:string) : string{
|
|
@@ -374,7 +265,6 @@ export async function sendTextMsg(
|
|
|
374
265
|
delims_right: has_at?"}}":"",
|
|
375
266
|
},
|
|
376
267
|
};
|
|
377
|
-
console.log(`[${CHANNEL_ID}] sendTeamsPost to ${chatId} ${auditCtx} - `, msg);
|
|
378
268
|
await postTuituiMsg(account, msg, auditCtx);
|
|
379
269
|
} catch(err) {
|
|
380
270
|
if(!has_at)throw err;
|
|
@@ -388,7 +278,6 @@ export async function sendTextMsg(
|
|
|
388
278
|
delims_right: ""
|
|
389
279
|
},
|
|
390
280
|
};
|
|
391
|
-
console.log(`[${CHANNEL_ID}] retry no @ sendTeamsPost to ${chatId} ${auditCtx} - `, msg);
|
|
392
281
|
await postTuituiMsg(account, msg, auditCtx);
|
|
393
282
|
}
|
|
394
283
|
} else {
|
|
@@ -399,7 +288,6 @@ export async function sendTextMsg(
|
|
|
399
288
|
at: at_from_text,
|
|
400
289
|
text: { content },
|
|
401
290
|
};
|
|
402
|
-
console.log(`[${CHANNEL_ID}] sendTextMsg to ${chatId} ${auditCtx} - `, msg);
|
|
403
291
|
await postTuituiMsg(account, msg, auditCtx);
|
|
404
292
|
}
|
|
405
293
|
}
|
|
@@ -415,7 +303,7 @@ export async function sendPageMsg(
|
|
|
415
303
|
): Promise<void> {
|
|
416
304
|
if (!chatId) return console.error(`[${CHANNEL_ID}] sendPageMsg Error ${auditCtx}: Missing "target"`);
|
|
417
305
|
if(chatType == CHAT_TYPE_CHANNEL) {
|
|
418
|
-
console.log(`[${CHANNEL_ID}] sendPageMsg to teams
|
|
306
|
+
console.log(`[${CHANNEL_ID}] sendPageMsg to teams NOT supported ${auditCtx} - `, page);
|
|
419
307
|
return
|
|
420
308
|
}
|
|
421
309
|
const targets = getTargets(chatId, chatType);
|
|
@@ -427,7 +315,6 @@ export async function sendPageMsg(
|
|
|
427
315
|
togroups,
|
|
428
316
|
page: { ...(page || {})}
|
|
429
317
|
};
|
|
430
|
-
console.log(`[${CHANNEL_ID}] sendPageMsg ${auditCtx} - `, msg);
|
|
431
318
|
await postTuituiMsg(account, msg, auditCtx);
|
|
432
319
|
}
|
|
433
320
|
|
|
@@ -440,7 +327,6 @@ export async function sendMediaMsg(
|
|
|
440
327
|
atList?: string[],
|
|
441
328
|
): Promise<void> {
|
|
442
329
|
if (!chatId) return console.error(`[${CHANNEL_ID}] sendMediaMsg Error ${auditCtx}: Missing "target"`);
|
|
443
|
-
console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} uploading`);
|
|
444
330
|
// Check if mediaUrl looks like an image
|
|
445
331
|
const isImage = /^data:image\//i.test(mediaUrl) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(mediaUrl);
|
|
446
332
|
const mediaType = isImage ? 'image' : 'file';
|
|
@@ -462,7 +348,6 @@ export async function sendMediaMsg(
|
|
|
462
348
|
at: atList || [],
|
|
463
349
|
richtext: { markdown: content, delims_left: "{{", delims_right: "}}"},
|
|
464
350
|
}
|
|
465
|
-
console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} - `, msg);
|
|
466
351
|
await postTuituiMsg(account, msg, auditCtx);
|
|
467
352
|
} else {
|
|
468
353
|
const msg: TuiTuiOutboundImageMessage | TuiTuiOutboundAttachmentMessage =
|
|
@@ -472,7 +357,6 @@ export async function sendMediaMsg(
|
|
|
472
357
|
|
|
473
358
|
const realAtList = chatType == CHAT_TYPE_GROUP? atList : [];
|
|
474
359
|
msg.at = realAtList;
|
|
475
|
-
console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} - `, msg);
|
|
476
360
|
|
|
477
361
|
await postTuituiMsg(account, msg, auditCtx);
|
|
478
362
|
}
|
|
@@ -545,8 +429,6 @@ export async function getChatRecord(
|
|
|
545
429
|
return undefined;
|
|
546
430
|
}
|
|
547
431
|
|
|
548
|
-
checkAccount(account, 'getChatRecord');
|
|
549
|
-
|
|
550
432
|
let baseurl = "";
|
|
551
433
|
if (chatType == CHAT_TYPE_DIRECT) {
|
|
552
434
|
baseurl = "/message/single/sync";
|
|
@@ -572,52 +454,28 @@ export async function getChatRecord(
|
|
|
572
454
|
if (options.limit) body.limit = options.limit;
|
|
573
455
|
if (typeof options.orderAsc === 'boolean') body.order_asc = options.orderAsc;
|
|
574
456
|
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const clean: TuiTuiChatRecordResponseClean = {
|
|
596
|
-
errcode: parsed.errcode,
|
|
597
|
-
errmsg: parsed.errmsg,
|
|
598
|
-
cursor: parsed.cursor,
|
|
599
|
-
has_more: parsed.has_more,
|
|
600
|
-
current_time: parsed.time,
|
|
601
|
-
msgs: (parsed.msgs ?? []).map(({ user_account, user_name, timestamp, data }) => {
|
|
602
|
-
const { at, msgid, group_id, group_name, ...restData } = data; // 排除一些字段,减少大模型上下文大小
|
|
603
|
-
return {
|
|
604
|
-
...restData, // 使用排除 at 后的数据
|
|
605
|
-
user_account,
|
|
606
|
-
user_name,
|
|
607
|
-
msg_time: new Date(Number(timestamp) * 1000).toLocaleString('sv-SE', { hour12: false }).replace('T', ' '),
|
|
608
|
-
};
|
|
609
|
-
}),
|
|
610
|
-
};
|
|
457
|
+
const parsed: TuiTuiChatRecordResponse = await tuituiRobotApi(account, baseurl, body);
|
|
458
|
+
|
|
459
|
+
const clean: TuiTuiChatRecordResponseClean = {
|
|
460
|
+
errcode: parsed.errcode,
|
|
461
|
+
errmsg: parsed.errmsg,
|
|
462
|
+
cursor: parsed.cursor,
|
|
463
|
+
has_more: parsed.has_more,
|
|
464
|
+
current_time: parsed.time,
|
|
465
|
+
msgs: (parsed.msgs ?? []).map(({ user_account, user_name, timestamp, data }) => {
|
|
466
|
+
const { at, msgid, group_id, group_name, ...restData } = data; // 排除一些字段,减少大模型上下文大小
|
|
467
|
+
return {
|
|
468
|
+
...restData, // 使用排除 at 后的数据
|
|
469
|
+
user_account,
|
|
470
|
+
user_name,
|
|
471
|
+
msg_time: new Date(Number(timestamp) * 1000).toLocaleString('sv-SE', { hour12: false }).replace('T', ' '),
|
|
472
|
+
};
|
|
473
|
+
}),
|
|
474
|
+
};
|
|
611
475
|
|
|
612
|
-
|
|
476
|
+
console.log(`[${CHANNEL_ID}] getChatRecord result(cleaned)`, JSON.stringify(clean, null, 2));
|
|
613
477
|
|
|
614
|
-
|
|
615
|
-
} catch (err) {
|
|
616
|
-
console.error(`[${CHANNEL_ID}] getChatRecord error:`, err);
|
|
617
|
-
return undefined;
|
|
618
|
-
} finally {
|
|
619
|
-
await release();
|
|
620
|
-
}
|
|
478
|
+
return clean;
|
|
621
479
|
}
|
|
622
480
|
|
|
623
481
|
|
|
@@ -644,66 +502,40 @@ export async function getPostChain(
|
|
|
644
502
|
channelId: string,
|
|
645
503
|
threadId: string,
|
|
646
504
|
): Promise<TeamsPostChainItem[]> {
|
|
647
|
-
checkAccount(account, 'getPostChain');
|
|
648
|
-
|
|
649
|
-
const { appId: appid, appSecret: secret } = account;
|
|
650
|
-
const url = addTuituiParams2Url('/teams/post/chain', { appid, secret });
|
|
651
505
|
|
|
652
506
|
const payload = {
|
|
653
507
|
team_id: teamId,
|
|
654
508
|
channel_id: channelId,
|
|
655
509
|
post_id: threadId,
|
|
656
510
|
};
|
|
511
|
+
const data = await tuituiRobotApi(account, '/teams/post/chain', payload);
|
|
657
512
|
|
|
658
|
-
|
|
513
|
+
const datas = data.datas ?? {};
|
|
514
|
+
const topic = datas.topic ?? {};
|
|
515
|
+
const replyList: any[] = datas.reply_list ?? [];
|
|
659
516
|
|
|
660
|
-
const
|
|
661
|
-
try {
|
|
662
|
-
const bodyText = await response.text();
|
|
517
|
+
const posts: TeamsPostChainItem[] = [];
|
|
663
518
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
console.log(`[${CHANNEL_ID}] getPostChain `, data);
|
|
674
|
-
|
|
675
|
-
const datas = data.datas ?? {};
|
|
676
|
-
const topic = datas.topic ?? {};
|
|
677
|
-
const replyList: any[] = datas.reply_list ?? [];
|
|
678
|
-
|
|
679
|
-
const posts: TeamsPostChainItem[] = [];
|
|
519
|
+
posts.push({
|
|
520
|
+
post_id: topic.post_id ?? '',
|
|
521
|
+
time: topic.create_time ?? '',
|
|
522
|
+
name: topic.from_name ?? '',
|
|
523
|
+
content: topic.content ?? '',
|
|
524
|
+
properties: topic.properties ?? '',
|
|
525
|
+
});
|
|
680
526
|
|
|
527
|
+
for (const post of [...replyList].reverse()) {
|
|
681
528
|
posts.push({
|
|
682
|
-
post_id:
|
|
683
|
-
time:
|
|
684
|
-
name:
|
|
685
|
-
content:
|
|
686
|
-
properties:
|
|
529
|
+
post_id: post.post_id ?? '',
|
|
530
|
+
time: post.create_time ?? '',
|
|
531
|
+
name: post.from_name ?? '',
|
|
532
|
+
content: post.content ?? '',
|
|
533
|
+
properties: post.properties ?? '',
|
|
687
534
|
});
|
|
688
|
-
|
|
689
|
-
for (const post of [...replyList].reverse()) {
|
|
690
|
-
posts.push({
|
|
691
|
-
post_id: post.post_id ?? '',
|
|
692
|
-
time: post.create_time ?? '',
|
|
693
|
-
name: post.from_name ?? '',
|
|
694
|
-
content: post.content ?? '',
|
|
695
|
-
properties: post.properties ?? '',
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
console.log(`[${CHANNEL_ID}] getPostChain result: ${posts.length} posts`, posts);
|
|
700
|
-
return posts;
|
|
701
|
-
} catch (err) {
|
|
702
|
-
console.error(`[${CHANNEL_ID}] getPostChain error:`, err);
|
|
703
|
-
throw err;
|
|
704
|
-
} finally {
|
|
705
|
-
await release();
|
|
706
535
|
}
|
|
536
|
+
|
|
537
|
+
console.log(`[${CHANNEL_ID}] getPostChain result: ${posts.length} posts`, posts);
|
|
538
|
+
return posts;
|
|
707
539
|
}
|
|
708
540
|
|
|
709
541
|
export async function getPostChainByChatId(
|
|
@@ -714,17 +546,11 @@ export async function getPostChainByChatId(
|
|
|
714
546
|
return await getPostChain(account, team_id, channel_id, parent_id||"");
|
|
715
547
|
}
|
|
716
548
|
|
|
717
|
-
|
|
718
549
|
export async function file_space_list(
|
|
719
550
|
account: any,
|
|
720
|
-
spaceId: string,
|
|
551
|
+
spaceId: string = "",
|
|
721
552
|
spaceType: string = "2"
|
|
722
553
|
): Promise<FileSpaceItem[]> {
|
|
723
|
-
checkAccount(account, 'file_space_list');
|
|
724
|
-
|
|
725
|
-
const { appId: appid, appSecret: secret } = account;
|
|
726
|
-
const url = addTuituiParams2Url('/file_space/node/list', { appid, secret });
|
|
727
|
-
|
|
728
554
|
const guessType = guessChatType(spaceId);
|
|
729
555
|
let space_final_id = "";
|
|
730
556
|
if(guessType == CHAT_TYPE_CHANNEL){
|
|
@@ -739,30 +565,44 @@ export async function file_space_list(
|
|
|
739
565
|
space_type: spaceType,
|
|
740
566
|
};
|
|
741
567
|
|
|
742
|
-
|
|
568
|
+
const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
|
|
743
569
|
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
const bodyText = await response.text();
|
|
570
|
+
const list = data?.datas?.list;
|
|
571
|
+
const flat_list = flattenFileSpaceList(list);
|
|
747
572
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const data = JSON.parse(bodyText);
|
|
753
|
-
if (Number(data?.errcode) !== 0) {
|
|
754
|
-
throw new Error(`file_space_list failed (errcode unexpected): err=${bodyText}`);
|
|
755
|
-
}
|
|
573
|
+
console.log(`[${CHANNEL_ID}] file_space_list `, flat_list);
|
|
574
|
+
return flat_list
|
|
575
|
+
}
|
|
756
576
|
|
|
757
|
-
|
|
758
|
-
|
|
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
|
+
}
|
|
759
592
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
593
|
+
// TODO: 支持群公告
|
|
594
|
+
export async function get_announcement(account: any, id: any, id_is_session: boolean = true): Promise<any> {
|
|
595
|
+
let channel_id = id;
|
|
596
|
+
if(id_is_session) {
|
|
597
|
+
channel_id = parseSessionKey(id);
|
|
598
|
+
if(!channel_id) {
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
767
601
|
}
|
|
602
|
+
|
|
603
|
+
const payload = {channel_id: channel_id};
|
|
604
|
+
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
605
|
+
//console.log("info", data);
|
|
606
|
+
const announcement = body?.datas?.info?.announcement;
|
|
607
|
+
return announcement;
|
|
768
608
|
}
|
package/src/robot_api.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk/tlon';
|
|
2
|
+
import { CHANNEL_ID } from "./const";
|
|
3
|
+
import { TUITUI_SSRF_POLICY, getTuituiApiHost } from "./env"
|
|
4
|
+
|
|
5
|
+
function addTuituiParams2Url(urlStr: string, params: any) {
|
|
6
|
+
const url = new URL(getTuituiApiHost() + urlStr);
|
|
7
|
+
for (let k in params) url.searchParams.set(k, params[k]);
|
|
8
|
+
return url.toString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function checkAccount(account: any, ctxTips: string = 'send text') {
|
|
12
|
+
if (!account || !account.appId || !account.appSecret) {
|
|
13
|
+
throw new Error(`[${CHANNEL_ID}] appId and appSecret are required for ${ctxTips}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function tuituiRobotApi(account: any, api: string, payload: any) {
|
|
18
|
+
checkAccount(account);
|
|
19
|
+
|
|
20
|
+
console.log(`[${CHANNEL_ID}] ${api} request`, payload);
|
|
21
|
+
|
|
22
|
+
const { appId: appid, appSecret: secret } = account;
|
|
23
|
+
const url = addTuituiParams2Url(api, { appid, secret });
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
let fetch_fun = _fetchJson;
|
|
27
|
+
if (payload instanceof FormData) {
|
|
28
|
+
fetch_fun = _fetchForm;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { response, release } = await fetch_fun(url, payload);
|
|
32
|
+
try {
|
|
33
|
+
const bodyText = await response.text();
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`${api} failed : ${response.status} ${response.statusText}; body=${bodyText}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = JSON.parse(bodyText);
|
|
40
|
+
if (Number(data?.errcode) !== 0) {
|
|
41
|
+
throw new Error(`${api} failed : ${bodyText}`);
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// 必须抛出错误,否则上层无法知道失败了
|
|
46
|
+
console.error(`[${CHANNEL_ID}] ${api} error:`, err);
|
|
47
|
+
throw err;
|
|
48
|
+
} finally {
|
|
49
|
+
await release();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _fetch(opts: any): Promise<any> {
|
|
54
|
+
return fetchWithSsrFGuard({
|
|
55
|
+
policy: TUITUI_SSRF_POLICY,
|
|
56
|
+
...opts,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
function _fetchJson(url: string, json: any, auditCtx: string = "tuitui.api.call"): Promise<any> {
|
|
60
|
+
return _fetch({
|
|
61
|
+
url,
|
|
62
|
+
init: {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(json),
|
|
66
|
+
},
|
|
67
|
+
auditCtx
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _fetchForm(url: string, form: any, auditCtx: string = "tuitui.api.call"): Promise<any> {
|
|
72
|
+
return _fetch({
|
|
73
|
+
url,
|
|
74
|
+
init: {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
body: form,
|
|
77
|
+
},
|
|
78
|
+
auditCtx
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Download content from a URL and extract filename and content type
|
|
84
|
+
* @param url - The URL to download from
|
|
85
|
+
* @returns Object with buffer, filename and content type
|
|
86
|
+
*/
|
|
87
|
+
export async function downloadUrl(url: string): Promise<{ buffer: ArrayBuffer; filename: string; contentType: string }> {
|
|
88
|
+
const { response, release } = await _fetch({
|
|
89
|
+
url,
|
|
90
|
+
init: { method: 'GET' },
|
|
91
|
+
auditCtx: "tuitui.download"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`[${CHANNEL_ID}] Failed to download from ${url}: ${response.status}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const buffer = await response.arrayBuffer();
|
|
100
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
101
|
+
|
|
102
|
+
// Extract filename from URL or Content-Disposition header
|
|
103
|
+
let filename = 'media';
|
|
104
|
+
try {
|
|
105
|
+
const urlPath = new URL(url).pathname;
|
|
106
|
+
const pathParts = urlPath.split('/');
|
|
107
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
108
|
+
if (lastPart) filename = lastPart;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Ignore URL parsing errors
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const contentDisposition = response.headers.get('content-disposition');
|
|
114
|
+
if (contentDisposition) {
|
|
115
|
+
const match = contentDisposition.match(/filename\*?=(?:UTF-8''|")?([^";\r\n]+)"?/i);
|
|
116
|
+
if (match) filename = decodeURIComponent(match[1]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { buffer, filename, contentType };
|
|
120
|
+
} finally {
|
|
121
|
+
await release();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -3,9 +3,8 @@ import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
import { CHANNEL_ID} from "./const";
|
|
4
4
|
import { resolveAccount } from "./accounts"
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import {DEFAULT_ONLINE} from "./env"
|
|
7
6
|
|
|
8
|
-
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, getChatRecord, getPostChainByChatId, sendTextMsg, teamsBuildChatId} from "./outbound"
|
|
7
|
+
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, getChatRecord, getPostChainByChatId, sendTextMsg, teamsBuildChatId, get_announcement} from "./outbound"
|
|
9
8
|
import {file_space_list} from "./outbound"
|
|
10
9
|
|
|
11
10
|
function tool_errmsg(str:string) {
|
|
@@ -87,6 +86,27 @@ const tuitui_send_channel_post_factory = (ctx: OpenClawPluginToolContext) => {
|
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
|
|
89
|
+
const tuitui_get_announcement_factory = (ctx: OpenClawPluginToolContext) => {
|
|
90
|
+
return {
|
|
91
|
+
name: "tuitui_get_announcement",
|
|
92
|
+
label: "tuitui_get_announcement",
|
|
93
|
+
description: "获取推推(tuitui) 当前会话的公告信息\n\n",
|
|
94
|
+
parameters: Type.Object({
|
|
95
|
+
}),
|
|
96
|
+
execute: async (_toolCallId: any, params: any) => {
|
|
97
|
+
console.log(`tuitui_get_announcement(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
|
|
98
|
+
const account = resolveAccount(ctx.config, ctx.agentAccountId);
|
|
99
|
+
if(!account || !account.enabled || !account.appId || !account.appSecret) {
|
|
100
|
+
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
101
|
+
}
|
|
102
|
+
return await get_announcement(account, ctx.sessionKey);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
90
110
|
const tuitui_file_space_factory = (ctx: OpenClawPluginToolContext) => {
|
|
91
111
|
return {
|
|
92
112
|
name: "tuitui_file_space_list",
|
|
@@ -115,8 +135,7 @@ export function registerTuituiTools(api: OpenClawPluginApi) {
|
|
|
115
135
|
|
|
116
136
|
api.registerTool(tuitui_im_get_messages_factory);
|
|
117
137
|
api.registerTool(tuitui_send_channel_post_factory);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
138
|
+
api.registerTool(tuitui_get_announcement_factory);
|
|
139
|
+
api.registerTool(tuitui_file_space_factory);
|
|
121
140
|
api.logger.info?.(`tuitui: Registered tool`);
|
|
122
141
|
}
|