mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-09-28 04:05:12 +00:00
commit
1dc844435a
@ -27,18 +27,19 @@ export async function InitWebUi() {
|
||||
}
|
||||
app.use(express.json());
|
||||
// 初始服务
|
||||
app.all('/', (_req, res) => {
|
||||
// WebUI只在config.prefix所示路径上提供服务,可配合Nginx挂载到子目录中
|
||||
app.all(config.prefix + '/', (_req, res) => {
|
||||
res.json({
|
||||
msg: 'NapCat WebAPI is now running!',
|
||||
});
|
||||
});
|
||||
// 配置静态文件服务,提供./static目录下的文件服务,访问路径为/webui
|
||||
app.use('/webui', express.static(resolve(__dirname, './static')));
|
||||
app.use(config.prefix + '/webui', express.static(resolve(__dirname, './static')));
|
||||
//挂载API接口
|
||||
app.use('/api', ALLRouter);
|
||||
app.listen(config.port, async () => {
|
||||
log(`[NapCat] [WebUi] Current WebUi is running at IP:${config.port}`);
|
||||
app.use(config.prefix + '/api', ALLRouter);
|
||||
app.listen(config.port, config.host, async () => {
|
||||
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
|
||||
log(`[NapCat] [WebUi] Login URL is http://${config.host}:${config.port}${config.prefix}/webui`);
|
||||
log(`[NapCat] [WebUi] Login Token is ${config.token}`);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,33 @@ const __dirname = dirname(__filename);
|
||||
// 限制尝试端口的次数,避免死循环
|
||||
const MAX_PORT_TRY = 100;
|
||||
|
||||
async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
|
||||
async function tryUseHost(host: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
server.on('listening', () => {
|
||||
server.close();
|
||||
resolve(host);
|
||||
});
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRNOTAVAIL') {
|
||||
reject("主机地址验证失败,可能为非本机地址");
|
||||
} else {
|
||||
reject(`遇到错误: ${err.code}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试监听 让系统随机分配一个端口
|
||||
server.listen(0, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(`服务器启动时发生错误: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const server = net.createServer();
|
||||
@ -25,7 +51,7 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (tryCount < MAX_PORT_TRY) {
|
||||
// 使用循环代替递归
|
||||
resolve(tryUsePort(port + 1, tryCount + 1));
|
||||
resolve(tryUsePort(port + 1, host, tryCount + 1));
|
||||
} else {
|
||||
reject(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`);
|
||||
}
|
||||
@ -35,7 +61,7 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
|
||||
});
|
||||
|
||||
// 尝试监听端口
|
||||
server.listen(port);
|
||||
server.listen(port, host);
|
||||
} catch (error) {
|
||||
// 这里捕获到的错误应该是启动服务器时的同步错误
|
||||
reject(`服务器启动时发生错误: ${error}`);
|
||||
@ -44,44 +70,73 @@ async function tryUsePort(port: number, tryCount: number = 0): Promise<number> {
|
||||
}
|
||||
|
||||
export interface WebUiConfigType {
|
||||
host: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
token: string;
|
||||
loginRate: number
|
||||
}
|
||||
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
|
||||
class WebUiConfigWrapper {
|
||||
WebUiConfigData: WebUiConfigType | undefined = undefined;
|
||||
private applyDefaults<T>(obj: Partial<T>, defaults: T): T {
|
||||
return { ...defaults, ...obj };
|
||||
}
|
||||
async GetWebUIConfig(): Promise<WebUiConfigType> {
|
||||
if (this.WebUiConfigData) {
|
||||
return this.WebUiConfigData;
|
||||
}
|
||||
const defaultconfig: WebUiConfigType = {
|
||||
host: "0.0.0.0",
|
||||
port: 6099,
|
||||
prefix: "",
|
||||
token: "", // 默认先填空,空密码无法登录
|
||||
loginRate: 3
|
||||
};
|
||||
try {
|
||||
defaultconfig.token = Math.random().toString(36).slice(2); //生成随机密码
|
||||
} catch (e) {
|
||||
logError('随机密码生成失败', e);
|
||||
}
|
||||
try {
|
||||
const configPath = resolve(__dirname, './config/webui.json');
|
||||
const config: WebUiConfigType = {
|
||||
port: 6099,
|
||||
token: Math.random().toString(36).slice(2),//生成随机密码
|
||||
loginRate: 3
|
||||
};
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 4));
|
||||
writeFileSync(configPath, JSON.stringify(defaultconfig, null, 4));
|
||||
}
|
||||
|
||||
const fileContent = readFileSync(configPath, 'utf-8');
|
||||
const parsedConfig = JSON.parse(fileContent) as WebUiConfigType;
|
||||
// 更新配置字段后新增字段可能会缺失,同步一下
|
||||
const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial<WebUiConfigType>, defaultconfig);
|
||||
|
||||
// 修正端口占用情况
|
||||
const [err, data] = await tryUsePort(parsedConfig.port).then(data => [null, data as number]).catch(err => [err, null]);
|
||||
parsedConfig.port = data;
|
||||
if (err) {
|
||||
//一般没那么离谱 如果真有这么离谱 考虑下 向外抛出异常
|
||||
if (!parsedConfig.prefix.startsWith("/")) parsedConfig.prefix = "/" + parsedConfig.prefix;
|
||||
if (parsedConfig.prefix.endsWith("/")) parsedConfig.prefix = parsedConfig.prefix.slice(0, -1);
|
||||
// 配置已经被操作过了,还是回写一下吧,不然新配置不会出现在配置文件里
|
||||
writeFileSync(configPath, JSON.stringify(parsedConfig, null, 4));
|
||||
// 不希望回写的配置放后面
|
||||
|
||||
// 查询主机地址是否可用
|
||||
const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data as string]).catch(err => [err, null]);
|
||||
if (host_err) {
|
||||
logError("host不可用", host_err)
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
} else {
|
||||
parsedConfig.host = host;
|
||||
// 修正端口占用情况
|
||||
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data as number]).catch(err => [err, null]);
|
||||
if (port_err) {
|
||||
logError("port不可用", port_err)
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
} else {
|
||||
parsedConfig.port = port;
|
||||
}
|
||||
}
|
||||
this.WebUiConfigData = parsedConfig;
|
||||
return this.WebUiConfigData;
|
||||
} catch (e) {
|
||||
logError('读取配置文件失败', e);
|
||||
}
|
||||
return {} as WebUiConfigType; // 理论上这行代码到不了,为了保持函数完整性而保留
|
||||
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
|
||||
}
|
||||
}
|
||||
export const WebUiConfig = new WebUiConfigWrapper();
|
||||
export const WebUiConfig = new WebUiConfigWrapper();
|
||||
|
@ -15,7 +15,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
} else if (configKey.length === 3) {
|
||||
ob11Config[configKey[1]][configKey[2]] = value;
|
||||
}
|
||||
OB11ConfigWrapper.SetOB11Config(ob11Config);
|
||||
// OB11ConfigWrapper.SetOB11Config(ob11Config); // 只有当点保存时才下发配置,而不是在修改值后立即下发
|
||||
};
|
||||
|
||||
const parser = new DOMParser();
|
||||
@ -192,7 +192,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
|
||||
// 外链按钮
|
||||
doc.querySelector('#open-github')?.addEventListener('click', () => {
|
||||
window.open('https://napneko.github.io/', '_blank');
|
||||
window.open('https://github.com/NapNeko/NapCatQQ', '_blank');
|
||||
});
|
||||
doc.querySelector('#open-telegram')?.addEventListener('click', () => {
|
||||
window.open('https://t.me/+nLZEnpne-pQ1OWFl');
|
||||
@ -201,7 +201,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
window.open('https://qm.qq.com/q/bDnHRG38aI');
|
||||
});
|
||||
doc.querySelector('#open-docs')?.addEventListener('click', () => {
|
||||
window.open('https://github.com/NapNeko/NapCatQQ');
|
||||
window.open('https://napneko.github.io/', '_blank');
|
||||
});
|
||||
// 生成反向地址列表
|
||||
const buildHostListItem = (
|
||||
|
@ -38,7 +38,7 @@ class WebUiApiOB11ConfigWrapper {
|
||||
this.retCredential = Credential;
|
||||
}
|
||||
async GetOB11Config(): Promise<OB11Config> {
|
||||
const ConfigResponse = await fetch('/api/OB11Config/GetConfig', {
|
||||
const ConfigResponse = await fetch('../api/OB11Config/GetConfig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
@ -54,7 +54,7 @@ class WebUiApiOB11ConfigWrapper {
|
||||
return {} as OB11Config;
|
||||
}
|
||||
async SetOB11Config(config: OB11Config): Promise<boolean> {
|
||||
const ConfigResponse = await fetch('/api/OB11Config/SetConfig', {
|
||||
const ConfigResponse = await fetch('../api/OB11Config/SetConfig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
|
@ -1,5 +1,7 @@
|
||||
{
|
||||
"host": "0.0.0.0",
|
||||
"port": 6099,
|
||||
"prefix": "",
|
||||
"token": "random",
|
||||
"loginRate": 3
|
||||
|
||||
|
@ -160,7 +160,7 @@
|
||||
</div>
|
||||
<script>
|
||||
async function GetQQLoginQrcode(retCredential) {
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/GetQQLoginQrcode', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/GetQQLoginQrcode', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
@ -180,7 +180,7 @@
|
||||
return "";
|
||||
}
|
||||
async function CheckQQLoginStatus(retCredential) {
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/CheckLoginStatus', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
@ -200,7 +200,7 @@
|
||||
return false;
|
||||
}
|
||||
async function GetQQQucickLoginList(retCredential) {
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/GetQuickLoginList', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/GetQuickLoginList', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
@ -216,7 +216,7 @@
|
||||
return [];
|
||||
}
|
||||
async function SetQuickLogin(uin, retCredential) {
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/SetQuickLogin', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/SetQuickLogin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
|
@ -1,7 +1,7 @@
|
||||
const SettingList = (items, title, isCollapsible = false, direction = "column") => {
|
||||
return `<setting-section ${title && !isCollapsible ? `data-title="${title}"` : ""}>
|
||||
return `<setting-section ${""}>
|
||||
<setting-panel>
|
||||
<setting-list ${direction ? `data-direction="${direction}"` : ""} ${isCollapsible ? "is-collapsible" : ""} ${title && isCollapsible ? `data-title="${title}"` : ""}>
|
||||
<setting-list ${direction ? `data-direction="${direction}"` : ""} ${isCollapsible ? "is-collapsible" : ""} ${""}>
|
||||
${items.join("")}
|
||||
</setting-list>
|
||||
</setting-panel>
|
||||
@ -66,16 +66,13 @@ window.customElements.define(
|
||||
window[`${isHidden ? "remove" : "add"}EventListener`]("pointerdown", windowPointerDown);
|
||||
};
|
||||
const windowPointerDown = ({ target }) => {
|
||||
if (!this.contains(target))
|
||||
buttonClick();
|
||||
if (!this.contains(target)) buttonClick();
|
||||
};
|
||||
this._button.addEventListener("click", buttonClick);
|
||||
this._context.addEventListener("click", ({ target }) => {
|
||||
if (target.tagName !== "SETTING-OPTION")
|
||||
return;
|
||||
if (target.tagName !== "SETTING-OPTION") return;
|
||||
buttonClick();
|
||||
if (target.hasAttribute("is-selected"))
|
||||
return;
|
||||
if (target.hasAttribute("is-selected")) return;
|
||||
this.querySelectorAll("setting-option[is-selected]").forEach((dom) => dom.toggleAttribute("is-selected"));
|
||||
target.toggleAttribute("is-selected");
|
||||
this._text.value = target.textContent;
|
||||
@ -95,9 +92,9 @@ window.customElements.define(
|
||||
}
|
||||
);
|
||||
const SettingSelect = (items, configKey, configValue) => {
|
||||
return `<ob-setting-select ${configKey ? `data-config-key="${configKey}"` : ""}>
|
||||
return `<ob-setting-select ${`data-config-key="${configKey}"` }>
|
||||
${items.map((e, i) => {
|
||||
return SettingOption(e.text, e.value, configKey && configValue ? configValue === e.value : i === 0);
|
||||
return SettingOption(e.text, e.value, configValue ? configValue === e.value : i === 0);
|
||||
}).join("")}
|
||||
</ob-setting-select>`;
|
||||
};
|
||||
@ -108,7 +105,7 @@ class WebUiApiOB11ConfigWrapper {
|
||||
this.retCredential = Credential;
|
||||
}
|
||||
async GetOB11Config() {
|
||||
const ConfigResponse = await fetch("/api/OB11Config/GetConfig", {
|
||||
const ConfigResponse = await fetch("../api/OB11Config/GetConfig", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + this.retCredential,
|
||||
@ -124,7 +121,7 @@ class WebUiApiOB11ConfigWrapper {
|
||||
return {};
|
||||
}
|
||||
async SetOB11Config(config) {
|
||||
const ConfigResponse = await fetch("/api/OB11Config/SetConfig", {
|
||||
const ConfigResponse = await fetch("../api/OB11Config/SetConfig", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + this.retCredential,
|
||||
@ -154,7 +151,6 @@ async function onSettingWindowCreated(view) {
|
||||
} else if (configKey.length === 3) {
|
||||
ob11Config[configKey[1]][configKey[2]] = value;
|
||||
}
|
||||
OB11ConfigWrapper.SetOB11Config(ob11Config);
|
||||
};
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(
|
||||
@ -326,7 +322,7 @@ async function onSettingWindowCreated(view) {
|
||||
"text/html"
|
||||
);
|
||||
doc.querySelector("#open-github")?.addEventListener("click", () => {
|
||||
window.open("https://napneko.github.io/", "_blank");
|
||||
window.open("https://github.com/NapNeko/NapCatQQ", "_blank");
|
||||
});
|
||||
doc.querySelector("#open-telegram")?.addEventListener("click", () => {
|
||||
window.open("https://t.me/+nLZEnpne-pQ1OWFl");
|
||||
@ -335,7 +331,7 @@ async function onSettingWindowCreated(view) {
|
||||
window.open("https://qm.qq.com/q/bDnHRG38aI");
|
||||
});
|
||||
doc.querySelector("#open-docs")?.addEventListener("click", () => {
|
||||
window.open("https://github.com/NapNeko/NapCatQQ");
|
||||
window.open("https://napneko.github.io/", "_blank");
|
||||
});
|
||||
const buildHostListItem = (type, host, index, inputAttrs = {}) => {
|
||||
const dom = {
|
||||
@ -431,19 +427,15 @@ async function onSettingWindowCreated(view) {
|
||||
dom.addEventListener("click", () => {
|
||||
const active = dom.getAttribute("is-active") == void 0;
|
||||
setOB11Config(dom.dataset.configKey, active);
|
||||
if (active)
|
||||
dom.setAttribute("is-active", "");
|
||||
else
|
||||
dom.removeAttribute("is-active");
|
||||
if (active) dom.setAttribute("is-active", "");
|
||||
else dom.removeAttribute("is-active");
|
||||
if (!isEmpty(dom.dataset.controlDisplayId)) {
|
||||
const displayDom = document.querySelector(
|
||||
//@ts-expect-error 等待修复
|
||||
`#${dom.dataset.controlDisplayId}`
|
||||
);
|
||||
if (active)
|
||||
displayDom?.removeAttribute("is-hidden");
|
||||
else
|
||||
displayDom?.setAttribute("is-hidden", "");
|
||||
if (active) displayDom?.removeAttribute("is-hidden");
|
||||
else displayDom?.setAttribute("is-hidden", "");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<script>
|
||||
async function CheckQQLoginStatus(retCredential) {
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/CheckLoginStatus', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
@ -30,7 +30,7 @@
|
||||
return false;
|
||||
}
|
||||
async function CheckWebUiLogined(retCredential) {
|
||||
let LoginResponse = await fetch('/api/auth/check', {
|
||||
let LoginResponse = await fetch('../api/auth/check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
|
@ -86,7 +86,7 @@
|
||||
let data = "";
|
||||
|
||||
try {
|
||||
let loginResponse = await fetch('/api/auth/login', {
|
||||
let loginResponse = await fetch('../api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -99,7 +99,7 @@
|
||||
//登录成功
|
||||
let retCredential = loginResponseJson.data.Credential;
|
||||
localStorage.setItem('auth', retCredential);
|
||||
let QQLoginResponse = await fetch('/api/QQLogin/CheckLoginStatus', {
|
||||
let QQLoginResponse = await fetch('../api/QQLogin/CheckLoginStatus', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': "Bearer " + retCredential,
|
||||
|
Loading…
Reference in New Issue
Block a user