session.execute 从 bot.session(event) 各系统表现不一致

适配器 - onebot
本地:

System:
OS: Windows 10 10.0.19045
CPU: (6) x64 AMD Ryzen 5 3500X 6-Core Processor

Binaries:
Node: 24.13.0
npm: 11.11.0

Koishi:
Core: 4.18.11
Console: 5.30.11

服务器:

System:
OS: Linux 6.8 Ubuntu 24.04 LTS 24.04 LTS (Noble Numbat)
CPU: (2) x64 AMD EPYC 7K62 48-Core Processor

Binaries:
Node: 20.12.2
Yarn: 4.1.1

Koishi:
Core: 4.18.11
Console: 5.30.11
Koishi Desktop: 1.1.3

问题代码如下:

ctx.on('bot-status-updated', async (bot) => {
    if (bot.status === 1) {
        for (const gid of cfg.retry) {
            const event: Partial<Event> = {
                channel: await bot.getChannel(gid),
                guild: await bot.getGuild(gid),
                user: await bot.getUser(bot.selfId),
                member: await bot.getGuildMember(gid, bot.selfId),
                type: 'guild-member-request',
            }
            const session = bot.session(event)
            ctx.logger.warn(session)
            await session.send('debug#start').then(() => {
                session.execute('help').then(() => {
                    session.send('debug#end')
                })
            })
        }
    }
})

首先从本地触发一遍,可以观察到:

Bot@1: 03-21 10:03:43
debug#start

Bot@1: 03-21 10:03:43
当前可用的指令有:
afd
help 显示帮助信息
plugin 插件管理
status 查看运行状态
timer 定时器信息
usage 调用次数信息
输入“help 指令名”查看特定指令的语法和使用示例。

Bot@1: 03-21 10:03:43
debug#end

而让服务器执行一遍,结果是这样:

Bot@2: 03-21 10:06:32
debug#start

Bot@: 03-21 10:06:32
debug#end

本地无法启动虚拟机试验,我需要帮助:sob:
这可能和 nodejs 版本没有太大关系,我将服务器上的 koishi 用 v24.14.0 运行也同样如此

1 个赞

这不是 Windows / Linux 或 Node 20 / 24 的差异,而是你这里用的 bot.session(event) 本身就是“半手搓 session”,本地能跑只是碰巧,服务端没跑出来反而更符合实现。

核心点有两个:

  1. bot.session(event) 只是把你传进去的 Partial<Event> 包成一个 Session,不会像适配器收到真实事件那样帮你补全各种字段。
  2. session.execute('help') 不是单纯“找到命令然后执行”,它会依赖当前 session 的上下文去做过滤、权限、用户/频道附着等步骤。只要其中某一步对这个“伪造 session”不满意,就可能直接返回空,不报错。

你这段代码里虽然塞了:

  • channel
  • guild
  • user
  • member
  • type: 'guild-member-request'

但它仍然不是一个“真实的消息 session”。

尤其是你拿的是 guild-member-request 去执行命令,这本身就不属于正常用法。

命令系统更偏向“message session”,而不是“request session”。


从实现上看,真实的 onebot 请求事件在适配器里会额外补很多原始字段,例如:

  • userId
  • channelId
  • guildId
  • messageId
  • content
  • isDirect

而你手写的 event 没有走这条标准适配链,所以行为本来就不稳定。

本地能出帮助菜单,只能说明你本地当时的数据库状态 / 权限 / 过滤条件刚好让这个 session 通过了;服务端这边只要用户记录、频道记录、插件过滤或权限环境稍有不同,就会变成“send 能发,execute 没输出”。

一个更直接的判断:

  • session.send() 只需要 channelId 基本就能发出去
  • session.execute() 需要这个 session “看起来像一次真实命令调用”

所以“能 send 但不能 execute”完全可能,而且不奇怪。


这问题其实也不止 onebot,别的适配器只要你拿非消息事件、或者自己手搓 Partial<Event> 去跑 session.execute(),都可能踩到同类问题。

如果你想快速验证,可以打印这些字段对比本地和服务端:

ctx.logger.warn({
  type: session.type,
  userId: session.userId,
  channelId: session.channelId,
  guildId: session.guildId,
  isDirect: session.isDirect,
  content: session.content,
  platform: session.platform,
})

如果这里面有任何一项在两边不一致,或者某项是 undefined,那结果分叉就很正常。


结论是:

  • 不要把 guild-member-request 这种 request session 当命令 session 用
  • 不要指望 bot.session(Partial<Event>) 和“适配器实际收到的事件”完全等价
  • 如果要程序化执行命令,最好构造一个真正的 message session,或者干脆直接调用你要复用的业务逻辑,不要绕 session.execute()

