This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1 @@
lib

View File

@@ -0,0 +1,3 @@
## Document
visit website to learn more: [https://tailchat.msgbyte.com/docs/advanced-usage/openapp/about](https://tailchat.msgbyte.com/docs/advanced-usage/openapp/about)

View File

@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@@ -0,0 +1,27 @@
{
"name": "tailchat-client-sdk",
"version": "1.0.9",
"description": "",
"main": "lib/index.js",
"scripts": {
"prepare": "tsc",
"release": "npm publish --registry https://registry.npmjs.com/",
"test": "jest"
},
"keywords": [],
"author": "moonrailgun <moonrailgun@gmail.com>",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^18.16.1",
"jest": "27.5.1",
"ts-jest": "27.1.4",
"typescript": "^4.9.5"
},
"dependencies": {
"axios": "^1.3.2",
"tailchat-types": "workspace:*",
"socket.io-client": "^4.7.1",
"socket.io-msgpack-parser": "^3.0.2"
}
}

View File

@@ -0,0 +1,21 @@
import { stripMentionTag } from '../utils';
describe('stripMentionTag', () => {
test('simple', () => {
expect(
stripMentionTag('[at=6448e822834c12425646f473]Robot[/at] Hello')
).toBe('Hello');
});
test('not remove other message', () => {
expect(
stripMentionTag(
'[at=6448e822834c12425646f473]Robot[/at] Hello [at=6448e822834c12425646f4732]Robot[/at]'
)
).toBe('Hello [at=6448e822834c12425646f4732]Robot[/at]');
});
test('also can remove mention ', () => {
expect(stripMentionTag('@Robot Hello')).toBe('Hello');
});
});

View File

@@ -0,0 +1,3 @@
export * from './openapi';
export * from './plugins/simplenotify';
export * from './utils';

View File

@@ -0,0 +1,162 @@
import axios, { AxiosInstance } from 'axios';
import crypto from 'crypto';
export class TailchatBaseClient {
request: AxiosInstance;
jwt: string | null = null;
userId: string | null = null;
loginP: Promise<void>;
constructor(
public url: string,
public appId: string,
public appSecret: string
) {
if (!url || !appId || !appSecret) {
throw new Error(
'Require params: apiUrl, appId, appSecret. You can set it with env'
);
}
this.request = axios.create({
baseURL: url,
});
this.request.interceptors.request.use(async (val) => {
if (
this.jwt &&
['post', 'get'].includes(String(val.method).toLowerCase()) &&
!val.headers['X-Token']
) {
// 任何请求都尝试增加token
val.headers['X-Token'] = this.jwt;
}
return val;
});
this.loginP = this.login();
}
async login() {
try {
console.log('Login...');
const { data } = await this.request.post('/api/openapi/bot/login', {
appId: this.appId,
token: this.getBotToken(),
});
// NOTICE: 注意有30天过期时间需要定期重新登录以换取新的token
// 这里先不换
this.jwt = data.data?.jwt;
this.userId = data.data?.userId;
console.log('tailchat openapp login success!');
// 尝试调用函数
// this.whoami().then(console.log);
} catch (err) {
console.error(err);
throw new Error(
`Login failed, please check application credentials or network(Error: ${String(
err
)})`
);
}
}
async waitingForLogin(): Promise<void> {
await Promise.resolve(this.loginP);
}
async call(action: string, params = {}) {
try {
await this.waitingForLogin();
console.log('Calling:', action);
const { data } = await this.request.post(
'/api/' + action.replace(/\./g, '/'),
params
);
return data.data;
} catch (err: any) {
console.error('Service Call Failed:', err);
const data: string = err?.response?.data;
if (data) {
throw new Error(
JSON.stringify({
action,
data,
})
);
} else {
throw err;
}
}
}
async whoami(): Promise<{
userAgent: string;
language: string;
user: {
_id: string;
nickname: string;
email: string;
avatar: string;
};
token: string;
userId: string;
}> {
return this.call('user.whoami');
}
getBotToken() {
return crypto
.createHash('md5')
.update(this.appId + this.appSecret)
.digest('hex');
}
/**
* Send normal message to tailchat
*/
async sendMessage(payload: {
converseId: string;
groupId?: string;
content: string;
plain?: string;
meta?: object;
}) {
return this.call('chat.message.sendMessage', payload);
}
/**
* Reply message
*/
async replyMessage(
replyInfo: {
messageId: string;
author: string;
content: string;
},
payload: {
converseId: string;
groupId?: string;
content: string;
plain?: string;
meta?: object;
}
) {
return this.sendMessage({
...payload,
meta: {
...payload.meta,
mentions: [replyInfo.author],
reply: {
_id: replyInfo.messageId,
author: replyInfo.author,
content: replyInfo.content,
},
},
content: `[at=${replyInfo.author}][/at] ${payload.content}`,
});
}
}

