From 5aecf45959598a008d974397e526806f3cb076df Mon Sep 17 00:00:00 2001 From: spring Date: Mon, 18 Mar 2024 11:32:56 +0800 Subject: [PATCH 1/4] add: support for file download --- src/onebot11/action/go-cqhttp/DownloadFile.ts | 103 ++++++++++++++++++ src/onebot11/action/index.ts | 2 + src/onebot11/action/types.ts | 1 + 3 files changed, 106 insertions(+) create mode 100644 src/onebot11/action/go-cqhttp/DownloadFile.ts diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts new file mode 100644 index 0000000..6c457e1 --- /dev/null +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -0,0 +1,103 @@ +import BaseAction from "../BaseAction"; +import {ActionName} from "../types"; +import fs from "fs"; +import {join as joinPath} from "node:path"; +import {calculateFileMD5, DATA_DIR} from "../../../common/utils"; + +interface Payload { + thread_count?: number + url?: string + base64?: string + name?: string + headers?: string | string[] +} + +interface FileResponse { + file: string +} + +const localPath = joinPath(DATA_DIR, "file_cache") +export default class GoCQHTTPDownloadFile extends BaseAction { + actionName = ActionName.GoCQHTTP_DownloadFile + + constructor() { + super(); + if (!fs.existsSync(localPath)) { + fs.mkdirSync(localPath) + } + } + + protected async _handle(payload: Payload): Promise { + let name = payload.name || ""; + const isRandomName = !payload.name + + if (isRandomName) { + do { + name = this.generateRandomString(10); + // 使用循环防止极低概率的情况下随机出已有的文件, 导致覆盖 + } while (fs.existsSync(joinPath(localPath, name))); + } + + const filePath = joinPath(localPath, name); + + if (payload.base64) { + fs.writeFileSync(filePath, payload.base64, 'base64') + } else if (payload.url) { + const headers = this.getHeaders(payload.headers); + + const result = await fetch(payload.url, {headers}) + if (! result.ok) throw new Error(`下载文件失败: ${result.statusText}`) + + const blob = await result.blob(); + let buffer = await blob.arrayBuffer(); + fs.writeFileSync(filePath, Buffer.from(buffer), 'binary'); + } else { + throw new Error("不存在任何文件, 无法下载") + } + if (fs.existsSync(filePath)) { + + if (isRandomName) { + // 默认实现要名称未填写时文件名为文件 md5 + const md5 = await calculateFileMD5(filePath); + const newPath = joinPath(localPath, md5); + fs.renameSync(filePath, newPath); + return { file: newPath } + } + return { file: filePath } + } else { + throw new Error("文件写入失败, 检查权限") + } + } + + generateRandomString(length: number): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let randomString = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + randomString += characters.charAt(randomIndex); + } + return randomString; + } + + getHeaders(headersIn?: string | string[]): any { + const headers = {}; + if (typeof headersIn == 'string') { + headersIn = headersIn.split('[\\r\\n]'); + } + if (Array.isArray(headersIn)) { + for (const headerItem of headersIn) { + const spilt = headerItem.indexOf('='); + if (spilt < 0) { + headers[headerItem] = ""; + } else { + const key = headerItem.substring(0, spilt); + headers[key] = headerItem.substring(0, spilt + 1); + } + } + } + if (!headers['Content-Type']) { + headers['Content-Type'] = 'application/octet-stream'; + } + return headers; + } +} \ No newline at end of file diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index af557be..def74d6 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -36,6 +36,7 @@ import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile"; import {GetConfigAction, SetConfigAction} from "./llonebot/Config"; import GetGroupAddRequest from "./llonebot/GetGroupAddRequest"; import SetQQAvatar from './llonebot/SetQQAvatar' +import GoCQHTTPDownloadFile from "./go-cqhttp/DownloadFile"; export const actionHandlers = [ new Debug(), @@ -72,6 +73,7 @@ export const actionHandlers = [ new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendPrivateForwardMsg(), new GoCQHTTPGetStrangerInfo(), + new GoCQHTTPDownloadFile(), new GetGuildList(), new GoCQHTTPMarkMsgAsRead(), new GoCQHTTPUploadGroupFile(), diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index 48db8fd..a00bf13 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -54,4 +54,5 @@ export enum ActionName { GetGuildList = "get_guild_list", GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read", GoCQHTTP_UploadGroupFile = "upload_group_file", + GoCQHTTP_DownloadFile = "download_file", } \ No newline at end of file From 6ea6b33e9ad9297e2b0ec3f5c0534f06f04dbac9 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Tue, 19 Mar 2024 00:33:51 +0800 Subject: [PATCH 2/4] refactor: file utils --- src/common/data.ts | 4 +- src/common/server/http.ts | 18 ++- src/common/server/websocket.ts | 9 +- src/common/types.ts | 2 + src/common/utils/file.ts | 131 +++++++++++++++++- src/common/utils/helper.ts | 24 +++- src/common/utils/index.ts | 9 +- src/main/main.ts | 26 ++-- src/main/setConfig.ts | 12 +- src/onebot11/action/GetFile.ts | 23 ++- src/onebot11/action/SendMsg.ts | 2 +- src/onebot11/action/go-cqhttp/DownloadFile.ts | 35 +---- .../action/go-cqhttp/UploadGroupFile.ts | 2 +- src/onebot11/action/llonebot/SetQQAvatar.ts | 3 +- src/onebot11/utils.ts | 130 ----------------- src/preload.ts | 2 +- src/renderer/index.ts | 83 ++++++----- src/renderer/style.css | 8 ++ 18 files changed, 281 insertions(+), 242 deletions(-) delete mode 100644 src/onebot11/utils.ts diff --git a/src/common/data.ts b/src/common/data.ts index 5bfb876..b006762 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -21,7 +21,9 @@ export let friends: Friend[] = [] export let friendRequests: Map = new Map() export const llonebotError: LLOneBotError = { ffmpegError: '', - otherError: '' + httpServerError: '', + wsServerError: '', + otherError: 'LLOnebot未能正常启动,请检查日志查看错误' } diff --git a/src/common/server/http.ts b/src/common/server/http.ts index 32dd4bb..ccaf49f 100644 --- a/src/common/server/http.ts +++ b/src/common/server/http.ts @@ -1,7 +1,8 @@ -import express, {Express, json, Request, Response} from "express"; +import express, {Express, Request, Response} from "express"; import http from "http"; import {log} from "../utils/log"; import {getConfigUtil} from "../config"; +import {llonebotError} from "../data"; type RegisterHandler = (res: Response, payload: any) => Promise @@ -52,13 +53,20 @@ export abstract class HttpServerBase { }; start(port: number) { - this.expressAPP.get('/', (req: Request, res: Response) => { - res.send(`${this.name}已启动`); - }) - this.listen(port); + try { + this.expressAPP.get('/', (req: Request, res: Response) => { + res.send(`${this.name}已启动`); + }) + this.listen(port); + llonebotError.httpServerError = "" + } catch (e) { + log("HTTP服务启动失败", e.toString()) + llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString() + } } stop() { + llonebotError.httpServerError = "" if (this.server) { this.server.close() this.server = null; diff --git a/src/common/server/websocket.ts b/src/common/server/websocket.ts index 0920b01..a9e4d3d 100644 --- a/src/common/server/websocket.ts +++ b/src/common/server/websocket.ts @@ -3,6 +3,7 @@ import urlParse from "url"; import {IncomingMessage} from "node:http"; import {log} from "../utils/log"; import {getConfigUtil} from "../config"; +import {llonebotError} from "../data"; class WebsocketClientBase { private wsClient: WebSocket @@ -29,7 +30,12 @@ export class WebsocketServerBase { } start(port: number) { - this.ws = new WebSocketServer({port}); + try { + this.ws = new WebSocketServer({port}); + llonebotError.wsServerError = '' + }catch (e) { + llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString() + } this.ws.on("connection", (wsClient, req) => { const url = req.url.split("?").shift() this.authorize(wsClient, req); @@ -41,6 +47,7 @@ export class WebsocketServerBase { } stop() { + llonebotError.wsServerError = '' this.ws.close((err) => { log("ws server close failed!", err) }); diff --git a/src/common/types.ts b/src/common/types.ts index 060a517..aa4d98b 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -28,6 +28,8 @@ export interface Config { } export interface LLOneBotError { + httpServerError?: string + wsServerError?: string ffmpegError?: string otherError?: string } diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index f473aef..e7cc046 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -5,9 +5,10 @@ import util from "util"; import {encode, getDuration, isWav} from "silk-wasm"; import path from "node:path"; import {v4 as uuidv4} from "uuid"; -import {DATA_DIR} from "./index"; -import {log} from "./log"; +import {DATA_DIR, log, TEMP_DIR} from "./index"; import {getConfigUtil} from "../config"; +import {dbUtil} from "../db"; +import * as fileType from "file-type"; export function isGIF(path: string) { const buffer = Buffer.alloc(4); @@ -64,8 +65,11 @@ export async function file2base64(path: string) { export function checkFfmpeg(newPath: string = null): Promise { return new Promise((resolve, reject) => { + log("开始检查ffmpeg", newPath); if (newPath) { ffmpeg.setFfmpegPath(newPath); + } + try { ffmpeg.getAvailableFormats((err, formats) => { if (err) { log('ffmpeg is not installed or not found in PATH:', err); @@ -75,6 +79,8 @@ export function checkFfmpeg(newPath: string = null): Promise { resolve(true); } }) + }catch (e) { + resolve(false); } }); } @@ -260,4 +266,125 @@ export function calculateFileMD5(filePath: string): Promise { reject(err); }); }); +} + +type Uri2LocalRes = { + success: boolean, + errMsg: string, + fileName: string, + ext: string, + path: string, + isLocal: boolean +} + +export async function uri2local(uri: string, fileName: string = null): Promise { + let res = { + success: false, + errMsg: "", + fileName: "", + ext: "", + path: "", + isLocal: false + } + if (!fileName) { + fileName = uuidv4(); + } + let filePath = path.join(TEMP_DIR, fileName) + let url = null; + try { + url = new URL(uri); + } catch (e) { + res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` + return res + } + + // log("uri protocol", url.protocol, uri); + if (url.protocol == "base64:") { + // base64转成文件 + let base64Data = uri.split("base64://")[1] + try { + const buffer = Buffer.from(base64Data, 'base64'); + fs.writeFileSync(filePath, buffer); + + } catch (e: any) { + res.errMsg = `base64文件下载失败,` + e.toString() + return res + } + } else if (url.protocol == "http:" || url.protocol == "https:") { + // 下载文件 + let fetchRes: Response; + try { + fetchRes = await fetch(url) + } catch (e) { + res.errMsg = `${url}下载失败` + return res + } + if (!fetchRes.ok) { + res.errMsg = `${url}下载失败,` + fetchRes.statusText + return res + } + let blob = await fetchRes.blob(); + let buffer = await blob.arrayBuffer(); + try { + const pathInfo = path.parse(decodeURIComponent(url.pathname)) + if (pathInfo.name) { + fileName = pathInfo.name + if (pathInfo.ext) { + fileName += pathInfo.ext + // res.ext = pathInfo.ext + } + } + res.fileName = fileName + filePath = path.join(TEMP_DIR, uuidv4() + fileName) + fs.writeFileSync(filePath, Buffer.from(buffer)); + } catch (e: any) { + res.errMsg = `${url}下载失败,` + e.toString() + return res + } + } else { + let pathname: string; + if (url.protocol === "file:") { + // await fs.copyFile(url.pathname, filePath); + pathname = decodeURIComponent(url.pathname) + if (process.platform === "win32") { + filePath = pathname.slice(1) + } else { + filePath = pathname + } + } else { + const cache = await dbUtil.getFileCache(uri); + if (cache) { + filePath = cache.filePath + } else { + filePath = uri; + } + } + + res.isLocal = true + } + // else{ + // res.errMsg = `不支持的file协议,` + url.protocol + // return res + // } + // if (isGIF(filePath) && !res.isLocal) { + // await fs.rename(filePath, filePath + ".gif"); + // filePath += ".gif"; + // } + if (!res.isLocal && !res.ext) { + try { + let ext: string = (await fileType.fileTypeFromFile(filePath)).ext + if (ext) { + log("获取文件类型", ext, filePath) + fs.renameSync(filePath, filePath + `.${ext}`) + filePath += `.${ext}` + res.fileName += `.${ext}` + res.ext = ext + } + } catch (e) { + // log("获取文件类型失败", filePath,e.stack) + } + } + res.success = true + res.path = filePath + return res } \ No newline at end of file diff --git a/src/common/utils/helper.ts b/src/common/utils/helper.ts index 65a999a..48862b8 100644 --- a/src/common/utils/helper.ts +++ b/src/common/utils/helper.ts @@ -43,4 +43,26 @@ export function mergeNewProperties(newObj: any, oldObj: any) { export function isNull(value: any) { return value === undefined || value === null; -} \ No newline at end of file +} + +/** + * 将字符串按最大长度分割并添加换行符 + * @param str 原始字符串 + * @param maxLength 每行的最大字符数 + * @returns 处理后的字符串,超过长度的地方将会换行 + */ +export function wrapText(str: string, maxLength: number): string { + // 初始化一个空字符串用于存放结果 + let result: string = ''; + + // 循环遍历字符串,每次步进maxLength个字符 + for (let i = 0; i < str.length; i += maxLength) { + // 从i开始,截取长度为maxLength的字符串段,并添加到结果字符串 + // 如果不是第一段,先添加一个换行符 + if (i > 0) result += '\n'; + result += str.substring(i, i + maxLength); + } + + return result; +} + diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index af279cf..81ef39b 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,3 +1,6 @@ +import path from "node:path"; +import fs from "fs"; + export * from './file' export * from './helper' export * from './log' @@ -5,6 +8,8 @@ export * from './qqlevel' export * from './qqpkg' export * from './update' export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; +export const TEMP_DIR = path.join(DATA_DIR, "temp"); - - +if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR); +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 15da2d2..352d0ad 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -13,7 +13,7 @@ import { CHANNEL_UPDATE, } from "../common/channels"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; -import {DATA_DIR} from "../common/utils"; +import {DATA_DIR, wrapText} from "../common/utils"; import { friendRequests, getFriend, @@ -92,8 +92,14 @@ function onLoad() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, {recursive: true}); } - ipcMain.handle(CHANNEL_ERROR, (event, arg) => { - return llonebotError; + ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { + const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) + llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频无法发送" + let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError; + let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` + error = error.replace("\n\n", "\n") + error = error.trim(); + return error; }) ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { const config = getConfigUtil().getConfig() @@ -331,7 +337,7 @@ function onLoad() { async function start() { log("llonebot pid", process.pid) - + llonebotError.otherError = ""; startTime = Date.now(); dbUtil.getReceivedTempUinMap().then(m=>{ for (const [key, value] of Object.entries(m)) { @@ -341,18 +347,8 @@ function onLoad() { startReceiveHook().then(); NTQQGroupApi.getGroups(true).then() const config = getConfigUtil().getConfig() - // 检查ffmpeg - checkFfmpeg(config.ffmpeg).then(exist => { - if (!exist) { - llonebotError.ffmpegError = `没有找到ffmpeg,音频只能发送wav和silk` - } - }) if (config.ob11.enableHttp) { - try { - ob11HTTPServer.start(config.ob11.httpPort) - } catch (e) { - log("http server start failed", e); - } + ob11HTTPServer.start(config.ob11.httpPort) } if (config.ob11.enableWs) { ob11WebsocketServer.start(config.ob11.wsPort); diff --git a/src/main/setConfig.ts b/src/main/setConfig.ts index eb72ede..b82af07 100644 --- a/src/main/setConfig.ts +++ b/src/main/setConfig.ts @@ -3,7 +3,6 @@ import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import {llonebotError} from "../common/data"; -import {checkFfmpeg} from "../common/utils/file"; import {getConfigUtil} from "../common/config"; export async function setConfig(config: Config) { @@ -21,6 +20,7 @@ export async function setConfig(config: Config) { // 正向ws端口变化,重启服务 if (config.ob11.wsPort != oldConfig.ob11.wsPort) { ob11WebsocketServer.restart(config.ob11.wsPort); + llonebotError.wsServerError = '' } // 判断是否启用或关闭正向ws if (config.ob11.enableWs != oldConfig.ob11.enableWs) { @@ -51,14 +51,4 @@ export async function setConfig(config: Config) { } } } - - // 检查ffmpeg - if (config.ffmpeg) { - checkFfmpeg(config.ffmpeg).then(success => { - if (success) { - llonebotError.ffmpegError = '' - } - }) - } - } \ No newline at end of file diff --git a/src/onebot11/action/GetFile.ts b/src/onebot11/action/GetFile.ts index ace5163..0687dfd 100644 --- a/src/onebot11/action/GetFile.ts +++ b/src/onebot11/action/GetFile.ts @@ -2,6 +2,7 @@ import BaseAction from "./BaseAction"; import fs from "fs/promises"; import {dbUtil} from "../../common/db"; import {getConfigUtil} from "../../common/config"; +import {log, uri2local} from "../../common/utils"; export interface GetFilePayload { file: string // 文件名 @@ -26,6 +27,18 @@ export class GetFileBase extends BaseAction { if (cache.downloadFunc) { await cache.downloadFunc() } + try { + await fs.access(cache.filePath, fs.constants.F_OK) + } catch (e) { + log("file not found", e) + const downloadResult = await uri2local(cache.url) + if (downloadResult.success) { + cache.filePath = downloadResult.path + dbUtil.addFileCache(payload.file, cache).then() + } else { + throw new Error("file download failed. " + downloadResult.errMsg) + } + } let res: GetFileResponse = { file: cache.filePath, url: cache.url, @@ -37,11 +50,11 @@ export class GetFileBase extends BaseAction { res.base64 = await fs.readFile(cache.filePath, 'base64') } } - if (autoDeleteFile) { - setTimeout(() => { - fs.unlink(cache.filePath) - }, autoDeleteFileSecond * 1000) - } + // if (autoDeleteFile) { + // setTimeout(() => { + // fs.unlink(cache.filePath) + // }, autoDeleteFileSecond * 1000) + // } return res } } \ No newline at end of file diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index b7eb2fa..b10976a 100644 --- a/src/onebot11/action/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -25,7 +25,6 @@ import { } from '../types'; import {Peer} from "../../ntqqapi/api/msg"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; -import {uri2local} from "../utils"; import BaseAction from "./BaseAction"; import {ActionName, BaseCheckResult} from "./types"; import * as fs from "node:fs"; @@ -35,6 +34,7 @@ import {ALLOW_SEND_TEMP_MSG} from "../../common/config"; import {NTQQMsgApi} from "../../ntqqapi/api/msg"; import {log} from "../../common/utils/log"; import {sleep} from "../../common/utils/helper"; +import {uri2local} from "../../common/utils"; function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkUri(uri: string): boolean { diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts index 6c457e1..1444ad8 100644 --- a/src/onebot11/action/go-cqhttp/DownloadFile.ts +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -2,7 +2,8 @@ import BaseAction from "../BaseAction"; import {ActionName} from "../types"; import fs from "fs"; import {join as joinPath} from "node:path"; -import {calculateFileMD5, DATA_DIR} from "../../../common/utils"; +import {calculateFileMD5, TEMP_DIR} from "../../../common/utils"; +import {v4 as uuid4} from "uuid"; interface Payload { thread_count?: number @@ -16,29 +17,13 @@ interface FileResponse { file: string } -const localPath = joinPath(DATA_DIR, "file_cache") export default class GoCQHTTPDownloadFile extends BaseAction { actionName = ActionName.GoCQHTTP_DownloadFile - constructor() { - super(); - if (!fs.existsSync(localPath)) { - fs.mkdirSync(localPath) - } - } - protected async _handle(payload: Payload): Promise { - let name = payload.name || ""; const isRandomName = !payload.name - - if (isRandomName) { - do { - name = this.generateRandomString(10); - // 使用循环防止极低概率的情况下随机出已有的文件, 导致覆盖 - } while (fs.existsSync(joinPath(localPath, name))); - } - - const filePath = joinPath(localPath, name); + let name = payload.name || uuid4(); + const filePath = joinPath(TEMP_DIR, name); if (payload.base64) { fs.writeFileSync(filePath, payload.base64, 'base64') @@ -59,7 +44,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction{ - let res = { - success: false, - errMsg: "", - fileName: "", - ext: "", - path: "", - isLocal: false - } - if (!fileName) { - fileName = uuidv4(); - } - let filePath = path.join(DATA_DIR, fileName) - let url = null; - try{ - url = new URL(uri); - }catch (e) { - res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` - return res - } - - // log("uri protocol", url.protocol, uri); - if (url.protocol == "base64:") { - // base64转成文件 - let base64Data = uri.split("base64://")[1] - try { - const buffer = Buffer.from(base64Data, 'base64'); - await fs.writeFile(filePath, buffer); - - } catch (e: any) { - res.errMsg = `base64文件下载失败,` + e.toString() - return res - } - } else if (url.protocol == "http:" || url.protocol == "https:") { - // 下载文件 - let fetchRes: Response; - try{ - fetchRes = await fetch(url) - }catch (e) { - res.errMsg = `${url}下载失败` - return res - } - if (!fetchRes.ok) { - res.errMsg = `${url}下载失败,` + fetchRes.statusText - return res - } - let blob = await fetchRes.blob(); - let buffer = await blob.arrayBuffer(); - try { - const pathInfo = path.parse(decodeURIComponent(url.pathname)) - if (pathInfo.name){ - fileName = pathInfo.name - if (pathInfo.ext){ - fileName += pathInfo.ext - // res.ext = pathInfo.ext - } - } - res.fileName = fileName - filePath = path.join(DATA_DIR, uuidv4() + fileName) - await fs.writeFile(filePath, Buffer.from(buffer)); - } catch (e: any) { - res.errMsg = `${url}下载失败,` + e.toString() - return res - } - } else { - let pathname: string; - if (url.protocol === "file:") { - // await fs.copyFile(url.pathname, filePath); - pathname = decodeURIComponent(url.pathname) - if (process.platform === "win32") { - filePath = pathname.slice(1) - } else { - filePath = pathname - } - } else { - const cache = await dbUtil.getFileCache(uri); - if (cache) { - filePath = cache.filePath - } else { - filePath = uri; - } - } - - res.isLocal = true - } - // else{ - // res.errMsg = `不支持的file协议,` + url.protocol - // return res - // } - // if (isGIF(filePath) && !res.isLocal) { - // await fs.rename(filePath, filePath + ".gif"); - // filePath += ".gif"; - // } - if (!res.isLocal && !res.ext) { - try { - let ext: string = (await fileType.fileTypeFromFile(filePath)).ext - if (ext) { - log("获取文件类型", ext, filePath) - await fs.rename(filePath, filePath + `.${ext}`) - filePath += `.${ext}` - res.fileName += `.${ext}` - res.ext = ext - } - } catch (e) { - // log("获取文件类型失败", filePath,e.stack) - } - } - res.success = true - res.path = filePath - return res -} \ No newline at end of file diff --git a/src/preload.ts b/src/preload.ts index ed83aa0..ebdb027 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -30,7 +30,7 @@ const llonebot = { getConfig: async (): Promise => { return ipcRenderer.invoke(CHANNEL_GET_CONFIG); }, - getError: async (): Promise => { + getError: async (): Promise => { return ipcRenderer.invoke(CHANNEL_ERROR); }, selectFile: (): Promise => { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index c16f12b..71a5736 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,11 +1,5 @@ /// -import { - SettingButton, - SettingItem, - SettingList, - SettingSelect, - SettingSwitch -} from './components'; +import {SettingButton, SettingItem, SettingList, SettingSwitch} from './components'; import StyleRaw from './style.css?raw'; // 打开设置界面时触发 @@ -14,18 +8,21 @@ 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 }; + let ob11Config = {...config.ob11}; const setConfig = (key: string, value: any) => { const configKey = key.split('.'); if (key.indexOf('ob11') === 0) { + if (configKey[1] === "messagePostFormat") { + value = value ? "string" : "array" + } 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; - if (!['heartInterval', 'token', 'ffmpeg'].includes(key)){ + if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) { window.llonebot.setConfig(config); } } @@ -35,16 +32,19 @@ async function onSettingWindowCreated(view: Element) { const doc = parser.parseFromString([ '
', ``, + SettingList([ + '
', + ]), SettingList([ SettingItem('启用 HTTP 服务', null, - SettingSwitch('ob11.enableHttp', config.ob11.enableHttp, { 'control-display-id': 'config-ob11-httpPort' }), + SettingSwitch('ob11.enableHttp', config.ob11.enableHttp, {'control-display-id': 'config-ob11-httpPort'}), ), SettingItem('HTTP 服务监听端口', null, `
`, 'config-ob11-httpPort', config.ob11.enableHttp ), SettingItem('启用 HTTP 事件上报', null, - SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, { 'control-display-id': 'config-ob11-httpHosts' }), + SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, {'control-display-id': 'config-ob11-httpHosts'}), ), `
@@ -56,14 +56,14 @@ async function onSettingWindowCreated(view: Element) {
`, SettingItem('启用正向 WebSocket 服务', null, - SettingSwitch('ob11.enableWs', config.ob11.enableWs, { 'control-display-id': 'config-ob11-wsPort' }), + SettingSwitch('ob11.enableWs', config.ob11.enableWs, {'control-display-id': 'config-ob11-wsPort'}), ), SettingItem('正向 WebSocket 服务监听端口', null, `
`, 'config-ob11-wsPort', config.ob11.enableWs ), SettingItem('启用反向 WebSocket 服务', null, - SettingSwitch('ob11.enableWsReverse', config.ob11.enableWsReverse, { 'control-display-id': 'config-ob11-wsHosts' }), + SettingSwitch('ob11.enableWsReverse', config.ob11.enableWsReverse, {'control-display-id': 'config-ob11-wsHosts'}), ), `
@@ -82,12 +82,13 @@ async function onSettingWindowCreated(view: Element) { `
`, ), SettingItem( - '消息上报格式类型', + '启用CQ码上报格式,不启用则为消息段格式', '如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档', - SettingSelect([ - { text: '消息段', value: 'array' }, - { text: 'CQ码', value: 'string' }, - ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), + // SettingSelect([ + // {text: '消息段', value: 'array'}, + // {text: 'CQ码', value: 'string'}, + // ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), + SettingSwitch('ob11.messagePostFormat', config.ob11.messagePostFormat === "string"), ), SettingItem( 'ffmpeg 路径,发送语音、视频需要,同时保证ffprobe和ffmpeg在一起', ` 下载地址 , 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}`, @@ -122,7 +123,7 @@ async function onSettingWindowCreated(view: Element) { SettingItem( '自动删除收到的文件', '在收到文件后的指定时间内删除该文件', - SettingSwitch('autoDeleteFile', config.autoDeleteFile, { 'control-display-id': 'config-auto-delete-file-second' }), + SettingSwitch('autoDeleteFile', config.autoDeleteFile, {'control-display-id': 'config-auto-delete-file-second'}), ), SettingItem( '自动删除文件时间', @@ -166,6 +167,18 @@ async function onSettingWindowCreated(view: Element) { '
', ].join(''), "text/html"); + let errorEle = doc.querySelector("#llonebot-error"); + errorEle.style.display = 'none'; + const showError = async () => { + setTimeout(async () => { + let errMessage = await window.llonebot.getError(); + console.log(errMessage) + errMessage = errMessage.replace(/\n/g, '
') + errorEle.innerHTML = errMessage; + errorEle.style.display = errMessage ? 'flex' : 'none'; + }, 1000) + } + showError().then() // 外链按钮 doc.querySelector('#open-github').addEventListener('click', () => { window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot') @@ -180,7 +193,7 @@ async function onSettingWindowCreated(view: Element) { window.LiteLoader.api.openExternal('https://llonebot.github.io/') }) // 生成反向地址列表 - const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any={}) => { + const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any = {}) => { const dom = { container: document.createElement('setting-item'), input: document.createElement('input'), @@ -212,23 +225,23 @@ async function onSettingWindowCreated(view: Element) { return dom.container; }; - const buildHostList = (hosts: string[], type: string, inputAttr: any={}) => { + const buildHostList = (hosts: string[], type: string, inputAttr: any = {}) => { const result: HTMLElement[] = []; - + hosts.forEach((host, index) => { result.push(buildHostListItem(type, host, index, inputAttr)); }); - + return result; }; - const addReverseHost = (type: string, doc: Document = document, inputAttr: any={}) => { + const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => { const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`); hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr)); 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()); + [...hostContainerDom.childNodes].forEach(dom => dom.remove()); buildHostList(ob11Config[type], type).forEach(dom => { hostContainerDom.appendChild(dom); }); @@ -236,8 +249,8 @@ async function onSettingWindowCreated(view: Element) { initReverseHost('httpHosts', doc); initReverseHost('wsHosts', doc); - doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如:http://127.0.0.1:5140/onebot' })); - doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如:ws://127.0.0.1:5140/onebot' })); + doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如:http://127.0.0.1:5140/onebot'})); + doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如:ws://127.0.0.1:5140/onebot'})); doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => { window.llonebot.selectFile() @@ -297,6 +310,8 @@ async function onSettingWindowCreated(view: Element) { config.ob11 = ob11Config; window.llonebot.setConfig(config); + // window.location.reload(); + showError().then() alert('保存成功'); }); @@ -305,19 +320,19 @@ async function onSettingWindowCreated(view: Element) { }); } -function init () { - const hash = location.hash - if (hash === '#/blank') { +function init() { + const hash = location.hash + if (hash === '#/blank') { - } + } } if (location.hash === '#/blank') { - (window as any).navigation.addEventListener('navigatesuccess', init, { once: true }) + (window as any).navigation.addEventListener('navigatesuccess', init, {once: true}) } else { - init() + init() } export { - onSettingWindowCreated + onSettingWindowCreated } diff --git a/src/renderer/style.css b/src/renderer/style.css index 028aa96..675442a 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -61,4 +61,12 @@ setting-item a:hover { setting-item a:active, setting-item a:visited { color: var(--text-link); +} + +#llonebot-error{ + color: red; + height: 100px; + overflow: visible; + display: flex; + align-items: center; } \ No newline at end of file From b12d205059ddb60d51d864e150f1920fc859d61d Mon Sep 17 00:00:00 2001 From: linyuchen Date: Tue, 19 Mar 2024 00:37:20 +0800 Subject: [PATCH 3/4] feat: stranger info add sex & qq_level --- src/onebot11/action/go-cqhttp/GetStrangerInfo.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts index a8a21ca..bbf6c88 100644 --- a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts @@ -3,6 +3,8 @@ import {OB11User} from "../../types"; import {getFriend, getGroupMember, groups} from "../../../common/data"; import {OB11Constructor} from "../../constructor"; import {ActionName} from "../types"; +import {isNull, log} from "../../../common/utils"; +import {NTQQUserApi} from "../../../ntqqapi/api/user"; export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { @@ -17,9 +19,13 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: numbe for (const group of groups) { const member = await getGroupMember(group.groupCode, user_id) if (member) { + if (isNull(member.sex)){ + let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) + Object.assign(member, info); + } return OB11Constructor.groupMember(group.groupCode, member) as OB11User } } - throw ("查无此人") + throw new Error("查无此人") } } \ No newline at end of file From dda5ea39721f7f5eee40f1dfe5d33ddf0f0be6b4 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Tue, 19 Mar 2024 00:45:59 +0800 Subject: [PATCH 4/4] feat: stranger info add sex & qq_level --- src/onebot11/action/go-cqhttp/GetStrangerInfo.ts | 13 +++++++++---- src/onebot11/constructor.ts | 5 +++-- src/onebot11/types.ts | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts index bbf6c88..340742f 100644 --- a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts @@ -5,24 +5,29 @@ import {OB11Constructor} from "../../constructor"; import {ActionName} from "../types"; import {isNull, log} from "../../../common/utils"; import {NTQQUserApi} from "../../../ntqqapi/api/user"; +import {Friend, GroupMember} from "../../../ntqqapi/types"; export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { actionName = ActionName.GoCQHTTP_GetStrangerInfo + private async refreshInfo(user: Friend | GroupMember){ + if (isNull(user.sex)){ + let info = (await NTQQUserApi.getUserDetailInfo(user.uid)) + Object.assign(user, info); + } + } protected async _handle(payload: { user_id: number }): Promise { const user_id = payload.user_id.toString() const friend = await getFriend(user_id) if (friend) { + await this.refreshInfo(friend); return OB11Constructor.friend(friend); } for (const group of groups) { const member = await getGroupMember(group.groupCode, user_id) if (member) { - if (isNull(member.sex)){ - let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) - Object.assign(member, info); - } + await this.refreshInfo(member); return OB11Constructor.groupMember(group.groupCode, member) as OB11User } } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index a9e258f..2d66b9c 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -305,9 +305,10 @@ export class OB11Constructor { return { user_id: parseInt(friend.uin), nickname: friend.nick, - remark: friend.remark + remark: friend.remark, + sex: OB11Constructor.sex(friend.sex), + qq_level: friend.qqLevel && calcQQLevel(friend.qqLevel) || 0 } - } static selfInfo(selfInfo: SelfInfo): OB11User { diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index b8db420..3ebce72 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -4,7 +4,9 @@ import {EventType} from "./event/OB11BaseEvent"; export interface OB11User { user_id: number; nickname: string; - remark?: string + remark?: string; + sex?: OB11UserSex; + qq_level?: number; } export enum OB11UserSex {