diff --git a/Dockerfile b/Dockerfile
index 8155b1e..6d2e161 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,8 @@
+FROM node:16 as frontend
+ADD . /app
+WORKDIR /app/admin-frontend
+RUN yarn && yarn build
+
 FROM python:3.9
 RUN python3 -m pip install poetry && poetry config virtualenvs.create false
 WORKDIR /app
@@ -6,5 +11,6 @@ RUN poetry install --no-root --no-dev
 # RUN PYPPETEER_DOWNLOAD_HOST='http://npm.taobao.org/mirrors' pyppeteer-install
 ADD src /app/src
 ADD bot.py /app/
+COPY --from=frontend /app/src/plugins/nonebot_bison/admin_page/dist /app/src/plugins/nonebot_bison/admin_page/dist
 ENV HOST=0.0.0.0
 CMD ["python", "bot.py"]
diff --git a/admin-frontend/src/api/config.ts b/admin-frontend/src/api/config.ts
index 7854058..ea88904 100644
--- a/admin-frontend/src/api/config.ts
+++ b/admin-frontend/src/api/config.ts
@@ -1,5 +1,5 @@
 import axios from "axios";
-import { GlobalConf, TokenResp, SubscribeResp, TargetNameResp, CreateSubscribeReq } from "../utils/type";
+import { GlobalConf, TokenResp, SubscribeResp, TargetNameResp, SubscribeConfig } from "../utils/type";
 import { baseUrl } from './utils';
 
 export async function getGlobalConf(): Promise<GlobalConf> {
@@ -22,7 +22,7 @@ export async function getTargetName(platformName: string, target: string): Promi
   return res.data;
 }
 
-export async function addSubscribe(groupNumber: string, req: CreateSubscribeReq) {
+export async function addSubscribe(groupNumber: string, req: SubscribeConfig) {
   const res = await axios.post(`${baseUrl}subs`, req, {params: {groupNumber}})
   return res.data;
 }
diff --git a/admin-frontend/src/pages/admin.tsx b/admin-frontend/src/pages/admin.tsx
index 3fe40e9..fb66804 100644
--- a/admin-frontend/src/pages/admin.tsx
+++ b/admin-frontend/src/pages/admin.tsx
@@ -1,7 +1,7 @@
-import React, { ReactElement, useContext, useEffect, useState } from "react";
+import React, { FC, ReactElement, ReactNode, useContext, useEffect, useState } from "react";
 import { LoginContext, GlobalConfContext } from "../utils/context";
 import { Layout, Menu, Empty, Collapse, Card, Tag, Row, Col, Form, Tooltip, Button, Modal, Select,
-  Input, Popconfirm} from 'antd';
+  Input, Popconfirm, message } from 'antd';
 import { SubscribeConfig, SubscribeResp, PlatformConfig, CategoryConfig } from '../utils/type';
 import { SettingOutlined, BugOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons';
 import { getSubscribe, getTargetName, addSubscribe, delSubscribe } from '../api/config';
@@ -74,7 +74,9 @@ function ConfigPage(prop: ConfigPageProp) {
             onConfirm={handleDelete(groupNumber, config.platformName, config.target || 'default')}>
             <Tooltip title="删除" ><DeleteOutlined /></Tooltip>
           </Popconfirm>, 
-          <Tooltip title="添加到其他群"><CopyOutlined /></Tooltip>
+          <TargetGroupSelection config={config} groups={configData}>
+            <Tooltip title="添加到其他群"><CopyOutlined /></Tooltip>
+          </TargetGroupSelection>
           ]}>
         <Form labelCol={{ span: 6 }}>
         <Form.Item label="订阅类型">
@@ -119,6 +121,43 @@ function ConfigPage(prop: ConfigPageProp) {
   }
 }
 
