feat: add websocket support

This commit is contained in:
Disy 2024-02-14 22:11:07 +08:00
parent 9bb69058c2
commit c875cfda15
11 changed files with 273 additions and 106 deletions

35
package-lock.json generated

@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-ws": "^5.0.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
@ -3123,6 +3124,20 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-ws": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/express-ws/-/express-ws-5.0.2.tgz",
"integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==",
"dependencies": {
"ws": "^7.4.6"
},
"engines": {
"node": ">=4.5.0"
},
"peerDependencies": {
"express": "^4.0.0 || ^5.0.0-alpha.1"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -4691,6 +4706,26 @@
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true "dev": true
}, },
"node_modules/ws": {
"version": "7.5.9",
"resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

@ -5,7 +5,7 @@
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install electron --no-save", "postinstall": "set ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
"build": "npm run build-main && npm run build-preload && npm run build-renderer", "build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js", "build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js", "build-preload": "webpack --config webpack.preload.config.js",
@ -19,6 +19,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-ws": "^5.0.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },

@ -1,6 +1,6 @@
import {Config} from "./types"; import {Config} from "./types";
const fs = require("fs") const fs = require("fs");
export class ConfigUtil{ export class ConfigUtil{
configPath: string; configPath: string;
@ -9,20 +9,25 @@ export class ConfigUtil{
this.configPath = configPath; this.configPath = configPath;
} }
getConfig(): Config{ getConfig(): Config {
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
return {port:3000, hosts: ["http://192.168.1.2:5000/"]} return {
httpPort: 3000,
httpHosts: ["http://127.0.0.1:5000/"],
wsPort: 3001,
wsHosts: ["ws://127.0.0.1:3002/"]
}
} else { } else {
const data = fs.readFileSync(this.configPath, "utf-8"); const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData =JSON.parse(data); let jsonData = JSON.parse(data);
if (!jsonData.hosts){ if (!jsonData.hosts) {
jsonData.hosts = [] jsonData.hosts = [];
} }
return jsonData; return jsonData;
} }
} }
setConfig(config: Config){ setConfig(config: Config){
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
} }
} }

@ -1,9 +1,14 @@
export interface Config { export interface Config {
port: number httpPort: number
hosts: string[] httpHosts: string[]
wsPort: number
wsHosts: string[]
enableHttp?: boolean
enableHttpPost?: boolean
enableWs?: boolean
enableWsReverse?: boolean
enableBase64?: boolean enableBase64?: boolean
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean
log?: boolean log?: boolean
} }

