@qihoo/tuitui-openclaw-channel 1.0.19 → 1.0.20
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/accounts.ts +4 -4
- package/src/channel.ts +9 -0
- package/src/env.ts +15 -0
- package/src/outbound.ts +13 -19
- package/src/websocket.ts +4 -8
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -19,10 +19,10 @@ export const getDefaultAccountConfig = (acct?: any) => ({
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
export const resolveAccount = (cfg: any, accountId?: string | null) => {
|
|
22
|
-
|
|
22
|
+
const accId = accountId || DEFAULT_ACCOUNT_ID;
|
|
23
23
|
let currAccount = cfg?.channels?.[CHANNEL_ID] || {};
|
|
24
|
-
if (
|
|
25
|
-
currAccount = currAccount.accounts?.[
|
|
24
|
+
if (accId !== DEFAULT_ACCOUNT_ID) {
|
|
25
|
+
currAccount = currAccount.accounts?.[accId];
|
|
26
26
|
}
|
|
27
|
-
return { accountId, ...getDefaultAccountConfig(currAccount) };
|
|
27
|
+
return { accountId: accId, ...getDefaultAccountConfig(currAccount) };
|
|
28
28
|
};
|
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 } from 'openclaw/plugin-sdk/core';
|
|
8
8
|
import { CHANNEL_ID, CHANNEL_NAME } from "./const";
|
|
9
|
+
import { handleInboundMessage } from './inbound';
|
|
9
10
|
import {
|
|
10
11
|
checkAccount,
|
|
11
12
|
sendTextMsg,
|
|
@@ -256,6 +257,14 @@ export function createTuiTuiChannelPlugin(apiRuntime: any) {
|
|
|
256
257
|
apiRuntime,
|
|
257
258
|
onConnected: () => _setStatus({ running: true, connected: true }),
|
|
258
259
|
onDisconnected: () => _setStatus({ running: false, connected: false }),
|
|
260
|
+
onInbounMessage: (json: any) => {
|
|
261
|
+
_setStatus({ running: true, connected: true, lastInboundAt: Date.now() });
|
|
262
|
+
if (apiRuntime) {
|
|
263
|
+
handleInboundMessage({ json, account, apiRuntime, log });
|
|
264
|
+
} else {
|
|
265
|
+
log?.error?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket.onInbounMessage TuiTuiRuntime error`);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
259
268
|
});
|
|
260
269
|
|
|
261
270
|
// 保持账户运行状态,直至触发“abortSignal”信号为止。
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const DEFAULT_ONLINE = true;
|
|
2
|
+
|
|
3
|
+
export const getTuituiHost = (online: boolean = DEFAULT_ONLINE): string => {
|
|
4
|
+
return online ? "im.live.360.cn" : "im.qihoo.net";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const getTuituiWebsocketHost = (online: boolean = DEFAULT_ONLINE): string => {
|
|
8
|
+
return `wss://${getTuituiHost(online)}:8282/robot`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getTuituiApiHost = (online: boolean = DEFAULT_ONLINE): string => {
|
|
12
|
+
return `https://${getTuituiHost(online)}:8282/robot`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const TUITUI_SSRF_POLICY = { allowedHostnames: ['im.live.360.cn', 'im.qihoo.net'] } as const;
|
package/src/outbound.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk/tlon';
|
|
4
4
|
import { CHANNEL_ID } from "./const";
|
|
5
|
+
import { TUITUI_SSRF_POLICY, getTuituiApiHost } from "./env"
|
|
5
6
|
|
|
6
7
|
import type {
|
|
7
8
|
TuiTuiMessageData,
|
|
@@ -17,8 +18,6 @@ import type {
|
|
|
17
18
|
TuiTuiTeamsTarget
|
|
18
19
|
} from './types';
|
|
19
20
|
|
|
20
|
-
/* 一些常量配置 */
|
|
21
|
-
export const TUITUI_SSRF_POLICY = { allowedHostnames: ['im.live.360.cn'] } as const;
|
|
22
21
|
|
|
23
22
|
// ChatType定义与SessionKey定义一致,不可随意修改
|
|
24
23
|
// https://docs.openclaw.ai/channels/channel-routing#session-key-shapes-examples
|
|
@@ -33,8 +32,8 @@ export function guessChatType(chatId: string): ChatType {
|
|
|
33
32
|
return CHAT_TYPE_DIRECT;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export function
|
|
37
|
-
const url = new URL(urlStr);
|
|
35
|
+
export function addTuituiParams2Url(urlStr: string, params: any) {
|
|
36
|
+
const url = new URL(getTuituiApiHost() + urlStr);
|
|
38
37
|
for (let k in params) url.searchParams.set(k, params[k]);
|
|
39
38
|
return url.toString();
|
|
40
39
|
}
|
|
@@ -82,7 +81,7 @@ function _fetchJson(url: string, json: any, auditCtx: string): Promise<any> {
|
|
|
82
81
|
export async function postTuituiMsg(account: any, json: any, auditCtx: string): Promise<any> {
|
|
83
82
|
const { appId: appid, appSecret: secret } = account;
|
|
84
83
|
const { response, release } = await _fetchJson(
|
|
85
|
-
|
|
84
|
+
addTuituiParams2Url('/message/custom/send', { appid, secret }),
|
|
86
85
|
json,
|
|
87
86
|
auditCtx,
|
|
88
87
|
);
|
|
@@ -132,7 +131,7 @@ interface tuituiUploadResult {
|
|
|
132
131
|
* @param type - Media type: 'image' or "file" (auto-detected if not specified)
|
|
133
132
|
* @returns The media_id from TuiTui
|
|
134
133
|
*/
|
|
135
|
-
export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'image' | 'file'): Promise<tuituiUploadResult
|
|
134
|
+
export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'image' | 'file'): Promise<tuituiUploadResult> {
|
|
136
135
|
checkAccount(account, 'uploadFileToTuiTui');
|
|
137
136
|
|
|
138
137
|
const { appId: appid, appSecret: secret } = account;
|
|
@@ -219,23 +218,23 @@ export async function uploadFileToTuiTui(fileSrc: string, account: any, type: 'i
|
|
|
219
218
|
body.append('media', new Blob([fileBuffer], { type: contentType }), filename);
|
|
220
219
|
|
|
221
220
|
const { response, release } = await _fetch({
|
|
222
|
-
url:
|
|
221
|
+
url: addTuituiParams2Url('/media/upload', { appid, secret, type }),
|
|
223
222
|
init: { method: "POST", body },
|
|
224
223
|
auditCtx: "tuitui.media.upload",
|
|
225
224
|
});
|
|
226
225
|
try {
|
|
227
226
|
if (!response.ok) {
|
|
228
|
-
throw new Error(`[${CHANNEL_ID}] Failed to upload
|
|
227
|
+
throw new Error(`[${CHANNEL_ID}] Failed to upload file to TuiTui: HTTP ${response.status}`);
|
|
229
228
|
}
|
|
230
229
|
|
|
231
230
|
const result: TuiTuiMediaUploadResponse = await response.json();
|
|
232
231
|
if (result.errcode !== 0 || !result.media_id) {
|
|
233
|
-
throw new Error(`[${CHANNEL_ID}]
|
|
232
|
+
throw new Error(`[${CHANNEL_ID}] file upload failed: ${result.errmsg || "Unknown error"}`);
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
return {fid: result.media_id, filename};
|
|
237
236
|
} catch(err) {
|
|
238
|
-
|
|
237
|
+
throw err;
|
|
239
238
|
} finally {
|
|
240
239
|
await release();
|
|
241
240
|
}
|
|
@@ -264,7 +263,7 @@ export async function tuituiEmojiReaction(
|
|
|
264
263
|
const toTarget = (payload.togroups || payload.tousers || payload.toteams)[0];
|
|
265
264
|
const _logTxt =`[${CHANNEL_ID}] emoji_reaction "${emoji}"`;
|
|
266
265
|
console.log(`${_logTxt} request`, toTarget);
|
|
267
|
-
const sendUrl =
|
|
266
|
+
const sendUrl = addTuituiParams2Url('/message/custom/modify', { appid, secret });
|
|
268
267
|
const { response, release } = await _fetchJson(sendUrl, payload, 'tuitui.emoji_reaction');
|
|
269
268
|
|
|
270
269
|
try {
|
|
@@ -428,11 +427,6 @@ export async function sendMediaMsg(
|
|
|
428
427
|
const mediaType = isImage ? 'image' : 'file';
|
|
429
428
|
|
|
430
429
|
const uploadResult = await uploadFileToTuiTui(mediaUrl, account, mediaType);
|
|
431
|
-
if (!uploadResult || !uploadResult?.fid) {
|
|
432
|
-
console.error(`[${CHANNEL_ID}] uploadFileToTuiTui failed ${auditCtx}, {mediaUrl: ${mediaUrl}, mediaType: ${mediaType}}`);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
430
|
const {fid, filename} = uploadResult;
|
|
437
431
|
|
|
438
432
|
const targets = getTargets(chatId, chatType);
|
|
@@ -536,9 +530,9 @@ export async function getChatRecord(
|
|
|
536
530
|
|
|
537
531
|
let baseurl = "";
|
|
538
532
|
if (chatType == CHAT_TYPE_DIRECT) {
|
|
539
|
-
baseurl = "
|
|
533
|
+
baseurl = "/message/single/sync";
|
|
540
534
|
} else if (chatType == CHAT_TYPE_GROUP){
|
|
541
|
-
baseurl = "
|
|
535
|
+
baseurl = "/message/group/sync";
|
|
542
536
|
} else {
|
|
543
537
|
console.log(`[${CHANNEL_ID}] getChatRecord: chatType "${chatType}" is not supported`);
|
|
544
538
|
return undefined;
|
|
@@ -561,7 +555,7 @@ export async function getChatRecord(
|
|
|
561
555
|
|
|
562
556
|
const { appId: appid, appSecret: secret } = account;
|
|
563
557
|
|
|
564
|
-
const url =
|
|
558
|
+
const url = addTuituiParams2Url(baseurl, { appid, secret });
|
|
565
559
|
|
|
566
560
|
console.log(`[${CHANNEL_ID}] getChatRecord request `, body);
|
|
567
561
|
|
package/src/websocket.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { CHANNEL_ID } from "./const";
|
|
3
|
-
import {
|
|
3
|
+
import { getTuituiWebsocketHost } from "./env"
|
|
4
4
|
|
|
5
5
|
const wsReadyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const;
|
|
6
6
|
let wsNum = 0;
|
|
7
7
|
|
|
8
|
-
export default function createWebSocket({ account, log, abortSignal, onConnected, onDisconnected,
|
|
8
|
+
export default function createWebSocket({ account, log, abortSignal, onConnected, onDisconnected, onInbounMessage }: any): () => void {
|
|
9
9
|
const { accountId, appId, appSecret } = account;
|
|
10
10
|
let ws: any = null;
|
|
11
11
|
const _closeWS = () => ws && ws.readyState !== WebSocket.CLOSED && ws.close();
|
|
@@ -20,7 +20,7 @@ export default function createWebSocket({ account, log, abortSignal, onConnected
|
|
|
20
20
|
const _clearTimeoutTimer = () => _timeoutId && clearTimeout(_timeoutId);
|
|
21
21
|
|
|
22
22
|
const wsEvtIds = new Set<string>();
|
|
23
|
-
const wsUrl =
|
|
23
|
+
const wsUrl = getTuituiWebsocketHost() + `/callback/ws?auth=${appId}.${appSecret}`;
|
|
24
24
|
const _startWS = () => {
|
|
25
25
|
_closeWS(); // 启动新链接前先关闭旧链接,防止产生空指针链接
|
|
26
26
|
|
|
@@ -81,11 +81,7 @@ export default function createWebSocket({ account, log, abortSignal, onConnected
|
|
|
81
81
|
if (!json?.header || !wsEvent || !json?.body?.data) {
|
|
82
82
|
return log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] invalid message`);
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
return log?.error?.(`[${CHANNEL_ID}] AccountId: ${accountId}, WebSocket[${wsId}] TuiTuiRuntime error`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
handleInboundMessage({ json, account, apiRuntime, log });
|
|
84
|
+
onInbounMessage(json);
|
|
89
85
|
});
|
|
90
86
|
|
|
91
87
|
const onErrOrClose = (errStr: string) => {
|