From b0d88d3705709990575e078ebbf22923bb8fc2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Wed, 14 Jan 2026 17:52:38 +0800 Subject: [PATCH] Refactor Satori actions with schema validation and router Refactored all Satori action classes to use TypeBox schemas for payload validation and unified action naming via a new router. Added schema-based parameter checking to the SatoriAction base class. Introduced new actions for guild and member approval, and login retrieval. Centralized action name constants and types in a new router module. Enhanced event and message APIs with more structured event types and parsing logic. Added helper utilities for XML parsing. Updated exports and registration logic to support the new structure. --- packages/napcat-satori/action/SatoriAction.ts | 69 ++- .../action/channel/ChannelGet.ts | 19 +- .../action/channel/ChannelList.ts | 21 +- .../action/guild/GuildApprove.ts | 39 ++ .../napcat-satori/action/guild/GuildGet.ts | 19 +- .../napcat-satori/action/guild/GuildList.ts | 17 +- .../action/guild/GuildMemberApprove.ts | 39 ++ .../action/guild/GuildMemberGet.ts | 19 +- .../action/guild/GuildMemberKick.ts | 21 +- .../action/guild/GuildMemberList.ts | 19 +- .../action/guild/GuildMemberMute.ts | 21 +- packages/napcat-satori/action/index.ts | 10 +- .../napcat-satori/action/login/LoginGet.ts | 26 ++ .../action/message/MessageCreate.ts | 19 +- .../action/message/MessageDelete.ts | 19 +- .../action/message/MessageGet.ts | 19 +- packages/napcat-satori/action/router.ts | 57 +++ .../action/upload/UploadCreate.ts | 15 +- .../action/user/FriendApprove.ts | 23 +- .../napcat-satori/action/user/FriendList.ts | 17 +- packages/napcat-satori/action/user/UserGet.ts | 17 +- packages/napcat-satori/api/event.ts | 408 ++++++++++++------ packages/napcat-satori/api/msg.ts | 403 ++++++++++------- packages/napcat-satori/helper/index.ts | 1 + packages/napcat-satori/helper/xml.ts | 320 ++++++++++++++ packages/napcat-satori/index.ts | 20 +- packages/napcat-satori/network/http-server.ts | 266 ++---------- packages/napcat-satori/package.json | 5 +- packages/napcat-satori/tsconfig.json | 9 +- pnpm-lock.yaml | 260 +++++++++++ tsconfig.base.json | 2 +- 31 files changed, 1575 insertions(+), 644 deletions(-) create mode 100644 packages/napcat-satori/action/guild/GuildApprove.ts create mode 100644 packages/napcat-satori/action/guild/GuildMemberApprove.ts create mode 100644 packages/napcat-satori/action/login/LoginGet.ts create mode 100644 packages/napcat-satori/action/router.ts create mode 100644 packages/napcat-satori/helper/index.ts create mode 100644 packages/napcat-satori/helper/xml.ts diff --git a/packages/napcat-satori/action/SatoriAction.ts b/packages/napcat-satori/action/SatoriAction.ts index f0fb7f8f..cd505796 100644 --- a/packages/napcat-satori/action/SatoriAction.ts +++ b/packages/napcat-satori/action/SatoriAction.ts @@ -1,17 +1,84 @@ import { NapCatCore } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; +import Ajv, { ErrorObject, ValidateFunction } from 'ajv'; +import { TSchema } from '@sinclair/typebox'; + +export interface SatoriCheckResult { + valid: boolean; + message?: string; +} + +export interface SatoriResponse { + data?: T; + error?: { + code: number; + message: string; + }; +} + +export class SatoriResponseHelper { + static success (data: T): SatoriResponse { + return { data }; + } + + static error (code: number, message: string): SatoriResponse { + return { error: { code, message } }; + } +} export abstract class SatoriAction { abstract actionName: string; protected satoriAdapter: NapCatSatoriAdapter; protected core: NapCatCore; + payloadSchema?: TSchema = undefined; + private validate?: ValidateFunction = undefined; constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { this.satoriAdapter = satoriAdapter; this.core = core; } - abstract handle (payload: PayloadType): Promise; + /** + * 验证请求参数 + */ + protected async check (payload: PayloadType): Promise { + if (this.payloadSchema) { + this.validate = new Ajv({ + allowUnionTypes: true, + useDefaults: true, + coerceTypes: true, + }).compile(this.payloadSchema); + } + + if (this.validate && !this.validate(payload)) { + const errors = this.validate.errors as ErrorObject[]; + const errorMessages = errors.map( + (e) => `Key: ${e.instancePath.split('/').slice(1).join('.')}, Message: ${e.message}` + ); + return { + valid: false, + message: errorMessages.join('\n') ?? '未知错误', + }; + } + + return { valid: true }; + } + + /** + * 处理请求入口(带验证) + */ + async handle (payload: PayloadType): Promise { + const checkResult = await this.check(payload); + if (!checkResult.valid) { + throw new Error(checkResult.message || '参数验证失败'); + } + return this._handle(payload); + } + + /** + * 实际处理逻辑(子类实现) + */ + protected abstract _handle (payload: PayloadType): Promise; protected get logger () { return this.core.context.logger; diff --git a/packages/napcat-satori/action/channel/ChannelGet.ts b/packages/napcat-satori/action/channel/ChannelGet.ts index c88049e6..ac03ab9a 100644 --- a/packages/napcat-satori/action/channel/ChannelGet.ts +++ b/packages/napcat-satori/action/channel/ChannelGet.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriChannel, SatoriChannelType } from '../../types'; -interface ChannelGetPayload { - channel_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), +}); -export class ChannelGetAction extends SatoriAction { - actionName = 'channel.get'; +type Payload = Static; - async handle (payload: ChannelGetPayload): Promise { +export class ChannelGetAction extends SatoriAction { + actionName = SatoriActionName.ChannelGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id } = payload; const parts = channel_id.split(':'); @@ -31,7 +36,7 @@ export class ChannelGetAction extends SatoriAction e.groupCode === id); + const group = groups.find((e) => e.groupCode === id); if (!group) { // 如果缓存中没有,尝试获取详细信息 diff --git a/packages/napcat-satori/action/channel/ChannelList.ts b/packages/napcat-satori/action/channel/ChannelList.ts index 55f47d56..b0de47c0 100644 --- a/packages/napcat-satori/action/channel/ChannelList.ts +++ b/packages/napcat-satori/action/channel/ChannelList.ts @@ -1,21 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriChannel, SatoriChannelType, SatoriPageResult } from '../../types'; -interface ChannelListPayload { - guild_id: string; - next?: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + next: Type.Optional(Type.String()), +}); -export class ChannelListAction extends SatoriAction> { - actionName = 'channel.list'; +type Payload = Static; - async handle (payload: ChannelListPayload): Promise> { +export class ChannelListAction extends SatoriAction> { + actionName = SatoriActionName.ChannelList; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise> { const { guild_id } = payload; // 在 QQ 中,群组只有一个文本频道 // 先从群列表缓存中查找 const groups = await this.core.apis.GroupApi.getGroups(); - let group = groups.find(e => e.groupCode === guild_id); + const group = groups.find((e) => e.groupCode === guild_id); let groupName: string; if (!group) { diff --git a/packages/napcat-satori/action/guild/GuildApprove.ts b/packages/napcat-satori/action/guild/GuildApprove.ts new file mode 100644 index 00000000..caf1b880 --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildApprove.ts @@ -0,0 +1,39 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { GroupNotifyMsgType, NTGroupRequestOperateTypes } from 'napcat-core'; + +const SchemaData = Type.Object({ + message_id: Type.String(), // 邀请请求的 seq + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class GuildApproveAction extends SatoriAction { + actionName = SatoriActionName.GuildApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { + const { message_id, approve, comment } = payload; + + // message_id 是邀请请求的 seq + const notifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100); + const notify = notifies.find( + (e) => + e.seq == message_id && // 使用 loose equality 以防类型不匹配 + (e.type === GroupNotifyMsgType.INVITED_BY_MEMBER || e.type === GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS) + ); + + if (!notify) { + throw new Error(`未找到加群邀请: ${message_id}`); + } + + const operateType = approve + ? NTGroupRequestOperateTypes.KAGREE + : NTGroupRequestOperateTypes.KREFUSE; + + await this.core.apis.GroupApi.handleGroupRequest(false, notify, operateType, comment); + } +} diff --git a/packages/napcat-satori/action/guild/GuildGet.ts b/packages/napcat-satori/action/guild/GuildGet.ts index d254902c..521451e5 100644 --- a/packages/napcat-satori/action/guild/GuildGet.ts +++ b/packages/napcat-satori/action/guild/GuildGet.ts @@ -1,19 +1,24 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuild } from '../../types'; -interface GuildGetPayload { - guild_id: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), +}); -export class GuildGetAction extends SatoriAction { - actionName = 'guild.get'; +type Payload = Static; - async handle (payload: GuildGetPayload): Promise { +export class GuildGetAction extends SatoriAction { + actionName = SatoriActionName.GuildGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id } = payload; // 先从群列表缓存中查找 const groups = await this.core.apis.GroupApi.getGroups(); - let group = groups.find(e => e.groupCode === guild_id); + const group = groups.find((e) => e.groupCode === guild_id); if (!group) { // 如果缓存中没有,尝试获取详细信息 diff --git a/packages/napcat-satori/action/guild/GuildList.ts b/packages/napcat-satori/action/guild/GuildList.ts index 5b5bc20e..934357ce 100644 --- a/packages/napcat-satori/action/guild/GuildList.ts +++ b/packages/napcat-satori/action/guild/GuildList.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuild, SatoriPageResult } from '../../types'; -interface GuildListPayload { - next?: string; -} +const SchemaData = Type.Object({ + next: Type.Optional(Type.String()), +}); -export class GuildListAction extends SatoriAction> { - actionName = 'guild.list'; +type Payload = Static; - async handle (_payload: GuildListPayload): Promise> { +export class GuildListAction extends SatoriAction> { + actionName = SatoriActionName.GuildList; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise> { const groups = await this.core.apis.GroupApi.getGroups(true); const guilds: SatoriGuild[] = groups.map((group) => ({ diff --git a/packages/napcat-satori/action/guild/GuildMemberApprove.ts b/packages/napcat-satori/action/guild/GuildMemberApprove.ts new file mode 100644 index 00000000..6e2189ea --- /dev/null +++ b/packages/napcat-satori/action/guild/GuildMemberApprove.ts @@ -0,0 +1,39 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { GroupNotifyMsgType, NTGroupRequestOperateTypes } from 'napcat-core'; + +const SchemaData = Type.Object({ + message_id: Type.String(), // 入群请求的 seq + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class GuildMemberApproveAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { + const { message_id, approve, comment } = payload; + + // message_id 是入群请求的 seq + const notifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100); + const notify = notifies.find( + (e) => + e.seq === message_id && + e.type === GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS + ); + + if (!notify) { + throw new Error(`未找到入群请求: ${message_id}`); + } + + const operateType = approve + ? NTGroupRequestOperateTypes.KAGREE + : NTGroupRequestOperateTypes.KREFUSE; + + await this.core.apis.GroupApi.handleGroupRequest(false, notify, operateType, comment); + } +} diff --git a/packages/napcat-satori/action/guild/GuildMemberGet.ts b/packages/napcat-satori/action/guild/GuildMemberGet.ts index 07616fa8..8750daeb 100644 --- a/packages/napcat-satori/action/guild/GuildMemberGet.ts +++ b/packages/napcat-satori/action/guild/GuildMemberGet.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuildMember } from '../../types'; -interface GuildMemberGetPayload { - guild_id: string; - user_id: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), +}); -export class GuildMemberGetAction extends SatoriAction { - actionName = 'guild.member.get'; +type Payload = Static; - async handle (payload: GuildMemberGetPayload): Promise { +export class GuildMemberGetAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id } = payload; const memberInfo = await this.core.apis.GroupApi.getGroupMember(guild_id, user_id); diff --git a/packages/napcat-satori/action/guild/GuildMemberKick.ts b/packages/napcat-satori/action/guild/GuildMemberKick.ts index b0230be0..8fb26305 100644 --- a/packages/napcat-satori/action/guild/GuildMemberKick.ts +++ b/packages/napcat-satori/action/guild/GuildMemberKick.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface GuildMemberKickPayload { - guild_id: string; - user_id: string; - permanent?: boolean; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), + permanent: Type.Optional(Type.Boolean({ default: false })), +}); -export class GuildMemberKickAction extends SatoriAction { - actionName = 'guild.member.kick'; +type Payload = Static; - async handle (payload: GuildMemberKickPayload): Promise { +export class GuildMemberKickAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberKick; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id, permanent } = payload; await this.core.apis.GroupApi.kickMember( diff --git a/packages/napcat-satori/action/guild/GuildMemberList.ts b/packages/napcat-satori/action/guild/GuildMemberList.ts index 2f748848..5474ad25 100644 --- a/packages/napcat-satori/action/guild/GuildMemberList.ts +++ b/packages/napcat-satori/action/guild/GuildMemberList.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriGuildMember, SatoriPageResult } from '../../types'; import { GroupMember } from 'napcat-core'; -interface GuildMemberListPayload { - guild_id: string; - next?: string; -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + next: Type.Optional(Type.String()), +}); -export class GuildMemberListAction extends SatoriAction> { - actionName = 'guild.member.list'; +type Payload = Static; - async handle (payload: GuildMemberListPayload): Promise> { +export class GuildMemberListAction extends SatoriAction> { + actionName = SatoriActionName.GuildMemberList; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise> { const { guild_id } = payload; // 使用 getGroupMemberAll 获取所有群成员 diff --git a/packages/napcat-satori/action/guild/GuildMemberMute.ts b/packages/napcat-satori/action/guild/GuildMemberMute.ts index a8481ec8..ad7f5b52 100644 --- a/packages/napcat-satori/action/guild/GuildMemberMute.ts +++ b/packages/napcat-satori/action/guild/GuildMemberMute.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface GuildMemberMutePayload { - guild_id: string; - user_id: string; - duration?: number; // 禁言时长(毫秒),0 表示解除禁言 -} +const SchemaData = Type.Object({ + guild_id: Type.String(), + user_id: Type.String(), + duration: Type.Optional(Type.Number({ default: 0 })), // 禁言时长(毫秒),0 表示解除禁言 +}); -export class GuildMemberMuteAction extends SatoriAction { - actionName = 'guild.member.mute'; +type Payload = Static; - async handle (payload: GuildMemberMutePayload): Promise { +export class GuildMemberMuteAction extends SatoriAction { + actionName = SatoriActionName.GuildMemberMute; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { guild_id, user_id, duration } = payload; // 将毫秒转换为秒 diff --git a/packages/napcat-satori/action/index.ts b/packages/napcat-satori/action/index.ts index e19bd170..b9d6b3ec 100644 --- a/packages/napcat-satori/action/index.ts +++ b/packages/napcat-satori/action/index.ts @@ -10,13 +10,16 @@ import { ChannelGetAction } from './channel/ChannelGet'; import { ChannelListAction } from './channel/ChannelList'; import { GuildGetAction } from './guild/GuildGet'; import { GuildListAction } from './guild/GuildList'; +import { GuildApproveAction } from './guild/GuildApprove'; import { GuildMemberGetAction } from './guild/GuildMemberGet'; import { GuildMemberListAction } from './guild/GuildMemberList'; import { GuildMemberKickAction } from './guild/GuildMemberKick'; import { GuildMemberMuteAction } from './guild/GuildMemberMute'; +import { GuildMemberApproveAction } from './guild/GuildMemberApprove'; import { UserGetAction } from './user/UserGet'; import { FriendListAction } from './user/FriendList'; import { FriendApproveAction } from './user/FriendApprove'; +import { LoginGetAction } from './login/LoginGet'; import { UploadCreateAction } from './upload/UploadCreate'; export type SatoriActionMap = Map>; @@ -38,14 +41,18 @@ export function createSatoriActionMap ( // 群组相关 new GuildGetAction(satoriAdapter, core), new GuildListAction(satoriAdapter, core), + new GuildApproveAction(satoriAdapter, core), new GuildMemberGetAction(satoriAdapter, core), new GuildMemberListAction(satoriAdapter, core), new GuildMemberKickAction(satoriAdapter, core), new GuildMemberMuteAction(satoriAdapter, core), + new GuildMemberApproveAction(satoriAdapter, core), // 用户相关 new UserGetAction(satoriAdapter, core), new FriendListAction(satoriAdapter, core), new FriendApproveAction(satoriAdapter, core), + // 登录相关 + new LoginGetAction(satoriAdapter, core), // 上传相关 new UploadCreateAction(satoriAdapter, core), ]; @@ -57,4 +64,5 @@ export function createSatoriActionMap ( return actionMap; } -export { SatoriAction } from './SatoriAction'; +export { SatoriAction, SatoriCheckResult, SatoriResponse, SatoriResponseHelper } from './SatoriAction'; +export { SatoriActionName, SatoriActionNameType } from './router'; diff --git a/packages/napcat-satori/action/login/LoginGet.ts b/packages/napcat-satori/action/login/LoginGet.ts new file mode 100644 index 00000000..bc2847f1 --- /dev/null +++ b/packages/napcat-satori/action/login/LoginGet.ts @@ -0,0 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; +import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; +import { SatoriLogin, SatoriLoginStatus } from '../../types'; + +const SchemaData = Type.Object({}); + +type Payload = Static; + +export class LoginGetAction extends SatoriAction { + actionName = SatoriActionName.LoginGet; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise { + return { + user: { + id: this.selfInfo.uin, + name: this.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfInfo.uin}&s=640`, + }, + self_id: this.selfInfo.uin, + platform: this.platform, + status: SatoriLoginStatus.ONLINE, + }; + } +} diff --git a/packages/napcat-satori/action/message/MessageCreate.ts b/packages/napcat-satori/action/message/MessageCreate.ts index 47f724c6..ed19fdc4 100644 --- a/packages/napcat-satori/action/message/MessageCreate.ts +++ b/packages/napcat-satori/action/message/MessageCreate.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriMessage, SatoriChannelType } from '../../types'; import { ChatType, SendMessageElement } from 'napcat-core'; -interface MessageCreatePayload { - channel_id: string; - content: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + content: Type.String(), +}); -export class MessageCreateAction extends SatoriAction { - actionName = 'message.create'; +type Payload = Static; - async handle (payload: MessageCreatePayload): Promise { +export class MessageCreateAction extends SatoriAction { + actionName = SatoriActionName.MessageCreate; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, content } = payload; // 解析 channel_id,格式: private:{user_id} 或 group:{group_id} diff --git a/packages/napcat-satori/action/message/MessageDelete.ts b/packages/napcat-satori/action/message/MessageDelete.ts index ebc408a6..1e03ab57 100644 --- a/packages/napcat-satori/action/message/MessageDelete.ts +++ b/packages/napcat-satori/action/message/MessageDelete.ts @@ -1,15 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { ChatType } from 'napcat-core'; -interface MessageDeletePayload { - channel_id: string; - message_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + message_id: Type.String(), +}); -export class MessageDeleteAction extends SatoriAction { - actionName = 'message.delete'; +type Payload = Static; - async handle (payload: MessageDeletePayload): Promise { +export class MessageDeleteAction extends SatoriAction { + actionName = SatoriActionName.MessageDelete; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, message_id } = payload; const parts = channel_id.split(':'); diff --git a/packages/napcat-satori/action/message/MessageGet.ts b/packages/napcat-satori/action/message/MessageGet.ts index f9bd2c17..35a9ed9d 100644 --- a/packages/napcat-satori/action/message/MessageGet.ts +++ b/packages/napcat-satori/action/message/MessageGet.ts @@ -1,16 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriMessage, SatoriChannelType } from '../../types'; import { ChatType } from 'napcat-core'; -interface MessageGetPayload { - channel_id: string; - message_id: string; -} +const SchemaData = Type.Object({ + channel_id: Type.String(), + message_id: Type.String(), +}); -export class MessageGetAction extends SatoriAction { - actionName = 'message.get'; +type Payload = Static; - async handle (payload: MessageGetPayload): Promise { +export class MessageGetAction extends SatoriAction { + actionName = SatoriActionName.MessageGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { channel_id, message_id } = payload; const parts = channel_id.split(':'); diff --git a/packages/napcat-satori/action/router.ts b/packages/napcat-satori/action/router.ts new file mode 100644 index 00000000..96628354 --- /dev/null +++ b/packages/napcat-satori/action/router.ts @@ -0,0 +1,57 @@ +/** + * Satori Action 名称映射 + */ +export const SatoriActionName = { + // 消息相关 + MessageCreate: 'message.create', + MessageGet: 'message.get', + MessageDelete: 'message.delete', + MessageUpdate: 'message.update', + MessageList: 'message.list', + + // 频道相关 + ChannelGet: 'channel.get', + ChannelList: 'channel.list', + ChannelCreate: 'channel.create', + ChannelUpdate: 'channel.update', + ChannelDelete: 'channel.delete', + ChannelMute: 'channel.mute', + + // 群组/公会相关 + GuildGet: 'guild.get', + GuildList: 'guild.list', + GuildApprove: 'guild.approve', + + // 群成员相关 + GuildMemberGet: 'guild.member.get', + GuildMemberList: 'guild.member.list', + GuildMemberKick: 'guild.member.kick', + GuildMemberMute: 'guild.member.mute', + GuildMemberApprove: 'guild.member.approve', + GuildMemberRole: 'guild.member.role', + + // 角色相关 + GuildRoleList: 'guild.role.list', + GuildRoleCreate: 'guild.role.create', + GuildRoleUpdate: 'guild.role.update', + GuildRoleDelete: 'guild.role.delete', + + // 用户相关 + UserGet: 'user.get', + UserChannelCreate: 'user.channel.create', + + // 好友相关 + FriendList: 'friend.list', + FriendApprove: 'friend.approve', + + // 登录相关 + LoginGet: 'login.get', + + // 上传相关 + UploadCreate: 'upload.create', + + // 内部互操作(Satori 可选) + InternalAction: 'internal.action', +} as const; + +export type SatoriActionNameType = typeof SatoriActionName[keyof typeof SatoriActionName]; diff --git a/packages/napcat-satori/action/upload/UploadCreate.ts b/packages/napcat-satori/action/upload/UploadCreate.ts index 0bb2042e..daa71f34 100644 --- a/packages/napcat-satori/action/upload/UploadCreate.ts +++ b/packages/napcat-satori/action/upload/UploadCreate.ts @@ -1,17 +1,20 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface UploadCreatePayload { - [key: string]: unknown; -} +const SchemaData = Type.Record(Type.String(), Type.Unknown()); + +type Payload = Static; interface UploadResult { [key: string]: string; } -export class UploadCreateAction extends SatoriAction { - actionName = 'upload.create'; +export class UploadCreateAction extends SatoriAction { + actionName = SatoriActionName.UploadCreate; + override payloadSchema = SchemaData; - async handle (payload: UploadCreatePayload): Promise { + protected async _handle (payload: Payload): Promise { const result: UploadResult = {}; // 处理上传的文件 diff --git a/packages/napcat-satori/action/user/FriendApprove.ts b/packages/napcat-satori/action/user/FriendApprove.ts index 8b3a13f8..50792b87 100644 --- a/packages/napcat-satori/action/user/FriendApprove.ts +++ b/packages/napcat-satori/action/user/FriendApprove.ts @@ -1,21 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; -interface FriendApprovePayload { - message_id: string; - approve: boolean; - comment?: string; -} +const SchemaData = Type.Object({ + message_id: Type.String(), + approve: Type.Boolean(), + comment: Type.Optional(Type.String()), +}); -export class FriendApproveAction extends SatoriAction { - actionName = 'friend.approve'; +type Payload = Static; - async handle (payload: FriendApprovePayload): Promise { +export class FriendApproveAction extends SatoriAction { + actionName = SatoriActionName.FriendApprove; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { message_id, approve } = payload; // message_id 格式: reqTime (好友请求的时间戳) // 需要从好友请求列表中找到对应的请求 const buddyReqData = await this.core.apis.FriendApi.getBuddyReq(); - const notify = buddyReqData.buddyReqs.find(e => e.reqTime === message_id); + const notify = buddyReqData.buddyReqs.find((e) => e.reqTime === message_id); if (!notify) { throw new Error(`未找到好友请求: ${message_id}`); diff --git a/packages/napcat-satori/action/user/FriendList.ts b/packages/napcat-satori/action/user/FriendList.ts index 95145ca6..cbe0e179 100644 --- a/packages/napcat-satori/action/user/FriendList.ts +++ b/packages/napcat-satori/action/user/FriendList.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriUser, SatoriPageResult } from '../../types'; -interface FriendListPayload { - next?: string; -} +const SchemaData = Type.Object({ + next: Type.Optional(Type.String()), +}); -export class FriendListAction extends SatoriAction> { - actionName = 'friend.list'; +type Payload = Static; - async handle (_payload: FriendListPayload): Promise> { +export class FriendListAction extends SatoriAction> { + actionName = SatoriActionName.FriendList; + override payloadSchema = SchemaData; + + protected async _handle (_payload: Payload): Promise> { const friends = await this.core.apis.FriendApi.getBuddy(); const friendList: SatoriUser[] = friends.map((friend) => ({ diff --git a/packages/napcat-satori/action/user/UserGet.ts b/packages/napcat-satori/action/user/UserGet.ts index da5ec042..de23c178 100644 --- a/packages/napcat-satori/action/user/UserGet.ts +++ b/packages/napcat-satori/action/user/UserGet.ts @@ -1,14 +1,19 @@ +import { Static, Type } from '@sinclair/typebox'; import { SatoriAction } from '../SatoriAction'; +import { SatoriActionName } from '../router'; import { SatoriUser } from '../../types'; -interface UserGetPayload { - user_id: string; -} +const SchemaData = Type.Object({ + user_id: Type.String(), +}); -export class UserGetAction extends SatoriAction { - actionName = 'user.get'; +type Payload = Static; - async handle (payload: UserGetPayload): Promise { +export class UserGetAction extends SatoriAction { + actionName = SatoriActionName.UserGet; + override payloadSchema = SchemaData; + + protected async _handle (payload: Payload): Promise { const { user_id } = payload; const uid = await this.core.apis.UserApi.getUidByUinV2(user_id); diff --git a/packages/napcat-satori/api/event.ts b/packages/napcat-satori/api/event.ts index 7d90bd80..f30d133c 100644 --- a/packages/napcat-satori/api/event.ts +++ b/packages/napcat-satori/api/event.ts @@ -1,4 +1,4 @@ -import { NapCatCore, RawMessage, ChatType } from 'napcat-core'; +import { NapCatCore, RawMessage, ChatType, GroupNotify, FriendRequest } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; import { SatoriEvent, @@ -6,6 +6,51 @@ import { SatoriLoginStatus, } from '../types'; +/** + * Satori 事件类型定义 + */ +export const SatoriEventType = { + // 消息事件 + MESSAGE_CREATED: 'message-created', + MESSAGE_UPDATED: 'message-updated', + MESSAGE_DELETED: 'message-deleted', + + // 频道事件 + CHANNEL_CREATED: 'channel-created', + CHANNEL_UPDATED: 'channel-updated', + CHANNEL_DELETED: 'channel-deleted', + + // 群组/公会事件 + GUILD_ADDED: 'guild-added', + GUILD_UPDATED: 'guild-updated', + GUILD_REMOVED: 'guild-removed', + GUILD_REQUEST: 'guild-request', + + // 群成员事件 + GUILD_MEMBER_ADDED: 'guild-member-added', + GUILD_MEMBER_UPDATED: 'guild-member-updated', + GUILD_MEMBER_REMOVED: 'guild-member-removed', + GUILD_MEMBER_REQUEST: 'guild-member-request', + + // 角色事件 + GUILD_ROLE_CREATED: 'guild-role-created', + GUILD_ROLE_UPDATED: 'guild-role-updated', + GUILD_ROLE_DELETED: 'guild-role-deleted', + + // 好友事件 + FRIEND_REQUEST: 'friend-request', + + // 登录事件 + LOGIN_ADDED: 'login-added', + LOGIN_REMOVED: 'login-removed', + LOGIN_UPDATED: 'login-updated', + + // 内部事件 + INTERNAL: 'internal', +} as const; + +export type SatoriEventTypeName = typeof SatoriEventType[keyof typeof SatoriEventType]; + export class SatoriEventApi { private satoriAdapter: NapCatSatoriAdapter; private core: NapCatCore; @@ -28,6 +73,19 @@ export class SatoriEventApi { return this.core.selfInfo.uin; } + /** + * 创建基础事件结构 + */ + private createBaseEvent (type: SatoriEventTypeName): SatoriEvent { + return { + id: this.getNextEventId(), + type, + platform: this.platform, + self_id: this.selfId, + timestamp: Date.now(), + }; + } + /** * 将 NapCat 消息转换为 Satori 事件 */ @@ -36,25 +94,20 @@ export class SatoriEventApi { const content = await this.satoriAdapter.apis.MsgApi.parseElements(message.elements); const isPrivate = message.chatType === ChatType.KCHATTYPEC2C; - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'message-created', - platform: this.platform, - self_id: this.selfId, - timestamp: parseInt(message.msgTime) * 1000, - channel: { - id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, - type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, - }, - user: { - id: message.senderUin, - name: message.sendNickName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, - }, - message: { - id: message.msgId, - content, - }, + const event = this.createBaseEvent(SatoriEventType.MESSAGE_CREATED); + event.timestamp = parseInt(message.msgTime) * 1000; + event.channel = { + id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: message.senderUin, + name: message.sendNickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, + }; + event.message = { + id: message.msgId, + content, }; if (!isPrivate) { @@ -75,65 +128,95 @@ export class SatoriEventApi { } } + /** + * 创建消息更新事件 + */ + async createMessageUpdatedEvent (message: RawMessage): Promise { + try { + const content = await this.satoriAdapter.apis.MsgApi.parseElements(message.elements); + const isPrivate = message.chatType === ChatType.KCHATTYPEC2C; + + const event = this.createBaseEvent(SatoriEventType.MESSAGE_UPDATED); + event.channel = { + id: isPrivate ? `private:${message.senderUin}` : `group:${message.peerUin}`, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: message.senderUin, + name: message.sendNickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${message.senderUin}&s=640`, + }; + event.message = { + id: message.msgId, + content, + }; + + return event; + } catch (error) { + this.core.context.logger.logError('[Satori] 创建消息更新事件失败:', error); + return null; + } + } + /** * 创建好友请求事件 */ - createFriendRequestEvent ( - userId: string, - userName: string, - comment: string, - flag: string - ): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'friend-request', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: flag, - content: comment, - }, + createFriendRequestEvent (request: FriendRequest): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.FRIEND_REQUEST); + event.user = { + id: request.friendUid, + name: request.friendNick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${request.friendUid}&s=640`, }; + event.message = { + id: request.reqTime, + content: request.extWords, + }; + return event; } /** * 创建群组加入请求事件 */ - createGuildMemberRequestEvent ( - guildId: string, - guildName: string, - userId: string, - userName: string, - comment: string, - flag: string - ): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'guild-member-request', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: flag, - content: comment, - }, + createGuildMemberRequestEvent (notify: GroupNotify): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_REQUEST); + event.guild = { + id: notify.group.groupCode, + name: notify.group.groupName, + avatar: `https://p.qlogo.cn/gh/${notify.group.groupCode}/${notify.group.groupCode}/640`, }; + event.user = { + id: notify.user1.uid, + name: notify.user1.nickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${notify.user1.uid}&s=640`, + }; + event.message = { + id: notify.seq, + content: notify.postscript, + }; + return event; + } + + /** + * 创建群组邀请事件 + */ + createGuildRequestEvent (notify: GroupNotify): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_REQUEST); + event.guild = { + id: notify.group.groupCode, + name: notify.group.groupName, + avatar: `https://p.qlogo.cn/gh/${notify.group.groupCode}/${notify.group.groupCode}/640`, + }; + event.user = { + id: notify.user2.uid, + name: notify.user2.nickName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${notify.user2.uid}&s=640`, + }; + event.message = { + id: notify.seq, + content: notify.postscript, + }; + return event; } /** @@ -146,22 +229,16 @@ export class SatoriEventApi { userName: string, operatorId?: string ): SatoriEvent { - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'guild-member-added', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_ADDED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + event.user = { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, }; if (operatorId) { @@ -184,22 +261,16 @@ export class SatoriEventApi { userName: string, operatorId?: string ): SatoriEvent { - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'guild-member-removed', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - guild: { - id: guildId, - name: guildName, - avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, - }, - user: { - id: userId, - name: userName, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, + const event = this.createBaseEvent(SatoriEventType.GUILD_MEMBER_REMOVED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + event.user = { + id: userId, + name: userName, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, }; if (operatorId) { @@ -212,6 +283,38 @@ export class SatoriEventApi { return event; } + /** + * 创建群添加事件(自己被邀请或加入群) + */ + createGuildAddedEvent ( + guildId: string, + guildName: string + ): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_ADDED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + return event; + } + + /** + * 创建群移除事件(被踢出或退出群) + */ + createGuildRemovedEvent ( + guildId: string, + guildName: string + ): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.GUILD_REMOVED); + event.guild = { + id: guildId, + name: guildName, + avatar: `https://p.qlogo.cn/gh/${guildId}/${guildId}/640`, + }; + return event; + } + /** * 创建消息删除事件 */ @@ -222,24 +325,18 @@ export class SatoriEventApi { operatorId?: string ): SatoriEvent { const isPrivate = channelId.startsWith('private:'); - const event: SatoriEvent = { - id: this.getNextEventId(), - type: 'message-deleted', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - channel: { - id: channelId, - type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, - }, - user: { - id: userId, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, - }, - message: { - id: messageId, - content: '', - }, + const event = this.createBaseEvent(SatoriEventType.MESSAGE_DELETED); + event.channel = { + id: channelId, + type: isPrivate ? SatoriChannelType.DIRECT : SatoriChannelType.TEXT, + }; + event.user = { + id: userId, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`, + }; + event.message = { + id: messageId, + content: '', }; if (operatorId) { @@ -252,26 +349,69 @@ export class SatoriEventApi { return event; } + /** + * 创建登录添加事件 + */ + createLoginAddedEvent (): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.LOGIN_ADDED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, + }, + self_id: this.selfId, + platform: this.platform, + status: SatoriLoginStatus.ONLINE, + }; + return event; + } + + /** + * 创建登录移除事件 + */ + createLoginRemovedEvent (): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.LOGIN_REMOVED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, + }, + self_id: this.selfId, + platform: this.platform, + status: SatoriLoginStatus.OFFLINE, + }; + return event; + } + /** * 创建登录状态更新事件 */ createLoginUpdatedEvent (status: SatoriLoginStatus): SatoriEvent { - return { - id: this.getNextEventId(), - type: 'login-updated', - platform: this.platform, - self_id: this.selfId, - timestamp: Date.now(), - login: { - user: { - id: this.selfId, - name: this.core.selfInfo.nick, - avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, - }, - self_id: this.selfId, - platform: this.platform, - status, + const event = this.createBaseEvent(SatoriEventType.LOGIN_UPDATED); + event.login = { + user: { + id: this.selfId, + name: this.core.selfInfo.nick, + avatar: `https://q1.qlogo.cn/g?b=qq&nk=${this.selfId}&s=640`, }, + self_id: this.selfId, + platform: this.platform, + status, }; + return event; + } + + /** + * 创建内部事件(用于扩展) + */ + createInternalEvent (typeName: string, data: Record): SatoriEvent { + const event = this.createBaseEvent(SatoriEventType.INTERNAL); + event._type = typeName; + event._data = data; + return event; } } + +export { SatoriEventType as EventType }; diff --git a/packages/napcat-satori/api/msg.ts b/packages/napcat-satori/api/msg.ts index edc7116d..588648bf 100644 --- a/packages/napcat-satori/api/msg.ts +++ b/packages/napcat-satori/api/msg.ts @@ -1,75 +1,32 @@ import { NapCatCore, MessageElement, ElementType, NTMsgAtType } from 'napcat-core'; import { NapCatSatoriAdapter } from '../index'; +import SatoriElement from '@satorijs/element'; +/** + * Satori 消息处理 API + * 使用 @satorijs/element 处理消息格式转换 + */ export class SatoriMsgApi { private core: NapCatCore; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private _adapter: NapCatSatoriAdapter; - constructor (_satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) { + this._adapter = satoriAdapter; this.core = core; } /** * 解析 Satori 消息内容为 NapCat 消息元素 + * 使用 @satorijs/element 解析 */ async parseContent (content: string): Promise { const elements: MessageElement[] = []; + const parsed = SatoriElement.parse(content); - // 简单的 XML 解析 - const tagRegex = /<(\w+)([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - - while ((match = tagRegex.exec(content)) !== null) { - // 处理标签前的文本 - if (match.index > lastIndex) { - const text = content.slice(lastIndex, match.index); - if (text.trim()) { - elements.push(this.createTextElement(text)); - } - } - - const [, tagName, attrs = '', innerContent] = match; - const parsedAttrs = this.parseAttributes(attrs); - - switch (tagName) { - case 'at': - elements.push(await this.createAtElement(parsedAttrs)); - break; - case 'img': - case 'image': - elements.push(await this.createImageElement(parsedAttrs)); - break; - case 'audio': - elements.push(await this.createAudioElement(parsedAttrs)); - break; - case 'video': - elements.push(await this.createVideoElement(parsedAttrs)); - break; - case 'file': - elements.push(await this.createFileElement(parsedAttrs)); - break; - case 'face': - elements.push(this.createFaceElement(parsedAttrs)); - break; - case 'quote': - elements.push(await this.createQuoteElement(parsedAttrs)); - break; - default: - // 未知标签,作为文本处理 - if (innerContent) { - elements.push(this.createTextElement(innerContent)); - } - } - - lastIndex = match.index + match[0].length; - } - - // 处理剩余文本 - if (lastIndex < content.length) { - const text = content.slice(lastIndex); - if (text.trim()) { - elements.push(this.createTextElement(text)); - } + for (const elem of parsed) { + const parsedElements = await this.parseSatoriElement(elem); + elements.push(...parsedElements); } // 如果没有解析到任何元素,将整个内容作为文本 @@ -81,73 +38,231 @@ export class SatoriMsgApi { } /** - * 解析 NapCat 消息元素为 Satori 消息内容 + * 解析 satorijs 元素为消息元素 */ - async parseElements (elements: MessageElement[]): Promise { - const parts: string[] = []; + private async parseSatoriElement (elem: SatoriElement): Promise { + const elements: MessageElement[] = []; - for (const element of elements) { - switch (element.elementType) { - case ElementType.TEXT: - if (element.textElement) { - parts.push(this.escapeXml(element.textElement.content)); - } - break; - case ElementType.PIC: - if (element.picElement) { - const src = element.picElement.sourcePath || ''; - parts.push(``); - } - break; - case ElementType.PTT: - if (element.pttElement) { - const src = element.pttElement.filePath || ''; - parts.push(`