@ -10,7 +10,7 @@ import {
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
} from "../common/channels"; } from "../common/channels";
import { postMsg, startExpress } from "../onebot11/server"; import {initWebsocket, postMsg, startExpress, startWebsocketServer} from "../onebot11/server";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { addHistoryMsg, msgHistory, selfInfo } from "../common/data"; import { addHistoryMsg, msgHistory, selfInfo } from "../common/data";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook";
@ -29,56 +29,53 @@ function onLoad() {
// const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data; // const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data;
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true}); fs.mkdirSync(CONFIG_DIR, {recursive: true});
} }
ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => { ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => {
return getConfigUtil().getConfig() return getConfigUtil().getConfig();
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => {
getConfigUtil().setConfig(arg) getConfigUtil().setConfig(arg);
}) })
ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => {
log(arg) log(arg);
}) })
function postRawMsg(msgList: RawMessage[]) { function postRawMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
message.msgShortId = msgHistory[message.msgId]?.msgShortId message.msgShortId = msgHistory[message.msgId]?.msgShortId;
if (!message.msgShortId) { if (!message.msgShortId) {
addHistoryMsg(message) addHistoryMsg(message);
} }
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
} }
if (msg.user_id == selfInfo.uin && !reportSelfMessage) { if (msg.user_id == selfInfo.uin && !reportSelfMessage) {
return return;
} }
postMsg(msg); postMsg(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
} }
} }
function start() { function start() {
log("llonebot start") log("llonebot start");
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try { try {
// log("received msg length", payload.msgList.length);
postRawMsg(payload.msgList); postRawMsg(payload.msgList);
} catch (e) { } catch (e) {
log("report message error: ", e.toString()) log("report message error: ", e.toString());
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig() const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) { if (!reportSelfMessage) {
return return
} }
@ -86,42 +83,46 @@ function onLoad() {
try { try {
postRawMsg([payload.msgRecord]); postRawMsg([payload.msgRecord]);
} catch (e) { } catch (e) {
log("report self message error: ", e.toString()) log("report self message error: ", e.toString());
} }
}) })
NTQQApi.getGroups(true).then() NTQQApi.getGroups(true).then();
startExpress(getConfigUtil().getConfig().port)
const config = getConfigUtil().getConfig();
startExpress(config.httpPort);
startWebsocketServer(config.wsPort);
initWebsocket();
} }
const init = async () => { const init = async () => {
try { try {
const _ = await NTQQApi.getSelfInfo() const _ = await NTQQApi.getSelfInfo();
Object.assign(selfInfo, _) Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin selfInfo.nick = selfInfo.uin;
log("get self simple info", _) log("get self simple info", _);
} catch (e) { } catch (e) {
log("retry get self info") log("retry get self info");
} }
if (selfInfo.uin) { if (selfInfo.uin) {
try { try {
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid)) const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid));
log("self info", userInfo); log("self info", userInfo);
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick;
} else { } else {
return setTimeout(init, 1000) return setTimeout(init, 1000);
} }
} catch (e) { } catch (e) {
log("get self nickname failed", e.toString()) log("get self nickname failed", e.toString());
return setTimeout(init, 1000) return setTimeout(init, 1000);
} }
start(); start();
} }
else{ else{
setTimeout(init, 1000) setTimeout(init, 1000);
} }
} }
setTimeout(init, 1000) setTimeout(init, 1000);
} }

