@qihoo/tuitui-openclaw-channel 1.0.16 → 1.0.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/package.json +1 -1
- package/src/channel.ts +31 -7
- package/src/confs.ts +4 -2
- package/src/inbound.ts +6 -4
- package/src/inbound_body_parse.ts +100 -0
- package/src/outbound.ts +13 -76
- package/src/tools.ts +63 -58
- package/src/types.ts +47 -27
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* TuiTui Channel Plugin for OpenClaw.
|
|
3
3
|
*
|
|
4
4
|
* Implements the ChannelPlugin interface following the Synology Chat pattern.
|
|
5
|
-
* Supports single chat (text, image, voice, file) and group chat with @mentions.
|
|
6
5
|
*/
|
|
7
6
|
import WebSocket from 'ws';
|
|
8
7
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
@@ -24,8 +23,27 @@ const isConfigured = (account: any)=> !!(account?.appId && account?.appSecret);
|
|
|
24
23
|
|
|
25
24
|
const wsReadyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const;
|
|
26
25
|
let wsNumber = 0;
|
|
26
|
+
const checkTuiTuiConfig = async (apiRuntime: any) => {
|
|
27
|
+
const cfg = await apiRuntime.config.loadConfig();
|
|
28
|
+
const channels = cfg?.channels || {};
|
|
29
|
+
const currChannel = channels?.[CHANNEL_ID] || {};
|
|
30
|
+
if (currChannel?.appId === undefined && !currChannel.accounts) {
|
|
31
|
+
await apiRuntime.config.writeConfigFile({
|
|
32
|
+
...cfg,
|
|
33
|
+
channels: {
|
|
34
|
+
...channels,
|
|
35
|
+
[CHANNEL_ID]: {
|
|
36
|
+
...currChannel,
|
|
37
|
+
...baseFildsDefault,
|
|
38
|
+
appSecret: undefined,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
27
44
|
|
|
28
45
|
export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
46
|
+
checkTuiTuiConfig(apiRuntime);
|
|
29
47
|
return {
|
|
30
48
|
id: CHANNEL_ID,
|
|
31
49
|
meta: {
|
|
@@ -44,16 +62,15 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
44
62
|
config: {
|
|
45
63
|
listAccountIds: (cfg: any) => {
|
|
46
64
|
const base = cfg?.channels?.[CHANNEL_ID];
|
|
47
|
-
if (!base) return [];
|
|
48
65
|
const ids = new Set<string>();
|
|
49
|
-
|
|
50
|
-
if (base
|
|
66
|
+
ids.add(DEFAULT_ACCOUNT_ID);
|
|
67
|
+
if (base?.accounts) for (const k in base.accounts) ids.add(k);
|
|
51
68
|
return Array.from(ids);
|
|
52
69
|
},
|
|
53
70
|
|
|
54
71
|
resolveAccount,
|
|
55
72
|
|
|
56
|
-
defaultAccountId: (
|
|
73
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
57
74
|
|
|
58
75
|
isEnabled,
|
|
59
76
|
|
|
@@ -102,11 +119,18 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
102
119
|
},
|
|
103
120
|
|
|
104
121
|
status: {
|
|
122
|
+
defaultRuntime: {
|
|
123
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
124
|
+
running: false,
|
|
125
|
+
lastStartAt: null,
|
|
126
|
+
lastStopAt: null,
|
|
127
|
+
lastError: null,
|
|
128
|
+
},
|
|
105
129
|
buildAccountSnapshot: (params: any) => {
|
|
106
130
|
const account = params.account;
|
|
107
131
|
return {
|
|
108
132
|
...params.runtime,
|
|
109
|
-
accountId: params.accountId
|
|
133
|
+
accountId: params.accountId || account?.accountId || DEFAULT_ACCOUNT_ID,
|
|
110
134
|
enabled: isEnabled(account?.enabled),
|
|
111
135
|
configured: isConfigured(account),
|
|
112
136
|
};
|
|
@@ -220,7 +244,7 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
220
244
|
wsNumber++;
|
|
221
245
|
const wsId = `${wsNumber}-${Date.now()}`;
|
|
222
246
|
const wsEvtIds = new Set<string>();
|
|
223
|
-
let wsRetryTimerId = 0;
|
|
247
|
+
let wsRetryTimerId: any = 0;
|
|
224
248
|
|
|
225
249
|
const wsUrl = `wss://im.live.360.cn:8282/robot/callback/ws?auth=${account.appId}.${account.appSecret}`;
|
|
226
250
|
const defSendMsg = (msg: any) => {
|
package/src/confs.ts
CHANGED
|
@@ -13,8 +13,8 @@ export const capabilities = {
|
|
|
13
13
|
|
|
14
14
|
/*** 一些配置字段设定信息 ***/
|
|
15
15
|
const baseFields = {
|
|
16
|
-
appId: { type: 'string' } ,
|
|
17
|
-
appSecret: { type: 'string' },
|
|
16
|
+
appId: { type: 'string', default: '' } ,
|
|
17
|
+
appSecret: { type: 'string', default: '' },
|
|
18
18
|
dmPolicy: {
|
|
19
19
|
type: 'string',
|
|
20
20
|
default: 'pairing',
|
|
@@ -22,6 +22,7 @@ const baseFields = {
|
|
|
22
22
|
},
|
|
23
23
|
allowFrom: {
|
|
24
24
|
type: 'array',
|
|
25
|
+
default: [],
|
|
25
26
|
items: { type: 'string' }
|
|
26
27
|
},
|
|
27
28
|
groupPolicy: {
|
|
@@ -35,6 +36,7 @@ const baseFields = {
|
|
|
35
36
|
},
|
|
36
37
|
groupAllowFrom: {
|
|
37
38
|
type: 'array',
|
|
39
|
+
default: [],
|
|
38
40
|
items: { type: 'string' }
|
|
39
41
|
},
|
|
40
42
|
channelContext: {
|
package/src/inbound.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { TuiTuiInboundMessage, TuiTuiOutboundDeliverOptions } from './types
|
|
|
14
14
|
import { CHANNEL_ID } from './const';
|
|
15
15
|
import {
|
|
16
16
|
CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType,
|
|
17
|
-
buildMessageBody,
|
|
18
17
|
tuituiEmojiReaction,
|
|
19
18
|
sendTextMsg,
|
|
20
19
|
sendPageMsg,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
teamsBuildChatId,
|
|
23
22
|
teamsParseChatId,
|
|
24
23
|
} from "./outbound";
|
|
24
|
+
import { parseChatMessageBody } from './inbound_body_parse';
|
|
25
25
|
import { parseAllowFroms } from './utils';
|
|
26
26
|
import {
|
|
27
27
|
addUnmentionedHistory,
|
|
@@ -87,11 +87,13 @@ function getSessionKey(cfg: any, payload: ChatPayload, account: InboundAccount,
|
|
|
87
87
|
return String(sessionKey).replace(/\//g, '_');
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function getMediaUrls({ msg_type, images, voice, file }: any): string[] | undefined {
|
|
90
|
+
function getMediaUrls({ msg_type, images, voice, video, file }: any): string[] | undefined {
|
|
91
91
|
if (msg_type === 'image' || msg_type === 'mixed') {
|
|
92
92
|
if (images?.length) return images;
|
|
93
93
|
} else if (msg_type === 'voice') {
|
|
94
94
|
if (voice) return [voice];
|
|
95
|
+
} else if (msg_type === 'video') {
|
|
96
|
+
if (video) return [video];
|
|
95
97
|
} else if (msg_type === 'file') {
|
|
96
98
|
if (file?.url) return [file.url];
|
|
97
99
|
}
|
|
@@ -189,7 +191,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
189
191
|
|
|
190
192
|
payload.chatType = CHAT_TYPE_DIRECT;
|
|
191
193
|
payload.chatId = chatId;
|
|
192
|
-
payload.text =
|
|
194
|
+
payload.text = parseChatMessageBody(msgData);
|
|
193
195
|
payload.msgId = msgData.msgid;
|
|
194
196
|
log?.debug?.(
|
|
195
197
|
`[${CHANNEL_ID}] AccountId: ${accountId}, inbound single_chat:
|
|
@@ -233,7 +235,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
233
235
|
payload.chatId = chatId;
|
|
234
236
|
payload.groupName = msgData.group_name;
|
|
235
237
|
payload.msgId = msgData.msgid;
|
|
236
|
-
payload.text =
|
|
238
|
+
payload.text = parseChatMessageBody(msgData);
|
|
237
239
|
log?.debug?.(
|
|
238
240
|
`[${CHANNEL_ID}] AccountId: ${accountId}, inbound group_chat:
|
|
239
241
|
tuituiAccount=${payload.tuituiAccount},
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { TuiTuiMessageData, TuiTuiMergedData, TuituiMsgBase } from './types';
|
|
2
|
+
|
|
3
|
+
function buildSenderDesc(msg: TuituiMsgBase) {
|
|
4
|
+
return `${msg?.user_name} (${msg.user_account})`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildMsgText(msg: TuituiMsgBase): string[] {
|
|
8
|
+
const parts: string[] = [];
|
|
9
|
+
|
|
10
|
+
const pushImgs = (imgs: string[] | undefined) => {
|
|
11
|
+
if (!imgs || imgs.length === 0) return;
|
|
12
|
+
if (imgs.length === 1) {
|
|
13
|
+
parts.push(`[图片] ${imgs[0]}`);
|
|
14
|
+
} else {
|
|
15
|
+
parts.push(`[图片] 共 ${imgs.length} 张图片:`);
|
|
16
|
+
imgs.forEach((url, i) => parts.push(` ${i + 1}. ${url}`));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
switch (msg.msg_type) {
|
|
21
|
+
case 'text':
|
|
22
|
+
parts.push(msg.text || '');
|
|
23
|
+
break;
|
|
24
|
+
case 'mixed':
|
|
25
|
+
if (msg.text) parts.push(msg.text);
|
|
26
|
+
pushImgs(msg.images);
|
|
27
|
+
break;
|
|
28
|
+
case 'image':
|
|
29
|
+
pushImgs(msg.images);
|
|
30
|
+
break;
|
|
31
|
+
case 'voice':
|
|
32
|
+
if (msg.voice) parts.push(`[语音] ${msg.voice}`);
|
|
33
|
+
break;
|
|
34
|
+
case 'video':
|
|
35
|
+
if (msg.video) parts.push(`[视频] ${msg.video}`);
|
|
36
|
+
break;
|
|
37
|
+
case 'file':
|
|
38
|
+
if (msg.file) parts.push(`[文件] ${msg.file?.name} : ${msg.file?.url}`);
|
|
39
|
+
break;
|
|
40
|
+
case 'card':
|
|
41
|
+
if (msg.card) parts.push(`[名片]\n名字: ${msg.card?.name}\n推推账号: ${msg.card?.account}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'link':
|
|
44
|
+
if (msg.link) parts.push(`[网页链接]\n${msg.link?.title}\n${msg.link?.url}`);
|
|
45
|
+
break;
|
|
46
|
+
case 'merged':
|
|
47
|
+
parts.push(buildMergedBody(msg.merged));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 将合并转发(merged)数据渲染为可读文本。
|
|
56
|
+
* 支持递归嵌套:子消息的 msg_type 也可以是 merged。
|
|
57
|
+
*/
|
|
58
|
+
function buildMergedBody(merged: TuiTuiMergedData | undefined): string {
|
|
59
|
+
if (!merged) return '[合并转发]';
|
|
60
|
+
|
|
61
|
+
const source = merged.source || '聊天记录';
|
|
62
|
+
const lines: string[] = [`[合并转发:${source}]`];
|
|
63
|
+
|
|
64
|
+
for (const msg of merged.msgs ?? []) {
|
|
65
|
+
lines.push("------");
|
|
66
|
+
if (msg.timestamp) {
|
|
67
|
+
const date = new Date(Number(msg.timestamp) * 1000);
|
|
68
|
+
lines.push("时间: " + date.toLocaleString('sv-SE', { hour12: false }).replace('T', ' '));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(`发言人: ` + buildSenderDesc(msg));
|
|
72
|
+
lines.push(`内容:`);
|
|
73
|
+
|
|
74
|
+
// 消息内容,每个片段单独一行
|
|
75
|
+
const msgParts = buildMsgText(msg);
|
|
76
|
+
for (const part of msgParts) {
|
|
77
|
+
lines.push(part);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 将 TuiTui 入站消息数据解析为可读文本。
|
|
86
|
+
*/
|
|
87
|
+
export function parseChatMessageBody(data: TuiTuiMessageData): string {
|
|
88
|
+
const parts: string[] = buildMsgText(data);
|
|
89
|
+
|
|
90
|
+
// Handle reference/reply
|
|
91
|
+
const { ref } = data;
|
|
92
|
+
if (ref) {
|
|
93
|
+
const refParts = buildMsgText(ref);
|
|
94
|
+
const refContent = refParts.join('\n');
|
|
95
|
+
const senderDesc = buildSenderDesc(ref);
|
|
96
|
+
parts.push(`\n[引用来自 ${senderDesc} 的消息,内容如下]\n ${refContent}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return parts.join('\n');
|
|
100
|
+
}
|
package/src/outbound.ts
CHANGED
|
@@ -101,6 +101,8 @@ export async function postTuituiMsg(account: any, json: any, auditCtx: string):
|
|
|
101
101
|
}
|
|
102
102
|
} catch(err) {
|
|
103
103
|
console.error(`[${CHANNEL_ID}] ${auditCtx} postTuituiMsg error:`, err, `\njson: ${JSON.stringify(json)}`);
|
|
104
|
+
// 必须抛出错误,否则上层无法知道失败了(channel机器人问答、agent调用tool的场景)
|
|
105
|
+
throw err;
|
|
104
106
|
} finally {
|
|
105
107
|
await release();
|
|
106
108
|
}
|
|
@@ -239,77 +241,6 @@ export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'i
|
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
|
|
242
|
-
/**
|
|
243
|
-
* Build message body text from TuiTui inbound message data.
|
|
244
|
-
* Handles text, image, voice, file, and reference messages.
|
|
245
|
-
*/
|
|
246
|
-
export function buildMessageBody(data: TuiTuiMessageData): string {
|
|
247
|
-
const parts: string[] = [];
|
|
248
|
-
|
|
249
|
-
const pushTxt = () => parts.push(data.text || '');
|
|
250
|
-
const pushImgs = (imgs: string[] | undefined, parts: string[]) => {
|
|
251
|
-
if (!imgs) return;
|
|
252
|
-
const imgLen = imgs?.length || 0;
|
|
253
|
-
if (imgLen === 0) return;
|
|
254
|
-
if (imgLen === 1) {
|
|
255
|
-
parts.push(`[图片] ${imgs[0]}`);
|
|
256
|
-
} else {
|
|
257
|
-
parts.push(`[图片] 共 ${imgLen} 张图片:`);
|
|
258
|
-
imgs.forEach((url, i) => parts.push(` ${i + 1}. ${url}`));
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
switch (data.msg_type) {
|
|
263
|
-
case 'text':
|
|
264
|
-
pushTxt();
|
|
265
|
-
break;
|
|
266
|
-
case 'mixed':
|
|
267
|
-
pushTxt();
|
|
268
|
-
pushImgs(data.images, parts);
|
|
269
|
-
break;
|
|
270
|
-
case 'image':
|
|
271
|
-
pushImgs(data.images, parts);
|
|
272
|
-
break;
|
|
273
|
-
case 'voice':
|
|
274
|
-
if (data.voice) parts.push(`[语音] ${data.voice}`);
|
|
275
|
-
break;
|
|
276
|
-
case "file":
|
|
277
|
-
if (data.file) parts.push(`[文件] ${data.file.name}: ${data.file.url}`);
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Handle reference/reply
|
|
282
|
-
const { ref } = data;
|
|
283
|
-
if (ref) {
|
|
284
|
-
const { msg_type, is_me, user_name, user_account} = ref;
|
|
285
|
-
let refContent = `[${msg_type}]`;
|
|
286
|
-
let refParts: string[] = [];
|
|
287
|
-
switch (msg_type) {
|
|
288
|
-
case 'text':
|
|
289
|
-
refContent = ref.text || '';
|
|
290
|
-
break;
|
|
291
|
-
case 'image':
|
|
292
|
-
pushImgs(ref.images, refParts);
|
|
293
|
-
refContent = refParts.join("\n");
|
|
294
|
-
break;
|
|
295
|
-
case 'mixed':
|
|
296
|
-
refContent = ref.text || '';
|
|
297
|
-
pushImgs(ref.images, refParts);
|
|
298
|
-
refContent += "\n" + refParts.join("\n");
|
|
299
|
-
break;
|
|
300
|
-
case "file":
|
|
301
|
-
const name = ref.file?.name || '';
|
|
302
|
-
const url = ref.file?.url || '';
|
|
303
|
-
refContent = `\n文件名: ${name} 下载地址 ${url}\n`;
|
|
304
|
-
break;
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
parts.push(`\n[引用来自 ${user_name} 的消息,内容如下]\n ${refContent}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return parts.join("\n");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
244
|
export async function tuituiEmojiReaction(
|
|
314
245
|
account: any,
|
|
315
246
|
target: string,
|
|
@@ -351,17 +282,23 @@ export async function tuituiEmojiReaction(
|
|
|
351
282
|
}
|
|
352
283
|
|
|
353
284
|
export function teamsBuildChatId(team_id: string, channel_id:string, thread_id:string) : string{
|
|
354
|
-
|
|
285
|
+
if(thread_id) {
|
|
286
|
+
return `teams_${team_id}_${channel_id}_${thread_id}`;
|
|
287
|
+
} else {
|
|
288
|
+
return `teams_${team_id}_${channel_id}`;
|
|
289
|
+
}
|
|
355
290
|
}
|
|
356
291
|
|
|
357
292
|
export function teamsParseChatId(chatId: string): TuiTuiTeamsTarget {
|
|
358
293
|
const [team_id, channel_id, parent_id] = chatId.replace(/^teams_/, '').split('_');
|
|
359
294
|
|
|
360
|
-
if (!team_id || !channel_id
|
|
295
|
+
if (!team_id || !channel_id) {
|
|
361
296
|
throw new Error('Invalid teams chat ID format');
|
|
362
297
|
}
|
|
363
|
-
|
|
364
|
-
|
|
298
|
+
|
|
299
|
+
const ret = { team_id, channel_id} as TuiTuiTeamsTarget;
|
|
300
|
+
if(parent_id) ret.parent_id = parent_id;
|
|
301
|
+
return ret;
|
|
365
302
|
}
|
|
366
303
|
|
|
367
304
|
interface TuiTuiToTargets { tousers?: string[], togroups?: string[], toteams?: TuiTuiTeamsTarget[] }
|
|
@@ -376,7 +313,7 @@ function getTargets(chatId: string, chatType: ChatType): TuiTuiToTargets {
|
|
|
376
313
|
|
|
377
314
|
function replaceSingleNewlines(content: string): string {
|
|
378
315
|
// 匹配单个换行符,前后都不是换行符
|
|
379
|
-
return content.replace(/([^\n])\n([^\n])/g, '$1\n\n$2');
|
|
316
|
+
return content.replace(/([^\n|])\n([^\n|])/g, '$1\n\n$2');
|
|
380
317
|
}
|
|
381
318
|
|
|
382
319
|
/*
|
package/src/tools.ts
CHANGED
|
@@ -4,74 +4,78 @@ import { CHANNEL_ID} from "./const";
|
|
|
4
4
|
import { resolveAccount } from "./accounts"
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
|
|
7
|
-
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType, getChatRecord} from "./outbound"
|
|
7
|
+
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType, getChatRecord, sendTextMsg, teamsBuildChatId} from "./outbound"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export const tuitui_im_get_messages_schema = Type.Object(
|
|
12
|
-
{
|
|
13
|
-
chatId: Type.String({ description: "聊天ID,单聊指tuitui用户的account,群聊是群ID" }),
|
|
14
|
-
chatType: Type.String({ description: `聊天类型。 单聊:${CHAT_TYPE_DIRECT} 群聊:${CHAT_TYPE_GROUP}`}),
|
|
15
|
-
relativeTime: Type.Optional(Type.String({ description: `相对时间范围:today / yesterday / day_before_yesterday / this_week / last_week / this_month / last_month / last_{N}_{unit}(unit: minutes/hours/days)。与 startTime/endTime 互斥`})),
|
|
16
|
-
startTime: Type.Optional(Type.String({ description: `起始时间(ISO 8601 格式,如 2026-02-27T00:00:00+08:00),非必填,默认指2000年代表。与 relativeTime 互斥`})),
|
|
17
|
-
endTime: Type.Optional(Type.String({ description: `结束时间(ISO 8601 格式,如 2026-02-27T00:00:00+08:00),非必填,默认当前时间。与 relativeTime 互斥`})),
|
|
18
|
-
limit: Type.Optional(Type.Number({ description: `每页条数,1~100,默认 100`})),
|
|
19
|
-
cursor: Type.Optional(Type.String({ description: `游标首次调用填 "0",返回结果中has_more为true时代表可以获取下一页,如果你需要下一页,可以传返回结果的cursor代表继续拉取下一页`})),
|
|
20
|
-
orderAsc: Type.Optional(Type.Boolean({ description: `返回数据是否按时间正序排序,默认 false(按时间逆序,即从最新的开始拉取)`})),
|
|
21
|
-
},
|
|
22
|
-
{ additionalProperties: false },
|
|
23
|
-
);
|
|
24
|
-
|
|
25
9
|
function tool_errmsg(str:string) {
|
|
26
|
-
const ret = `
|
|
27
|
-
console.
|
|
10
|
+
const ret = `error: ${str}`
|
|
11
|
+
console.error(ret);
|
|
28
12
|
return ret;
|
|
29
13
|
}
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
const tuitui_im_get_messages_factory = (ctx: OpenClawPluginToolContext) => {
|
|
16
|
+
return {
|
|
17
|
+
name: "tuitui_im_get_messages",
|
|
18
|
+
label: "tuitui_im_get_messages",
|
|
19
|
+
description: "推推(tuitui) 聊天记录获取,可查询私聊和群聊的聊天记录。\n\n",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
chatId: Type.String({ description: "聊天ID,单聊指tuitui用户的account,群聊是群ID" }),
|
|
22
|
+
chatType: Type.String({ description: `聊天类型。 单聊:${CHAT_TYPE_DIRECT} 群聊:${CHAT_TYPE_GROUP}`}),
|
|
23
|
+
relativeTime: Type.Optional(Type.String({ description: `相对时间范围:today / yesterday / day_before_yesterday / this_week / last_week / this_month / last_month / last_{N}_{unit}(unit: minutes/hours/days)。与 startTime/endTime 互斥`})),
|
|
24
|
+
startTime: Type.Optional(Type.String({ description: `起始时间(ISO 8601 格式,如 2026-02-27T00:00:00+08:00),非必填,默认指2000年代表。与 relativeTime 互斥`})),
|
|
25
|
+
endTime: Type.Optional(Type.String({ description: `结束时间(ISO 8601 格式,如 2026-02-27T00:00:00+08:00),非必填,默认当前时间。与 relativeTime 互斥`})),
|
|
26
|
+
limit: Type.Optional(Type.Number({ description: `每页条数,1~100,默认 100`})),
|
|
27
|
+
cursor: Type.Optional(Type.String({ description: `游标首次调用填 "0",返回结果中has_more为true时代表可以获取下一页,如果你需要下一页,可以传返回结果的cursor代表继续拉取下一页`})),
|
|
28
|
+
orderAsc: Type.Optional(Type.Boolean({ description: `返回数据是否按时间正序排序,默认 false(按时间逆序,即从最新的开始拉取)`})),
|
|
29
|
+
}),
|
|
30
|
+
execute: async (_toolCallId: any, params: any) => {
|
|
31
|
+
console.log(`tuitui_im_get_messages(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
|
|
32
|
+
const account = resolveAccount(ctx.config, ctx.agentAccountId);
|
|
33
|
+
if(!account || !account.enabled || !account.appId || !account.appSecret) {
|
|
34
|
+
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const chatType = params?.chatType || "";
|
|
38
|
+
const chatId = params?.chatId || "";
|
|
39
|
+
if(!chatId || !chatType) {
|
|
40
|
+
return tool_errmsg(`chatType or chatId empty`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if(chatType != CHAT_TYPE_DIRECT && chatType != CHAT_TYPE_GROUP) {
|
|
44
|
+
return tool_errmsg(`unknown chatType: ${chatType}`);
|
|
45
|
+
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
}
|
|
47
|
+
return await getChatRecord(account, chatId, chatType, {
|
|
48
|
+
startTime: params?.startTime,
|
|
49
|
+
endTime: params?.endTime,
|
|
50
|
+
relativeTime: params?.relativeTime,
|
|
51
|
+
limit: params?.limit,
|
|
52
|
+
cursor: params?.cursor,
|
|
53
|
+
orderAsc: params?.orderAsc,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
56
58
|
|
|
57
|
-
const tuituiToolFactory = (ctx: OpenClawPluginToolContext) => {
|
|
58
|
-
const agentAccountId = ctx.agentAccountId;
|
|
59
|
-
const messageChannel = ctx.messageChannel;
|
|
60
|
-
const sessionKey = ctx.sessionKey;
|
|
61
|
-
const config = ctx.config;
|
|
62
59
|
|
|
60
|
+
const tuitui_send_channel_post_factory = (ctx: OpenClawPluginToolContext) => {
|
|
63
61
|
return {
|
|
64
|
-
name: "
|
|
65
|
-
label: "
|
|
66
|
-
description: "推推(tuitui)
|
|
67
|
-
parameters:
|
|
62
|
+
name: "tuitui_send_channel_post",
|
|
63
|
+
label: "tuitui_send_channel_post",
|
|
64
|
+
description: "推推(tuitui) 发送团队中的频道里面的帖子,帖子内容是markdown格式\n\n",
|
|
65
|
+
parameters: Type.Object({
|
|
66
|
+
team_id: Type.String({ description: "推推团队ID" }),
|
|
67
|
+
channel_id: Type.String({ description: "推推频道ID,频道属于团队" }),
|
|
68
|
+
markdown: Type.String({ description: `帖子正文,markdown格式`}),
|
|
69
|
+
parent_id: Type.Optional(Type.String({ description: `帖子ID,如果要回复一个帖子,需要填写这个字段`})),
|
|
70
|
+
}),
|
|
68
71
|
execute: async (_toolCallId: any, params: any) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
console.log(`tuitui_send_channel_post(): agentAccountId: ${ctx.agentAccountId}, sessionKey: ${ctx.sessionKey}`, params);
|
|
73
|
+
const account = resolveAccount(ctx.config, ctx.agentAccountId);
|
|
74
|
+
if(!account || !account.enabled || !account.appId || !account.appSecret) {
|
|
75
|
+
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
return await
|
|
77
|
+
const chatId = teamsBuildChatId(params?.team_id, params?.channel_id, params?.parent_id);
|
|
78
|
+
return await sendTextMsg(account, chatId, CHAT_TYPE_CHANNEL, params?.markdown);
|
|
75
79
|
},
|
|
76
80
|
};
|
|
77
81
|
};
|
|
@@ -82,6 +86,7 @@ export function registerTuituiTools(api: OpenClawPluginApi) {
|
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
api.registerTool(
|
|
89
|
+
api.registerTool(tuitui_im_get_messages_factory);
|
|
90
|
+
api.registerTool(tuitui_send_channel_post_factory);
|
|
86
91
|
api.logger.info?.(`tuitui: Registered tool`);
|
|
87
92
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,47 +2,67 @@
|
|
|
2
2
|
* Shared TypeScript types for the TuiTui channel plugin.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
export interface TuituiMsgBase {
|
|
7
|
+
user_account: string;
|
|
8
|
+
user_name: string;
|
|
9
|
+
msg_type: string;
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
images?: string[];
|
|
13
|
+
voice?: string;
|
|
14
|
+
video?: string;
|
|
15
|
+
file?: { name?: string; url?: string };
|
|
16
|
+
card?: { name?: string; account?: string };
|
|
17
|
+
link?: { title?: string; url?: string };
|
|
18
|
+
merged?: TuiTuiMergedData;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export interface TuiTuiInboundMessage {
|
|
6
22
|
cid: string;
|
|
7
23
|
uid: string;
|
|
8
24
|
user_account: string;
|
|
9
25
|
user_name: string;
|
|
10
26
|
timestamp: string;
|
|
11
|
-
event:
|
|
27
|
+
event: string;
|
|
12
28
|
data: TuiTuiMessageData;
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
|
|
31
|
+
/** 合并转发消息中的单条子消息,继承 TuituiMsgBase 并添加额外字段 */
|
|
32
|
+
export interface TuiTuiMergedMsg extends TuituiMsgBase {
|
|
33
|
+
cid: string;
|
|
34
|
+
uid: string;
|
|
35
|
+
is_me: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 合并转发的 merged 字段 */
|
|
39
|
+
export interface TuiTuiMergedData {
|
|
40
|
+
/** 聊天记录来源描述,例如"XXX和YYY的聊天记录" */
|
|
41
|
+
source?: string;
|
|
42
|
+
/** 摘要文本 */
|
|
43
|
+
summary?: string;
|
|
44
|
+
/** 子消息列表 */
|
|
45
|
+
msgs: TuiTuiMergedMsg[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TuiTuiRefMsg extends TuituiMsgBase {
|
|
49
|
+
cid: string;
|
|
50
|
+
uid: string;
|
|
51
|
+
is_me?: boolean;
|
|
52
|
+
msgid?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/** 实际消息数据结构,继承 TuituiMsgBase 并添加额外字段 */
|
|
57
|
+
export interface TuiTuiMessageData extends TuituiMsgBase {
|
|
16
58
|
msgid?: string;
|
|
17
|
-
msg_type: 'text' | 'image' | 'mixed' | 'voice' | 'file';
|
|
18
|
-
text?: string;
|
|
19
|
-
images?: string[];
|
|
20
|
-
image_ids?: string[];
|
|
21
|
-
voice?: string;
|
|
22
|
-
voice_id?: string;
|
|
23
|
-
file?: { name: string; url: string; file_id: string };
|
|
24
59
|
// Group chat fields
|
|
25
60
|
group_id?: string;
|
|
26
61
|
group_name?: string;
|
|
27
62
|
at_me?: boolean;
|
|
28
63
|
at?: Array<{ is_at_all: boolean; cid?: string; uid?: string; name?: string }>;
|
|
29
64
|
// Reference/reply fields
|
|
30
|
-
ref?:
|
|
31
|
-
cid: string;
|
|
32
|
-
uid: string;
|
|
33
|
-
user_account?: string;
|
|
34
|
-
user_name: string;
|
|
35
|
-
is_me?: boolean;
|
|
36
|
-
msgid: string;
|
|
37
|
-
msg_type: 'text' | 'image' | 'mixed' | 'file' | 'voice';
|
|
38
|
-
text?: string;
|
|
39
|
-
images?: string[];
|
|
40
|
-
image_ids?: string[];
|
|
41
|
-
file?: {
|
|
42
|
-
url?: string;
|
|
43
|
-
name?: string;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
65
|
+
ref?: TuiTuiRefMsg;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
export interface TuiTuiOutboundTextMessage {
|
|
@@ -143,6 +163,6 @@ export interface TuiTuiOutboundDeliverOptions {
|
|
|
143
163
|
export interface TuiTuiTeamsTarget {
|
|
144
164
|
team_id: string;
|
|
145
165
|
channel_id: string;
|
|
146
|
-
post_id
|
|
147
|
-
parent_id
|
|
166
|
+
post_id?: string;
|
|
167
|
+
parent_id?: string;
|
|
148
168
|
}
|