如果你有更多的疑问,可以多阅读几个适配器的源码,看看session、event是怎么下发的

1 个赞

问题已经通过抽出成函数来复用解决了,更多是对这个差异发生在哪里感到好奇,多谢回复

有个我所不理解的地方,下面是更多信息的补充和试验

代码如下:

    ctx.command('test.command').action(async ({ session }) => {
        console.log('test.command#action')
        console.log(session)
    })

    ctx.on('bot-status-updated', async (bot) => {
        if (bot.status === 1) {
            for (const gid of cfg.retry) {
                const event: Partial<Event> = {
                    channel: await bot.getChannel(gid),
                    guild: await bot.getGuild(gid),
                    user: await bot.getUser(bot.selfId),
                    member: await bot.getGuildMember(gid, bot.selfId),
                    type: 'guild-member-request',
                }
                console.log(event, '\n')

                const session = bot.session(event)
                console.log(session, '\n')

                await session.send('debug#start').then(() => {
                    console.log('debug#start', '\n')

                    session.execute('test.command').then((ret) => {
                        console.log(ret)
                        console.log('debug#end')
                    })
                })
            }
        }
    })

本地机器上打印可见

{
  channel: { id: '1019536708', name: 'IWS2000-DEBUG', type: 0 },
  guild: { id: '1019536708', name: 'IWS2000-DEBUG' },
  user: {
    id: '3951441492',
    name: 'IWS-2000',
    userId: '3951441492',
    avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=3951441492&spec=640',
    username: 'IWS-2000'
  },
  member: {
    user: {
      id: '3951441492',
      name: 'IWS-2000',
      userId: '3951441492',
      avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=3951441492&spec=640',
      username: 'IWS-2000'
    },
    nick: 'iws-2000',
    roles: [ 'owner' ]
  },
  type: 'guild-member-request'
} 

Session {
  id: 57,
  sn: 57,
  event: {
    channel: { id: '1019536708', name: 'IWS2000-DEBUG', type: 0 },
    guild: { id: '1019536708', name: 'IWS2000-DEBUG' },
    user: {
      id: '3951441492',
      name: 'IWS-2000',
      userId: '3951441492',
      avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=3951441492&spec=640',
      username: 'IWS-2000'
    },
    member: { user: [Object], nick: 'iws-2000', roles: [Array] },
    type: 'guild-member-request',
    selfId: '3951441492',
    platform: 'onebot',
    timestamp: 1774092556377
  },
  locales: [],
  Symbol(cordis.tracker): { associate: 'session', property: 'ctx' }
} 

debug#start 

test.command#action
Session {
  id: 57,
  sn: 57,
  event: {
    channel: { id: '1019536708', name: 'IWS2000-DEBUG', type: 0 },
    guild: { id: '1019536708', name: 'IWS2000-DEBUG' },
    user: {
      id: '3951441492',
      name: 'IWS-2000',
      userId: '3951441492',
      avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=3951441492&spec=640',
      username: 'IWS-2000'
    },
    member: { user: [Object], nick: 'iws-2000', roles: [Array] },
    type: 'guild-member-request',
    selfId: '3951441492',
    platform: 'onebot',
    timestamp: 1774092556377
  },
  locales: [],
  guild: {
    permissions: [],
    locales: [],
    platform: 'onebot',
    id: '1019536708'
  },
  channel: {
    permissions: [],
    locales: [],
    platform: 'onebot',
    id: '1019536708'
  },
  user: { authority: 1, permissions: [], locales: [] },
  scope: 'commands.test.command.messages',
  Symbol(cordis.tracker): { associate: 'session', property: 'ctx' }
}
[]
debug#end

服务器上的打印

{
  channel: { id: '468674513', name: 'GRAYGOO-DEBUG', type: 0 },
  guild: { id: '468674513', name: 'GRAYGOO-DEBUG' },
  user: {
    id: '2817987433',
    name: '小灰',
    userId: '2817987433',
    avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=2817987433&spec=640',
    username: '小灰'
  },
  member: {
    user: {
      id: '2817987433',
      name: '小灰',
      userId: '2817987433',
      avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=2817987433&spec=640',
      username: '小灰'
    },
    nick: '',
    roles: [ 'owner' ]
  },
  type: 'guild-member-request'
} 

Session {
  id: 24,
  sn: 24,
  event: {
    channel: { id: '468674513', name: 'GRAYGOO-DEBUG', type: 0 },
    guild: { id: '468674513', name: 'GRAYGOO-DEBUG' },
    user: {
      id: '2817987433',
      name: '小灰',
      userId: '2817987433',
      avatar: 'http://q.qlogo.cn/headimg_dl?dst_uin=2817987433&spec=640',
      username: '小灰'
    },
    member: { user: [Object], nick: '', roles: [Array] },
    type: 'guild-member-request',
    selfId: '2817987433',
    platform: 'onebot',
    timestamp: 1774092920882
  },
  locales: [],
  Symbol(cordis.tracker): { associate: 'session', property: 'ctx' }
} 