+interface TargetGroupSelectionProp {
+  config: SubscribeConfig,
+  groups: SubscribeResp
+  children: ReactNode
+}
+function TargetGroupSelection({ config, groups, children }: TargetGroupSelectionProp) {
+  let [ selectedGroups, setSelectGroups ] = useState<Array<string>>([]);
+  const submitCopy = () => {
+    let promise = null
+    for (let selectGroup of selectedGroups) {
+      if (! promise) {
+        promise = addSubscribe(selectGroup, config)
+      } else {
+        promise = promise.then(() => addSubscribe(selectGroup, config))
+      }
+    }
+    if (promise) {
+      promise.then(() => message.success("复制订阅成功"))
+    }
+    return promise;
+  }
+  return <>
+    <Popconfirm title={
+        <Select mode="multiple" onChange={(value: Array<string>) => setSelectGroups(value)}>
+          {
+            Object.keys(groups).map((groupNumber) => 
+              <Select.Option value={groupNumber} key={groupNumber}>
+                {`${groupNumber} - ${groups[groupNumber].name}`}
+              </Select.Option>)
+            }
+        </Select>
+      } onConfirm={submitCopy} >
+      { children }
+    </Popconfirm>
+  </>
+}
+
 interface InputTagCustomProp {
   value?: Array<string>,
   onChange?: (value: Array<string>) => void,
diff --git a/admin-frontend/src/utils/type.ts b/admin-frontend/src/utils/type.ts
index 3fe0ea0..fc4a838 100644
--- a/admin-frontend/src/utils/type.ts
+++ b/admin-frontend/src/utils/type.ts
@@ -19,7 +19,7 @@ export type LoginContextType = {
 
 export interface SubscribeConfig {
   platformName: string
-  target?: string
+  target: string
   targetName: string
   cats: Array<number>
   tags: Array<string>
@@ -66,11 +66,3 @@ export interface SubscribeResp {
 export interface TargetNameResp {
   targetName: string
 }
-
-export interface CreateSubscribeReq {
-  platformName: string,
-  targetName: string,
-  target: string,
-  categories: Array<string>,
-  tags: Array<string>
-}
diff --git a/pyproject.toml b/pyproject.toml
index ddeb53b..8193608 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,6 @@ expiringdict = "^1.2.1"
 pyjwt = "^2.1.0"
 aiofiles = "^0.7.0"
 python-socketio = "^5.4.0"
-jinja2 = "^3.0.1"
 
 [tool.poetry.dev-dependencies]
 ipdb = "^0.13.4"
diff --git a/src/plugins/nonebot_bison/admin_page/__init__.py b/src/plugins/nonebot_bison/admin_page/__init__.py
index 81b576e..01a1566 100644
--- a/src/plugins/nonebot_bison/admin_page/__init__.py
+++ b/src/plugins/nonebot_bison/admin_page/__init__.py
@@ -1,5 +1,7 @@
 from dataclasses import dataclass
 from pathlib import Path
+import os
+from typing import Union
 
 from fastapi.staticfiles import StaticFiles
 from fastapi.templating import Jinja2Templates
@@ -31,6 +33,17 @@ TEST_URL = f'{URL_BASE}test'
 sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
 socket_app = socketio.ASGIApp(sio, socketio_path="socket")
 
+class SinglePageApplication(StaticFiles):
+
+    def __init__(self, directory: os.PathLike, index='index.html'):
+        self.index = index
+        super().__init__(directory=directory, packages=None, html=True, check_dir=True)
+
+    async def lookup_path(self, path: str) -> tuple[str, Union[os.stat_result, None]]:
+        full_path, stat_res = await super().lookup_path(path)
+        if stat_res is None:
+            return await super().lookup_path(self.index)
+        return (full_path, stat_res)
 
 def register_router_fastapi(driver: Driver, socketio):
     from fastapi.security import OAuth2PasswordBearer
@@ -61,7 +74,7 @@ def register_router_fastapi(driver: Driver, socketio):
         tags: list[str]
 
     app = driver.server_app
-    static_path = str((Path(__file__).parent / "dist").resolve())
+    static_path = (Path(__file__).parent / "dist").resolve()
     app.get(TEST_URL)(test)
     app.get(GLOBAL_CONF_URL)(get_global_conf)
     app.get(AUTH_URL)(auth)
@@ -76,15 +89,12 @@ def register_router_fastapi(driver: Driver, socketio):
     async def _add_group_subs(groupNumber: str, req: AddSubscribeReq):
         return await add_group_sub(group_number=groupNumber, platform_name=req.platformName,
                 target=req.target, target_name=req.targetName, cats=req.categories, tags=req.tags)
+
     @app.delete(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)])
     async def _del_group_subs(groupNumber: str, target: str, platformName: str):
         return await del_group_sub(groupNumber, platformName, target)
-    app.mount(URL_BASE, StaticFiles(directory=static_path, html=True), name="bison")
-    templates = Jinja2Templates(directory=static_path)
 
-    @app.get(f'{URL_BASE}{{rest_path:path}}')
-    async def serve_sap(request: Request, rest_path: str):
-        return templates.TemplateResponse("index.html", {"request": request})
+    app.mount(URL_BASE, SinglePageApplication(directory=static_path), name="bison")
 
 
 def init():
@@ -108,10 +118,6 @@ get_token = on_command('后台管理', rule=to_me(), priority=5)
 @get_token.handle()
 async def send_token(bot: "Bot", event: PrivateMessageEvent, state: T_State):
     driver = nonebot.get_driver()
-    superusers = driver.config.superusers
-    if event.get_user_id() not in superusers:
-        await get_token.finish('你不是管理员')
-    else:
-        token = tm.get_user_token((event.get_user_id(), event.sender.nickname))
-        await get_token.finish(f'请访问: {plugin_config.bison_outer_url}auth/{token}')
+    token = tm.get_user_token((event.get_user_id(), event.sender.nickname))
+    await get_token.finish(f'请访问: {plugin_config.bison_outer_url}auth/{token}')
 
diff --git a/src/plugins/nonebot_bison/admin_page/api.py b/src/plugins/nonebot_bison/admin_page/api.py
index de737c1..dcedbce 100644
--- a/src/plugins/nonebot_bison/admin_page/api.py
+++ b/src/plugins/nonebot_bison/admin_page/api.py
@@ -20,6 +20,17 @@ async def get_global_conf():
             }
     return { 'platformConf': res }
 
+async def get_admin_groups(qq: int):
+    bot = nonebot.get_bot()
+    groups = await bot.call_api('get_group_list')
+    res = []
+    for group in groups:
+        group_id = group['group_id']
+        users = await bot.call_api('get_group_member_list', group_id=group_id)
+        for user in users:
+            if user['user_id'] == qq and user['role'] in ('owner', 'admin'):
+                res.append({'id': group_id, 'name': group['group_name']})
+    return res
 
 async def auth(token: str):
     if qq_tuple := token_manager.get_user(token):
@@ -41,6 +52,18 @@ async def auth(token: str):
                     'token': pack_jwt(jwt_obj)
                     }
             return { 'status': 200, **ret_obj }
+        if admin_groups := await get_admin_groups(int(qq)):
+            jwt_obj = {
+                    'id': str(qq),
+                    'groups': admin_groups
+                    }
+            ret_obj = {
+                    'type': 'user',
+                    'name': nickname,
+                    'id': str(qq),
+                    'token': pack_jwt(jwt_obj)
+                    }
+            return { 'status': 200, **ret_obj }
         else:
             return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' }
     else: