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,7 @@
const path = require('path');
module.exports = {
externalDeps: ['react'],
pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'),
};

View File

@@ -0,0 +1,44 @@
import { db } from 'tailchat-server-sdk';
const { getModelForClass, prop, modelOptions, TimeStamps } = db;
@modelOptions({
options: {
customName: 'p_simplenotify',
},
})
export class SimpleNotify extends TimeStamps implements db.Base {
_id: db.Types.ObjectId;
id: string;
@prop({
default: 'group',
})
type: 'user' | 'group';
// 群组
@prop()
groupId?: string;
@prop()
textPanelId?: string;
// 个人
@prop()
userConverseId?: string;
get converseId(): string {
if (this.type === 'user') {
return this.userConverseId;
}
return this.textPanelId;
}
}
export type SimpleNotifyDocument = db.DocumentType<SimpleNotify>;
const model = getModelForClass(SimpleNotify);
export type SimpleNotifyModel = typeof model;
export default model;

View File

@@ -0,0 +1,16 @@
{
"name": "tailchat-plugin-simplenotify",
"version": "1.0.0",
"main": "index.js",
"author": "moonrailgun",
"license": "MIT",
"private": true,
"scripts": {
"build:web": "ministar buildPlugin all",
"build:web:watch": "ministar watchPlugin all"
},
"devDependencies": {
"mini-star": "^1.2.8"
},
"dependencies": {}
}

View File

