@qihoo/tuitui-openclaw-channel 1.0.32 → 1.0.34
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/README.md +20 -17
- package/package.json +1 -1
- package/src/chat_base.ts +2 -0
- package/src/filespace.ts +2 -2
- package/src/inbound.ts +25 -24
- package/src/monitor.ts +29 -10
- package/src/outbound.ts +0 -27
- package/src/robot_api.ts +35 -30
- package/src/teams_api.ts +38 -0
- package/src/tools.ts +3 -2
- package/tsconfig.json +34 -0
package/README.md
CHANGED
|
@@ -10,20 +10,15 @@
|
|
|
10
10
|
<a href="https://github.com/openclaw/openclaw"><img alt="OpenClaw" src="https://img.shields.io/badge/OpenClaw-%3E%3D2026.3.13-0A7CFF"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
OpenClaw 的推推机器人 Channel 渠道插件
|
|
14
14
|
|
|
15
15
|
## 功能特性
|
|
16
16
|
|
|
17
|
-
- WebSocket 订阅模式,无需 Webhook 和公网环境依赖
|
|
18
17
|
- 支持私聊、群聊和 @机器人、团队帖子等
|
|
19
|
-
-
|
|
20
|
-
- 支持多 Agent
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- 推推的公开网站:<https://tuitui.cn>
|
|
25
|
-
- 了解推推机器人:<https://easydoc.soft.360.cn/doc?project=38ed795130e25371ef319aeb60d5b4fa&doc=d1f11ba4cac8886301ede97079df824e&config=menu_toc>
|
|
26
|
-
|
|
18
|
+
- 支持文本、图片、语音、视频、文件、频道帖子
|
|
19
|
+
- 支持多 Agent + 多机器人绑定
|
|
20
|
+
- 支持安全控制(私聊模式与白名单、群聊模式与白名单)
|
|
21
|
+
- WebSocket 订阅模式,个人电脑可用,无需 Webhook 和公网ip依赖
|
|
27
22
|
|
|
28
23
|
## 安装与配置指南
|
|
29
24
|
|
|
@@ -32,30 +27,33 @@
|
|
|
32
27
|
龙虾+推推插件配置指南
|
|
33
28
|
|
|
34
29
|
### 什么是龙虾推推channel插件
|
|
35
|
-
在openclaw上安装推推插件后,可以通过推推与你的openclaw
|
|
30
|
+
在openclaw上安装推推插件后,可以通过推推与你的openclaw聊天。
|
|
36
31
|
|
|
37
32
|
### 第一步、创建你的专属推推机器人
|
|
38
33
|
推推搜索 推推机器人助手,和它私聊。使用 /创建 可自助创建一个机器人,得到appid和secret后用。
|
|
39
34
|
另外你也可以对它使用 /改头像 指令。
|
|
40
35
|
|
|
41
36
|
### 第二步、安装openclaw 推推插件
|
|
37
|
+
在终端命令行运行命令
|
|
38
|
+
|
|
42
39
|
`openclaw plugins install @qihoo/tuitui-openclaw-channel`
|
|
43
|
-
(如果以前你装过推推插件zip包内测版,需要先把原来的目录删了,不然装不上)
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
如果遇到 clawHub 429 报错、npm install failed Error: package.json missing openclaw.hooks 等。
|
|
42
|
+
均为下载连接npm仓库不稳定导致,可以多试几次。
|
|
47
43
|
|
|
48
44
|
### 第三步、配置插件。
|
|
49
|
-
打开龙虾dashboard页面,频道->Tuitui,在appid和secret
|
|
45
|
+
打开龙虾dashboard页面,频道->Tuitui,在appid和secret填入第一步获取的密钥,下方点保存。
|
|
50
46
|
|
|
51
47
|
### 第四步、与你的机器人聊天
|
|
52
48
|
|
|
53
49
|
私聊你的龙虾机器人,会提示配对。复制提示消息末尾的那行指令,龙虾主人在终端执行你复制的那行命令,然后就能聊了。
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
如果不想配对,可以在dashboard中把Dm Policy切换为allowlist,然后从Allow From为你的账号添加白名单。
|
|
52
|
+
|
|
53
|
+
群聊的话,你把机器人拉到群里。需要@机器人触发。主人可以直接使用。其他人@机器人会提示让主人配置白名单,主人复制群id在dashboard上配置白名单以后就能聊了。
|
|
56
54
|
|
|
57
55
|
## 插件升级
|
|
58
|
-
|
|
56
|
+
后续如果更新,可以用下述命令升级插件
|
|
59
57
|
`openclaw plugins update tuitui-openclaw-channel`
|
|
60
58
|
|
|
61
59
|
## 安全备忘
|
|
@@ -87,3 +85,8 @@ openclaw plugins install -l .
|
|
|
87
85
|
}
|
|
88
86
|
```
|
|
89
87
|
|
|
88
|
+
|
|
89
|
+
## 参考文档
|
|
90
|
+
|
|
91
|
+
- 推推的公开网站:<https://tuitui.cn>
|
|
92
|
+
- 了解推推机器人:<https://easydoc.soft.360.cn/doc?project=38ed795130e25371ef319aeb60d5b4fa&doc=d1f11ba4cac8886301ede97079df824e&config=menu_toc>
|
package/package.json
CHANGED
package/src/chat_base.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ChatType = typeof CHAT_TYPE_DIRECT | typeof CHAT_TYPE_GROUP | typeof
|
|
|
9
9
|
|
|
10
10
|
export function guessChatType(chatId: string): ChatType {
|
|
11
11
|
if (chatId.startsWith("teams_")) return CHAT_TYPE_CHANNEL;
|
|
12
|
+
const isMobileNumber = /^1[3-9]\d{9}$/.test(chatId);
|
|
13
|
+
if (isMobileNumber) return CHAT_TYPE_DIRECT; // fix: 部分租户号为手机号被误猜测为群id的问题。大陆手机号格式
|
|
12
14
|
if (/^\d+$/.test(chatId)) return CHAT_TYPE_GROUP;
|
|
13
15
|
return CHAT_TYPE_DIRECT;
|
|
14
16
|
}
|
package/src/filespace.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CHANNEL_ID } from "./const";
|
|
2
|
-
import { tuituiRobotApi,
|
|
2
|
+
import { tuituiRobotApi, tuituiRobotUpload} from "./robot_api"
|
|
3
3
|
import {CHAT_TYPE_CHANNEL, parseSessionKey} from "./chat_base"
|
|
4
4
|
import {getChannelInfoByChannelId} from "./robot_helper"
|
|
5
5
|
|
|
@@ -120,7 +120,7 @@ async function ensureFolderPath(
|
|
|
120
120
|
|
|
121
121
|
// 构建父目录ID映射,便于查找
|
|
122
122
|
const parentIdToNodes = new Map<string, any[]>();
|
|
123
|
-
allNodes.forEach(node => {
|
|
123
|
+
allNodes.forEach((node: any) => {
|
|
124
124
|
const parentId = node.parent_id || "";
|
|
125
125
|
if (!parentIdToNodes.has(parentId)) {
|
|
126
126
|
parentIdToNodes.set(parentId, []);
|
package/src/inbound.ts
CHANGED
|
@@ -13,14 +13,8 @@
|
|
|
13
13
|
import type { TuiTuiInboundMessage, TuiTuiOutboundDeliverOptions } from './types';
|
|
14
14
|
import { CHANNEL_ID } from './const';
|
|
15
15
|
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType,teamsParseChatId,teamsBuildChatId} from "./chat_base"
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
sendTextMsg,
|
|
19
|
-
sendPageMsg,
|
|
20
|
-
sendMediaMsg,
|
|
21
|
-
get_announcement,
|
|
22
|
-
get_team_members,
|
|
23
|
-
} from "./outbound";
|
|
16
|
+
import {tuituiEmojiReaction, sendTextMsg, sendPageMsg, sendMediaMsg } from "./outbound";
|
|
17
|
+
import { teams_get_members, teams_get_members_text, teams_get_announcement } from "./teams_api"
|
|
24
18
|
import { parseChatMessageBody } from './inbound_body_parse';
|
|
25
19
|
import { parseAllowFroms } from './utils';
|
|
26
20
|
import { addUnmentionedHistory, popUnmentionedHistories } from "./histories";
|
|
@@ -139,7 +133,7 @@ async function dispatchReply(ctx: any, cfg: any, account: InboundAccount, payloa
|
|
|
139
133
|
},
|
|
140
134
|
|
|
141
135
|
onReplyStart: () => {
|
|
142
|
-
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, Agent
|
|
136
|
+
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, Agent is typing to ${payload.tuituiAccount}`);
|
|
143
137
|
},
|
|
144
138
|
},
|
|
145
139
|
});
|
|
@@ -207,7 +201,7 @@ async function isChannelCoordAuthorized(tuituiAccount: string, apiRuntime: any,
|
|
|
207
201
|
|
|
208
202
|
// 当前 thread 中有白名单用户发帖,则协调员继承该权限
|
|
209
203
|
const [members, posts] = await Promise.all([
|
|
210
|
-
|
|
204
|
+
teams_get_members(account, team_id),
|
|
211
205
|
getPostChainByChatId(account, chat_id),
|
|
212
206
|
]);
|
|
213
207
|
if (!members?.length || !posts?.length) return false;
|
|
@@ -387,20 +381,7 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
|
|
|
387
381
|
payload.text += `[文件] ${file?.name} : ${file?.url} \n`;
|
|
388
382
|
}
|
|
389
383
|
}
|
|
390
|
-
|
|
391
|
-
if(_session_ctx_injected.checkAndRecord(accountId + "_" + payload.chatId)) {
|
|
392
|
-
// 注入上下文
|
|
393
|
-
// 解决机器人不知道自己是谁的问题
|
|
394
|
-
if(payload.botName) {
|
|
395
|
-
payload.text += `\n[备忘]\n你在当前session中的名字叫: ${payload.botName}\n 如果有人@这个名字,就是在命令你`;
|
|
396
|
-
}
|
|
397
|
-
// 公告
|
|
398
|
-
const annnouncement = await get_announcement(account, channel_id, false);
|
|
399
|
-
if(annnouncement) {
|
|
400
|
-
payload.text += `\n[当前公告内容如下--有需要时可参考]\n${annnouncement}`;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
384
|
+
}
|
|
404
385
|
|
|
405
386
|
if (!msgData.at_me && account.requireMention) {
|
|
406
387
|
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned), add to history ${chatId} -> ${payload.text}`);
|
|
@@ -408,6 +389,26 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
|
|
|
408
389
|
return false;
|
|
409
390
|
}
|
|
410
391
|
|
|
392
|
+
if(_session_ctx_injected.checkAndRecord(accountId + "_" + payload.chatId)) {
|
|
393
|
+
// 注入上下文
|
|
394
|
+
// 解决机器人不知道自己是谁的问题
|
|
395
|
+
if(payload.botName) {
|
|
396
|
+
payload.text += `\n\n*** 参考信息: 你在当前session中的名字叫: ${payload.botName} 如果有人@这个名字,就是在命令你 ***\n`;
|
|
397
|
+
}
|
|
398
|
+
// 公告
|
|
399
|
+
const annnouncement = await teams_get_announcement(account, channel_id, false);
|
|
400
|
+
if(annnouncement) {
|
|
401
|
+
payload.text += "\n\n*** 参考信息: 当前公告内容如下 ***\n";
|
|
402
|
+
payload.text += "```\n" + annnouncement + "\n```";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 成员
|
|
406
|
+
const members = await teams_get_members_text(account, team_id);
|
|
407
|
+
if(members) {
|
|
408
|
+
payload.text += "\n\n*** 参考信息: 当前团队成员清单如下,每行一个成员名。如果需要询问成员、或者发送给成员时,需要用 @名字 的语法;并且@时必须从如下清单中匹配到最接近的,禁止@不存在的成员 ***\n";
|
|
409
|
+
payload.text += "```\n" + members + "\n```";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
411
412
|
|
|
412
413
|
// 私聊白名单对团队仍然生效,限制特定人@时,可以不配置groupAllowFrom,而是配置 allowFrom
|
|
413
414
|
if(await isAllowAccount(tuituiAccount, apiRuntime, account)){
|
package/src/monitor.ts
CHANGED
|
@@ -15,10 +15,12 @@ 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
17
|
import {parseChannelIdBySessionKey} from "./chat_base"
|
|
18
|
+
import { monitorEnabledDefault } from './accounts';
|
|
18
19
|
|
|
19
20
|
// 批量上报参数
|
|
20
21
|
const BATCH_MAX_SIZE = 100; // 达到此条数立即 flush
|
|
21
22
|
const BATCH_INTERVAL_MS = 5_000; // 定时 flush 间隔(ms)
|
|
23
|
+
const PAYLOAD_SIZE_LIMIT = 100 * 1024; // 单条 payload 兜底截断:100 KB
|
|
22
24
|
|
|
23
25
|
// ─────────────────────────────────────────────────────────────
|
|
24
26
|
// 上报载荷结构
|
|
@@ -109,6 +111,11 @@ function enqueue(target: MonitorTarget, payload: MonitorPayload): void {
|
|
|
109
111
|
batchQueues.set(target.accountId, queue);
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
// 兜底:data 字段超过 100 KB 时暴力截断
|
|
115
|
+
if (payload.data.length > PAYLOAD_SIZE_LIMIT) {
|
|
116
|
+
payload.data = payload.data.slice(0, PAYLOAD_SIZE_LIMIT) + '...[truncated]';
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
queue.items.push(payload);
|
|
113
120
|
|
|
114
121
|
// 达到上限,立即 flush
|
|
@@ -149,23 +156,30 @@ function resolveMonitorTargets(cfg: any): MonitorTarget[] {
|
|
|
149
156
|
accountAgentId.get(accountId) ?? 'main';
|
|
150
157
|
|
|
151
158
|
// 默认账户
|
|
152
|
-
if (channelCfg.monitorEnabled === true) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
if ((channelCfg.monitorEnabled ?? monitorEnabledDefault) === true) {
|
|
160
|
+
const appId = channelCfg.appId || '';
|
|
161
|
+
const appSecret = channelCfg.appSecret || '';
|
|
162
|
+
if (appId && appSecret) {
|
|
163
|
+
targets.push({
|
|
164
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
165
|
+
appId,
|
|
166
|
+
appSecret,
|
|
167
|
+
agentId: resolveAgentId(DEFAULT_ACCOUNT_ID),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
// 子账户
|
|
162
173
|
if (channelCfg.accounts) {
|
|
163
174
|
for (const [accountId, acct] of Object.entries(channelCfg.accounts) as [string, any][]) {
|
|
164
|
-
if (acct?.monitorEnabled !== true) continue;
|
|
175
|
+
if ((acct?.monitorEnabled ?? monitorEnabledDefault) !== true) continue;
|
|
176
|
+
const appId = acct?.appId || channelCfg.appId || '';
|
|
177
|
+
const appSecret = acct?.appSecret || channelCfg.appSecret || '';
|
|
178
|
+
if (!appId || !appSecret) continue;
|
|
165
179
|
targets.push({
|
|
166
180
|
accountId,
|
|
167
|
-
appId
|
|
168
|
-
appSecret
|
|
181
|
+
appId,
|
|
182
|
+
appSecret,
|
|
169
183
|
agentId: resolveAgentId(accountId),
|
|
170
184
|
});
|
|
171
185
|
}
|
|
@@ -227,6 +241,11 @@ function trimData(eventType: MonitorEventType, data: unknown): unknown {
|
|
|
227
241
|
const d = data as any;
|
|
228
242
|
switch (eventType) {
|
|
229
243
|
case 'agent_end':
|
|
244
|
+
if (Array.isArray(d?.messages)) {
|
|
245
|
+
return { ...d, messages: d.messages.slice(-10).map(truncateContent) };
|
|
246
|
+
}
|
|
247
|
+
return d;
|
|
248
|
+
|
|
230
249
|
// case 'before_prompt_build':
|
|
231
250
|
// case 'before_agent_start':
|
|
232
251
|
case 'before_compaction':
|
package/src/outbound.ts
CHANGED
|
@@ -216,30 +216,3 @@ export async function sendMediaMsg(
|
|
|
216
216
|
await postTuituiMsg(account, msg, auditCtx);
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// TODO: 支持群公告
|
|
222
|
-
export async function get_announcement(account: any, id: any, id_is_session: boolean = true): Promise<any> {
|
|
223
|
-
let channel_id = id;
|
|
224
|
-
if(id_is_session) {
|
|
225
|
-
channel_id = parseChannelIdBySessionKey(id);
|
|
226
|
-
if(!channel_id) {
|
|
227
|
-
return "";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const payload = {channel_id: channel_id};
|
|
232
|
-
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
233
|
-
//console.log("info", data);
|
|
234
|
-
const announcement = body?.datas?.info?.announcement;
|
|
235
|
-
return announcement;
|
|
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
|
@@ -1,6 +1,5 @@
|
|
|
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
4
|
import { TUITUI_SSRF_POLICY, getTuituiApiHost } from "./env"
|
|
6
5
|
|
|
@@ -34,8 +33,8 @@ export async function tuituiRobotApi(account: any, api: string, payload: any, lo
|
|
|
34
33
|
fetch_fun = _fetchForm;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
const { response, release } = await fetch_fun(url, payload);
|
|
38
36
|
try {
|
|
37
|
+
const response = await fetch_fun(url, payload);
|
|
39
38
|
const bodyText = await response.text();
|
|
40
39
|
|
|
41
40
|
if (!response.ok) {
|
|
@@ -52,41 +51,26 @@ export async function tuituiRobotApi(account: any, api: string, payload: any, lo
|
|
|
52
51
|
console.error(`[${CHANNEL_ID}] ${api} error:`, err);
|
|
53
52
|
throw err;
|
|
54
53
|
} finally {
|
|
55
|
-
await release();
|
|
56
54
|
if (log) {
|
|
57
|
-
const endTime = Date.now();
|
|
58
|
-
const duration = endTime - startTime;
|
|
55
|
+
const endTime = Date.now();
|
|
56
|
+
const duration = endTime - startTime;
|
|
59
57
|
console.log(`[${CHANNEL_ID}] ${api} response took ${duration}ms`);
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
function
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
function _fetchJson(url: string, json: any, auditCtx: string = "tuitui.api.call"): Promise<any> {
|
|
71
|
-
return _fetch({
|
|
72
|
-
url,
|
|
73
|
-
init: {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: { 'Content-Type': 'application/json' },
|
|
76
|
-
body: JSON.stringify(json),
|
|
77
|
-
},
|
|
78
|
-
auditCtx
|
|
62
|
+
function _fetchJson(url: string, json: any): Promise<Response> {
|
|
63
|
+
return fetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify(json),
|
|
79
67
|
});
|
|
80
68
|
}
|
|
81
69
|
|
|
82
|
-
function _fetchForm(url: string, form: any
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
method: 'POST',
|
|
87
|
-
body: form,
|
|
88
|
-
},
|
|
89
|
-
auditCtx
|
|
70
|
+
function _fetchForm(url: string, form: any): Promise<Response> {
|
|
71
|
+
return fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: form,
|
|
90
74
|
});
|
|
91
75
|
}
|
|
92
76
|
|
|
@@ -96,10 +80,29 @@ function _fetchForm(url: string, form: any, auditCtx: string = "tuitui.api.call"
|
|
|
96
80
|
* @returns Object with buffer, filename and content type
|
|
97
81
|
*/
|
|
98
82
|
export async function downloadUrl(url: string): Promise<{ buffer: ArrayBuffer; filename: string; contentType: string }> {
|
|
99
|
-
|
|
83
|
+
console.log(`[${CHANNEL_ID}] downloadUrl prepair ssrf`);
|
|
84
|
+
|
|
85
|
+
let fetchWithSsrFGuard: any;
|
|
86
|
+
try {
|
|
87
|
+
const ssrfRuntimeImport: any = await import('openclaw/plugin-sdk/ssrf-runtime');
|
|
88
|
+
fetchWithSsrFGuard = (ssrfRuntimeImport.default ?? ssrfRuntimeImport).fetchWithSsrFGuard;
|
|
89
|
+
} catch {
|
|
90
|
+
try {
|
|
91
|
+
fetchWithSsrFGuard = (await import('openclaw/plugin-sdk/tlon')).fetchWithSsrFGuard;
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!fetchWithSsrFGuard) {
|
|
96
|
+
throw new Error(`[${CHANNEL_ID}] fetchWithSsrFGuard() API import failed`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`[${CHANNEL_ID}] downloadUrl start ${url}`);
|
|
100
|
+
|
|
101
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
102
|
+
policy: TUITUI_SSRF_POLICY,
|
|
100
103
|
url,
|
|
101
104
|
init: { method: 'GET' },
|
|
102
|
-
|
|
105
|
+
auditContext: "tuitui.download",
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
try {
|
|
@@ -127,6 +130,8 @@ export async function downloadUrl(url: string): Promise<{ buffer: ArrayBuffer; f
|
|
|
127
130
|
if (match) filename = decodeURIComponent(match[1]);
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
console.log(`[${CHANNEL_ID}] downloadUrl ok`);
|
|
134
|
+
|
|
130
135
|
return { buffer, filename, contentType };
|
|
131
136
|
} finally {
|
|
132
137
|
await release();
|
package/src/teams_api.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
import { CHANNEL_ID } from "./const";
|
|
3
|
+
import { tuituiRobotApi, tuituiRobotUpload } from "./robot_api"
|
|
4
|
+
import { CHAT_TYPE_GROUP, CHAT_TYPE_DIRECT, CHAT_TYPE_CHANNEL, ChatType, teamsParseChatId, parseChannelIdBySessionKey } from "./chat_base";
|
|
5
|
+
|
|
6
|
+
// TODO: 支持群公告
|
|
7
|
+
export async function teams_get_announcement(account: any, id: any, id_is_session: boolean = true): Promise<any> {
|
|
8
|
+
let channel_id = id;
|
|
9
|
+
if(id_is_session) {
|
|
10
|
+
channel_id = parseChannelIdBySessionKey(id);
|
|
11
|
+
if(!channel_id) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const payload = {channel_id: channel_id};
|
|
17
|
+
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
18
|
+
//console.log("info", data);
|
|
19
|
+
const announcement = body?.datas?.info?.announcement;
|
|
20
|
+
return announcement;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export async function teams_get_members(account: any, team_id: string): Promise<any> {
|
|
25
|
+
const payload = {team_id: team_id};
|
|
26
|
+
const body = await tuituiRobotApi(account, '/teams/member/list', payload);
|
|
27
|
+
const members = body?.datas?.members;
|
|
28
|
+
//console.log("info", members);
|
|
29
|
+
return members;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function teams_get_members_text(account: any, team_id: string): Promise<string> {
|
|
33
|
+
const members = await teams_get_members(account, team_id);
|
|
34
|
+
if (!members?.length) return "";
|
|
35
|
+
|
|
36
|
+
return members.map((m: any) => String(m?.name ?? "")).join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
package/src/tools.ts
CHANGED
|
@@ -3,7 +3,8 @@ 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
|
-
import {sendTextMsg
|
|
6
|
+
import {sendTextMsg} from "./outbound"
|
|
7
|
+
import { teams_get_announcement } from "./teams_api"
|
|
7
8
|
import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,guessChatType, teamsBuildChatId} from "./chat_base"
|
|
8
9
|
import {getChatRecord, getChannelInfoById} from "./chat_record"
|
|
9
10
|
import {file_space_list, file_space_add} from "./filespace"
|
|
@@ -108,7 +109,7 @@ const tuitui_get_announcement_factory = (ctx: OpenClawPluginToolContext) => {
|
|
|
108
109
|
if(!account || !account.enabled || !account.appId || !account.appSecret) {
|
|
109
110
|
return tool_errmsg(`invalid tuitui account ${ctx.agentAccountId}`);
|
|
110
111
|
}
|
|
111
|
-
return await
|
|
112
|
+
return await teams_get_announcement(account, ctx.sessionKey);
|
|
112
113
|
},
|
|
113
114
|
};
|
|
114
115
|
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./",
|
|
15
|
+
"removeComments": false,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"inlineSources": true,
|
|
18
|
+
"importHelpers": true,
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": {
|
|
22
|
+
"openclaw/*": ["node_modules/openclaw/dist/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"index.ts",
|
|
27
|
+
"src/**/*",
|
|
28
|
+
"openclaw.plugin.json"
|
|
29
|
+
],
|
|
30
|
+
"exclude": [
|
|
31
|
+
"node_modules",
|
|
32
|
+
"dist"
|
|
33
|
+
]
|
|
34
|
+
}
|