diff --git a/client/web/src/plugin/component/index.tsx b/client/web/src/plugin/component/index.tsx
index 17ebe87a..3fe703d0 100644
--- a/client/web/src/plugin/component/index.tsx
+++ b/client/web/src/plugin/component/index.tsx
@@ -56,6 +56,7 @@ export { GroupPanelSelector } from '@/components/GroupPanelSelector';
export { Emoji } from '@/components/Emoji';
export { PortalAdd, PortalRemove } from '@/components/Portal';
export { ErrorBoundary } from '@/components/ErrorBoundary';
+export { ErrorView } from '@/components/ErrorView';
export { UserAvatar } from '@/components/UserAvatar';
export { UserName } from '@/components/UserName';
export { Markdown } from '@/components/Markdown';
diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts
index 41f990e9..dcc8499f 100644
--- a/client/web/tailchat.d.ts
+++ b/client/web/tailchat.d.ts
@@ -410,6 +410,8 @@ declare module '@capital/component' {
export const MessageAckContainer: any;
+ export const GroupExtraDataPanel: any;
+
export const Image: any;
export const IconBtn: React.FC<{
@@ -497,6 +499,10 @@ declare module '@capital/component' {
export const ErrorBoundary: any;
+ export const ErrorView: React.FC<{
+ error: Error;
+ }>;
+
export const UserAvatar: React.FC<{
userId: string;
className?: string;
@@ -512,6 +518,8 @@ declare module '@capital/component' {
export const Markdown: any;
+ export const MarkdownEditor: any;
+
export const Webview: any;
export const WebviewKeepAlive: any;
diff --git a/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts b/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts
similarity index 97%
rename from server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts
rename to server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts
index 5ad17c58..ee3b1822 100644
--- a/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts
+++ b/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts
@@ -114,9 +114,9 @@ class WxpusherService extends TcService {
return null;
}
- const uid = user.wxpusherUserId;
+ const wxpusherUserId = user.wxpusherUserId;
- return uid;
+ return wxpusherUserId;
}
/**
diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx
new file mode 100644
index 00000000..a0ced86b
--- /dev/null
+++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { createPluginRequest, useAsync } from '@capital/common';
+import { LoadingSpinner, ErrorView } from '@capital/component';
+import { Translate } from './translate';
+
+const request = createPluginRequest('com.msgbyte.wxpusher');
+
+const SettingsPanel: React.FC = React.memo(() => {
+ const [wxpusherUserId, setWxpusherUserId] = useState('');
+ const { loading, error } = useAsync(async () => {
+ const { data } = await request.get('getWXPusherUserId');
+
+ setWxpusherUserId(data);
+ }, []);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ return wxpusherUserId ? (
+
+
{Translate.binded}
+
+ {Translate.currentWXPusherId}: {wxpusherUserId}
+
+
+ ) : (
+ {
+ setWxpusherUserId(wxpusherUserId);
+ }}
+ />
+ );
+});
+SettingsPanel.displayName = 'SettingsPanel';
+
+export const QRCode: React.FC<{
+ onBindSuccess: (wxpusherUserId: string) => void;
+}> = React.memo((props) => {
+ const {
+ loading,
+ error,
+ value: url,
+ } = useAsync(async () => {
+ const { data } = await request.post('createQRCode');
+ return data.data.url;
+ }, []);
+
+ const onBindSuccessRef = useRef(props.onBindSuccess);
+ onBindSuccessRef.current = props.onBindSuccess;
+ useLayoutEffect(() => {
+ let timer: number;
+ async function loop() {
+ const { data: wxpusherUserId } = await request.get('getWXPusherUserId');
+ if (wxpusherUserId) {
+ onBindSuccessRef.current(wxpusherUserId);
+ } else {
+ timer = window.setTimeout(loop, 4 * 1000); // 4s loop
+ }
+ }
+
+ loop();
+
+ return () => {
+ if (timer) {
+ window.clearTimeout(timer);
+ }
+ };
+ }, []);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ return (
+
+
{Translate.useWechatBindTip}
+

+
+ );
+});
+QRCode.displayName = 'QRCode';
+
+export default SettingsPanel;
diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx
index 04ecbf7d..60fb6e73 100644
--- a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx
+++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx
@@ -1 +1,12 @@
+import { regCustomPanel } from '@capital/common';
+import { Loadable } from '@capital/component';
+
console.log('Plugin wxpusher is loaded');
+
+regCustomPanel({
+ position: 'setting',
+ icon: '',
+ name: 'com.msgbyte.wxpusher/settings',
+ label: 'WxPusher',
+ render: Loadable(() => import('./SettingsPanel')),
+});
diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts
new file mode 100644
index 00000000..405f7380
--- /dev/null
+++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts
@@ -0,0 +1,24 @@
+import { localTrans } from '@capital/common';
+
+export const Translate = {
+ loadingState: localTrans({
+ 'zh-CN': '正在检查绑定状态',
+ 'en-US': 'Checking binding status',
+ }),
+ binded: localTrans({
+ 'zh-CN': '已绑定',
+ 'en-US': 'Binded',
+ }),
+ currentWXPusherId: localTrans({
+ 'zh-CN': '当前 wxpusher ID',
+ 'en-US': 'Current wxpusher uid',
+ }),
+ loadingQRCode: localTrans({
+ 'zh-CN': '正在加载绑定二维码',
+ 'en-US': 'Binding QR code is loading',
+ }),
+ useWechatBindTip: localTrans({
+ 'zh-CN': '使用微信扫码绑定 wxpusher',
+ 'en-US': 'Use wechat scan QRCode to bind wxpusher',
+ }),
+};
diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts
index 49f524ae..aece80b9 100644
--- a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts
+++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts
@@ -1,2 +1,532 @@
-declare module '@capital/common';
-declare module '@capital/component';
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+///
+
+/**
+ * 该文件由 Tailchat 自动生成
+ * 用于插件的类型声明
+ * 生成命令: pnpm run plugins:declaration:generate
+ */
+
+/**
+ * Tailchat 通用
+ */
+declare module '@capital/common' {
+ export const useGroupPanelParams: any;
+
+ /**
+ * 打开模态框
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const openModal: (
+ content: React.ReactNode,
+
+ props?: {
+ /**
+ * 是否显示右上角的关闭按钮
+ * @default false
+ */
+ closable?: boolean;
+
+ /**
+ * 遮罩层是否可关闭
+ */
+ maskClosable?: boolean;
+
+ /**
+ * 关闭modal的回调
+ */
+ onCloseModal?: () => void;
+ }
+ ) => number;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const closeModal: any;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const ModalWrapper: any;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const useModalContext: any;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const openConfirmModal: any;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const openReconfirmModal: any;
+
+ /**
+ * @deprecated 请从 @capital/component 引入
+ */
+ export const Loadable: any;
+
+ export const getGlobalState: any;
+
+ export const useGlobalSocketEvent: (
+ eventName: string,
+ callback: (data: T) => void
+ ) => void;
+
+ 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 getServiceWorkerRegistration: 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 getCachedBaseGroupInfo: any;
+
+ export const getCachedUserSettings: any;
+
+ /**
+ * 本地翻译
+ * @example
+ * localTrans({'zh-CN': '你好', 'en-US': 'Hello'});
+ *
+ * @param trans 翻译对象
+ */
+ export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string;
+
+ export const getLanguage: any;
+
+ export const sharedEvent: any;
+
+ export const useAsync: Promise>(
+ fn: T,
+ deps?: React.DependencyList
+ ) => { loading: boolean; value?: any; error?: Error };
+
+ export const useAsyncFn: Promise>(
+ fn: T,
+ deps?: React.DependencyList
+ ) => [{ loading: boolean; value?: any; error?: Error }, T];
+
+ export const useAsyncRefresh: Promise>(
+ fn: T,
+ deps?: React.DependencyList
+ ) => { loading: boolean; value?: any; error?: Error; refresh: () => void };
+
+ export const useAsyncRequest: Promise>(
+ fn: T,
+ deps?: React.DependencyList
+ ) => [{ loading: boolean; value?: any }, T];
+
+ export const uploadFile: any;
+
+ export const showToasts: (
+ message: string,
+ type?: 'info' | 'success' | 'error' | 'warning'
+ ) => void;
+
+ export const showSuccessToasts: any;
+
+ export const showErrorToasts: (error: any) => void;
+
+ export const fetchAvailableServices: any;
+
+ export const isValidStr: (str: any) => str is string;
+
+ export const useGroupPanelInfo: any;
+
+ export const sendMessage: any;
+
+ export const showMessageTime: any;
+
+ export const joinArray: any;
+
+ export const navigate: any;
+
+ export const useLocation: any;
+
+ export const useNavigate: any;
+
+ /**
+ * @deprecated please use createMetaFormSchema from @capital/component
+ */
+ export const createFastFormSchema: any;
+
+ /**
+ * @deprecated please use metaFormFieldSchema from @capital/component
+ */
+ export const fieldSchema: any;
+
+ export const useCurrentUserInfo: any;
+
+ export const createPluginRequest: (pluginName: string) => {
+ get: (actionName: string, config?: any) => Promise;
+ post: (actionName: string, data?: any, config?: any) => Promise;
+ };
+
+ export const postRequest: any;
+
+ export const pluginCustomPanel: any;
+
+ export const regCustomPanel: (info: {
+ position:
+ | 'personal'
+ | 'setting'
+ | 'groupdetail'
+ | 'navbar-more'
+ | 'navbar-group'
+ | 'navbar-personal';
+ icon: string;
+ name: string;
+ label: string;
+ render: React.ComponentType;
+ }) => void;
+
+ export const pluginGroupPanel: any;
+
+ export const regGroupPanel: any;
+
+ export const messageInterpreter: {
+ name?: string;
+ explainMessage: (message: string) => React.ReactNode;
+ }[];
+
+ export const regMessageInterpreter: (interpreter: {
+ name?: string;
+ explainMessage: (message: string) => React.ReactNode;
+ }) => void;
+
+ export const getMessageRender: (message: string) => React.ReactNode;
+
+ export const regMessageRender: (
+ render: (message: string) => React.ReactNode
+ ) => void;
+
+ 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: (
+ action:
+ | {
+ name: string;
+ label: string;
+ icon: string;
+ position: 'group';
+ onClick: (ctx: { groupId: string; panelId: string }) => void;
+ }
+ | {
+ name: string;
+ label: string;
+ icon: string;
+ position: 'dm';
+ onClick: (ctx: { converseId: string }) => void;
+ }
+ ) => void;
+
+ 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;
+
+ export const pluginGroupPanelBadges: any;
+
+ export const regGroupPanelBadge: any;
+
+ export const pluginGroupTextPanelExtraMenus: any;
+
+ export const regPluginGroupTextPanelExtraMenu: any;
+
+ export const pluginUserExtraInfo: any;
+
+ export const regUserExtraInfo: any;
+
+ export const pluginSettings: any;
+
+ export const regPluginSettings: any;
+
+ export const pluginInboxItemMap: any;
+
+ export const regPluginInboxItemMap: any;
+
+ export const useGroupIdContext: () => string;
+
+ export const useGroupPanelContext: () => {
+ groupId: string;
+ panelId: string;
+ } | null;
+
+ export const useSocketContext: any;
+}
+
+/**
+ * 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 Empty: React.FC<
+ React.PropsWithChildren<{
+ prefixCls?: string;
+ className?: string;
+ style?: React.CSSProperties;
+ imageStyle?: React.CSSProperties;
+ image?: React.ReactNode;
+ description?: React.ReactNode;
+ }>
+ >;
+
+ export const TextArea: any;
+
+ export const Avatar: any;
+
+ export const SensitiveText: React.FC<{ className?: string; text: string }>;
+
+ export const Icon: React.FC<{ icon: string } & React.SVGProps>;
+
+ export const CopyableText: React.FC<{
+ className?: string;
+ style?: React.CSSProperties;
+ config?:
+ | boolean
+ | {
+ text?: string;
+ onCopy?: (event?: React.MouseEvent) => void;
+ icon?: React.ReactNode;
+ tooltips?: boolean | React.ReactNode;
+ format?: 'text/plain' | 'text/html';
+ };
+ }>;
+
+ export const WebFastForm: any;
+
+ export const WebMetaForm: any;
+
+ export const createMetaFormSchema: any;
+
+ export const metaFormFieldSchema: any;
+
+ export const Link: any;
+
+ export const MessageAckContainer: any;
+
+ export const Image: any;
+
+ export const IconBtn: React.FC<{
+ icon: string;
+ className?: string;
+ iconClassName?: string;
+ size?: 'small' | 'middle' | 'large';
+ shape?: 'circle' | 'square';
+ title?: string;
+ danger?: boolean;
+ active?: boolean;
+ disabled?: boolean;
+ onClick?: React.MouseEventHandler;
+ }>;
+
+ export const PillTabs: any;
+
+ export const PillTabPane: any;
+
+ export const LoadingSpinner: React.FC<{ tip?: string }>;
+
+ export const FullModalField: any;
+
+ export const DefaultFullModalInputEditorRender: any;
+
+ export const DefaultFullModalTextAreaEditorRender: 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 Loading: React.FC<{
+ spinning: boolean;
+ className?: string;
+ style?: React.CSSProperties;
+ children?: React.ReactNode;
+ }>;
+
+ export const LoadingOnFirst: React.FC<{
+ spinning: boolean;
+ className?: string;
+ style?: React.CSSProperties;
+ children?: React.ReactNode;
+ }>;
+
+ 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 ErrorView: React.FC<{
+ error: Error;
+ }>;
+
+ export const UserAvatar: React.FC<{
+ userId: string;
+ className?: string;
+ style?: React.CSSProperties;
+ size?: 'large' | 'small' | 'default' | number;
+ }>;
+
+ export const UserName: React.FC<{
+ userId: string;
+ className?: string;
+ style?: React.CSSProperties;
+ }>;
+
+ export const Markdown: any;
+
+ export const Webview: any;
+
+ export const WebviewKeepAlive: any;
+
+ export const Card: any;
+
+ export const Problem: any;
+
+ export const JumpToButton: any;
+
+ export const JumpToGroupPanelButton: any;
+
+ export const JumpToConverseButton: any;
+}