@@ -0,0 +1,264 @@
import {
TcService,
TcDbService,
TcContext,
TcPureContext,
call,
NoPermissionError,
} from 'tailchat-server-sdk';
import type {
SimpleNotifyDocument,
SimpleNotifyModel,
} from '../models/simplenotify';
const PERMISSION_MANAGE = 'plugin.com.msgbyte.simplenotify.subscribe.manage';
/**
* 任务管理服务
*/
interface SimpleNotifyService
extends TcService,
TcDbService<SimpleNotifyDocument, SimpleNotifyModel> {}
class SimpleNotifyService extends TcService {
botUserId: string | undefined;
get serviceName() {
return 'plugin:com.msgbyte.simplenotify';
}
onInit() {
this.registerLocalDb(require('../models/simplenotify').default);
this.registerAction('addGroupSubscribe', this.addGroupSubscribe, {
params: {
groupId: 'string',
textPanelId: 'string',
},
});
this.registerAction('addUserSubscribe', this.addUserSubscribe);
this.registerAction('list', this.list, {
params: {
groupId: 'string',
type: {
type: 'enum',
values: ['user', 'group'],
},
},
});
this.registerAction('delete', this.delete, {
params: {
groupId: 'string',
subscribeId: 'string',
},
});
this.registerAction('webhook.callback', this.webhookHandler, {
params: {
subscribeId: 'string',
text: 'string',
},
});
this.registerAuthWhitelist(['/webhook/callback']);
}
protected onInited(): void {
// 确保机器人用户存在, 并记录机器人用户id
this.waitForServices(['user']).then(async () => {
try {
const botUserId = await this.broker.call('user.ensurePluginBot', {
botId: 'simple-notify-bot',
nickname: 'Notify Bot',
avatar: '/images/avatar/robot.webp',
});
this.logger.info('Simple Notify Bot Id:', botUserId);
this.botUserId = String(botUserId);
} catch (e) {
this.logger.error(e);
}
});
}
/**
* 添加群组订阅
*/
async addGroupSubscribe(
ctx: TcContext<{
groupId: string;
textPanelId: string;
}>
) {
const { groupId, textPanelId } = ctx.params;
const { userId, t } = ctx.meta;
if (!groupId || !textPanelId) {
throw new Error('参数不全');
}
const [hasPermission] = await call(ctx).checkUserPermissions(
groupId,
userId,
[PERMISSION_MANAGE]
);
if (!hasPermission) {
throw new NoPermissionError(t('没有操作权限'));
}
// TODO: 需要检查textPanelId是否合法
await this.adapter.model.create({
groupId,
textPanelId,
type: 'group',
});
}
/**
* 添加个人订阅
*/
async addUserSubscribe(ctx: TcContext) {
const userId = ctx.meta.userId;
const t = ctx.meta.t;
if (!this.botUserId) {
throw new Error('机器人未被初始化');
}
/**
* 创建一条测试消息以确保会话被生成
*/
const converse: any = await ctx.call('chat.converse.createDMConverse', {
memberIds: [this.botUserId, userId],
});
if (!converse._id) {
throw new Error('会话创建失败');
}
const res = await this.adapter.model.create({
userConverseId: converse._id,
type: 'user',
});
await this.sendPluginBotMessage(ctx, {
converseId: converse._id,
content: t('个人消息订阅已创建, subscribeId: {{subscribeId}}', {
subscribeId: String(res._id),
}),
});
}
/**
* 列出所有订阅
*/
async list(
ctx: TcContext<{
groupId: string;
type: string;
}>
) {
const { groupId, type } = ctx.params;
const { userId, t } = ctx.meta;
const [hasPermission] = await call(ctx).checkUserPermissions(
groupId,
userId,
[PERMISSION_MANAGE]
);
if (!hasPermission) {
throw new NoPermissionError(t('没有查看权限'));
}
const docs = await this.adapter.model
.find({
groupId,
type: type as 'user' | 'group',
})
.exec();
return await this.transformDocuments(ctx, {}, docs);
}
/**
* 列出指定订阅
*/
async delete(
ctx: TcContext<{
groupId: string;
subscribeId: string;
}>
) {
const { groupId, subscribeId } = ctx.params;
const { userId, t } = ctx.meta;
const [hasPermission] = await call(ctx).checkUserPermissions(
groupId,
userId,
[PERMISSION_MANAGE]
);
if (!hasPermission) {
throw new NoPermissionError(t('没有删除权限'));
}
await this.adapter.model.deleteOne({
_id: subscribeId,
});
}
/**
* 处理github webhook 回调
*/
async webhookHandler(
ctx: TcPureContext<{
subscribeId: string;
text: string;
}>
) {
if (!this.botUserId) {
throw new Error('Not Simple Notify bot');
}
const subscribe = await this.adapter.model.findById(ctx.params.subscribeId);
if (!subscribe) {
throw new Error('没有找到该订阅');
}
const groupId = subscribe.groupId;
const converseId = String(subscribe.converseId);
await this.sendPluginBotMessage(ctx, {
groupId,
converseId,
content: ctx.params.text,
});
}
private async sendPluginBotMessage(
ctx: TcPureContext<any>,
messagePayload: {
converseId: string;
groupId?: string;
content: string;
meta?: any;
}
) {
if (!this.botUserId) {
throw new Error('Not Simple Notify bot');
}
const res = await ctx.call(
'chat.message.sendMessage',
{
...messagePayload,
},
{
meta: {
userId: this.botUserId,
},
}
);
return res;
}
}
export default SimpleNotifyService;

View File

@@ -0,0 +1,12 @@
{
"label": "Simple Notify Bot",
"label.zh-CN": "简易机器人",
"name": "com.msgbyte.simplenotify",
"url": "{BACKEND}/plugins/com.msgbyte.simplenotify/index.js",
"icon": "/images/avatar/robot.webp",
"version": "0.0.0",
"author": "moonrailgun",
"description": "A simple generic notification bot for sending messages directly to groups",
"description.zh-CN": "一个简单的通用通知机器人, 用于直接向群组发送消息",
"requireRestart": true
}

View File

@@ -0,0 +1,10 @@
{
"name": "@plugins/com.msgbyte.simplenotify",
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@types/react": "18.0.20",
"react": "18.2.0"
}
}

View File

@@ -0,0 +1,72 @@
import React, { useMemo } from 'react';
import {
ModalWrapper,
createFastFormSchema,
fieldSchema,
useAsyncRequest,
showToasts,
} from '@capital/common';
import { WebFastForm, GroupPanelSelector } from '@capital/component';
import { request } from '../request';
import { Translate } from '../translate';
interface Values {
repoName: string;
textPanelId: string;
}
const schema = createFastFormSchema({
textPanelId: fieldSchema.string().required(Translate.textPanelEmpty),
});
export const AddGroupSubscribeModal: React.FC<{
groupId: string;
onSuccess?: () => void;
}> = React.memo((props) => {
const groupId = props.groupId;
const [, handleSubmit] = useAsyncRequest(
async (values: Values) => {
const { textPanelId } = values;
await request.post('addGroupSubscribe', {
groupId,
textPanelId,
});
showToasts(Translate.success, 'success');
props.onSuccess?.();
},
[groupId, props.onSuccess]
);
const fields = useMemo(
() => [
{
type: 'custom',
name: 'textPanelId',
label: Translate.textPanel,
render: (props: {
value: any;
error: string | undefined;
onChange: (val: any) => void; // 修改数据的回调函数
}) => {
return (
<GroupPanelSelector
style={{ width: '100%' }}
value={props.value}
onChange={props.onChange}
groupId={groupId}
/>
);
},
},
],
[groupId]
);
return (
<ModalWrapper title={Translate.createNotify}>
<WebFastForm schema={schema} fields={fields} onSubmit={handleSubmit} />
</ModalWrapper>
);
});
AddGroupSubscribeModal.displayName = 'AddGroupSubscribeModal';

View File

@@ -0,0 +1,147 @@
import React, { useCallback, useMemo } from 'react';
import {
openModal,
closeModal,
useGroupIdContext,
useAsyncRefresh,
useAsyncRequest,
getServiceUrl,
useGroupPanelInfo,
} from '@capital/common';
import { Button, SensitiveText, Space, Table } from '@capital/component';
import { Translate } from '../translate';
import { AddGroupSubscribeModal } from './AddGroupSubscribeModal';
import { request } from '../request';
interface SubscribeItem {
_id: string;
groupId: string;
textPanelId: string;
createdAt: string;
updatedAt: string;
}
const GroupPanelName: React.FC<{
groupId: string;
panelId: string;
}> = React.memo(({ groupId, panelId }) => {
const groupPanelInfo = useGroupPanelInfo(groupId, panelId);
return groupPanelInfo?.name ?? '';
});
GroupPanelName.displayName = 'GroupPanelName';
const GroupSubscribePanel: React.FC = React.memo(() => {
const groupId = useGroupIdContext();
const { value: subscribes, refresh } = useAsyncRefresh(async () => {
const { data } = await request.post('list', { groupId, type: 'group' });
return data;
}, [groupId]);
const handleAdd = useCallback(() => {
const key = openModal(
<AddGroupSubscribeModal
groupId={groupId}
onSuccess={() => {
closeModal(key);
refresh();
}}
/>
);
}, [groupId, refresh]);
const [, handleDelete] = useAsyncRequest(
async (subscribeId) => {
await request.post('delete', {
groupId,
subscribeId,
});
refresh();
},
[groupId, refresh]
);
const columns = useMemo(
() => [
{
title: 'ID',
key: '_id',
dataIndex: '_id',
width: 250,
render: (text: string) => <SensitiveText text={text} />,
},
{
title: Translate.panel,
key: 'textPanelId',
dataIndex: 'textPanelId',
render: (panelId: string) => (
<GroupPanelName groupId={groupId} panelId={panelId} />
),
},
{
title: Translate.createdTime,
key: 'createdAt',
dataIndex: 'createdAt',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: Translate.action,
key: 'action',
render: (_, record: SubscribeItem) => (
<Space>
<Button onClick={() => handleDelete(record._id)}>
{Translate.delete}
</Button>
</Space>
),
},
],
[handleDelete]
);
const url = `${getServiceUrl()}/api/plugin:com.msgbyte.simplenotify/webhook/callback?subscribeId=<ID>&text=<文本内容>`;
return (
<div>
<div
style={{
marginBottom: 10,
display: 'flex',
justifyContent: 'space-between',
}}
>
<h2>{Translate.groupSubscribe}</h2>
<Button type="primary" onClick={handleAdd}>
{Translate.add}
</Button>
</div>
<Table
rowKey="_id"
columns={columns}
dataSource={subscribes}
pagination={false}
/>
{Array.isArray(subscribes) && subscribes.length > 0 && (
<div style={{ marginTop: 10 }}>
<h3>:</h3>
<p>
:&nbsp;
<code style={{ userSelect: 'text' }}>{url}</code>
</p>
<p>GET与POST</p>
<p>
<b>ID</b>
</p>
</div>
)}
</div>
);
});
GroupSubscribePanel.displayName = 'GroupSubscribePanel';
export default GroupSubscribePanel;

