diff --git a/README.md b/README.md
index b4d2a7c..01c2b4e 100644
--- a/README.md
+++ b/README.md
@@ -34,9 +34,9 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboo
 ```
 ├── plugins
 │   ├── LLOneBot
-│   │   └── main.js
-│   │   └── preload.js
-│   │   └── renderer.js
+│   │   └── main/
+│   │   └── preload/
+│   │   └── renderer/
 │   │   └── manifest.json
 │   │   └── node_modules/...
 ```
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index a72dbd8..a30979c 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -52,11 +52,11 @@ let config = {
             emptyOutDir: true,
             lib: {
                 formats: ["es"],
-                entry: { "renderer": "src/renderer.ts" },
+                entry: { "renderer": "src/renderer/index.ts" },
             },
             rollupOptions: {
                 // external: externalAll,
-                input: "src/renderer.ts",
+                input: "src/renderer/index.ts",
             }
         },
         resolve:{
diff --git a/manifest.json b/manifest.json
index 2152c02..4f19d6f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -26,7 +26,7 @@
     "darwin"
   ],
   "injects": {
-    "renderer": "./renderer/renderer.js",
+    "renderer": "./renderer/index.js",
     "main": "./main/main.cjs",
     "preload": "./preload/preload.cjs"
   }
diff --git a/src/renderer.ts b/src/renderer.ts
deleted file mode 100644
index 7611793..0000000
--- a/src/renderer.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-/// <reference path="./global.d.ts" />
-
-// 打开设置界面时触发
-
-async function onSettingWindowCreated (view: Element) {
-  window.llonebot.log('setting window created')
-  const isEmpty = (value: any) => value === undefined || value === null || value === ''
-  const config = await window.llonebot.getConfig()
-  const httpClass = 'http'
-  const httpPostClass = 'http-post'
-  const wsClass = 'ws'
-  const reverseWSClass = 'reverse-ws'
-  const llonebotError = await window.llonebot.getError()
-  window.llonebot.log('获取error' + JSON.stringify(llonebotError))
-
-  function createHttpHostEleStr (host: string) {
-    const eleStr = `
-            <setting-item data-direction="row" class="hostItem vertical-list-item ${httpPostClass}">
-                <h2>HTTP事件上报地址(http)</h2>
-                <input class="httpHost input-text" type="text" value="${host}" 
-                style="width:60%;padding: 5px"
-                placeholder="如:http://127.0.0.1:8080/onebot/v11/http"/>
-            </setting-item>
-            `
-    return eleStr
-  }
-
-  function createWsHostEleStr (host: string) {
-    const eleStr = `
-            <setting-item data-direction="row" class="hostItem vertical-list-item ${reverseWSClass}">
-                <h2>反向websocket地址:</h2>
-                <input class="wsHost input-text" type="text" value="${host}" 
-                style="width:60%;padding: 5px"
-                placeholder="如: ws://127.0.0.1:5410/onebot"/>
-            </setting-item>
-            `
-    return eleStr
-  }
-
-  let httpHostsEleStr = ''
-  for (const host of config.ob11.httpHosts) {
-    httpHostsEleStr += createHttpHostEleStr(host)
-  }
-
-  let wsHostsEleStr = ''
-  for (const host of config.ob11.wsHosts) {
-    wsHostsEleStr += createWsHostEleStr(host)
-  }
-
-  const html = `
-    <div class="config_view llonebot">
-        <setting-section>
-            <setting-panel id="llonebotError" style="display:${llonebotError.ffmpegError || llonebotError.otherError ? '' : 'none'}">
-                <setting-item id="ffmpegError" data-direction="row" 
-                    style="diplay:${llonebotError.ffmpegError ? '' : 'none'}"
-                    class="hostItem vertical-list-item">
-                    <setting-text data-type="secondary" class="err-content">${llonebotError.ffmpegError}</setting-text>
-                </setting-item>
-                <setting-item id="otherError" data-direction="row" 
-                    style="diplay:${llonebotError.otherError ? '' : 'none'}"
-                    class="hostItem vertical-list-item">
-                    <setting-text data-type="secondary" class="err-content">${llonebotError.otherError}</setting-text>
-                </setting-item>
-            </setting-panel>
-            <setting-panel>
-                <setting-list class="wrap">
-                    <setting-item data-direction="row" class="hostItem vertical-list-item">
-                        <div>
-                            <div>启用HTTP服务</div>
-                        </div>
-                        <setting-switch id="http" ${config.ob11.enableHttp ? 'is-active' : ''}></setting-switch>
-                    </setting-item>
-                    <setting-item class="vertical-list-item ${httpClass}" data-direction="row" style="display: ${config.ob11.enableHttp ? '' : 'none'}">
-                        <setting-text>HTTP监听端口</setting-text>
-                        <input id="httpPort" type="number" value="${config.ob11.httpPort}"/>
-                    </setting-item>
-                    <setting-item data-direction="row" class="hostItem vertical-list-item">
-                        <div>
-                            <div>启用HTTP事件上报</div>
-                        </div>
-                        <setting-switch id="httpPost" ${config.ob11.enableHttpPost ? 'is-active' : ''}></setting-switch>
-                    </setting-item>
-                    <div class="${httpPostClass}" style="display: ${config.ob11.enableHttpPost ? '' : 'none'}">
-                        <div >
-                            <button id="addHttpHost" class="q-button">添加HTTP POST上报地址</button>
-                        </div>
-                        <div id="httpHostItems">
-                            ${httpHostsEleStr}
-                        </div>
-                    </div>
-                    <setting-item data-direction="row" class="hostItem vertical-list-item">
-                        <div>
-                            <div>启用正向Websocket协议</div>
-                        </div>
-                        <setting-switch id="websocket" ${config.ob11.enableWs ? 'is-active' : ''}></setting-switch>
-                    </setting-item>
-                    <setting-item class="vertical-list-item ${wsClass}" data-direction="row" style="display: ${config.ob11.enableWs ? '' : 'none'}">
-                        <setting-text>正向Websocket监听端口</setting-text>
-                        <input id="wsPort" type="number" value="${config.ob11.wsPort}"/>
-                    </setting-item>
-                    
-                    <setting-item data-direction="row" class="hostItem vertical-list-item">
-                        <div>
-                            <div>启用反向Websocket协议</div>
-                        </div>
-                        <setting-switch id="websocketReverse" ${config.ob11.enableWsReverse ? 'is-active' : ''}></setting-switch>
-                    </setting-item>
-                    <div class="${reverseWSClass}" style="display: ${config.ob11.enableWsReverse ? '' : 'none'}">
-                        <div>
-                            <button id="addWsHost" class="q-button">添加反向Websocket地址</button>
-                        </div>
-                        <div id="wsHostItems">
-                            ${wsHostsEleStr}
-                        </div>
-                    </div>
-                    <setting-item class="vertical-list-item" data-direction="row">
-                        <setting-text>Access Token</setting-text>
-                        <input id="token" type="text" placeholder="可为空" value="${config.token}"/>
-                    </setting-item>
-                    <setting-item data-direction="row" class="vertical-list-item">
-                        <setting-item data-direction="row" class="vertical-list-item" style="width: 80%">
-                            <setting-text>ffmpeg路径</setting-text>
-                            <input id="ffmpegPath" class="input-text" type="text" 
-                                style="width:80%;padding: 5px"
-                                value="${config.ffmpeg || ''}"/>
-                        </setting-item>
-                        <button id="selectFFMPEG" class="q-button q-button--small q-button--secondary">选择ffmpeg</button>
-                    </setting-item>
-                    <button id="save" class="q-button">保存</button>
-                </setting-list>
-            </setting-panel>
-            <setting-panel>
-
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <setting-text>消息上报数据类型</setting-text>
-                        <setting-text data-type="secondary">如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 <a href="javascript:LiteLoader.api.openExternal('https://github.com/botuniverse/onebot-11/tree/master/message#readme');">OneBot v11 文档</a></setting-text>
-                    </div>
-                    <setting-select id="messagePostFormat">
-                        <setting-option data-value="array" ${config.ob11.messagePostFormat !== 'string' ? 'is-selected' : ''}>消息段</setting-option>
-                        <setting-option data-value="string" ${config.ob11.messagePostFormat === 'string' ? 'is-selected' : ''}>CQ码</setting-option>
-                    </setting-select>
-                </setting-item>
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <div>获取文件使用base64编码</div>
-                        <div class="tips">开启后,调用/get_image、/get_record时,获取不到url时添加一个base64字段</div>
-                    </div>
-                    <setting-switch id="switchFileUrl" ${config.enableLocalFile2Url ? 'is-active' : ''}></setting-switch>
-                </setting-item>
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <div>debug模式</div>
-                        <div class="tips">开启后上报消息添加raw字段附带原始消息</div>
-                    </div>
-                    <setting-switch id="debug" ${config.debug ? 'is-active' : ''}></setting-switch>
-                </setting-item>
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <div>上报自身发送消息</div>
-                        <div class="tips"></div>
-                    </div>
-                    <setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? 'is-active' : ''}></setting-switch>
-                </setting-item>
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <div>日志</div>
-                        <div class="tips">目录:${window.LiteLoader.plugins.LLOneBot.path.data}</div>
-                    </div>
-                    <setting-switch id="log" ${config.log ? 'is-active' : ''}></setting-switch>
-                </setting-item>
-                <setting-item data-direction="row" class="vertical-list-item">
-                    <div>
-                        <div>自动删除收到的文件</div>
-                        <div class="tips">
-                            收到文件
-                            <input id="autoDeleteMin" 
-                                min="1" style="width: 50px"
-                                value="${config.autoDeleteFileSecond || 60}" type="number"/>秒后自动删除
-                        </div>
-                    </div>
-                    <setting-switch id="autoDeleteFile" ${config.autoDeleteFile ? 'is-active' : ''}></setting-switch>
-                </setting-item>
-            </setting-panel>
-        </setting-section>
-    </div>
-    <style>
-        setting-panel {
-            padding: 10px;
-        }
-        .tips {
-            font-size: 0.75rem;
-        }
-        @media (prefers-color-scheme: dark){
-            .llonebot input {
-                color: white;
-            }
-        }
-    </style>
-    `
-
-  const parser = new DOMParser()
-  const doc = parser.parseFromString(html, 'text/html')
-
-  const getError = async () => {
-    const llonebotError = await window.llonebot.getError()
-    console.log(llonebotError)
-    const llonebotErrorEle = document.getElementById('llonebotError')
-    const ffmpegErrorEle = document.getElementById('ffmpegError')
-    const otherErrorEle = document.getElementById('otherError')
-    if (llonebotError.otherError || llonebotError.ffmpegError) {
-      llonebotErrorEle.style.display = ''
-    } else {
-      llonebotErrorEle.style.display = 'none'
-    }
-    if (llonebotError.ffmpegError) {
-      const errContentEle = doc.querySelector('#ffmpegError .err-content')
-      // const errContent = ffmpegErrorEle.getElementsByClassName("err-content")[0];
-      errContentEle.textContent = llonebotError.ffmpegError;
-      (ffmpegErrorEle).style.display = ''
-    } else {
-      ffmpegErrorEle.style.display = ''
-    }
-    if (llonebotError.otherError) {
-      const errContentEle = doc.querySelector('#otherError .err-content')
-      errContentEle.textContent = llonebotError.otherError
-      otherErrorEle.style.display = ''
-    } else {
-      otherErrorEle.style.display = 'none'
-    }
-  }
-
-  function addHostEle (type: string, initValue: string = '') {
-    let addressEle, hostItemsEle
-    if (type === 'ws') {
-      const addressDoc = parser.parseFromString(createWsHostEleStr(initValue), 'text/html')
-      addressEle = addressDoc.querySelector('setting-item')
-      hostItemsEle = document.getElementById('wsHostItems')
-    } else {
-      const addressDoc = parser.parseFromString(createHttpHostEleStr(initValue), 'text/html')
-      addressEle = addressDoc.querySelector('setting-item')
-      hostItemsEle = document.getElementById('httpHostItems')
-    }
-
-    hostItemsEle.appendChild(addressEle)
-  }
-
-  doc.getElementById('addHttpHost').addEventListener('click', () => {
-    addHostEle('http')
-  })
-  doc.getElementById('addWsHost').addEventListener('click', () => {
-    addHostEle('ws')
-  })
-  doc.getElementById('messagePostFormat').addEventListener('selected', (e: CustomEvent) => {
-    config.ob11.messagePostFormat = e.detail && !isEmpty(e.detail.value) ? e.detail.value : 'array'
-    window.llonebot.setConfig(config)
-  })
-
-  function switchClick (eleId: string, configKey: string, _config = null) {
-    if (!_config) {
-      _config = config
-    }
-    doc.getElementById(eleId)?.addEventListener('click', (e) => {
-      const switchEle = e.target as HTMLInputElement
-      if (_config[configKey]) {
-        _config[configKey] = false
-        switchEle.removeAttribute('is-active')
-      } else {
-        _config[configKey] = true
-        switchEle.setAttribute('is-active', '')
-      }
-      // 妈蛋,手动操作DOM越写越麻烦,要不用vue算了
-      const keyClassMap = {
-        enableHttp: httpClass,
-        enableHttpPost: httpPostClass,
-        enableWs: wsClass,
-        enableWsReverse: reverseWSClass
-      }
-      for (const e of document.getElementsByClassName(keyClassMap[configKey])) {
-        (e as HTMLElement).style.display = _config[configKey] ? '' : 'none'
-      }
-
-      window.llonebot.setConfig(config)
-    })
-  }
-
-  switchClick('http', 'enableHttp', config.ob11)
-  switchClick('httpPost', 'enableHttpPost', config.ob11)
-  switchClick('websocket', 'enableWs', config.ob11)
-  switchClick('websocketReverse', 'enableWsReverse', config.ob11)
-  switchClick('debug', 'debug')
-  switchClick('switchFileUrl', 'enableLocalFile2Url')
-  switchClick('reportSelfMessage', 'reportSelfMessage')
-  switchClick('log', 'log')
-  switchClick('autoDeleteFile', 'autoDeleteFile')
-
-  doc.getElementById('save')?.addEventListener('click',
-    () => {
-      const httpPortEle: HTMLInputElement = document.getElementById('httpPort') as HTMLInputElement
-      const httpHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName('httpHost') as HTMLCollectionOf<HTMLInputElement>
-      const wsPortEle: HTMLInputElement = document.getElementById('wsPort') as HTMLInputElement
-      const wsHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName('wsHost') as HTMLCollectionOf<HTMLInputElement>
-      const tokenEle = document.getElementById('token') as HTMLInputElement
-      const ffmpegPathEle = document.getElementById('ffmpegPath') as HTMLInputElement
-
-      // 获取端口和host
-      const httpPort = httpPortEle.value
-      const httpHosts: string[] = []
-
-      for (const hostEle of httpHostEles) {
-        const value = hostEle.value.trim()
-        value && httpHosts.push(value)
-      }
-
-      const wsPort = wsPortEle.value
-      const token = tokenEle.value.trim()
-      const wsHosts: string[] = []
-
-      for (const hostEle of wsHostEles) {
-        const value = hostEle.value.trim()
-        value && wsHosts.push(value)
-      }
-
-      config.ob11.httpPort = parseInt(httpPort)
-      config.ob11.httpHosts = httpHosts
-      config.ob11.wsPort = parseInt(wsPort)
-      config.ob11.wsHosts = wsHosts
-      config.token = token
-      config.ffmpeg = ffmpegPathEle.value.trim()
-      window.llonebot.setConfig(config)
-      setTimeout(() => {
-        getError().then()
-      }, 1000)
-      alert('保存成功')
-    })
-
-  doc.getElementById('selectFFMPEG')?.addEventListener('click', () => {
-    window.llonebot.selectFile().then(selectPath => {
-      if (selectPath) {
-        config.ffmpeg = (document.getElementById('ffmpegPath') as HTMLInputElement).value = selectPath
-        // window.llonebot.setConfig(config);
-      }
-    })
-  })
-
-  // 自动保存删除文件延时时间
-  const autoDeleteMinEle = doc.getElementById('autoDeleteMin') as HTMLInputElement
-  let st = null
-  autoDeleteMinEle.addEventListener('change', () => {
-    if (st) {
-      clearTimeout(st)
-    }
-    st = setTimeout(() => {
-      console.log('auto delete file minute change')
-      config.autoDeleteFileSecond = parseInt(autoDeleteMinEle.value) || 1
-      window.llonebot.setConfig(config)
-    }, 1000)
-  })
-
-  doc.body.childNodes.forEach(node => {
-    view.appendChild(node)
-  })
-}
-
-function init () {
-  const hash = location.hash
-  if (hash === '#/blank') {
-
-  }
-}
-
-if (location.hash === '#/blank') {
-  (window as any).navigation.addEventListener('navigatesuccess', init, { once: true })
-} else {
-  init()
-}
-
-export {
-  onSettingWindowCreated
-}
diff --git a/src/renderer/components/button.ts b/src/renderer/components/button.ts
new file mode 100644
index 0000000..fda59c8
--- /dev/null
+++ b/src/renderer/components/button.ts
@@ -0,0 +1,4 @@
+
+export const SettingButton = (text: string, id?: string, type: string = 'secondary') => {
+    return `<setting-button ${type ? `data-type="${type}"` : ''} ${id ? `id="${id}"` : ''}>${text}</setting-button>`;
+}
\ No newline at end of file
diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts
new file mode 100644
index 0000000..7fd2fe8
--- /dev/null
+++ b/src/renderer/components/index.ts
@@ -0,0 +1,5 @@
+export * from './list';
+export * from './item';
+export * from './button';
+export * from './switch';
+export * from './select';
diff --git a/src/renderer/components/item.ts b/src/renderer/components/item.ts
new file mode 100644
index 0000000..2f054a0
--- /dev/null
+++ b/src/renderer/components/item.ts
@@ -0,0 +1,10 @@
+
+export const SettingItem = (title: string, subtitle?: string, action?: string, id?: string, visible: boolean = true) => {
+    return `<setting-item ${id ? `id="${id}"` : ''} ${!visible ? 'is-hidden' : ''}>
+    <div>
+        <setting-text>${title}</setting-text>
+        ${subtitle ? `<setting-text data-type="secondary">${subtitle}</setting-text>` : ''}
+    </div>
+    ${action ? `<div>${action}</div>` : ''}
+</setting-item>`;
+}
\ No newline at end of file
diff --git a/src/renderer/components/list.ts b/src/renderer/components/list.ts
new file mode 100644
index 0000000..af2d8ff
--- /dev/null
+++ b/src/renderer/components/list.ts
@@ -0,0 +1,10 @@
+
+export const SettingList = (items: string[], title?: string, isCollapsible: boolean = false, direction: string = 'column') => {
+    return `<setting-section ${title && !isCollapsible ? `data-title="${title}"` : ''}>
+    <setting-panel>
+        <setting-list ${direction ? `data-direction="${direction}"` : ''} ${isCollapsible ? 'is-collapsible' : ''} ${title && isCollapsible ? `data-title="${title}"` : ''}>
+            ${items.join('')}
+        </setting-list>
+    </setting-panel>
+</setting-section>`;
+}
\ No newline at end of file
diff --git a/src/renderer/components/option.ts b/src/renderer/components/option.ts
new file mode 100644
index 0000000..d134ba3
--- /dev/null
+++ b/src/renderer/components/option.ts
@@ -0,0 +1,4 @@
+
+export const SettingOption = (text: string, value?: string, isSelected: boolean = false) => {
+    return `<setting-option ${value ? `data-value="${value}"` : ''} ${isSelected ? 'is-selected' : ''}>${text}</setting-option>`;
+}
\ No newline at end of file
diff --git a/src/renderer/components/select.ts b/src/renderer/components/select.ts
new file mode 100644
index 0000000..0c33332
--- /dev/null
+++ b/src/renderer/components/select.ts
@@ -0,0 +1,11 @@
+import { SettingOption } from "./option";
+
+export const SettingSelect = (items: Array<{ text: string, value: string }>, configKey?: string, configValue?: any) => {
+    return `<setting-select ${configKey ? `data-config-key="${configKey}"` : ''}>
+    <div>
+        ${items.map((e, i) => {
+            return SettingOption(e.text, e.value, (configKey && configValue ? configValue === e.value : i === 0));
+        }).join('')}
+    </div>
+</setting-select>`;
+}
\ No newline at end of file
diff --git a/src/renderer/components/switch.ts b/src/renderer/components/switch.ts
new file mode 100644
index 0000000..1978a47
--- /dev/null
+++ b/src/renderer/components/switch.ts
@@ -0,0 +1,9 @@
+
+export const SettingSwitch = (configKey?: string, isActive: boolean = false, extraData?: Record<string, string>) => {
+    return `<setting-switch 
+    ${configKey ? `data-config-key="${configKey}"` : ''} 
+    ${isActive ? 'is-active' : ''} 
+    ${extraData ? Object.keys(extraData).map(key => `data-${key}="${extraData[key]}"`) : ''} 
+    >
+</setting-switch>`;
+}
\ No newline at end of file
diff --git a/src/renderer/index.ts b/src/renderer/index.ts
new file mode 100644
index 0000000..98f0a1c
--- /dev/null
+++ b/src/renderer/index.ts
@@ -0,0 +1,282 @@
+/// <reference path="../global.d.ts" />
+import {
+    SettingButton,
+    SettingItem,
+    SettingList,
+    SettingSelect,
+    SettingSwitch
+} from './components';
+import StyleRaw from './style.css?raw';
+
+// 打开设置界面时触发
+
+async function onSettingWindowCreated(view: Element) {
+    window.llonebot.log("setting window created");
+    const isEmpty = (value: any) => value === undefined || value === null || value === '';
+    let config = await window.llonebot.getConfig();
+    let ob11Config = { ...config.ob11 };
+    const setConfig = (key: string, value: any) => {
+        const configKey = key.split('.');
+
+        if (key.indexOf('ob11') === 0) {
+            if (configKey.length === 2) ob11Config[configKey[1]] = value;
+            else ob11Config[key] = value;
+        } else {
+            if (configKey.length === 2) config[configKey[0]][configKey[1]] = value;
+            else config[key] = value;
+
+            window.llonebot.setConfig(config);
+        }
+    };
+
+    const parser = new DOMParser();
+    const doc = parser.parseFromString([
+        '<div>',
+        `<style>${StyleRaw}</style>`,
+        SettingList([
+            SettingItem('启用 HTTP 服务', null,
+                SettingSwitch('ob11.enableHttp', config.ob11.enableHttp, { 'control-display-id': 'config-ob11-httpPort' }),
+            ),
+            SettingItem('HTTP 服务监听端口', null,
+                `<div class="q-input"><input class="q-input__inner" data-config-key="ob11.httpPort" type="number" min="1" max="65534" value="${config.ob11.httpPort}" placeholder="${config.ob11.httpPort}" /></div>`,
+                'config-ob11-httpPort', config.ob11.enableHttp
+            ),
+            SettingItem('启用 HTTP 事件上报', null,
+                SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, { 'control-display-id': 'config-ob11-httpHosts' }),
+            ),
+            `<div class="config-host-list" id="config-ob11-httpHosts" ${config.ob11.enableHttpPost ? '' : 'is-hidden'}>
+                <setting-item data-direction="row">
+                    <div>
+                        <setting-text>HTTP 事件上报地址</setting-text>
+                    </div>
+                    <setting-button id="config-ob11-httpHosts-add" data-type="primary">添加</setting-button>
+                </setting-item>
+                <div id="config-ob11-httpHosts-list"></div>
+            </div>`,
+            SettingItem('启用正向 WebSocket 服务', null,
+                SettingSwitch('ob11.enableWs', config.ob11.enableWs, { 'control-display-id': 'config-ob11-wsPort' }),
+            ),
+            SettingItem('正向 WebSocket 服务监听端口', null,
+                `<div class="q-input"><input class="q-input__inner" data-config-key="ob11.wsPort" type="number" min="1" max="65534" value="${config.ob11.wsPort}" placeholder="${config.ob11.wsPort}" /></div>`,
+                'config-ob11-wsPort', config.ob11.enableWs
+            ),
+            SettingItem('启用反向 WebSocket 服务', null,
+                SettingSwitch('ob11.enableWsReverse', config.ob11.enableWsReverse, { 'control-display-id': 'config-ob11-wsHosts' }),
+            ),
+            `<div class="config-host-list" id="config-ob11-wsHosts" ${config.ob11.enableWsReverse ? '' : 'is-hidden'}>
+                <setting-item data-direction="row">
+                    <div>
+                        <setting-text>反向 WebSocket 监听地址</setting-text>
+                    </div>
+                    <setting-button id="config-ob11-wsHosts-add" data-type="primary">添加</setting-button>
+                </setting-item>
+                <div id="config-ob11-wsHosts-list"></div>
+            </div>`,
+            SettingItem('反向 WebSocket 服务心跳间隔',
+                '控制每隔多久发送一个心跳包,单位为毫秒',
+                `<div class="q-input"><input class="q-input__inner" data-config-key="heartInterval" type="number" min="1000" value="${config.heartInterval}" placeholder="${config.heartInterval}" /></div>`,
+            ),
+            SettingItem('Access token', null,
+                `<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="token" type="text" value="${config.token}" placeholder="未设置" /></div>`,
+            ),
+            SettingItem(
+                '消息上报格式类型',
+                '如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 <a href="javascript:LiteLoader.api.openExternal(\'https://github.com/botuniverse/onebot-11/tree/master/message#readme\');">OneBot v11 文档</a>',
+                SettingSelect([
+                    { text: '消息段', value: 'array' },
+                    { text: 'CQ码', value: 'string' },
+                ], 'ob11.messagePostFormat', config.ob11.messagePostFormat),
+            ),
+            SettingItem(
+                'ffmpeg 路径', `<span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`,
+                SettingButton('选择', 'config-ffmpeg-select'),
+            ),
+            SettingItem(
+                '', null,
+                SettingButton('保存', 'config-ob11-save', 'primary'),
+            )
+        ]),
+        SettingList([
+            SettingItem(
+                '使用 Base64 编码获取文件',
+                '开启后,调用 /get_image、/get_record 时,获取不到 url 时添加一个 Base64 字段',
+                SettingSwitch('enableLocalFile2Url', config.enableLocalFile2Url),
+            ),
+            SettingItem(
+                '调试模式',
+                '开启后上报信息会添加 raw 字段以附带原始信息',
+                SettingSwitch('debug', config.debug),
+            ),
+            SettingItem(
+                '上报 Bot 自身发送的消息',
+                '慎用,可能会导致自己跟自己聊个不停',
+                SettingSwitch('reportSelfMessage', config.reportSelfMessage),
+            ),
+            SettingItem(
+                '自动删除收到的文件',
+                '在收到文件后的指定时间内删除该文件',
+                SettingSwitch('autoDeleteFile', config.autoDeleteFile, { 'control-display-id': 'config-auto-delete-file-second' }),
+            ),
+            SettingItem(
+                '自动删除文件时间',
+                '单位为秒',
+                `<div class="q-input"><input class="q-input__inner" data-config-key="autoDeleteFileSecond" type="number" min="1" value="${config.autoDeleteFileSecond}" placeholder="${config.autoDeleteFileSecond}" /></div>`,
+                'config-auto-delete-file-second', config.autoDeleteFile
+            ),
+            SettingItem(
+                '写入日志',
+                `将日志文件写入插件的数据文件夹`,
+                SettingSwitch('log', config.log),
+            ),
+            SettingItem(
+                '日志文件目录',
+                `${window.LiteLoader.plugins['LLOneBot'].path.data}`,
+                SettingButton('打开', 'config-open-log-path'),
+            ),
+        ]),
+        '</div>',
+    ].join(''), "text/html");
+
+    // 生成反向地址列表
+    const buildHostListItem = (type: string, host: string, index: number) => {
+        const dom = {
+            container: document.createElement('setting-item'),
+            input: document.createElement('input'),
+            inputContainer: document.createElement('div'),
+            deleteBtn: document.createElement('setting-button'),
+        };
+
+        dom.container.classList.add('setting-host-list-item');
+        dom.container.dataset.direction = 'row';
+
+        dom.input.classList.add('q-input__inner');
+        dom.input.type = 'url';
+        dom.input.value = host;
+        dom.input.addEventListener('input', () => {
+            ob11Config[type][index] = dom.input.value;
+        });
+
+        dom.inputContainer.classList.add('q-input');
+        dom.inputContainer.appendChild(dom.input);
+
+        dom.deleteBtn.innerHTML = '删除';
+        dom.deleteBtn.dataset.type = 'secondary';
+        dom.deleteBtn.addEventListener('click', () => {
+            ob11Config[type].splice(index, 1);
+            initReverseHost(type);
+        });
+
+        dom.container.appendChild(dom.inputContainer);
+        dom.container.appendChild(dom.deleteBtn);
+
+        return dom.container;
+    };
+    const buildHostList = (hosts: string[], type: string) => {
+        const result: HTMLElement[] = [];
+    
+        hosts.forEach((host, index) => {
+            result.push(buildHostListItem(type, host, index));
+        });
+    
+        return result;
+    };
+    const addReverseHost = (type: string, doc: Document = document) => {
+        const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`);
+        hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length));
+        ob11Config[type].push('');
+    };
+    const initReverseHost = (type: string, doc: Document = document) => {
+        const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`);
+        [ ...hostContainerDom.childNodes ].forEach(dom => dom.remove());
+        buildHostList(ob11Config[type], type).forEach(dom => {
+            hostContainerDom.appendChild(dom);
+        });
+    };
+    initReverseHost('httpHosts', doc);
+    initReverseHost('wsHosts', doc);
+
+    doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts'));
+    doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts'));
+
+    doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => {
+        window.llonebot.selectFile()
+            .then(path => {
+                if (!isEmpty(path)) {
+                    setConfig('ffmpeg', path);
+                    document.querySelector('#config-ffmpeg-path-text').innerHTML = path;
+                }
+            })
+    });
+
+    doc.querySelector('#config-open-log-path').addEventListener('click', () => {
+        window.LiteLoader.api.openPath(window.LiteLoader.plugins['LLOneBot'].path.data);
+    })
+
+    // 开关
+    doc.querySelectorAll('setting-switch[data-config-key]').forEach((dom: HTMLElement) => {
+        dom.addEventListener('click', () => {
+            const active = dom.getAttribute('is-active') === null;
+
+            setConfig(dom.dataset.configKey, active);
+
+            if (active) dom.setAttribute('is-active', '');
+            else dom.removeAttribute('is-active');
+
+            if (!isEmpty(dom.dataset.controlDisplayId)) {
+                const displayDom = document.querySelector(`#${dom.dataset.controlDisplayId}`);
+                if (active) displayDom.removeAttribute('is-hidden');
+                else displayDom.setAttribute('is-hidden', '');
+            }
+        });
+    });
+
+    // 输入框
+    doc.querySelectorAll('setting-item .q-input input.q-input__inner[data-config-key]').forEach((dom: HTMLInputElement) => {
+        dom.addEventListener('input', () => {
+            const Type = dom.getAttribute('type');
+            const configKey = dom.dataset.configKey;
+            const configValue = Type === 'number' ? (parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1) : dom.value;
+
+            setConfig(configKey, configValue);
+        });
+    });
+
+    // 下拉框
+    doc.querySelectorAll('setting-select').forEach((dom: HTMLElement) => {
+        dom.addEventListener('selected', (e: CustomEvent) => {
+            const configKey = dom.dataset.configKey;
+            const configValue = e.detail.value;
+
+            setConfig(configKey, configValue);
+        });
+    });
+
+    // 保存按钮
+    doc.querySelector('#config-ob11-save').addEventListener('click', () => {
+        config.ob11 = ob11Config;
+
+        window.llonebot.setConfig(config);
+        alert('保存成功');
+    });
+
+    doc.body.childNodes.forEach(node => {
+        view.appendChild(node);
+    });
+}
+
+function init () {
+  const hash = location.hash
+  if (hash === '#/blank') {
+
+  }
+}
+
+if (location.hash === '#/blank') {
+  (window as any).navigation.addEventListener('navigatesuccess', init, { once: true })
+} else {
+  init()
+}
+
+export {
+  onSettingWindowCreated
+}
diff --git a/src/renderer/style.css b/src/renderer/style.css
new file mode 100644
index 0000000..028aa96
--- /dev/null
+++ b/src/renderer/style.css
@@ -0,0 +1,64 @@
+setting-item[is-hidden],
+setting-item[is-hidden] + setting-divider {
+  display: none !important;
+}
+
+.config-host-list {
+  width: 100%;
+  padding-left: 16px;
+  box-sizing: border-box;
+}
+.config-host-list[is-hidden],
+.config-host-list[is-hidden] + setting-divider {
+  display: none !important;
+}
+
+setting-item .q-input {
+  height: 24px;
+  width: 100px;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+  box-sizing: border-box;
+  position: relative;
+  background: var(--bg_bottom_light);
+  border: 1px solid var(--border_dark);
+}
+
+setting-item .q-input .q-input__inner {
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+  box-sizing: border-box;
+  color: var(--text_primary);
+  font-family: inherit;
+  font-size: 12px;
+  height: 24px;
+  line-height: 24px;
+  width: 100%;
+  border: 1px solid transparent;
+  padding: 0px 8px;
+}
+
+setting-item .q-input input[type=number].q-input__inner::-webkit-outer-spin-button,
+setting-item .q-input input[type=number].q-input__inner::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+.config-host-list setting-item.setting-host-list-item .q-input {
+  width: 260px;
+}
+
+setting-item a {
+  color: var(--text-link);
+}
+setting-item a:hover {
+  color: var(--hover-link);
+}
+setting-item a:active,
+setting-item a:visited {
+  color: var(--text-link);
+}
\ No newline at end of file