From 3fcc482774a177ea45615856a81cc561e570e928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B7=E6=9B=A6?= <2218872014@qq.com> Date: Fri, 30 Jan 2026 23:05:40 +0800 Subject: [PATCH] 1 --- .../napcat-onebot/network/plugin/loader.ts | 6 +- .../src/tools/packet-tools.ts | 179 ++++++++++++++++++ .../napcat-webui-backend/src/api/Plugin.ts | 49 ++--- 3 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 packages/napcat-plugin-aicat/src/tools/packet-tools.ts diff --git a/packages/napcat-onebot/network/plugin/loader.ts b/packages/napcat-onebot/network/plugin/loader.ts index 7c4b23c5..a4236c44 100644 --- a/packages/napcat-onebot/network/plugin/loader.ts +++ b/packages/napcat-onebot/network/plugin/loader.ts @@ -123,8 +123,8 @@ export class PluginLoader { const entryFile = this.findEntryFile(pluginDir, packageJson); const entryPath = entryFile ? path.join(pluginDir, entryFile) : undefined; - // 获取启用状态(默认启用) - const enable = statusConfig[pluginId] !== false; + // 获取启用状态(默认禁用,需手动配置后启用) + const enable = statusConfig[pluginId] === true; // 创建插件条目 const entry: PluginEntry = { @@ -159,7 +159,7 @@ export class PluginLoader { id: dirname, // 使用目录名作为 ID fileId: dirname, pluginPath: path.join(this.pluginPath, dirname), - enable: statusConfig[dirname] !== false, + enable: statusConfig[dirname] === true, loaded: false, runtime: { status: 'error', diff --git a/packages/napcat-plugin-aicat/src/tools/packet-tools.ts b/packages/napcat-plugin-aicat/src/tools/packet-tools.ts new file mode 100644 index 00000000..b7121be3 --- /dev/null +++ b/packages/napcat-plugin-aicat/src/tools/packet-tools.ts @@ -0,0 +1,179 @@ +import { pb, processJson, randomUint, jsonDumpsWithBytes } from './protobuf'; +import type { ToolResult } from '../types'; + +interface ActionMap { + call: (action: string, params: unknown, adapter: string, config: unknown) => Promise; +} + +let packetMode: 1 | 2 = 2; +export const setPacketMode = (mode: 1 | 2) => { packetMode = mode; }; +export const getPacketMode = () => packetMode; + +function tryDecodeHex (hexStr: string): Record | null { + if (!hexStr || !/^[0-9a-fA-F]+$/.test(hexStr)) return null; + try { + const decoded = pb.decode(hexStr); + return decoded && Object.keys(decoded).length > 0 ? decoded : null; + } catch { return null; } +} + +function extractHexData (obj: unknown): string | null { + if (typeof obj === 'string' && /^[0-9a-fA-F]+$/.test(obj) && obj.length > 10) return obj; + if (obj && typeof obj === 'object') { + const record = obj as Record; + if (record.data) { const found = extractHexData(record.data); if (found) return found; } + for (const value of Object.values(record)) { const found = extractHexData(value); if (found) return found; } + } + return null; +} + +function decodeResponseData (data: unknown): unknown { + if (typeof data === 'string') { const decoded = tryDecodeHex(data); return decoded ?? data; } + if (Array.isArray(data)) return data.map(item => decodeResponseData(item)); + if (data && typeof data === 'object') { + const result: Record = {}; + for (const [key, value] of Object.entries(data)) result[key] = decodeResponseData(value); + return result; + } + return data; +} + +export async function sendPacket ( + actions: ActionMap, adapter: string, config: unknown, cmd: string, content: unknown +): Promise { + try { + const dataHex = pb.bytesToHex(pb.encode(processJson(content))); + const result = await actions.call('send_packet', { cmd, data: dataHex }, adapter, config); + + if (result && typeof result === 'object') { + const hexData = extractHexData(result as Record); + if (hexData) { + const decoded = tryDecodeHex(hexData); + if (decoded) return { success: true, data: decoded }; + } + return { success: true, data: decodeResponseData(result) }; + } + if (typeof result === 'string') { + const decoded = tryDecodeHex(result); + if (decoded) return { success: true, data: decoded }; + } + return { success: true, data: result }; + } catch (error) { + return { success: false, error: `发送数据包失败: ${error}` }; + } +} + +export async function sendElem ( + actions: ActionMap, adapter: string, config: unknown, targetId: string, isGroup: boolean, content: unknown +): Promise { + const packet: Record = { + 1: { [isGroup ? 2 : 1]: { 1: parseInt(targetId) } }, + 2: { 1: 1, 2: 0, 3: 0 }, + 3: { 1: { 2: processJson(content) } }, + 4: randomUint(), + 5: randomUint(), + }; + return sendPacket(actions, adapter, config, 'MessageSvc.PbSendMsg', packet); +} + +async function uploadLong ( + actions: ActionMap, adapter: string, config: unknown, targetId: string, isGroup: boolean, content: unknown +): Promise { + const data: Record = { 2: { 1: 'MultiMsg', 2: { 1: [{ 3: { 1: { 2: processJson(content) } } }] } } }; + const packet: Record = { + 2: { 1: isGroup ? 3 : 1, 2: { 2: parseInt(targetId) }, 3: targetId, 4: pb.encode(data) }, + 15: { 1: 4, 2: 2, 3: 9, 4: 0 }, + }; + const resp = await sendPacket(actions, adapter, config, 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', packet); + if (resp.success && resp.data) { + const field2 = (resp.data as Record)[2] as Record | undefined; + return field2?.[3] as string | null; + } + return null; +} + +export async function sendLong ( + actions: ActionMap, adapter: string, config: unknown, targetId: string, isGroup: boolean, content: unknown +): Promise { + const resid = await uploadLong(actions, adapter, config, targetId, isGroup, content); + if (!resid) return { success: false, error: '上传长消息失败' }; + const elem: Record = { 37: { 6: 1, 7: resid, 17: 0, 19: { 15: 0, 31: 0, 41: 0 } } }; + return sendElem(actions, adapter, config, targetId, isGroup, elem); +} + +export async function getMessagePb ( + actions: ActionMap, adapter: string, config: unknown, groupId: string, messageId: string, realSeq?: string +): Promise { + if (!realSeq) { + const msgInfo = await actions.call('get_msg', { message_id: messageId }, adapter, config) as Record; + if (msgInfo) realSeq = (msgInfo.retcode === 0 ? (msgInfo.data as Record)?.real_seq : msgInfo.real_seq) as string; + if (!realSeq) return { success: false, error: '未找到 real_seq' }; + } + const seq = parseInt(realSeq); + const packet: Record = { 1: { 1: parseInt(groupId), 2: seq, 3: seq }, 2: true }; + return sendPacket(actions, adapter, config, 'trpc.msg.register_proxy.RegisterProxy.SsoGetGroupMsg', packet); +} + +export function extractSenderInfo (pbData: Record): { senderQQ: string | null; senderName: string | null; } { + try { + const field3 = pbData[3] as Record | undefined; + const field6 = field3?.[6] as Record | undefined; + const field1In6 = field6?.[1] as Record | undefined; + let senderQQ = field1In6?.[1] !== undefined ? String(field1In6[1]) : null; + const field8 = field1In6?.[8] as Record | undefined; + const senderName = typeof field8?.[4] === 'string' ? field8[4] : null; + if (senderQQ && !/^\d{5,12}$/.test(senderQQ)) senderQQ = null; + return { senderQQ, senderName }; + } catch { return { senderQQ: null, senderName: null }; } +} + +export function extractBodyData (pbData: Record): unknown { + try { + return (((pbData[3] as Record)?.[6] as Record)?.[3] as Record)?.[1]?.[2] ?? null; + } catch { return null; } +} + +function createFlatNode (botId: string, title: string, content: string): unknown { + return { type: 'node', data: { user_id: botId, nickname: title, content: [{ type: 'text', data: { text: content } }] } }; +} + +function createNestedNode (botId: string, title: string, description: string, content: string): unknown { + return { + type: 'node', + data: { + user_id: botId, + nickname: title, + content: [ + { type: 'node', data: { user_id: botId, nickname: '📌 说明', content: [{ type: 'text', data: { text: description } }] } }, + { type: 'node', data: { user_id: botId, nickname: '📄 数据', content: [{ type: 'text', data: { text: content } }] } }, + ], + }, + }; +} + +export function buildMessageNodes ( + botId: string, botName: string, realSeq: number, senderQQ: string | null, senderName: string | null, + pbData: Record, onebotData?: unknown +): unknown[] { + const nodes: unknown[] = []; + const infoParts = ['📦 消息基本信息', `Real Seq: ${realSeq}`]; + if (senderQQ) infoParts.push(`发送者QQ: ${senderQQ}`); + if (senderName) infoParts.push(`发送者昵称: ${senderName}`); + nodes.push({ type: 'node', data: { user_id: botId, nickname: botName, content: [{ type: 'text', data: { text: infoParts.join('\n') } }] } }); + + const bodyData = extractBodyData(pbData); + + if (packetMode === 1) { + if (onebotData) { nodes.push(createFlatNode(botId, '📋', 'OneBot 数据')); nodes.push(createFlatNode(botId, '📋', JSON.stringify(onebotData, null, 2))); } + if (bodyData) { nodes.push(createFlatNode(botId, '📦', 'Body 数据')); nodes.push(createFlatNode(botId, '📦', jsonDumpsWithBytes(bodyData))); } + nodes.push(createFlatNode(botId, '🔍', 'ProtoBuf 数据')); + nodes.push(createFlatNode(botId, '🔍', jsonDumpsWithBytes(pbData))); + } else { + if (onebotData) nodes.push(createNestedNode(botId, '📋 OneBot 数据', 'OneBot 标准格式的消息数据', JSON.stringify(onebotData, null, 2))); + if (bodyData) nodes.push(createNestedNode(botId, '📦 Body 数据', 'Body 数据', jsonDumpsWithBytes(bodyData))); + nodes.push(createNestedNode(botId, '🔍 ProtoBuf 数据', 'ProtoBuf 数据', jsonDumpsWithBytes(pbData))); + } + return nodes; +} + +export { jsonDumpsWithBytes }; diff --git a/packages/napcat-webui-backend/src/api/Plugin.ts b/packages/napcat-webui-backend/src/api/Plugin.ts index 0367ba2b..94b6ea5b 100644 --- a/packages/napcat-webui-backend/src/api/Plugin.ts +++ b/packages/napcat-webui-backend/src/api/Plugin.ts @@ -189,7 +189,7 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => { if (!pluginManager) return sendError(res, 'Plugin Manager not found'); const plugin = pluginManager.getPluginInfo(id); - if (!plugin) return sendError(res, 'Plugin not loaded'); + if (!plugin) return sendError(res, 'Plugin not found'); // 获取配置值 let config: unknown = {}; @@ -198,7 +198,7 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => { config = await plugin.runtime.module?.plugin_get_config(plugin.runtime.context); } catch (e) { } } else { - // Default behavior: read from default config path + // 直接从配置文件读取(支持未加载的插件) try { const configPath = plugin.runtime.context?.configPath || pluginManager.getPluginConfigPath(id); if (fs.existsSync(configPath)) { @@ -207,13 +207,16 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => { } catch (e) { } } - // 获取静态 schema + // 获取静态 schema(未加载的插件返回空数组) const schema = plugin.runtime.module?.plugin_config_schema || plugin.runtime.module?.plugin_config_ui || []; // 检查是否支持动态控制 const supportReactive = !!(plugin.runtime.module?.plugin_config_controller || plugin.runtime.module?.plugin_on_config_change); - return sendSuccess(res, { schema, config, supportReactive }); + // 标记插件是否已加载 + const loaded = plugin.loaded; + + return sendSuccess(res, { schema, config, supportReactive, loaded }); }; /** 活跃的 SSE 连接 */ @@ -425,8 +428,9 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => { if (!pluginManager) return sendError(res, 'Plugin Manager not found'); const plugin = pluginManager.getPluginInfo(id); - if (!plugin) return sendError(res, 'Plugin not loaded'); + if (!plugin) return sendError(res, 'Plugin not found'); + // 已加载的插件:使用插件提供的配置方法 if (plugin.runtime.module?.plugin_set_config && plugin.runtime.context) { try { await plugin.runtime.module.plugin_set_config(plugin.runtime.context, config); @@ -434,26 +438,25 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => { } catch (e: any) { return sendError(res, 'Error updating config: ' + e.message); } - } else if (plugin.runtime.module?.plugin_config_schema || plugin.runtime.module?.plugin_config_ui || plugin.runtime.module?.plugin_config_controller) { - // Default behavior: write to default config path - try { - const configPath = plugin.runtime.context?.configPath || pluginManager.getPluginConfigPath(id); + } - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); - - // Auto-Reload plugin to apply changes - await pluginManager.reloadPlugin(id); - - return sendSuccess(res, { message: 'Config saved and plugin reloaded' }); - } catch (e: any) { - return sendError(res, 'Error saving config: ' + e.message); + // 直接写入配置文件(支持未加载的插件预配置) + try { + const configPath = plugin.runtime.context?.configPath || pluginManager.getPluginConfigPath(id); + const configDir = path.dirname(configPath); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); } - } else { - return sendError(res, 'Plugin does not support config update'); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + + // 只有已加载的插件才重载 + if (plugin.loaded) { + await pluginManager.reloadPlugin(id); + return sendSuccess(res, { message: 'Config saved and plugin reloaded' }); + } + return sendSuccess(res, { message: 'Config saved' }); + } catch (e: any) { + return sendError(res, 'Error saving config: ' + e.message); } };