@ -1,6 +1,6 @@
import {ActionName, BaseCheckResult} from "./types" import {ActionName, BaseCheckResult} from "./types"
import { OB11Response } from "./utils" import {OB11Response, OB11WebsocketResponse} from "./utils"
import { OB11Return } from "../types"; import {OB11Return, OB11WebsocketReturn} from "../types";
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName
@ -11,20 +11,33 @@ class BaseAction<PayloadType, ReturnDataType> {
} }
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 400);
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
return OB11Response.error(e.toString(), 200);
}
}
public async websocketHandle(payload: PayloadType, echo: string): Promise<OB11WebsocketReturn<ReturnDataType | null>> {
const result = await this.check(payload) const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message) return OB11WebsocketResponse.error(result.message, 1400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData) return OB11WebsocketResponse.ok(resData, echo);
}catch (e) { } catch (e) {
return OB11Response.error(e.toString()) return OB11WebsocketResponse.error(e.toString(), 1200)
} }
} }
protected async _handle(payload: PayloadType): Promise<ReturnDataType> { protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle` throw `pleas override ${this.actionName} _handle`;
} }
} }

@ -9,6 +9,7 @@ import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg' import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./DeleteMsg";
import BaseAction from "./BaseAction";
export const actionHandlers = [ export const actionHandlers = [
new GetMsg(), new GetMsg(),
@ -17,4 +18,15 @@ export const actionHandlers = [
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg() new DeleteMsg()
] ]
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>();
for (const action of actionHandlers) {
actionMap.set(action.actionName, action);
}
return actionMap
}
export const actionMap = initActionMap();

@ -1,18 +1,36 @@
import { OB11Return } from '../types'; import {OB11Return, OB11WebsocketReturn} from '../types';
export class OB11Response { export class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> { static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {
return { return {
status: status, status: status,
retcode: status, retcode: retcode,
data: data, data: data,
message: message message: message
} }
} }
static ok<T>(data: T) { static ok<T>(data: T) {
return OB11Response.res<T>(data) return OB11Response.res<T>(data, "ok", 0)
} }
static error(err: string) { static error(err: string, retcode: number) {
return OB11Response.res(null, -1, err) return OB11Response.res(null, "failed", retcode, err)
}
}
export class OB11WebsocketResponse {
static res<T>(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn<T> {
return {
status: status,
retcode: retcode,
data: data,
echo: echo,
message: message
}
}
static ok<T>(data: T, echo: string = "") {
return OB11WebsocketResponse.res<T>(data, "ok", 0, echo)
}
static error(err: string, retcode: number, echo: string = "") {
return OB11WebsocketResponse.res(null, "failed", retcode, echo, err)
} }
} }

@ -1,13 +1,16 @@
import { getConfigUtil, log } from "../common/utils"; import { getConfigUtil, log } from "../common/utils";
const express = require("express"); const express = require("express");
const expressWs = require("express-ws");
import { Request } from 'express'; import { Request } from 'express';
import { Response } from 'express'; import { Response } from 'express';
const JSONbig = require('json-bigint')({ storeAsString: true }); const JSONbig = require('json-bigint')({ storeAsString: true });
import { selfInfo } from "../common/data"; import { selfInfo } from "../common/data";
import { OB11Message, OB11Return, OB11MessageData } from './types'; import { OB11Message, OB11Return, OB11MessageData } from './types';
import { actionHandlers } from "./actions"; import {actionHandlers, actionMap} from "./actions";
import {OB11Response, OB11WebsocketResponse} from "./actions/utils";
// @SiberianHusky 2021-08-15 // @SiberianHusky 2021-08-15
@ -48,27 +51,12 @@ function checkSendMessage(sendMsgList: OB11MessageData[]) {
// ==end== // ==end==
class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data)
}
static error(err: string) {
return OB11Response.res(null, -1, err)
}
}
const expressAPP = express(); const expressAPP = express();
expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" })); expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" }));
const expressWsApp = express();
const websocketClientConnections = [];
expressAPP.use((req, res, next) => { expressAPP.use((req, res, next) => {
let data = ''; let data = '';
req.on('data', chunk => { req.on('data', chunk => {
@ -86,12 +74,6 @@ expressAPP.use((req, res, next) => {
next(); next();
}); });
}); });
// expressAPP.use(express.json({
// limit: '500mb',
// verify: (req: any, res: any, buf: any, encoding: any) => {
// req.rawBody = buf;
// }
// }));
export function startExpress(port: number) { export function startExpress(port: number) {
@ -99,33 +81,120 @@ export function startExpress(port: number) {
res.send('llonebot已启动'); res.send('llonebot已启动');
}) })
expressAPP.listen(port, "0.0.0.0", () => { if (getConfigUtil().getConfig().enableHttp) {
console.log(`llonebot started 0.0.0.0:${port}`); expressAPP.listen(port, "0.0.0.0", () => {
}); console.log(`llonebot http service started 0.0.0.0:${port}`);
});
}
}
export function startWebsocketServer(port: number) {
const config = getConfigUtil().getConfig();
if (config.enableWs) {
expressWs(expressWsApp)
expressWsApp.listen(getConfigUtil().getConfig().wsPort, function () {
console.log(`llonebot websocket service started 0.0.0.0:${port}`);
});
}
}
export function initWebsocket() {
if (getConfigUtil().getConfig().enableWs) {
expressWsApp.ws("/api", onWebsocketMessage);
expressWsApp.ws("/", onWebsocketMessage);
}
initReverseWebsocket();
}
function initReverseWebsocket() {
const config = getConfigUtil().getConfig();
if (config.enableWsReverse) {
for (const url of config.wsHosts) {
try {
const wsClient = new WebSocket(url);
websocketClientConnections.push(wsClient);
wsClient.onclose = function (ev) {
let index = websocketClientConnections.indexOf(wsClient);
if (index !== -1) {
websocketClientConnections.splice(index, 1);
}
}
wsClient.onmessage = async function (ev) {
let message = ev.data;
if (typeof message === "string") {
try {
let recv = JSON.parse(message);
let echo = recv.echo ?? "";
if (actionMap.has(recv.action)) {
let action = actionMap.get(recv.action);
const result = await action.websocketHandle(recv.params, echo);
wsClient.send(JSON.stringify(result));
}
else {
wsClient.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo)));
}
} catch (e) {
log(e.stack);
wsClient.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200)));
}
}
}
}
catch (e) {}
}
}
}
function onWebsocketMessage(ws, req) {
ws.on("message", async function (message) {
try {
let recv = JSON.parse(message);
let echo = recv.echo ?? "";
if (actionMap.has(recv.action)) {
let action = actionMap.get(recv.action)
const result = await action.websocketHandle(recv.params, echo);
ws.send(JSON.stringify(result));
}
else {
ws.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo)));
}
} catch (e) {
log(e.stack);
ws.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200)));
}
})
} }
export function postMsg(msg: OB11Message) { export function postMsg(msg: OB11Message) {
const { reportSelfMessage } = getConfigUtil().getConfig() const config = getConfigUtil().getConfig();
if (!reportSelfMessage) { if (config.enableHttpPost) {
if (msg.user_id == selfInfo.uin) { if (!config.reportSelfMessage) {
return if (msg.user_id == selfInfo.uin) {
return
}
}
for (const host of config.httpHosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg));
});
} }
} }
for (const host of getConfigUtil().getConfig().hosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg));
});
}
} }
let routers: Record<string, (payload: any) => Promise<OB11Return<any>>> = {}; let routers: Record<string, (payload: any) => Promise<OB11Return<any>>> = {};
@ -143,7 +212,7 @@ function registerRouter(action: string, handle: (payload: any) => Promise<any>)
} }
catch (e) { catch (e) {
log(e.stack); log(e.stack);
res.send(OB11Response.error(e.stack.toString())) res.send(OB11Response.error(e.stack.toString(), 200))
} }
} }

@ -86,12 +86,20 @@ export type OB11ApiName =
| "get_msg" | "get_msg"
export interface OB11Return<DataType> { export interface OB11Return<DataType> {
status: number status: string
retcode: number retcode: number
data: DataType data: DataType
message: string message: string
} }
export interface OB11WebsocketReturn<DataType> {
status: string
retcode: number
data: DataType
echo: string
message: string
}
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
export enum OB11MessageDataType { export enum OB11MessageDataType {

@ -20,7 +20,7 @@ async function onSettingWindowCreated(view: Element) {
} }
let hostsEleStr = "" let hostsEleStr = ""
for (const host of config.hosts) { for (const host of config.httpHosts) {
hostsEleStr += creatHostEleStr(host); hostsEleStr += creatHostEleStr(host);
} }
let html = ` let html = `
@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
<setting-list class="wrap"> <setting-list class="wrap">
<setting-item class="vertical-list-item" data-direction="row"> <setting-item class="vertical-list-item" data-direction="row">
<setting-text></setting-text> <setting-text></setting-text>
<input id="port" type="number" value="${config.port}"/> <input id="port" type="number" value="${config.httpPort}"/>
</setting-item> </setting-item>
<div> <div>
<button id="addHost" class="q-button"></button> <button id="addHost" class="q-button"></button>
@ -135,8 +135,8 @@ async function onSettingWindowCreated(view: Element) {
hosts.push(hostEle.value); hosts.push(hostEle.value);
} }
} }
config.port = parseInt(port); config.httpPort = parseInt(port);
config.hosts = hosts; config.httpHosts = hosts;
window.llonebot.setConfig(config); window.llonebot.setConfig(config);
alert("保存成功"); alert("保存成功");
}) })