View File

@@ -0,0 +1,26 @@
import {
regCustomPanel,
Loadable,
regInspectService,
regPluginPermission,
} from '@capital/common';
import { Translate } from './translate';
regCustomPanel({
position: 'groupdetail',
name: 'com.msgbyte.simplenotify/groupSubscribe',
label: Translate.groupSubscribe,
render: Loadable(() => import('./GroupSubscribePanel')),
});
regInspectService({
name: 'plugin:com.msgbyte.simplenotify',
label: Translate.simplenotifyService,
});
regPluginPermission({
key: 'plugin.com.msgbyte.simplenotify.subscribe.manage',
title: Translate.permissionTitle,
desc: Translate.permissionDesc,
default: false,
});

View File

@@ -0,0 +1,3 @@
import { createPluginRequest } from '@capital/common';
export const request = createPluginRequest('com.msgbyte.simplenotify');

View File

@@ -0,0 +1,56 @@
import { localTrans } from '@capital/common';
export const Translate = {
groupSubscribe: localTrans({
'zh-CN': '简易通知群组订阅',
'en-US': 'Simple Notify Bot Group Subscribe',
}),
simplenotifyService: localTrans({
'zh-CN': '简易通知机器人服务',
'en-US': 'Simple Notify Bot Service',
}),
add: localTrans({
'zh-CN': '新增',
'en-US': 'Add',
}),
panel: localTrans({
'zh-CN': '面板',
'en-US': 'Panel',
}),
createdTime: localTrans({
'zh-CN': '创建时间',
'en-US': 'Created Time',
}),
action: localTrans({
'zh-CN': '操作',
'en-US': 'Action',
}),
delete: localTrans({
'zh-CN': '删除',
'en-US': 'Delete',
}),
textPanel: localTrans({
'zh-CN': '文本频道',
'en-US': 'Text Channel',
}),
createNotify: localTrans({
'zh-CN': '创建通知',
'en-US': 'Create Notify',
}),
success: localTrans({
'zh-CN': '成功',
'en-US': 'Success',
}),
textPanelEmpty: localTrans({
'zh-CN': '文本频道不能为空',
'en-US': 'Text Panel Not Allowd Empty',
}),
permissionTitle: localTrans({
'zh-CN': '简单通知管理',
'en-US': 'Simple Notify Manager',
}),
permissionDesc: localTrans({
'zh-CN': '允许管理群组简单通知机器人',
'en-US': 'Allows admin groups to simply notify bots',
}),
};

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

View File

