mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
1
This commit is contained in:
parent
d6b4c7fae8
commit
3fcc482774
@ -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',
|
||||
|
||||
179
packages/napcat-plugin-aicat/src/tools/packet-tools.ts
Normal file
179
packages/napcat-plugin-aicat/src/tools/packet-tools.ts
Normal file
@ -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<unknown>;
|
||||
}
|
||||
|
||||
let packetMode: 1 | 2 = 2;
|
||||
export const setPacketMode = (mode: 1 | 2) => { packetMode = mode; };
|
||||
export const getPacketMode = () => packetMode;
|
||||
|
||||
function tryDecodeHex (hexStr: string): Record<number, unknown> | 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<string, unknown>;
|
||||
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<string, unknown> = {};
|
||||
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<ToolResult> {
|
||||
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<string, unknown>);
|
||||
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<ToolResult> {
|
||||
const packet: Record<number, unknown> = {
|
||||
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<string | null> {
|
||||
const data: Record<number, unknown> = { 2: { 1: 'MultiMsg', 2: { 1: [{ 3: { 1: { 2: processJson(content) } } }] } } };
|
||||
const packet: Record<number, unknown> = {
|
||||
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<number, unknown>)[2] as Record<number, unknown> | 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<ToolResult> {
|
||||
const resid = await uploadLong(actions, adapter, config, targetId, isGroup, content);
|
||||
if (!resid) return { success: false, error: '上传长消息失败' };
|
||||
const elem: Record<number, unknown> = { 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<ToolResult> {
|
||||
if (!realSeq) {
|
||||
const msgInfo = await actions.call('get_msg', { message_id: messageId }, adapter, config) as Record<string, unknown>;
|
||||
if (msgInfo) realSeq = (msgInfo.retcode === 0 ? (msgInfo.data as Record<string, unknown>)?.real_seq : msgInfo.real_seq) as string;
|
||||
if (!realSeq) return { success: false, error: '未找到 real_seq' };
|
||||
}
|
||||
const seq = parseInt(realSeq);
|
||||
const packet: Record<number, unknown> = { 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<number, unknown>): { senderQQ: string | null; senderName: string | null; } {
|
||||
try {
|
||||
const field3 = pbData[3] as Record<number, unknown> | undefined;
|
||||
const field6 = field3?.[6] as Record<number, unknown> | undefined;
|
||||
const field1In6 = field6?.[1] as Record<number, unknown> | undefined;
|
||||
let senderQQ = field1In6?.[1] !== undefined ? String(field1In6[1]) : null;
|
||||
const field8 = field1In6?.[8] as Record<number, unknown> | 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<number, unknown>): unknown {
|
||||
try {
|
||||
return (((pbData[3] as Record<number, unknown>)?.[6] as Record<number, unknown>)?.[3] as Record<number, unknown>)?.[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<number, unknown>, 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 };
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user