View File

@@ -0,0 +1,3 @@
import { TailchatBaseClient } from './base';
export class TailchatHTTPClient extends TailchatBaseClient {}

View File

@@ -0,0 +1,8 @@
export {
/**
* @deprecated please rename to TailchatHTTPClient
*/
TailchatHTTPClient as TailchatClient,
TailchatHTTPClient,
} from './http';
export { TailchatWsClient } from './ws';

View File

@@ -0,0 +1,111 @@
import { TailchatBaseClient } from './base';
import io, { Socket } from 'socket.io-client';
import * as msgpackParser from 'socket.io-msgpack-parser';
import type { ChatMessage } from 'tailchat-types';
export class TailchatWsClient extends TailchatBaseClient {
public socket: Socket | null = null;
constructor(
public url: string,
public appId: string,
public appSecret: string,
public disableMsgpack: boolean = false
) {
super(url, appId, appSecret);
}
connect(): Promise<Socket> {
return new Promise<Socket>(async (resolve, reject) => {
await this.waitingForLogin();
const token = this.jwt;
const socket = (this.socket = io(this.url, {
transports: ['websocket'],
auth: {
token,
},
forceNew: true,
parser: this.disableMsgpack ? undefined : msgpackParser,
}));
socket.once('connect', () => {
// 连接成功
this.emit('chat.converse.findAndJoinRoom')
.then((res) => {
console.log('Joined rooms', res.data);
resolve(socket);
})
.catch((err) => {
reject(err);
});
});
socket.once('error', () => {
reject();
});
socket.on('disconnect', (reason) => {
console.log(`disconnect due to ${reason}`);
this.socket = null;
});
socket.onAny((ev) => {
console.log('onAny', ev);
});
});
}
disconnect() {
if (!this.socket) {
console.warn('You should call it after connect');
return;
}
this.socket.disconnect();
this.socket = null;
}
emit(eventName: string, eventData: any = {}) {
if (!this.socket) {
console.warn('You should call it after connect');
throw new Error('You should call it after connect');
}
return this.socket.emitWithAck(eventName, eventData);
}
on(eventName: string, callback: (payload: any) => void) {
if (!this.socket) {
console.warn('You should call it after connect');
return;
}
this.socket.on(eventName, callback);
}
once(eventName: string, callback: (payload: any) => void) {
if (!this.socket) {
console.warn('You should call it after connect');
return;
}
this.socket.once(eventName, callback);
}
off(eventName: string, callback: (payload: any) => void) {
if (!this.socket) {
console.warn('You should call it after connect');
return;
}
this.socket.off(eventName, callback);
}
onMessage(callback: (messagePayload: ChatMessage) => void) {
this.on('notify:chat.message.add', callback);
}
onMessageUpdate(callback: (messagePayload: ChatMessage) => void) {
this.on('notify:chat.message.update', callback);
}
}

View File

@@ -0,0 +1 @@
export * from './client';

View File

@@ -0,0 +1,24 @@
import axios from 'axios';
/**
* 基于简易推送插件的消息通知服务
*
* @param hostUrl 实例地址url
* @param subscribeId 订阅id
* @param text 发送的文本默认支持bbcode
*/
export async function sendSimpleNotify(
hostUrl: string,
subscribeId: string,
text: string
) {
await axios({
method: 'post',
baseURL: hostUrl,
url: '/api/plugin:com.msgbyte.simplenotify/webhook/callback',
data: {
subscribeId,
text,
},
});
}

View File

@@ -0,0 +1,10 @@
/**
* remove first [at=xxx]xxx[/at] in message first
*/
export function stripMentionTag(message: string): string {
return message
.trim()
.replace(/^\[at=.*?\[\/at\]/, '')
.replace(/^@\S*\s?/, '')
.trimStart();
}

View File

@@ -0,0 +1,20 @@
import { TailchatWsClient } from '../src';
const HOST = process.env.HOST;
const APPID = process.env.APPID;
const APPSECRET = process.env.APPSECRET;
if (!HOST || !APPID || !APPSECRET) {
console.log('require env: HOST, APPID, APPSECRET');
process.exit(1);
}
const client = new TailchatWsClient(HOST, APPID, APPSECRET);
client.connect().then(async () => {
console.log('Login Success!');
client.onMessage((message) => {
console.log('Receive message', message);
});
});

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["ESNext"],
"skipLibCheck": true,
"outDir": "lib",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"isolatedModules": true,
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"importsNotUsedAsValues": "error",
"typeRoots": ["./node_modules/@types"]
},
"include": ["./src/*"]
}