@@ -0,0 +1,299 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* 该文件由 Tailchat 自动生成
* 用于插件的类型声明
* 生成命令: pnpm run plugins:declaration:generate
*/
/**
* Tailchat 通用
*/
declare module '@capital/common' {
export const useGroupPanelParams: any;
/**
* 打开模态框
*/
export const openModal: (
content: React.ReactNode,
props?: {
/**
* 是否显示右上角的关闭按钮
* @default false
*/
closable?: boolean;
/**
* 遮罩层是否可关闭
*/
maskClosable?: boolean;
/**
* 关闭modal的回调
*/
onCloseModal?: () => void;
}
) => number;
export const closeModal: any;
export const ModalWrapper: any;
export const useModalContext: any;
export const openConfirmModal: any;
export const openReconfirmModal: any;
export const Loadable: any;
export const getGlobalState: any;
export const getJWTUserInfo: () => Promise<{
_id?: string;
nickname?: string;
email?: string;
avatar?: string;
}>;
export const dataUrlToFile: any;
export const urlSearchStringify: any;
export const urlSearchParse: any;
export const appendUrlSearch: any;
export const useGroupIdContext: any;
export const getServiceUrl: () => string;
export const getCachedUserInfo: (
userId: string,
refetch?: boolean
) => Promise<{
_id: string;
email: string;
nickname: string;
discriminator: string;
avatar: string | null;
temporary: boolean;
}>;
export const getCachedConverseInfo: any;
export const localTrans: any;
export const getLanguage: any;
export const sharedEvent: any;
export const useAsync: any;
export const useAsyncFn: any;
export const useAsyncRefresh: any;
export const useAsyncRequest: any;
export const uploadFile: any;
export const showToasts: any;
export const showErrorToasts: any;
export const fetchAvailableServices: any;
export const isValidStr: any;
export const useGroupPanelInfo: any;
export const sendMessage: any;
export const useLocation: any;
export const useHistory: any;
export const createFastFormSchema: any;
export const fieldSchema: any;
export const useCurrentUserInfo: any;
export const createPluginRequest: (pluginName: string) => {
get: (actionName: string, config?: any) => Promise<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
};
export const postRequest: any;
export const pluginCustomPanel: any;
export const regCustomPanel: any;
export const pluginGroupPanel: any;
export const regGroupPanel: any;
export const messageInterpreter: any;
export const regMessageInterpreter: any;
export const getMessageRender: any;
export const regMessageRender: any;
export const getMessageTextDecorators: any;
export const regMessageTextDecorators: any;
export const ChatInputActionContextProps: any;
export const pluginChatInputActions: any;
export const regChatInputAction: any;
export const regSocketEventListener: (item: {
eventName: string;
eventFn: (...args: any[]) => void;
}) => void;
export const pluginColorScheme: any;
export const regPluginColorScheme: any;
export const pluginInspectServices: any;
export const regInspectService: any;
export const pluginMessageExtraParsers: any;
export const regMessageExtraParser: any;
export const pluginRootRoute: any;
export const regPluginRootRoute: any;
export const pluginPanelActions: any;
export const regPluginPanelAction: any;
export const pluginPermission: any;
export const regPluginPermission: (permission: {
/**
* 权限唯一key, 用于写入数据库
* 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage
*/
key: string;
/**
* 权限点显示名称
*/
title: string;
/**
* 权限描述
*/
desc: string;
/**
* 是否默认开启
*/
default: boolean;
/**
* 是否依赖其他权限点
*/
required?: string[];
}) => void;
}
/**
* Tailchat 组件
*/
declare module '@capital/component' {
export const Button: any;
export const Checkbox: any;
export const Input: any;
export const Divider: any;
export const Space: any;
export const Menu: any;
export const Table: any;
export const Switch: any;
export const Tooltip: any;
/**
* @link https://ant.design/components/notification-cn/
*/
export const notification: any;
export const Avatar: any;
export const SensitiveText: React.FC<{ className?: string; text: string }>;
export const TextArea: any;
export const Image: any;
export const Icon: any;
export const IconBtn: any;
export const PillTabs: any;
export const PillTabPane: any;
export const LoadingSpinner: any;
export const WebFastForm: any;
export const WebMetaForm: any;
export const createMetaFormSchema: any;
export const metaFormFieldSchema: any;
export const FullModalField: any;
export const DefaultFullModalInputEditorRender: any;
export const DefaultFullModalTextAreaEditorRender: any;
export const openModal: any;
export const closeModal: any;
export const ModalWrapper: any;
export const useModalContext: any;
export const openConfirmModal: any;
export const openReconfirmModal: any;
export const Loading: any;
export const SidebarView: any;
export const GroupPanelSelector: any;
export const Emoji: any;
export const PortalAdd: any;
export const PortalRemove: any;
export const ErrorBoundary: any;
export const UserName: React.FC<{
userId: string;
className?: string;
}>;
}