@qihoo/tuitui-openclaw-channel 1.0.30 → 1.0.32
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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/accounts.ts +2 -2
- package/src/channel.ts +2 -1
- package/src/chat_base.ts +28 -8
- package/src/chat_record.ts +20 -1
- package/src/filespace.ts +25 -20
- package/src/inbound.ts +47 -6
- package/src/monitor.ts +15 -6
- package/src/outbound.ts +9 -0
- package/src/robot_api.ts +7 -0
- package/src/robot_helper.ts +7 -0
- package/src/tools.ts +21 -5
- package/src/websocket.ts +8 -0
package/openclaw.plugin.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"enum": ["channel", "thread"]
|
|
44
44
|
},
|
|
45
45
|
"emojiReaction": { "type": "boolean", "default": true },
|
|
46
|
-
"monitorEnabled": { "type": "boolean", "default":
|
|
46
|
+
"monitorEnabled": { "type": "boolean", "default": true },
|
|
47
47
|
"accounts": {
|
|
48
48
|
"type": "object",
|
|
49
49
|
"additionalProperties": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"enum": ["channel", "thread"]
|
|
80
80
|
},
|
|
81
81
|
"emojiReaction": { "type": "boolean", "default": true },
|
|
82
|
-
"monitorEnabled": { "type": "boolean", "default":
|
|
82
|
+
"monitorEnabled": { "type": "boolean", "default": true }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"advanced": true
|
|
132
132
|
},
|
|
133
133
|
"monitorEnabled": {
|
|
134
|
-
"help": "
|
|
134
|
+
"help": "是否开启频道agent事件上报,用于频道展示agent执行中间步骤(默认 true)",
|
|
135
135
|
"order": 17,
|
|
136
136
|
"advanced": true
|
|
137
137
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"advanced": true
|
|
186
186
|
},
|
|
187
187
|
"accounts.*.monitorEnabled": {
|
|
188
|
-
"help": "
|
|
188
|
+
"help": "是否开启频道agent事件上报,用于频道展示agent执行中间步骤(默认 true)。",
|
|
189
189
|
"order": 311,
|
|
190
190
|
"advanced": true
|
|
191
191
|
}
|
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const dmPolicyDefault = 'pairing';
|
|
|
7
7
|
export const groupPolicyDefault = 'allowlist';
|
|
8
8
|
export const requireMentionDefault = true;
|
|
9
9
|
export const emojiReactionDefault = true;
|
|
10
|
-
export const monitorEnabledDefault =
|
|
10
|
+
export const monitorEnabledDefault = true;
|
|
11
11
|
|
|
12
12
|
const mergeArrs = (arr1: any, arr2: any) => {
|
|
13
13
|
return [...new Set([...(arr1 || []), ...arr2 || []])];
|
|
@@ -20,7 +20,7 @@ export const getAccountInfo = (acct?: any) => ({
|
|
|
20
20
|
dmPolicy: acct?.dmPolicy || dmPolicyDefault,
|
|
21
21
|
allowFrom: parseAllowFroms(acct?.allowFrom || []),
|
|
22
22
|
// 群组策略与白名单、群组级覆盖
|
|
23
|
-
groupPolicy: acct?.
|
|
23
|
+
groupPolicy: acct?.groupPolicy || groupPolicyDefault,
|
|
24
24
|
groupAllowFrom: parseAllowFroms(acct?.groupAllowFrom || []),
|
|
25
25
|
requireMention: isEnabled(acct?.requireMention ?? requireMentionDefault),
|
|
26
26
|
emojiReaction: isEnabled(acct?.emojiReaction ?? emojiReactionDefault),
|
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
7
7
|
import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection, OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
|
|
8
8
|
import { CHANNEL_ID, CHANNEL_NAME } from "./const";
|
|
9
|
+
import { version } from "../package.json"
|
|
9
10
|
import { handleInboundMessage } from './inbound';
|
|
10
11
|
import { guessChatTypeV2 } from "./chat_record"
|
|
11
12
|
import {
|
|
@@ -55,7 +56,7 @@ function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
55
56
|
id: CHANNEL_ID,
|
|
56
57
|
label: CHANNEL_NAME,
|
|
57
58
|
selectionLabel: CHANNEL_NAME,
|
|
58
|
-
detailLabel: CHANNEL_NAME,
|
|
59
|
+
detailLabel: CHANNEL_NAME + " " + version,
|
|
59
60
|
docsPath: `/channels/${CHANNEL_ID}`,
|
|
60
61
|
blurb: `Connect to ${CHANNEL_NAME} bot via WebSocket`,
|
|
61
62
|
order: 100,
|
package/src/chat_base.ts
CHANGED
|
@@ -33,18 +33,38 @@ export function teamsParseChatId(chatId: string): TuiTuiTeamsTarget {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
export
|
|
37
|
-
|
|
36
|
+
export interface TuiTuiSessionKeyInfo {
|
|
37
|
+
chat_type: ChatType,
|
|
38
|
+
channel_id: string;
|
|
39
|
+
thread_id: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 暂时只支持频道(后续可以完善)
|
|
43
|
+
// 格式 agent:coder:tuitui:channel:123:thread:456
|
|
44
|
+
export function parseSessionKey(str: string): TuiTuiSessionKeyInfo {
|
|
45
|
+
const info: TuiTuiSessionKeyInfo = { chat_type: CHAT_TYPE_DIRECT, channel_id: '', thread_id: '' };
|
|
46
|
+
|
|
38
47
|
if (!str.includes('tuitui:channel:')) {
|
|
39
|
-
|
|
48
|
+
return info;
|
|
40
49
|
}
|
|
41
|
-
|
|
50
|
+
|
|
51
|
+
info.chat_type = CHAT_TYPE_CHANNEL;
|
|
52
|
+
|
|
42
53
|
const parts = str.split(':');
|
|
43
54
|
const channelIndex = parts.findIndex(part => part === 'channel');
|
|
44
|
-
|
|
45
55
|
if (channelIndex !== -1 && parts[channelIndex + 1]) {
|
|
46
|
-
|
|
56
|
+
info.channel_id = parts[channelIndex + 1];
|
|
47
57
|
}
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
|
|
59
|
+
const threadIndex = parts.findIndex(part => part === 'thread');
|
|
60
|
+
if (threadIndex !== -1 && parts[threadIndex + 1]) {
|
|
61
|
+
info.thread_id = parts[threadIndex + 1];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return info;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseChannelIdBySessionKey(str: string): string {
|
|
68
|
+
const info = parseSessionKey(str);
|
|
69
|
+
return info.channel_id;
|
|
50
70
|
}
|
package/src/chat_record.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CHANNEL_ID } from "./const";
|
|
2
2
|
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType, teamsParseChatId, guessChatType, teamsBuildChatId} from "./chat_base"
|
|
3
3
|
import { tuituiRobotApi } from "./robot_api"
|
|
4
|
+
import { parseSessionKey } from "./chat_base"
|
|
5
|
+
import {getChannelInfoByChannelId} from "./robot_helper"
|
|
4
6
|
import type { TuiTuiMessageData} from './types';
|
|
5
7
|
|
|
6
8
|
export interface TuiTuiChatRecordMessage {
|
|
@@ -236,6 +238,7 @@ export async function getChannelInfoById(account: any, channel_id: string): Prom
|
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
interface TeamsPostChainItem {
|
|
241
|
+
from_uid: string;
|
|
239
242
|
post_id: string;
|
|
240
243
|
time: string;
|
|
241
244
|
last_reply_time: string; // 只有主贴有最后回帖时间,所有的回帖更新的都是主贴的属性
|
|
@@ -261,6 +264,7 @@ function parsePost(item: any): TeamsPostChainItem[] {
|
|
|
261
264
|
const posts: TeamsPostChainItem[] = [];
|
|
262
265
|
|
|
263
266
|
posts.push({
|
|
267
|
+
from_uid: topic.from_uid ?? '',
|
|
264
268
|
post_id: topic.post_id ?? '',
|
|
265
269
|
time: topic.create_time ?? '',
|
|
266
270
|
last_reply_time: topic.last_reply_time ?? '',
|
|
@@ -271,6 +275,7 @@ function parsePost(item: any): TeamsPostChainItem[] {
|
|
|
271
275
|
|
|
272
276
|
for (const post of [...replyList].reverse()) {
|
|
273
277
|
posts.push({
|
|
278
|
+
from_uid: post.from_uid ?? '',
|
|
274
279
|
post_id: post.post_id ?? '',
|
|
275
280
|
time: post.create_time ?? '',
|
|
276
281
|
last_reply_time : '',
|
|
@@ -365,12 +370,26 @@ async function getPostChain(
|
|
|
365
370
|
|
|
366
371
|
const datas = data.datas ?? {};
|
|
367
372
|
|
|
368
|
-
const posts = parsePost(datas)
|
|
373
|
+
const posts = parsePost(datas)
|
|
369
374
|
|
|
370
375
|
console.log(`[${CHANNEL_ID}] getPostChain result: ${posts.length} posts`, posts);
|
|
371
376
|
return posts;
|
|
372
377
|
}
|
|
373
378
|
|
|
379
|
+
export async function getPostChainBySessionKey(
|
|
380
|
+
account: any,
|
|
381
|
+
sessionKey: string,
|
|
382
|
+
): Promise<TeamsPostChainItem[]> {
|
|
383
|
+
const session_info = parseSessionKey(sessionKey);
|
|
384
|
+
|
|
385
|
+
const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
|
|
386
|
+
if (!channelInfo) {
|
|
387
|
+
throw new Error(`无法获取频道信息`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return await getPostChain(account, channelInfo.team_id, session_info.channel_id, session_info.thread_id);
|
|
391
|
+
}
|
|
392
|
+
|
|
374
393
|
export async function getPostChainByChatId(
|
|
375
394
|
account: any,
|
|
376
395
|
chatId: string,
|
package/src/filespace.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CHANNEL_ID } from "./const";
|
|
2
2
|
import { tuituiRobotApi, downloadUrl, tuituiRobotUpload} from "./robot_api"
|
|
3
|
-
import {
|
|
3
|
+
import {CHAT_TYPE_CHANNEL, parseSessionKey} from "./chat_base"
|
|
4
|
+
import {getChannelInfoByChannelId} from "./robot_helper"
|
|
4
5
|
|
|
5
6
|
export const NODE_TYPE_DIR = '1';
|
|
6
7
|
export const NODE_TYPE_FILE = '2';
|
|
@@ -51,29 +52,21 @@ export function flattenFileSpaceList(list: any[]): FileSpaceItem[] {
|
|
|
51
52
|
}));
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
async function getChannelInfoBySessionKey(account: any, sessionKey: string) {
|
|
57
|
-
const channel_id = parseChannelIdBySessionKey(sessionKey);
|
|
58
|
-
if(!channel_id) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const payload = {channel_id: channel_id};
|
|
63
|
-
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
64
|
-
return body?.datas?.info;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
55
|
export async function file_space_list(
|
|
68
56
|
account: any,
|
|
69
57
|
sessionKey: string
|
|
70
58
|
): Promise<any> {
|
|
71
59
|
|
|
72
|
-
const
|
|
73
|
-
if
|
|
60
|
+
const session_info = parseSessionKey(sessionKey);
|
|
61
|
+
if(session_info.chat_type != CHAT_TYPE_CHANNEL) {
|
|
74
62
|
throw new Error(`私聊、群聊会话不支持共享空间。仅团队频道支持。`);
|
|
75
63
|
}
|
|
76
64
|
|
|
65
|
+
const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
|
|
66
|
+
if (!channelInfo) {
|
|
67
|
+
throw new Error(`无法获取频道信息`);
|
|
68
|
+
}
|
|
69
|
+
|
|
77
70
|
const payload = {
|
|
78
71
|
space_id: channelInfo.team_id,
|
|
79
72
|
space_type: SPACE_TYPE_TEAM,
|
|
@@ -81,6 +74,8 @@ export async function file_space_list(
|
|
|
81
74
|
|
|
82
75
|
const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
|
|
83
76
|
|
|
77
|
+
//console.log(JSON.stringify(data));
|
|
78
|
+
|
|
84
79
|
const list = data?.datas?.list;
|
|
85
80
|
const flat_list = flattenFileSpaceList(list);
|
|
86
81
|
|
|
@@ -98,7 +93,8 @@ export async function file_space_list(
|
|
|
98
93
|
async function ensureFolderPath(
|
|
99
94
|
account: any,
|
|
100
95
|
spaceId: string,
|
|
101
|
-
folderPath: string
|
|
96
|
+
folderPath: string,
|
|
97
|
+
source: string
|
|
102
98
|
): Promise<string> {
|
|
103
99
|
// 标准化路径
|
|
104
100
|
const normalizedPath = folderPath.startsWith('/') ? folderPath : '/' + folderPath;
|
|
@@ -151,6 +147,7 @@ async function ensureFolderPath(
|
|
|
151
147
|
node_type: NODE_TYPE_DIR,
|
|
152
148
|
name: folderName,
|
|
153
149
|
parent_id: currentParentId,
|
|
150
|
+
source: source,
|
|
154
151
|
};
|
|
155
152
|
|
|
156
153
|
const createResult = await tuituiRobotApi(account, '/file_space/node/add', createFolderPayload);
|
|
@@ -184,11 +181,18 @@ export async function file_space_add(
|
|
|
184
181
|
cloud_filepath: string,
|
|
185
182
|
url_or_localpath: string,
|
|
186
183
|
): Promise<any> {
|
|
187
|
-
const
|
|
188
|
-
if
|
|
184
|
+
const session_info = parseSessionKey(sessionKey);
|
|
185
|
+
if(session_info.chat_type != CHAT_TYPE_CHANNEL) {
|
|
189
186
|
throw new Error(`私聊、群聊会话不支持共享空间。仅团队频道支持。`);
|
|
190
187
|
}
|
|
191
188
|
|
|
189
|
+
const channelInfo = await getChannelInfoByChannelId(account, session_info.channel_id);
|
|
190
|
+
if (!channelInfo) {
|
|
191
|
+
throw new Error(`无法获取频道信息`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const source = session_info.thread_id;
|
|
195
|
+
|
|
192
196
|
// 1. 解析云路径,分离目录和文件名
|
|
193
197
|
const normalizedPath = cloud_filepath.startsWith('/')
|
|
194
198
|
? cloud_filepath
|
|
@@ -201,7 +205,7 @@ export async function file_space_add(
|
|
|
201
205
|
: '';
|
|
202
206
|
|
|
203
207
|
// 2. 确保目标目录存在,获取 parent_id
|
|
204
|
-
const parentId = await ensureFolderPath(account, channelInfo.team_id, folderPath);
|
|
208
|
+
const parentId = await ensureFolderPath(account, channelInfo.team_id, folderPath, source);
|
|
205
209
|
|
|
206
210
|
// 3. 上传文件获取 fid
|
|
207
211
|
const isImage = /^data:image\//i.test(url_or_localpath) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(url_or_localpath);
|
|
@@ -219,6 +223,7 @@ export async function file_space_add(
|
|
|
219
223
|
name: filename,
|
|
220
224
|
fid: fid,
|
|
221
225
|
parent_id: parentId,
|
|
226
|
+
source: source,
|
|
222
227
|
};
|
|
223
228
|
|
|
224
229
|
const body = await tuituiRobotApi(account, '/file_space/node/add', filePayload);
|
package/src/inbound.ts
CHANGED
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
sendPageMsg,
|
|
20
20
|
sendMediaMsg,
|
|
21
21
|
get_announcement,
|
|
22
|
+
get_team_members,
|
|
22
23
|
} from "./outbound";
|
|
23
24
|
import { parseChatMessageBody } from './inbound_body_parse';
|
|
24
25
|
import { parseAllowFroms } from './utils';
|
|
25
26
|
import { addUnmentionedHistory, popUnmentionedHistories } from "./histories";
|
|
26
27
|
import { StringDeduplicator } from "./deduplicator"
|
|
28
|
+
import { getPostChainByChatId } from "./chat_record"
|
|
27
29
|
|
|
28
30
|
// 会话上下文注入排重
|
|
29
31
|
let _session_ctx_injected = new StringDeduplicator(1000, 3600);
|
|
@@ -177,19 +179,52 @@ async function sendSingleChatPairingMsg(account: any, payload: any, log: any, ap
|
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
async function
|
|
182
|
-
if(!
|
|
182
|
+
//tuituiAccounts列表中是否存在至少1个允许私聊的用户(私聊白名单、配对、或者open)
|
|
183
|
+
async function isAllowOneOfAccount(tuituiAccounts: string[], apiRuntime: any, account: InboundAccount) {
|
|
184
|
+
if(!tuituiAccounts) return false;
|
|
183
185
|
const { accountId, allowFrom, dmPolicy } = account;
|
|
184
186
|
if (dmPolicy === 'open') return true;
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
// 是否包含任意一个元素
|
|
189
|
+
if (tuituiAccounts.some(item => allowFrom.includes(item))) return true;
|
|
190
|
+
|
|
187
191
|
try {
|
|
188
|
-
storeAllowFrom = parseAllowFroms(
|
|
192
|
+
let storeAllowFrom = parseAllowFroms(
|
|
189
193
|
await apiRuntime?.channel?.pairing?.readAllowFromStore?.({ channel: CHANNEL_ID, accountId })
|
|
190
194
|
);
|
|
195
|
+
if (tuituiAccounts.some(item => storeAllowFrom.includes(item))) return true;
|
|
191
196
|
} catch {}
|
|
192
|
-
return
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function isAllowAccount(tuituiAccount: string, apiRuntime: any, account: InboundAccount) {
|
|
201
|
+
return await isAllowOneOfAccount([tuituiAccount], apiRuntime, account);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function isChannelCoordAuthorized(tuituiAccount: string, apiRuntime: any, account: InboundAccount, team_id:string, chat_id:string) {
|
|
205
|
+
// 协调员身份校验
|
|
206
|
+
if (tuituiAccount != "bot-FiwwCeDw" && tuituiAccount != "bot-sUwUeknd") return false;
|
|
207
|
+
|
|
208
|
+
// 当前 thread 中有白名单用户发帖,则协调员继承该权限
|
|
209
|
+
const [members, posts] = await Promise.all([
|
|
210
|
+
get_team_members(account, team_id),
|
|
211
|
+
getPostChainByChatId(account, chat_id),
|
|
212
|
+
]);
|
|
213
|
+
if (!members?.length || !posts?.length) return false;
|
|
214
|
+
|
|
215
|
+
const from_uids = posts.map((p: any) => p.from_uid).filter(Boolean);
|
|
216
|
+
if (!from_uids.length) return false;
|
|
217
|
+
|
|
218
|
+
//console.log("from_uids", from_uids);
|
|
219
|
+
|
|
220
|
+
// 将 uid 映射到 user_account
|
|
221
|
+
const from_accounts: string[] = members
|
|
222
|
+
.filter((m: any) => from_uids.includes(m.uid) && m.account)
|
|
223
|
+
.map((m: any) => String(m.account));
|
|
224
|
+
if (!from_accounts.length) return false;
|
|
225
|
+
|
|
226
|
+
//console.log("from_accounts", from_accounts);
|
|
227
|
+
return await isAllowOneOfAccount(from_accounts, apiRuntime, account);
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
|
|
@@ -386,6 +421,12 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
|
|
|
386
421
|
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
|
|
387
422
|
return false;
|
|
388
423
|
}
|
|
424
|
+
|
|
425
|
+
// 频道中允许协调员@自己(且帖子里有白名单用户发帖)
|
|
426
|
+
if (await isChannelCoordAuthorized(tuituiAccount, apiRuntime, account, team_id, chatId)) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
389
430
|
if (needPairingThrottle(accountId, chatId)) {
|
|
390
431
|
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, teams pairing throttled for teamsChatId=${chatId}`);
|
|
391
432
|
return false;
|
package/src/monitor.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* TuiTui Monitor — 将 OpenClaw Hook 事件批量上报到监控接口。
|
|
3
3
|
*
|
|
4
4
|
* 每个账户(appId/appSecret)维护独立队列,满足以下任一条件时批量上报:
|
|
5
|
-
* - 队列积累达 BATCH_MAX_SIZE
|
|
6
|
-
* - 距上次 flush 超过 BATCH_INTERVAL_MS
|
|
5
|
+
* - 队列积累达 BATCH_MAX_SIZE 条
|
|
6
|
+
* - 距上次 flush 超过 BATCH_INTERVAL_MS
|
|
7
7
|
*
|
|
8
|
-
* 需在配置中将 monitorEnabled 设为 true
|
|
8
|
+
* 需在配置中将 monitorEnabled 设为 true 才会上报
|
|
9
9
|
*
|
|
10
10
|
* 配置示例(openclaw.json):
|
|
11
11
|
* channels.tuitui.monitorEnabled: true
|
|
@@ -14,6 +14,7 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
|
14
14
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
|
|
15
15
|
import { CHANNEL_ID } from './const';
|
|
16
16
|
import { tuituiRobotApi } from './robot_api';
|
|
17
|
+
import {parseChannelIdBySessionKey} from "./chat_base"
|
|
17
18
|
|
|
18
19
|
// 批量上报参数
|
|
19
20
|
const BATCH_MAX_SIZE = 100; // 达到此条数立即 flush
|
|
@@ -272,19 +273,27 @@ function report(
|
|
|
272
273
|
ctx: unknown,
|
|
273
274
|
data: unknown,
|
|
274
275
|
): void {
|
|
275
|
-
let
|
|
276
|
+
let sessionKey = "";
|
|
276
277
|
|
|
277
278
|
// subagent hook 的 ctx 有 childSessionKey,普通 hook 有 sessionKey
|
|
278
279
|
if (eventType === 'subagent_spawned' || eventType === 'subagent_ended') {
|
|
279
|
-
|
|
280
|
+
sessionKey = (ctx as any)?.childSessionKey
|
|
280
281
|
} else {
|
|
281
|
-
|
|
282
|
+
sessionKey = (ctx as any)?.sessionKey
|
|
282
283
|
}
|
|
283
284
|
|
|
285
|
+
const agentId = extractAgentId(sessionKey);
|
|
284
286
|
if (!agentId) return;
|
|
285
287
|
|
|
288
|
+
// 仅频道支持;私聊群聊不启用
|
|
289
|
+
const channelId = parseChannelIdBySessionKey(sessionKey);
|
|
290
|
+
if(!channelId) return;
|
|
291
|
+
|
|
292
|
+
const reportedAccountIds = new Map<string, boolean>();
|
|
286
293
|
for (const target of targets) {
|
|
287
294
|
if (target.agentId !== agentId) continue;
|
|
295
|
+
if (reportedAccountIds.has(target.accountId)) continue;
|
|
296
|
+
reportedAccountIds.set(target.accountId, true);
|
|
288
297
|
|
|
289
298
|
const payload: MonitorPayload = {
|
|
290
299
|
event: eventType,
|
package/src/outbound.ts
CHANGED
|
@@ -234,3 +234,12 @@ export async function get_announcement(account: any, id: any, id_is_session: boo
|
|
|
234
234
|
const announcement = body?.datas?.info?.announcement;
|
|
235
235
|
return announcement;
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
export async function get_team_members(account: any, team_id: string): Promise<any> {
|
|
240
|
+
const payload = {team_id: team_id};
|
|
241
|
+
const body = await tuituiRobotApi(account, '/teams/member/list', payload);
|
|
242
|
+
const members = body?.datas?.members;
|
|
243
|
+
//console.log("info", members);
|
|
244
|
+
return members;
|
|
245
|
+
}
|
package/src/robot_api.ts
CHANGED
|
@@ -19,6 +19,8 @@ export function checkAccount(account: any, ctxTips: string = 'send text') {
|
|
|
19
19
|
export async function tuituiRobotApi(account: any, api: string, payload: any, log: boolean = true) {
|
|
20
20
|
checkAccount(account);
|
|
21
21
|
|
|
22
|
+
const startTime = Date.now(); // 记录开始时间
|
|
23
|
+
|
|
22
24
|
if (log) {
|
|
23
25
|
console.log(`[${CHANNEL_ID}] ${api} request`, payload);
|
|
24
26
|
}
|
|
@@ -51,6 +53,11 @@ export async function tuituiRobotApi(account: any, api: string, payload: any, lo
|
|
|
51
53
|
throw err;
|
|
52
54
|
} finally {
|
|
53
55
|
await release();
|
|
56
|
+
if (log) {
|
|
57
|
+
const endTime = Date.now(); // 记录结束时间
|
|
58
|
+
const duration = endTime - startTime; // 计算耗时
|
|
59
|
+
console.log(`[${CHANNEL_ID}] ${api} response took ${duration}ms`);
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { tuituiRobotApi} from "./robot_api"
|
|
2
|
+
|
|
3
|
+
export async function getChannelInfoByChannelId(account: any, channel_id: string) {
|
|
4
|
+
const payload = {channel_id: channel_id};
|
|
5
|
+
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
6
|
+
return body?.datas?.info;
|
|
7
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
|
2
2
|
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { resolveAccount } from "./accounts"
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
-
|
|
5
|
+
import { CHANNEL_ID } from './const';
|
|
6
6
|
import {sendTextMsg, get_announcement} from "./outbound"
|
|
7
|
-
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL, teamsBuildChatId} from "./chat_base"
|
|
7
|
+
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, teamsBuildChatId} from "./chat_base"
|
|
8
8
|
import {getChatRecord, getChannelInfoById} from "./chat_record"
|
|
9
9
|
import {file_space_list, file_space_add} from "./filespace"
|
|
10
10
|
|
|
@@ -74,9 +74,17 @@ const tuitui_send_channel_post_factory = (ctx: OpenClawPluginToolContext) => {
|
|
|
74
74
|
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
77
|
+
let chatId = "";
|
|
78
|
+
const guessType = guessChatType(params?.channel_id);
|
|
79
|
+
if(guessType == CHAT_TYPE_CHANNEL) {
|
|
80
|
+
// 解决badcase: 发个帖子,内容是个笑话
|
|
81
|
+
// 如果用户没明确说频道ID,大模型在频道调用发帖tool会传sessionKey过来
|
|
82
|
+
chatId = params?.channel_id;
|
|
83
|
+
} else {
|
|
84
|
+
const channel_info = await getChannelInfoById(account, params?.channel_id);
|
|
85
|
+
const team_id = channel_info?.team_id;
|
|
86
|
+
chatId = teamsBuildChatId(team_id, params?.channel_id, params?.parent_id);
|
|
87
|
+
}
|
|
80
88
|
|
|
81
89
|
const result: Promise<any> = sendTextMsg(account, chatId, CHAT_TYPE_CHANNEL, params?.markdown);
|
|
82
90
|
return result;
|
|
@@ -154,6 +162,14 @@ export function registerTuituiTools(api: OpenClawPluginApi) {
|
|
|
154
162
|
api.logger.debug?.("tuitui: Registered tool: No config available");
|
|
155
163
|
return;
|
|
156
164
|
}
|
|
165
|
+
|
|
166
|
+
const channels = api.config.channels || {};
|
|
167
|
+
const currChannel = channels[CHANNEL_ID] || {};
|
|
168
|
+
if (!currChannel?.appId && !currChannel?.accounts) {
|
|
169
|
+
// 无推推配置时,不需要注册工具防止污染上下文
|
|
170
|
+
// api.logger.info?.(`tuitui: ignore Registered tools`);
|
|
171
|
+
return
|
|
172
|
+
}
|
|
157
173
|
|
|
158
174
|
api.registerTool(tuitui_im_get_messages_factory);
|
|
159
175
|
api.registerTool(tuitui_send_channel_post_factory);
|
package/src/websocket.ts
CHANGED
|
@@ -73,6 +73,14 @@ export default function createWebSocket({ account, log, abortSignal, onConnected
|
|
|
73
73
|
if (firsEvtId) wsEvtIds.delete(firsEvtId);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// 收到包含心跳在内的任意消息,则重置心跳超时计时器,如果 300 秒内没有收到任何消息(包括心跳),则认为连接已失效
|
|
77
|
+
_clearTimeoutTimer();
|
|
78
|
+
_timeoutId = setTimeout(() => {
|
|
79
|
+
log?.warn?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] Heartbeat Timeout`);
|
|
80
|
+
_closeWS();
|
|
81
|
+
_restartWS(1e3);
|
|
82
|
+
}, 3e5); // 300秒心跳超时
|
|
83
|
+
|
|
76
84
|
const wsEvent = json?.body?.event;
|
|
77
85
|
if (wsEvent === 'keepalive') return;
|
|
78
86
|
|