debug#start 

[]
debug#end

环境方面两者均新建了一个独立的 koishi 实例,可见两者的 Session 对象都是几乎一致,但 session.execute() 不会解析执行传入的字符串

不过另一个测试佐证了手搓的 session 确实不稳定

    ctx.command('test.debug').action(async ({ session }) => {
        console.log('debug#start')

        await session?.bot.session(session?.event).execute('test.command')

        console.log('debug#end')
    })

本地和服务器都 execute('test.command') 了命令

debug#start
test.command#action
debug#end

该读源码了

1 个赞

你这个补充其实已经把范围缩得很小了,关键结论有两个。

第一,session.execute() 返回 [] 本身不代表“没执行”。

因为你的 test.command action 没有 return,所以就算 action 正常进去了,execute() 最后也还是会 resolve 成 []

本地那次就是这种情况:你有 test.command#action 日志,说明命令确实执行了,只是没有返回消息片段。

第二,服务端这次确实是“没进 action”,而且大概率拦在 action 之前。
因为服务端没有打印 test.command#action,但 execute() 也没有报错,只是返回 []。这通常只剩两类可能:

  1. command.ctx.filter(session) 没过
  2. 某个 before('command/execute') 钩子提前返回了

你后面那个实验其实很能说明问题:

await session.bot.session(session.event).execute('test.command')

这段在本地和服务端都能进 test.command,说明:

  • test.command 本身没有问题
  • session.execute('test.command') 也没有问题
  • 真正有问题的是你手搓出来的那个 bot.session(event)

也就是说,差异不在“execute 会不会解析字符串”,而在“这个 session 在服务端看起来不像一个允许执行该命令的 session”。

还有一个特别容易误导人的点:
你打印出来的两个 Session { ... } “看起来几乎一致”,但这不等于它们真一样。Session 里很多关键字段是定义在原型上的 getter,不会完整显示在 console.log(session) 里,比如:

  • session.userId
  • session.channelId
  • session.guildId
  • session.isDirect
  • session.content
  • session.author
  • session.stripped

真正参与过滤和命令判断的,往往就是这些 getter 的值,不是你看到的那坨 own properties。

所以你现在这个现象,更像是:

  • 本地:手搓 session 恰好通过了过滤
  • 服务端:手搓 session 被某个过滤条件挡掉了
  • 真实消息 session:两边都能通过

而且这件事不一定是 onebot 独有的。只要是 bot.session(Partial<Event>) 手搓 session,又没走适配器真实的事件适配流程,都可能这样。

我反而觉得你这个补充说明了一个更具体的判断:

如果是“裸 Koishi 核心 + 裸 onebot + 这两个测试命令”,按理说两边应该一致。
既然现在服务端没进 action,而真实消息能进,那很像是服务端还有额外的上下文过滤或命令前置钩子,且它依赖 session.type / content / isDirect / guildId / channelId 之类字段。

最值得直接打出来的是这几个值,而不是只看 console.log(session)

console.log({
  type: session.type,
  userId: session.userId,
  channelId: session.channelId,
  guildId: session.guildId,
  isDirect: session.isDirect,
  content: session.content,
  author: session.author,
  stripped: session.stripped,
})

再加一条:

const cmd = ctx.$commander.get('test.command')
console.log('filter =', cmd?.ctx.filter(session))

如果这里服务端是 false,那就已经定位了:不是 execute 没解析,而是这个 session 被命令上下文过滤掉了。
如果这里是 true,那就继续查有没有别的 before('command/execute') 钩子提前 return。

你这个第二个实验其实已经证明了:
bot.session(session.event) 复刻“真实消息事件”是稳定的,
bot.session(你自己手拼的 Partial<Event>) 则不是稳定接口。


相关源码大概看这几处就够了:

node_modules/@satorijs/core/src/session.ts
node_modules/@satorijs/core/src/bot.ts
node_modules/@koishijs/core/src/session.ts
node_modules/@koishijs/core/src/command/command.ts
node_modules/koishi-plugin-adapter-onebot/src/utils.ts
1 个赞

(✪ω✪)小学好厉害!~

1 个赞

完整看了一遍你的代码,你自己创建了一个 Session 然后就调 Session 上的方法了,都没拿给 Koishi 过……

有什么需求就提对应的需求然后考虑最优的实现方法,如果你确定你的需求是「构建一个事件」的话(你已经在手动构建 event 了),可以参考现有的适配器写一个新的小适配器,写适配器不难,并且至少能保证你构建的事件可以正确传给 Koishi

2 个赞