mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 14:41:14 +00:00
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.
This commit is contained in:
parent
32c0c93f3b
commit
b0d88d3705
@ -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<T = unknown> {
|
||||
data?: T;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class SatoriResponseHelper {
|
||||
static success<T> (data: T): SatoriResponse<T> {
|
||||
return { data };
|
||||
}
|
||||
|
||||
static error (code: number, message: string): SatoriResponse<null> {
|
||||
return { error: { code, message } };
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SatoriAction<PayloadType, ReturnType> {
|
||||
abstract actionName: string;
|
||||
protected satoriAdapter: NapCatSatoriAdapter;
|
||||
protected core: NapCatCore;
|
||||
payloadSchema?: TSchema = undefined;
|
||||
private validate?: ValidateFunction<unknown> = undefined;
|
||||
|
||||
constructor (satoriAdapter: NapCatSatoriAdapter, core: NapCatCore) {
|
||||
this.satoriAdapter = satoriAdapter;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
abstract handle (payload: PayloadType): Promise<ReturnType>;
|
||||
/**
|
||||
* 验证请求参数
|
||||
*/
|
||||
protected async check (payload: PayloadType): Promise<SatoriCheckResult> {
|
||||
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<ReturnType> {
|
||||
const checkResult = await this.check(payload);
|
||||
if (!checkResult.valid) {
|
||||
throw new Error(checkResult.message || '参数验证失败');
|
||||
}
|
||||
return this._handle(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际处理逻辑(子类实现)
|
||||
*/
|
||||
protected abstract _handle (payload: PayloadType): Promise<ReturnType>;
|
||||
|
||||
protected get logger () {
|
||||
return this.core.context.logger;
|
||||
|
||||
@ -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<ChannelGetPayload, SatoriChannel> {
|
||||
actionName = 'channel.get';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: ChannelGetPayload): Promise<SatoriChannel> {
|
||||
export class ChannelGetAction extends SatoriAction<Payload, SatoriChannel> {
|
||||
actionName = SatoriActionName.ChannelGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriChannel> {
|
||||
const { channel_id } = payload;
|
||||
|
||||
const parts = channel_id.split(':');
|
||||
@ -31,7 +36,7 @@ export class ChannelGetAction extends SatoriAction<ChannelGetPayload, SatoriChan
|
||||
} else if (type === 'group') {
|
||||
// 先从群列表缓存中查找
|
||||
const groups = await this.core.apis.GroupApi.getGroups();
|
||||
let group = groups.find(e => e.groupCode === id);
|
||||
const group = groups.find((e) => e.groupCode === id);
|
||||
|
||||
if (!group) {
|
||||
// 如果缓存中没有,尝试获取详细信息
|
||||
|
||||
@ -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<ChannelListPayload, SatoriPageResult<SatoriChannel>> {
|
||||
actionName = 'channel.list';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: ChannelListPayload): Promise<SatoriPageResult<SatoriChannel>> {
|
||||
export class ChannelListAction extends SatoriAction<Payload, SatoriPageResult<SatoriChannel>> {
|
||||
actionName = SatoriActionName.ChannelList;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriPageResult<SatoriChannel>> {
|
||||
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) {
|
||||
|
||||
39
packages/napcat-satori/action/guild/GuildApprove.ts
Normal file
39
packages/napcat-satori/action/guild/GuildApprove.ts
Normal file
@ -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<typeof SchemaData>;
|
||||
|
||||
export class GuildApproveAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.GuildApprove;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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<GuildGetPayload, SatoriGuild> {
|
||||
actionName = 'guild.get';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: GuildGetPayload): Promise<SatoriGuild> {
|
||||
export class GuildGetAction extends SatoriAction<Payload, SatoriGuild> {
|
||||
actionName = SatoriActionName.GuildGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriGuild> {
|
||||
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) {
|
||||
// 如果缓存中没有,尝试获取详细信息
|
||||
|
||||
@ -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<GuildListPayload, SatoriPageResult<SatoriGuild>> {
|
||||
actionName = 'guild.list';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (_payload: GuildListPayload): Promise<SatoriPageResult<SatoriGuild>> {
|
||||
export class GuildListAction extends SatoriAction<Payload, SatoriPageResult<SatoriGuild>> {
|
||||
actionName = SatoriActionName.GuildList;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (_payload: Payload): Promise<SatoriPageResult<SatoriGuild>> {
|
||||
const groups = await this.core.apis.GroupApi.getGroups(true);
|
||||
|
||||
const guilds: SatoriGuild[] = groups.map((group) => ({
|
||||
|
||||
39
packages/napcat-satori/action/guild/GuildMemberApprove.ts
Normal file
39
packages/napcat-satori/action/guild/GuildMemberApprove.ts
Normal file
@ -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<typeof SchemaData>;
|
||||
|
||||
export class GuildMemberApproveAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.GuildMemberApprove;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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<GuildMemberGetPayload, SatoriGuildMember> {
|
||||
actionName = 'guild.member.get';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: GuildMemberGetPayload): Promise<SatoriGuildMember> {
|
||||
export class GuildMemberGetAction extends SatoriAction<Payload, SatoriGuildMember> {
|
||||
actionName = SatoriActionName.GuildMemberGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriGuildMember> {
|
||||
const { guild_id, user_id } = payload;
|
||||
|
||||
const memberInfo = await this.core.apis.GroupApi.getGroupMember(guild_id, user_id);
|
||||
|
||||
@ -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<GuildMemberKickPayload, void> {
|
||||
actionName = 'guild.member.kick';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: GuildMemberKickPayload): Promise<void> {
|
||||
export class GuildMemberKickAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.GuildMemberKick;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
const { guild_id, user_id, permanent } = payload;
|
||||
|
||||
await this.core.apis.GroupApi.kickMember(
|
||||
|
||||
@ -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<GuildMemberListPayload, SatoriPageResult<SatoriGuildMember>> {
|
||||
actionName = 'guild.member.list';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: GuildMemberListPayload): Promise<SatoriPageResult<SatoriGuildMember>> {
|
||||
export class GuildMemberListAction extends SatoriAction<Payload, SatoriPageResult<SatoriGuildMember>> {
|
||||
actionName = SatoriActionName.GuildMemberList;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriPageResult<SatoriGuildMember>> {
|
||||
const { guild_id } = payload;
|
||||
|
||||
// 使用 getGroupMemberAll 获取所有群成员
|
||||
|
||||
@ -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<GuildMemberMutePayload, void> {
|
||||
actionName = 'guild.member.mute';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: GuildMemberMutePayload): Promise<void> {
|
||||
export class GuildMemberMuteAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.GuildMemberMute;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
const { guild_id, user_id, duration } = payload;
|
||||
|
||||
// 将毫秒转换为秒
|
||||
|
||||
@ -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<string, SatoriAction<unknown, unknown>>;
|
||||
@ -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';
|
||||
|
||||
26
packages/napcat-satori/action/login/LoginGet.ts
Normal file
26
packages/napcat-satori/action/login/LoginGet.ts
Normal file
@ -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<typeof SchemaData>;
|
||||
|
||||
export class LoginGetAction extends SatoriAction<Payload, SatoriLogin> {
|
||||
actionName = SatoriActionName.LoginGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (_payload: Payload): Promise<SatoriLogin> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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<MessageCreatePayload, SatoriMessage[]> {
|
||||
actionName = 'message.create';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: MessageCreatePayload): Promise<SatoriMessage[]> {
|
||||
export class MessageCreateAction extends SatoriAction<Payload, SatoriMessage[]> {
|
||||
actionName = SatoriActionName.MessageCreate;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriMessage[]> {
|
||||
const { channel_id, content } = payload;
|
||||
|
||||
// 解析 channel_id,格式: private:{user_id} 或 group:{group_id}
|
||||
|
||||
@ -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<MessageDeletePayload, void> {
|
||||
actionName = 'message.delete';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: MessageDeletePayload): Promise<void> {
|
||||
export class MessageDeleteAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.MessageDelete;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
const { channel_id, message_id } = payload;
|
||||
|
||||
const parts = channel_id.split(':');
|
||||
|
||||
@ -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<MessageGetPayload, SatoriMessage> {
|
||||
actionName = 'message.get';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: MessageGetPayload): Promise<SatoriMessage> {
|
||||
export class MessageGetAction extends SatoriAction<Payload, SatoriMessage> {
|
||||
actionName = SatoriActionName.MessageGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriMessage> {
|
||||
const { channel_id, message_id } = payload;
|
||||
|
||||
const parts = channel_id.split(':');
|
||||
|
||||
57
packages/napcat-satori/action/router.ts
Normal file
57
packages/napcat-satori/action/router.ts
Normal file
@ -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];
|
||||
@ -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<typeof SchemaData>;
|
||||
|
||||
interface UploadResult {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export class UploadCreateAction extends SatoriAction<UploadCreatePayload, UploadResult> {
|
||||
actionName = 'upload.create';
|
||||
export class UploadCreateAction extends SatoriAction<Payload, UploadResult> {
|
||||
actionName = SatoriActionName.UploadCreate;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async handle (payload: UploadCreatePayload): Promise<UploadResult> {
|
||||
protected async _handle (payload: Payload): Promise<UploadResult> {
|
||||
const result: UploadResult = {};
|
||||
|
||||
// 处理上传的文件
|
||||
|
||||
@ -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<FriendApprovePayload, void> {
|
||||
actionName = 'friend.approve';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: FriendApprovePayload): Promise<void> {
|
||||
export class FriendApproveAction extends SatoriAction<Payload, void> {
|
||||
actionName = SatoriActionName.FriendApprove;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<void> {
|
||||
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}`);
|
||||
|
||||
@ -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<FriendListPayload, SatoriPageResult<SatoriUser>> {
|
||||
actionName = 'friend.list';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (_payload: FriendListPayload): Promise<SatoriPageResult<SatoriUser>> {
|
||||
export class FriendListAction extends SatoriAction<Payload, SatoriPageResult<SatoriUser>> {
|
||||
actionName = SatoriActionName.FriendList;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (_payload: Payload): Promise<SatoriPageResult<SatoriUser>> {
|
||||
const friends = await this.core.apis.FriendApi.getBuddy();
|
||||
|
||||
const friendList: SatoriUser[] = friends.map((friend) => ({
|
||||
|
||||
@ -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<UserGetPayload, SatoriUser> {
|
||||
actionName = 'user.get';
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
async handle (payload: UserGetPayload): Promise<SatoriUser> {
|
||||
export class UserGetAction extends SatoriAction<Payload, SatoriUser> {
|
||||
actionName = SatoriActionName.UserGet;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
protected async _handle (payload: Payload): Promise<SatoriUser> {
|
||||
const { user_id } = payload;
|
||||
|
||||
const uid = await this.core.apis.UserApi.getUidByUinV2(user_id);
|
||||
|
||||
@ -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<SatoriEvent | null> {
|
||||
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<string, unknown>): SatoriEvent {
|
||||
const event = this.createBaseEvent(SatoriEventType.INTERNAL);
|
||||
event._type = typeName;
|
||||
event._data = data;
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
export { SatoriEventType as EventType };
|
||||
|
||||
@ -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<MessageElement[]> {
|
||||
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<string> {
|
||||
const parts: string[] = [];
|
||||
private async parseSatoriElement (elem: SatoriElement): Promise<MessageElement[]> {
|
||||
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(`<img src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
if (element.pttElement) {
|
||||
const src = element.pttElement.filePath || '';
|
||||
parts.push(`<audio src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
if (element.videoElement) {
|
||||
const src = element.videoElement.filePath || '';
|
||||
parts.push(`<video src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
if (element.fileElement) {
|
||||
const src = element.fileElement.filePath || '';
|
||||
parts.push(`<file src="${this.escapeXml(src)}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.FACE:
|
||||
if (element.faceElement) {
|
||||
parts.push(`<face id="${element.faceElement.faceIndex}"/>`);
|
||||
}
|
||||
break;
|
||||
case ElementType.REPLY:
|
||||
if (element.replyElement) {
|
||||
parts.push(`<quote id="${element.replyElement.sourceMsgIdInRecords}"/>`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 其他类型暂不处理
|
||||
break;
|
||||
switch (elem.type) {
|
||||
case 'text':
|
||||
if (elem.attrs['content']) {
|
||||
elements.push(this.createTextElement(elem.attrs['content']));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'at': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createAtElement({
|
||||
id: attrs['id'] || '',
|
||||
type: attrs['type'] || '',
|
||||
name: attrs['name'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'img':
|
||||
case 'image': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createImageElement({
|
||||
src: attrs['src'] || '',
|
||||
width: attrs['width'] || '',
|
||||
height: attrs['height'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'audio': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createAudioElement({
|
||||
src: attrs['src'] || '',
|
||||
duration: attrs['duration'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'video': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createVideoElement({
|
||||
src: attrs['src'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'file': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createFileElement({
|
||||
src: attrs['src'] || '',
|
||||
title: attrs['title'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'face': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(this.createFaceElement({
|
||||
id: attrs['id'] || '0',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'quote': {
|
||||
const attrs = elem.attrs;
|
||||
elements.push(await this.createQuoteElement({
|
||||
id: attrs['id'] || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'a': {
|
||||
const href = elem.attrs['href'];
|
||||
if (href) {
|
||||
const linkText = elem.children.map((c) => c.toString()).join('');
|
||||
elements.push(this.createTextElement(`${linkText} (${href})`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'button': {
|
||||
const text = elem.attrs['text'];
|
||||
if (text) {
|
||||
elements.push(this.createTextElement(`[${text}]`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'br':
|
||||
elements.push(this.createTextElement('\n'));
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
for (const child of elem.children) {
|
||||
elements.push(...await this.parseSatoriElement(child));
|
||||
}
|
||||
elements.push(this.createTextElement('\n'));
|
||||
break;
|
||||
|
||||
default:
|
||||
// 递归处理子元素
|
||||
if (elem.children) {
|
||||
for (const child of elem.children) {
|
||||
elements.push(...await this.parseSatoriElement(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
return elements;
|
||||
}
|
||||
|
||||
private parseAttributes (attrString: string): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = attrRegex.exec(attrString)) !== null) {
|
||||
const key = match[1];
|
||||
const value = match[2];
|
||||
if (key !== undefined && value !== undefined) {
|
||||
attrs[key] = value;
|
||||
/**
|
||||
* 解析 NapCat 消息元素为 Satori XML 消息内容
|
||||
*/
|
||||
async parseElements (elements: MessageElement[]): Promise<string> {
|
||||
const satoriElements: SatoriElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
const node = await this.elementToSatoriElement(element);
|
||||
if (node) {
|
||||
satoriElements.push(node);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
|
||||
return satoriElements.map((e) => e.toString()).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个消息元素转换为 SatoriElement
|
||||
*/
|
||||
private async elementToSatoriElement (element: MessageElement): Promise<SatoriElement | null> {
|
||||
switch (element.elementType) {
|
||||
case ElementType.TEXT:
|
||||
if (element.textElement) {
|
||||
if (element.textElement.atType === NTMsgAtType.ATTYPEALL) {
|
||||
return SatoriElement('at', { type: 'all' });
|
||||
} else if (element.textElement.atType === NTMsgAtType.ATTYPEONE && element.textElement.atUid) {
|
||||
const uin = await this.core.apis.UserApi.getUinByUidV2(element.textElement.atUid);
|
||||
return SatoriElement('at', { id: uin, name: element.textElement.content?.replace('@', '') });
|
||||
}
|
||||
return SatoriElement.text(element.textElement.content);
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.PIC:
|
||||
if (element.picElement) {
|
||||
const src = await this.getMediaUrl(element.picElement.sourcePath || '', 'image');
|
||||
return SatoriElement('img', {
|
||||
src,
|
||||
width: element.picElement.picWidth,
|
||||
height: element.picElement.picHeight,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.PTT:
|
||||
if (element.pttElement) {
|
||||
const src = await this.getMediaUrl(element.pttElement.filePath || '', 'audio');
|
||||
return SatoriElement('audio', {
|
||||
src,
|
||||
duration: element.pttElement.duration,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.VIDEO:
|
||||
if (element.videoElement) {
|
||||
const src = await this.getMediaUrl(element.videoElement.filePath || '', 'video');
|
||||
return SatoriElement('video', { src });
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.FILE:
|
||||
if (element.fileElement) {
|
||||
const src = element.fileElement.filePath || '';
|
||||
return SatoriElement('file', {
|
||||
src,
|
||||
title: element.fileElement.fileName,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.FACE:
|
||||
if (element.faceElement) {
|
||||
return SatoriElement('face', { id: element.faceElement.faceIndex });
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.REPLY:
|
||||
if (element.replyElement) {
|
||||
const msgId = element.replyElement.sourceMsgIdInRecords || element.replyElement.replayMsgId || '';
|
||||
return SatoriElement('quote', { id: msgId });
|
||||
}
|
||||
break;
|
||||
|
||||
case ElementType.MFACE:
|
||||
if (element.marketFaceElement) {
|
||||
return SatoriElement('face', { id: element.marketFaceElement.emojiId || '0' });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取媒体资源 URL
|
||||
*/
|
||||
private async getMediaUrl (path: string, _type: 'image' | 'audio' | 'video'): Promise<string> {
|
||||
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
|
||||
return `file://${path.replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private createTextElement (content: string): MessageElement {
|
||||
@ -155,7 +270,7 @@ export class SatoriMsgApi {
|
||||
elementType: ElementType.TEXT,
|
||||
elementId: '',
|
||||
textElement: {
|
||||
content: this.unescapeXml(content),
|
||||
content,
|
||||
atType: NTMsgAtType.ATTYPEUNKNOWN,
|
||||
atUid: '',
|
||||
atTinyId: '',
|
||||
@ -164,9 +279,8 @@ export class SatoriMsgApi {
|
||||
};
|
||||
}
|
||||
|
||||
private async createAtElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const id = attrs['id'] || '';
|
||||
const type = attrs['type'];
|
||||
private async createAtElement (attrs: { id: string; type?: string; name?: string; }): Promise<MessageElement> {
|
||||
const { id, type } = attrs;
|
||||
|
||||
if (type === 'all') {
|
||||
return {
|
||||
@ -198,34 +312,33 @@ export class SatoriMsgApi {
|
||||
};
|
||||
}
|
||||
|
||||
private async createImageElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
// 这里需要根据 src 类型处理(URL、base64、本地路径等)
|
||||
private async createImageElement (attrs: { src: string; width?: string; height?: string; }): Promise<MessageElement> {
|
||||
const src = attrs.src;
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
sourcePath: src,
|
||||
picWidth: parseInt(attrs['width'] || '0', 10),
|
||||
picHeight: parseInt(attrs['height'] || '0', 10),
|
||||
picWidth: parseInt(attrs.width || '0', 10),
|
||||
picHeight: parseInt(attrs.height || '0', 10),
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createAudioElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
private async createAudioElement (attrs: { src: string; duration?: string; }): Promise<MessageElement> {
|
||||
const src = attrs.src;
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
filePath: src,
|
||||
duration: parseInt(attrs['duration'] || '0', 10),
|
||||
duration: parseInt(attrs.duration || '0', 10),
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createVideoElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
private async createVideoElement (attrs: { src: string; }): Promise<MessageElement> {
|
||||
const src = attrs.src;
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
@ -238,32 +351,32 @@ export class SatoriMsgApi {
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createFileElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const src = attrs['src'] || '';
|
||||
private async createFileElement (attrs: { src: string; title?: string; }): Promise<MessageElement> {
|
||||
const src = attrs.src;
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
filePath: src,
|
||||
fileName: attrs['title'] || '',
|
||||
fileName: attrs.title || '',
|
||||
fileSize: '',
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private createFaceElement (attrs: Record<string, string>): MessageElement {
|
||||
private createFaceElement (attrs: { id: string; }): MessageElement {
|
||||
return {
|
||||
elementType: ElementType.FACE,
|
||||
elementId: '',
|
||||
faceElement: {
|
||||
faceIndex: parseInt(attrs['id'] || '0', 10),
|
||||
faceIndex: parseInt(attrs.id || '0', 10),
|
||||
faceType: 1,
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private async createQuoteElement (attrs: Record<string, string>): Promise<MessageElement> {
|
||||
const id = attrs['id'] || '';
|
||||
private async createQuoteElement (attrs: { id: string; }): Promise<MessageElement> {
|
||||
const id = attrs.id;
|
||||
return {
|
||||
elementType: ElementType.REPLY,
|
||||
elementId: '',
|
||||
@ -276,22 +389,4 @@ export class SatoriMsgApi {
|
||||
},
|
||||
} as MessageElement;
|
||||
}
|
||||
|
||||
private escapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
private unescapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/napcat-satori/helper/index.ts
Normal file
1
packages/napcat-satori/helper/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './xml';
|
||||
320
packages/napcat-satori/helper/xml.ts
Normal file
320
packages/napcat-satori/helper/xml.ts
Normal file
@ -0,0 +1,320 @@
|
||||
/**
|
||||
* Satori XML 元素节点
|
||||
*/
|
||||
export interface SatoriXmlNode {
|
||||
type: string;
|
||||
attrs: Record<string, string>;
|
||||
children: (SatoriXmlNode | string)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Satori XML 工具类
|
||||
* 用于解析和构建 Satori 协议的 XML 格式消息
|
||||
* 使用简单的正则解析方式,避免外部依赖
|
||||
*/
|
||||
export class SatoriXmlUtils {
|
||||
/**
|
||||
* 解析 Satori XML 字符串为元素节点数组
|
||||
*/
|
||||
static parse (xmlString: string): SatoriXmlNode[] {
|
||||
const nodes: SatoriXmlNode[] = [];
|
||||
const tagRegex = /<(\w+)([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = tagRegex.exec(xmlString)) !== null) {
|
||||
// 处理标签前的文本
|
||||
if (match.index > lastIndex) {
|
||||
const text = xmlString.slice(lastIndex, match.index);
|
||||
if (text.trim()) {
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
attrs: { content: this.unescapeXml(text) },
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [, tagName, rawAttrs = '', innerContent] = match;
|
||||
if (!tagName) continue;
|
||||
|
||||
const attrs = this.parseAttributes(rawAttrs);
|
||||
const children: (SatoriXmlNode | string)[] = [];
|
||||
|
||||
// 如果有内部内容,递归解析
|
||||
if (innerContent) {
|
||||
const innerNodes = this.parse(innerContent);
|
||||
// 如果解析出来只有一个空文本,直接用内容
|
||||
if (innerNodes.length === 1 && innerNodes[0]?.type === 'text') {
|
||||
children.push(innerNodes[0]);
|
||||
} else if (innerNodes.length > 0) {
|
||||
children.push(...innerNodes);
|
||||
} else if (innerContent.trim()) {
|
||||
children.push({
|
||||
type: 'text',
|
||||
attrs: { content: this.unescapeXml(innerContent) },
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
type: tagName.toLowerCase(),
|
||||
attrs,
|
||||
children,
|
||||
});
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// 处理剩余文本
|
||||
if (lastIndex < xmlString.length) {
|
||||
const text = xmlString.slice(lastIndex);
|
||||
if (text.trim()) {
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
attrs: { content: this.unescapeXml(text) },
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析属性字符串
|
||||
*/
|
||||
private static parseAttributes (attrString: string): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = attrRegex.exec(attrString)) !== null) {
|
||||
const key = match[1];
|
||||
const value = match[2];
|
||||
if (key !== undefined && value !== undefined) {
|
||||
attrs[key] = this.unescapeXml(value);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元素节点数组序列化为 XML 字符串
|
||||
*/
|
||||
static serialize (nodes: SatoriXmlNode[]): string {
|
||||
return nodes.map((node) => this.serializeNode(node)).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化单个节点
|
||||
*/
|
||||
private static serializeNode (node: SatoriXmlNode): string {
|
||||
if (node.type === 'text') {
|
||||
return this.escapeXml(node.attrs['content'] || '');
|
||||
}
|
||||
|
||||
const attrs = Object.entries(node.attrs)
|
||||
.map(([key, value]) => `${key}="${this.escapeXml(value)}"`)
|
||||
.join(' ');
|
||||
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
if (!hasChildren) {
|
||||
return attrs ? `<${node.type} ${attrs}/>` : `<${node.type}/>`;
|
||||
}
|
||||
|
||||
const openTag = attrs ? `<${node.type} ${attrs}>` : `<${node.type}>`;
|
||||
const childrenStr = node.children
|
||||
.map((child) => (typeof child === 'string' ? this.escapeXml(child) : this.serializeNode(child)))
|
||||
.join('');
|
||||
|
||||
return `${openTag}${childrenStr}</${node.type}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本节点
|
||||
*/
|
||||
static createText (content: string): SatoriXmlNode {
|
||||
return { type: 'text', attrs: { content }, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 at 节点
|
||||
*/
|
||||
static createAt (id?: string, name?: string, type?: string): SatoriXmlNode {
|
||||
const attrs: Record<string, string> = {};
|
||||
if (id) attrs['id'] = id;
|
||||
if (name) attrs['name'] = name;
|
||||
if (type) attrs['type'] = type;
|
||||
return { type: 'at', attrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片节点
|
||||
*/
|
||||
static createImg (src: string, attrs?: { width?: number; height?: number; title?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = { src };
|
||||
if (attrs?.width) nodeAttrs['width'] = String(attrs.width);
|
||||
if (attrs?.height) nodeAttrs['height'] = String(attrs.height);
|
||||
if (attrs?.title) nodeAttrs['title'] = attrs.title;
|
||||
return { type: 'img', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建音频节点
|
||||
*/
|
||||
static createAudio (src: string, attrs?: { duration?: number; title?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = { src };
|
||||
if (attrs?.duration) nodeAttrs['duration'] = String(attrs.duration);
|
||||
if (attrs?.title) nodeAttrs['title'] = attrs.title;
|
||||
return { type: 'audio', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建视频节点
|
||||
*/
|
||||
static createVideo (src: string, attrs?: { width?: number; height?: number; duration?: number; title?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = { src };
|
||||
if (attrs?.width) nodeAttrs['width'] = String(attrs.width);
|
||||
if (attrs?.height) nodeAttrs['height'] = String(attrs.height);
|
||||
if (attrs?.duration) nodeAttrs['duration'] = String(attrs.duration);
|
||||
if (attrs?.title) nodeAttrs['title'] = attrs.title;
|
||||
return { type: 'video', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件节点
|
||||
*/
|
||||
static createFile (src: string, attrs?: { title?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = { src };
|
||||
if (attrs?.title) nodeAttrs['title'] = attrs.title;
|
||||
return { type: 'file', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建表情节点
|
||||
*/
|
||||
static createFace (id: string | number): SatoriXmlNode {
|
||||
return { type: 'face', attrs: { id: String(id) }, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建引用节点
|
||||
*/
|
||||
static createQuote (id: string): SatoriXmlNode {
|
||||
return { type: 'quote', attrs: { id }, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息节点(用于转发消息)
|
||||
*/
|
||||
static createMessage (attrs?: { id?: string; forward?: boolean; }, children?: SatoriXmlNode[]): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = {};
|
||||
if (attrs?.id) nodeAttrs['id'] = attrs.id;
|
||||
if (attrs?.forward !== undefined) nodeAttrs['forward'] = String(attrs.forward);
|
||||
return { type: 'message', attrs: nodeAttrs, children: children || [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建作者节点
|
||||
*/
|
||||
static createAuthor (attrs: { id?: string; name?: string; avatar?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = {};
|
||||
if (attrs.id) nodeAttrs['id'] = attrs.id;
|
||||
if (attrs.name) nodeAttrs['name'] = attrs.name;
|
||||
if (attrs.avatar) nodeAttrs['avatar'] = attrs.avatar;
|
||||
return { type: 'author', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建换行节点
|
||||
*/
|
||||
static createBr (): SatoriXmlNode {
|
||||
return { type: 'br', attrs: {}, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建按钮节点
|
||||
*/
|
||||
static createButton (attrs: { id?: string; type?: string; href?: string; text?: string; }): SatoriXmlNode {
|
||||
const nodeAttrs: Record<string, string> = {};
|
||||
if (attrs.id) nodeAttrs['id'] = attrs.id;
|
||||
if (attrs.type) nodeAttrs['type'] = attrs.type;
|
||||
if (attrs.href) nodeAttrs['href'] = attrs.href;
|
||||
if (attrs.text) nodeAttrs['text'] = attrs.text;
|
||||
return { type: 'button', attrs: nodeAttrs, children: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建样式标签节点
|
||||
*/
|
||||
static createStyled (type: 'b' | 'i' | 'u' | 's' | 'code' | 'sup' | 'sub' | 'spl', children: SatoriXmlNode[]): SatoriXmlNode {
|
||||
return { type, attrs: {}, children };
|
||||
}
|
||||
|
||||
/**
|
||||
* XML 转义
|
||||
*/
|
||||
static escapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* XML 反转义
|
||||
*/
|
||||
static unescapeXml (str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历所有节点
|
||||
*/
|
||||
static walk (nodes: SatoriXmlNode[], callback: (node: SatoriXmlNode) => void): void {
|
||||
for (const node of nodes) {
|
||||
callback(node);
|
||||
if (node.children) {
|
||||
const childNodes = node.children.filter((c): c is SatoriXmlNode => typeof c !== 'string');
|
||||
this.walk(childNodes, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定类型的节点
|
||||
*/
|
||||
static find (nodes: SatoriXmlNode[], type: string): SatoriXmlNode[] {
|
||||
const result: SatoriXmlNode[] = [];
|
||||
this.walk(nodes, (node) => {
|
||||
if (node.type === type) {
|
||||
result.push(node);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取纯文本内容
|
||||
*/
|
||||
static extractText (nodes: SatoriXmlNode[]): string {
|
||||
const texts: string[] = [];
|
||||
this.walk(nodes, (node) => {
|
||||
if (node.type === 'text' && node.attrs['content']) {
|
||||
texts.push(node.attrs['content']);
|
||||
}
|
||||
});
|
||||
return texts.join('');
|
||||
}
|
||||
}
|
||||
|
||||
export { SatoriXmlUtils as XmlUtils };
|
||||
@ -249,13 +249,7 @@ export class NapCatSatoriAdapter {
|
||||
}
|
||||
|
||||
try {
|
||||
const requesterUin = await this.core.apis.UserApi.getUinByUidV2(req.friendUid);
|
||||
const event = this.apis.EventApi.createFriendRequestEvent(
|
||||
requesterUin,
|
||||
req.friendNick || requesterUin,
|
||||
req.extWords,
|
||||
req.friendUid
|
||||
);
|
||||
const event = this.apis.EventApi.createFriendRequestEvent(req);
|
||||
await this.networkManager.emitEvent(event);
|
||||
} catch (error) {
|
||||
this.context.logger.logError('[Satori] 处理好友请求失败', error);
|
||||
@ -284,15 +278,7 @@ export class NapCatSatoriAdapter {
|
||||
[GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) &&
|
||||
notify.status === GroupNotifyMsgStatus.KUNHANDLE
|
||||
) {
|
||||
const requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
|
||||
const event = this.apis.EventApi.createGuildMemberRequestEvent(
|
||||
notify.group.groupCode,
|
||||
notify.group.groupName,
|
||||
requestUin,
|
||||
notify.user1.nickName || requestUin,
|
||||
notify.postscript,
|
||||
notify.seq
|
||||
);
|
||||
const event = this.apis.EventApi.createGuildMemberRequestEvent(notify);
|
||||
await this.networkManager.emitEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -322,3 +308,5 @@ export class NapCatSatoriAdapter {
|
||||
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
export * from './action';
|
||||
export * from './helper';
|
||||
|
||||
@ -2,14 +2,14 @@ import express, { Express, Request, Response, NextFunction } from 'express';
|
||||
import { createServer, Server } from 'http';
|
||||
import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatSatoriAdapter } from '../index';
|
||||
import { SatoriActionMap } from '../action';
|
||||
import { SatoriActionMap, SatoriResponseHelper } from '../action';
|
||||
import { SatoriHttpServerConfig } from '../config/config';
|
||||
import {
|
||||
ISatoriNetworkAdapter,
|
||||
SatoriEmitEventContent,
|
||||
SatoriNetworkReloadType,
|
||||
} from './adapter';
|
||||
import { SatoriApiResponse, SatoriLoginStatus } from '../types';
|
||||
import { SatoriLoginStatus } from '../types';
|
||||
|
||||
export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpServerConfig> {
|
||||
private app: Express | null = null;
|
||||
@ -30,7 +30,7 @@ export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpSer
|
||||
|
||||
try {
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
this.app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// Token 验证中间件
|
||||
this.app.use(this.config.path || '/v1', (req: Request, res: Response, next: NextFunction): void => {
|
||||
@ -38,7 +38,7 @@ export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpSer
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
|
||||
if (token !== this.config.token) {
|
||||
res.status(401).json({ error: { code: 401, message: 'Unauthorized' } });
|
||||
res.status(401).json(SatoriResponseHelper.error(401, 'Unauthorized'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,24 @@ export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpSer
|
||||
const basePath = this.config.path || '/v1';
|
||||
const router = express.Router();
|
||||
|
||||
// 获取登录信息
|
||||
// 通用 action 处理器
|
||||
const handleAction = async (actionName: string, req: Request, res: Response): Promise<void> => {
|
||||
const action = this.actions.get(actionName);
|
||||
if (!action) {
|
||||
res.status(404).json(SatoriResponseHelper.error(404, `未知的 action: ${actionName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await action.handle(req.body || {});
|
||||
res.json(SatoriResponseHelper.success(result));
|
||||
} catch (error) {
|
||||
this.logger.logError(`[Satori] Action ${actionName} 执行失败:`, error);
|
||||
res.status(500).json(SatoriResponseHelper.error(500, `${error}`));
|
||||
}
|
||||
};
|
||||
|
||||
// 登录信息(特殊处理,可以使用缓存)
|
||||
router.post('/login.get', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = {
|
||||
@ -123,234 +140,37 @@ export class SatoriHttpServerAdapter extends ISatoriNetworkAdapter<SatoriHttpSer
|
||||
platform: this.satoriContext.configLoader.configData.platform,
|
||||
status: SatoriLoginStatus.ONLINE,
|
||||
};
|
||||
this.sendSuccess(res, result);
|
||||
res.json(SatoriResponseHelper.success(result));
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取登录信息失败: ${error}`);
|
||||
res.status(500).json(SatoriResponseHelper.error(500, `获取登录信息失败: ${error}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
router.post('/message.create', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, content } = req.body;
|
||||
if (!channel_id || !content) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('message.create')?.handle({ channel_id, content });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `发送消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
// 动态注册所有 action 路由
|
||||
for (const [actionName] of this.actions) {
|
||||
const routePath = `/${actionName.replace(/\./g, '/')}`;
|
||||
router.post(routePath, (req, res) => handleAction(actionName, req, res));
|
||||
|
||||
// 获取消息
|
||||
router.post('/message.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, message_id } = req.body;
|
||||
if (!channel_id || !message_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('message.get')?.handle({ channel_id, message_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
// 同时支持点号格式的路由
|
||||
router.post(`/${actionName}`, (req, res) => handleAction(actionName, req, res));
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
router.post('/message.delete', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id, message_id } = req.body;
|
||||
if (!channel_id || !message_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('message.delete')?.handle({ channel_id, message_id });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `删除消息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取频道信息
|
||||
router.post('/channel.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { channel_id } = req.body;
|
||||
if (!channel_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('channel.get')?.handle({ channel_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取频道信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取频道列表
|
||||
router.post('/channel.list', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, next } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('channel.list')?.handle({ guild_id, next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取频道列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群组信息
|
||||
router.post('/guild.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.get')?.handle({ guild_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群组信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群组列表
|
||||
router.post('/guild.list', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { next } = req.body;
|
||||
const result = await this.actions.get('guild.list')?.handle({ next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群组列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群成员信息
|
||||
router.post('/guild.member.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.member.get')?.handle({ guild_id, user_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群成员信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取群成员列表
|
||||
router.post('/guild.member.list', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, next } = req.body;
|
||||
if (!guild_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('guild.member.list')?.handle({ guild_id, next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取群成员列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
router.post('/user.get', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { user_id } = req.body;
|
||||
if (!user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
const result = await this.actions.get('user.get')?.handle({ user_id });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取用户信息失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取好友列表
|
||||
router.post('/friend.list', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { next } = req.body;
|
||||
const result = await this.actions.get('friend.list')?.handle({ next });
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `获取好友列表失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理好友请求
|
||||
router.post('/friend.approve', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { message_id, approve, comment } = req.body;
|
||||
if (message_id === undefined || approve === undefined) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('friend.approve')?.handle({ message_id, approve, comment });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `处理好友请求失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 踢出群成员
|
||||
router.post('/guild.member.kick', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id, permanent } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('guild.member.kick')?.handle({ guild_id, user_id, permanent });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `踢出群成员失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 禁言群成员
|
||||
router.post('/guild.member.mute', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { guild_id, user_id, duration } = req.body;
|
||||
if (!guild_id || !user_id) {
|
||||
this.sendError(res, 400, '缺少必要参数');
|
||||
return;
|
||||
}
|
||||
await this.actions.get('guild.member.mute')?.handle({ guild_id, user_id, duration });
|
||||
this.sendSuccess(res, {});
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `禁言群成员失败: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
router.post('/upload.create', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await this.actions.get('upload.create')?.handle(req.body);
|
||||
this.sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
this.sendError(res, 500, `上传文件失败: ${error}`);
|
||||
// 通用 action 入口
|
||||
router.post('/:action(*)', async (req: Request, res: Response) => {
|
||||
const actionParam = req.params['action'];
|
||||
if (!actionParam) {
|
||||
res.status(400).json(SatoriResponseHelper.error(400, '缺少 action 参数'));
|
||||
return;
|
||||
}
|
||||
const actionName = actionParam.replace(/\//g, '.');
|
||||
await handleAction(actionName, req, res);
|
||||
});
|
||||
|
||||
this.app.use(basePath, router);
|
||||
}
|
||||
|
||||
private sendSuccess<T> (res: Response, data: T): void {
|
||||
const response: SatoriApiResponse<T> = { data };
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
private sendError (res: Response, code: number, message: string): void {
|
||||
const response: SatoriApiResponse = { error: { code, message } };
|
||||
res.status(code >= 400 && code < 600 ? code : 500).json(response);
|
||||
// Debug 日志
|
||||
if (this.config.debug) {
|
||||
this.logger.logDebug(`[Satori] 已注册 ${this.actions.size} 个 action 路由`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@
|
||||
"ws": "^8.18.0",
|
||||
"@sinclair/typebox": "^0.34.33",
|
||||
"ajv": "^8.17.1",
|
||||
"json5": "^2.2.3"
|
||||
"json5": "^2.2.3",
|
||||
"@satorijs/core": "^4.3.1",
|
||||
"@satorijs/element": "^3.1.3",
|
||||
"@satorijs/protocol": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
"*.ts",
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
260
pnpm-lock.yaml
260
pnpm-lock.yaml
@ -270,6 +270,15 @@ importers:
|
||||
|
||||
packages/napcat-satori:
|
||||
dependencies:
|
||||
'@satorijs/core':
|
||||
specifier: ^4.3.1
|
||||
version: 4.5.2(cordis@3.18.1)
|
||||
'@satorijs/element':
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.8
|
||||
'@satorijs/protocol':
|
||||
specifier: ^1.4.1
|
||||
version: 1.6.1
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.33
|
||||
version: 0.34.41
|
||||
@ -869,6 +878,34 @@ packages:
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
|
||||
'@cordisjs/core@3.18.1':
|
||||
resolution: {integrity: sha512-yRuATOamFxeD1ztE2L3o1SaHuT2zw5DTXijQYxt9azVNeILbvtomfGG6sLZegrynJO9XZbNvXbOlQ7LBJDOlAg==}
|
||||
|
||||
'@cordisjs/loader@0.13.1':
|
||||
resolution: {integrity: sha512-YGw22gT1F/jXT1UeWMjIavt7WHMfEmKve83v4PTg63SW4eC2s89ivsN4RQWi5YWe0LQNNg8LItL66FeEcgQS5w==}
|
||||
peerDependencies:
|
||||
'@cordisjs/core': ^3.18.1
|
||||
|
||||
'@cordisjs/logger@0.3.3':
|
||||
resolution: {integrity: sha512-UcyZRXAL86YLAEEta+fxJM1CcDanJvoIMD9ywCyP2AZJuib68UynENGTFo4mYkeg+8WMXrlA/aNNwENMiLUlXA==}
|
||||
peerDependencies:
|
||||
'@cordisjs/core': ^3.17.1
|
||||
|
||||
'@cordisjs/plugin-http@0.6.3':
|
||||
resolution: {integrity: sha512-kmw4G1t39ZZzFGpBKUNuTv2aefEUtxa1e4qu4+bTrM5Z8uYHTRzPpyJfkj9zuZwopP/Vz271OUr2UfDN5lrJKQ==}
|
||||
peerDependencies:
|
||||
cordis: ^3.18.1
|
||||
|
||||
'@cordisjs/schema@0.1.1':
|
||||
resolution: {integrity: sha512-AYTSyEXEASO6uVHqXBHIId+8OGFs0idtFoj3q7qee9J7OGcMjVEUsrsdiP9SnmzTR4ea17esShledUC50fco1g==}
|
||||
peerDependencies:
|
||||
'@cordisjs/core': ^3.17.0
|
||||
|
||||
'@cordisjs/timer@0.3.2':
|
||||
resolution: {integrity: sha512-frdIvc+1AqKVHMWnNacqqZ2Cm52FfoYKPpBQ+kZgr/cFs7HXDKqetL35CLy/EZAF22l/6VTvpQOYUwS6tvVJZA==}
|
||||
peerDependencies:
|
||||
'@cordisjs/core': ^3.12.0
|
||||
|
||||
'@dabh/diagnostics@2.0.8':
|
||||
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
|
||||
|
||||
@ -2639,6 +2676,17 @@ packages:
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@satorijs/core@4.5.2':
|
||||
resolution: {integrity: sha512-lW5gqt6z1h56GGkyXKEFn212JstDcVN2rUxb+FBvT9i6OKLdB2055QgynrkbLqzDN+Zq8hbW+9T6lpS/fpyMSw==}
|
||||
peerDependencies:
|
||||
cordis: ^3.18.1
|
||||
|
||||
'@satorijs/element@3.1.8':
|
||||
resolution: {integrity: sha512-Sm+YCO0GMKDtAsQ+JKZ6aJeJZYVeMehqmzk4EJr57MnvZt5kP9iqtN8vYOZc/idE69Y5nA/jxKNCSrgrm1yYrA==}
|
||||
|
||||
'@satorijs/protocol@1.6.1':
|
||||
resolution: {integrity: sha512-A/G/S5PWXGXg9B4aMAmvt8+wP2BnV6uNEzkd1MH1ZkqWfgJ2TX9L4ghR311mbde57MJOoSbX51lnO4J9XjeNKQ==}
|
||||
|
||||
'@simplewebauthn/server@13.2.2':
|
||||
resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@ -3205,6 +3253,10 @@ packages:
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -3475,6 +3527,9 @@ packages:
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
bufferstreams@3.0.0:
|
||||
resolution: {integrity: sha512-Qg0ggJUWJq90vtg4lDsGN9CDWvzBMQxhiEkSOD/sJfYt6BLect3eV1/S6K7SCSKJ34n60rf6U5eUPmQENVE4UA==}
|
||||
engines: {node: '>=8.12.0'}
|
||||
@ -3487,6 +3542,10 @@ packages:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
cacache@16.1.3:
|
||||
resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
@ -3712,6 +3771,11 @@ packages:
|
||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cordis@3.18.1:
|
||||
resolution: {integrity: sha512-9IbthFbFBVJ15WBDNPqHdl59TyEAMNWxF1Dxsdi14625ePQkMX35WvcqgGno0BcXlpXDbUaBZgQ+jfBYT+uuSQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@ -3719,6 +3783,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
cosmokit@1.8.1:
|
||||
resolution: {integrity: sha512-PDBv4l90xZKrUsZ0vtoycgZpO/j4iFsqJXrAxsyBDsnQRI7ZMJXIjgDJsKNjd5L8jnVnnlrDCdhkFbTncgCVjQ==}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
@ -3860,6 +3927,10 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dotenv@16.6.1:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -4173,9 +4244,17 @@ packages:
|
||||
event-source-polyfill@1.0.31:
|
||||
resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
@ -4261,6 +4340,10 @@ packages:
|
||||
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
file-type@16.5.4:
|
||||
resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
file-type@21.1.0:
|
||||
resolution: {integrity: sha512-boU4EHmP3JXkwDo4uhyBhTt5pPstxB6eEXKJBu2yu2l7aAMMm7QQYQEzssJmKReZYrFdFOJS8koVo6bXIBGDqA==}
|
||||
engines: {node: '>=20'}
|
||||
@ -4928,6 +5011,10 @@ packages:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
kuler@2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
|
||||
@ -5543,6 +5630,10 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
peek-readable@4.1.0:
|
||||
resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
@ -5641,6 +5732,10 @@ packages:
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
promise-inflight@1.0.1:
|
||||
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
||||
peerDependencies:
|
||||
@ -5862,6 +5957,14 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readable-stream@4.7.0:
|
||||
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
readable-web-to-node-stream@3.0.4:
|
||||
resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@ -5893,6 +5996,9 @@ packages:
|
||||
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
reggol@1.7.1:
|
||||
resolution: {integrity: sha512-0Vr1vRYDFnsA14BipkHRn3+4FxlZxQJ4VQG2pFE2obSrR68hTHVbbQzS3/ZAHNb6l/+ThaJrc5BPgrYDM3yNyg==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
@ -6011,6 +6117,9 @@ packages:
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
schemastery@3.17.2:
|
||||
resolution: {integrity: sha512-hKOOlPLRM4/4jbWzc3Hv9wKOy0aYv5G09Bj9+eZ6mLSCrPTfPs5q/6EMiQAKwfOOYqq4Eq86+cMLj6I2q6jhgA==}
|
||||
|
||||
screenfull@5.2.0:
|
||||
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6266,6 +6375,10 @@ packages:
|
||||
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
strtok3@6.3.0:
|
||||
resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
@ -6284,6 +6397,10 @@ packages:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -6396,6 +6513,10 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
token-types@4.2.1:
|
||||
resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
token-types@6.1.1:
|
||||
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
@ -7056,6 +7177,45 @@ snapshots:
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@cordisjs/core@3.18.1':
|
||||
dependencies:
|
||||
cosmokit: 1.8.1
|
||||
|
||||
'@cordisjs/loader@0.13.1(@cordisjs/core@3.18.1)':
|
||||
dependencies:
|
||||
'@cordisjs/core': 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
dotenv: 16.6.1
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@cordisjs/logger@0.3.3(@cordisjs/core@3.18.1)':
|
||||
dependencies:
|
||||
'@cordisjs/core': 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
reggol: 1.7.1
|
||||
|
||||
'@cordisjs/plugin-http@0.6.3(cordis@3.18.1)':
|
||||
dependencies:
|
||||
cordis: 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
file-type: 16.5.4
|
||||
mime-db: 1.54.0
|
||||
ws: 8.18.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@cordisjs/schema@0.1.1(@cordisjs/core@3.18.1)':
|
||||
dependencies:
|
||||
'@cordisjs/core': 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
schemastery: 3.17.2
|
||||
|
||||
'@cordisjs/timer@0.3.2(@cordisjs/core@3.18.1)':
|
||||
dependencies:
|
||||
'@cordisjs/core': 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
|
||||
'@dabh/diagnostics@2.0.8':
|
||||
dependencies:
|
||||
'@so-ric/colorspace': 1.1.6
|
||||
@ -9433,6 +9593,27 @@ snapshots:
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@satorijs/core@4.5.2(cordis@3.18.1)':
|
||||
dependencies:
|
||||
'@cordisjs/plugin-http': 0.6.3(cordis@3.18.1)
|
||||
'@satorijs/element': 3.1.8
|
||||
'@satorijs/protocol': 1.6.1
|
||||
cordis: 3.18.1
|
||||
cosmokit: 1.8.1
|
||||
path-to-regexp: 8.3.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@satorijs/element@3.1.8':
|
||||
dependencies:
|
||||
cosmokit: 1.8.1
|
||||
|
||||
'@satorijs/protocol@1.6.1':
|
||||
dependencies:
|
||||
'@satorijs/element': 3.1.8
|
||||
cosmokit: 1.8.1
|
||||
|
||||
'@simplewebauthn/server@13.2.2':
|
||||
dependencies:
|
||||
'@hexagon/base64': 1.1.28
|
||||
@ -10014,6 +10195,10 @@ snapshots:
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
@ -10357,6 +10542,11 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
bufferstreams@3.0.0:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
@ -10367,6 +10557,8 @@ snapshots:
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
cacache@16.1.3:
|
||||
dependencies:
|
||||
'@npmcli/fs': 2.1.2
|
||||
@ -10603,6 +10795,18 @@ snapshots:
|
||||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
cordis@3.18.1:
|
||||
dependencies:
|
||||
'@cordisjs/core': 3.18.1
|
||||
'@cordisjs/loader': 0.13.1(@cordisjs/core@3.18.1)
|
||||
'@cordisjs/logger': 0.3.3(@cordisjs/core@3.18.1)
|
||||
'@cordisjs/schema': 0.1.1(@cordisjs/core@3.18.1)
|
||||
'@cordisjs/timer': 0.3.2(@cordisjs/core@3.18.1)
|
||||
cac: 6.7.14
|
||||
cosmokit: 1.8.1
|
||||
kleur: 4.1.5
|
||||
reggol: 1.7.1
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cors@2.8.5:
|
||||
@ -10610,6 +10814,8 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
cosmokit@1.8.1: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
@ -10721,6 +10927,8 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@ -11188,8 +11396,12 @@ snapshots:
|
||||
|
||||
event-source-polyfill@1.0.31: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
expect-type@1.2.2: {}
|
||||
@ -11320,6 +11532,12 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
file-type@16.5.4:
|
||||
dependencies:
|
||||
readable-web-to-node-stream: 3.0.4
|
||||
strtok3: 6.3.0
|
||||
token-types: 4.2.1
|
||||
|
||||
file-type@21.1.0:
|
||||
dependencies:
|
||||
'@tokenizer/inflate': 0.3.1
|
||||
@ -12033,6 +12251,8 @@ snapshots:
|
||||
|
||||
kind-of@6.0.3: {}
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
kuler@2.0.0: {}
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
@ -12873,6 +13093,8 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
peek-readable@4.1.0: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
peowly@1.3.2: {}
|
||||
@ -12951,6 +13173,8 @@ snapshots:
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
promise-inflight@1.0.1: {}
|
||||
|
||||
promise-retry@2.0.1:
|
||||
@ -13200,6 +13424,18 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readable-stream@4.7.0:
|
||||
dependencies:
|
||||
abort-controller: 3.0.0
|
||||
buffer: 6.0.3
|
||||
events: 3.3.0
|
||||
process: 0.11.10
|
||||
string_decoder: 1.3.0
|
||||
|
||||
readable-web-to-node-stream@3.0.4:
|
||||
dependencies:
|
||||
readable-stream: 4.7.0
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
@ -13239,6 +13475,12 @@ snapshots:
|
||||
|
||||
regexpp@3.2.0: {}
|
||||
|
||||
reggol@1.7.1:
|
||||
dependencies:
|
||||
cosmokit: 1.8.1
|
||||
object-inspect: 1.13.4
|
||||
supports-color: 8.1.1
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@ -13397,6 +13639,10 @@ snapshots:
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
schemastery@3.17.2:
|
||||
dependencies:
|
||||
cosmokit: 1.8.1
|
||||
|
||||
screenfull@5.2.0: {}
|
||||
|
||||
scroll-into-view-if-needed@3.0.10:
|
||||
@ -13742,6 +13988,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
|
||||
strtok3@6.3.0:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
peek-readable: 4.1.0
|
||||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
style-to-js@1.1.19:
|
||||
@ -13766,6 +14017,10 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
synckit@0.9.3:
|
||||
@ -13913,6 +14168,11 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
token-types@4.2.1:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
ieee754: 1.2.1
|
||||
|
||||
token-types@6.1.1:
|
||||
dependencies:
|
||||
'@borewit/text-codec': 0.1.1
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user