@qihoo/tuitui-openclaw-channel 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
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)
package/src/inbound.ts CHANGED
@@ -451,7 +451,11 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
451
451
 
452
452
  if (account.emojiReaction) {
453
453
  // 因为回复较慢,先回复一个表情
454
- await tuituiEmojiReaction(account, payload.chatId, payload.chatType, payload.msgId, '收到');
454
+ try{
455
+ await tuituiEmojiReaction(account, payload.chatId, payload.chatType, payload.msgId, '收到');
456
+ } catch (err) {
457
+ // 出错不影响后续逻辑
458
+ }
455
459
  }
456
460
 
457
461
  // 路由判断
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
- const { appId: appid, appSecret: secret } = account;
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
- const { response, release } = await _fetch({ url: fileSrc, auditCtx: 'tuitui.media.download' });
172
- try {
173
- if (!response.ok) {
174
- throw new Error(`[${CHANNEL_ID}] Failed to download media from ${fileSrc}: ${response.status}`);
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 { response, release } = await _fetch({
222
- url: addTuituiParams2Url('/media/upload', { appid, secret, type }),
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
- const { appId: appid, appSecret: secret } = account;
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 notNOT supported ${auditCtx} - `, page);
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 { appId: appid, appSecret: secret } = account;
576
-
577
- const url = addTuituiParams2Url(baseurl, { appid, secret });
578
-
579
- console.log(`[${CHANNEL_ID}] getChatRecord request `, body);
580
-
581
- const { response, release } = await _fetchJson(url, body, 'tuitui.chat.record');
582
- try {
583
- const bodyText = await response.text();
584
- //console.log(`[${CHANNEL_ID}] getChatRecord response ${bodyText}`);
585
-
586
- if (!response.ok) {
587
- throw new Error(`getChatRecord failed (response.ok unexpected): ${response.status} ${response.statusText}; body=${bodyText}`);
588
- }
589
-
590
- const parsed: TuiTuiChatRecordResponse = JSON.parse(bodyText);
591
- if (Number(parsed?.errcode) !== 0) {
592
- throw new Error(`getChatRecord failed (errcode unexpected): errcode=${parsed.errcode} errmsg=${parsed.errmsg ?? 'Unknown error'}`);
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
- console.log(`[${CHANNEL_ID}] getChatRecord result(cleaned)`, JSON.stringify(clean, null, 2));
476
+ console.log(`[${CHANNEL_ID}] getChatRecord result(cleaned)`, JSON.stringify(clean, null, 2));
613
477
 
614
- return clean;
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
- console.log(`[${CHANNEL_ID}] getPostChain request`, payload);
513
+ const datas = data.datas ?? {};
514
+ const topic = datas.topic ?? {};
515
+ const replyList: any[] = datas.reply_list ?? [];
659
516
 
660
- const { response, release } = await _fetchJson(url, payload, 'tuitui.teams.post.chain');
661
- try {
662
- const bodyText = await response.text();
517
+ const posts: TeamsPostChainItem[] = [];
663
518
 
664
- if (!response.ok) {
665
- throw new Error(`getPostChain failed (response.ok unexpected): ${response.status} ${response.statusText}; body=${bodyText}`);
666
- }
667
-
668
- const data = JSON.parse(bodyText);
669
- if (Number(data?.errcode) !== 0) {
670
- throw new Error(`getPostChain failed (errcode unexpected): errcode=${data.errcode} errmsg=${data.errmsg ?? 'Unknown error'}`);
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: topic.post_id ?? '',
683
- time: topic.create_time ?? '',
684
- name: topic.from_name ?? '',
685
- content: topic.content ?? '',
686
- properties: topic.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,41 @@ export async function file_space_list(
739
565
  space_type: spaceType,
740
566
  };
741
567
 
742
- console.log(`[${CHANNEL_ID}] file_space_list request`, payload);
743
-
744
- const { response, release } = await _fetchJson(url, payload, 'tuitui.file_space_list');
745
- try {
746
- const bodyText = await response.text();
568
+ const data = await tuituiRobotApi(account, '/file_space/node/list', payload);
747
569
 
748
- if (!response.ok) {
749
- throw new Error(`file_space_list failed (response.ok unexpected): ${response.status} ${response.statusText}; body=${bodyText}`);
750
- }
570
+ const list = data?.datas?.list;
571
+ const flat_list = flattenFileSpaceList(list);
751
572
 
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
- const list = data?.datas?.list;
758
- const flat_list = flattenFileSpaceList(list);
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
- console.log(`[${CHANNEL_ID}] file_space_list `, flat_list);
761
- return flat_list
762
- } catch (err) {
763
- console.error(`[${CHANNEL_ID}] file_space_list error:`, err);
764
- throw err;
765
- } finally {
766
- await release();
593
+ // TODO: 支持群公告
594
+ export async function get_announcement(account: any, sessionKey: string = ""): Promise<any> {
595
+ const channel_id = parseSessionKey(sessionKey);
596
+ if(!channel_id) {
597
+ return {"announcement": ""};
767
598
  }
599
+
600
+ const payload = {channel_id: channel_id};
601
+ const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
602
+ //console.log("info", data);
603
+ const announcement = body?.datas?.info?.announcement;
604
+ return {"announcement": announcement};
768
605
  }
@@ -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
- if(!DEFAULT_ONLINE) {
119
- api.registerTool(tuitui_file_space_factory);
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
  }