1. 前言

最近研究OlivOS的源码和具体实现过程,简单研究了一下basic.json的部分配置,准备写一篇文章来做一些技术讨论。个人见解,难免错漏,求大佬轻喷,也欢迎大家一起讨论🧐

1.1 注意事项

**注意!!!本次的研究基于的 OlivOS 版本号为 0.11.13 (133) **
具体版本为 2023年7月26日发布的滚动版本 50e8e80 如需查阅具体版本,请移步github

本文仅为个人对于OlivOS的一些理解和技术讨论,看懂可能需要有较高的python基础和一定的OlivOS相关开发经验。
本文并非任何形式的文档或说明,难免会有错误,烦请各位多多交流指正!

1.2. OlivOS 整体工作原理

首先,我们必须明确一个事实,OlivOS是一个 多进程/多线程 的复杂系统,里面每个模块各司其职以实现整个功能。
当某平台消息发出后,OlivOS中大致经历了以下过程(仅为示意图):
Image description

1.3. 线程 or 进程?

注:具体是线程模式还是进程模式需要在 basic.json 中指定,默认好像是线程模式(?)。在之后的文章中,可能会出现进程、线程混用,可能需要各位根据源代码自行理解
线程和进程模式判断的源代码,摘自 OlivOS/bootAPI.py 141 ~ 149行:

tmp_proc_mode_raw = 'auto'
tmp_proc_mode = 'threading'
if 'proc_mode' in basic_conf['system']:
    tmp_proc_mode_raw = basic_conf['system']['proc_mode']
if 'proc_mode' in basic_conf_models_this:
    tmp_proc_mode_raw = basic_conf['system']['proc_mode']
tmp_proc_mode = tmp_proc_mode_raw
if 'auto' == tmp_proc_mode_raw:
    tmp_proc_mode = 'threading'
一些常见的进程(线程)类型:
  • 主进程:运行环境(主要就是 OlivOS/bootAPI.py 中的那个大循环)
  • 日志进程 [logger]: 负责所有OlivOS的日志记录(不论是输出到终端,还是GUI中)
  • 插件托盘进程 [plugin]:负责装载各个插件(plugin/app文件夹中的所有 .opk文件夹形式插件),在发生各种Event的时候依次运行各个插件
  • account.json 中填写了某个平台后,还会相应的构建一个负责那个平台的消息接收端服务器
    (不同平台不同实现,有些是基于对平台服务器主动进行轮询,也可能是在平台服务器端(如gcq上)设置post然后做一个本地服务器去接收post请求……)
  • 其他进程还有如OlivOS自动更新,Windows下的窗口GUI线程,自动调用的gocqhttp等第三方exe等

这张截图显示了在 WSL(Linux) 下,一个完全空白的 OlivOS 在运行过程中的进程信息,可以看出这是一个有很多个PID的进程树
Image description

2. basic.json 简要介绍

在当前版本的配置文件 basic.json 中,主要分以下三个部分

{
        "system": {
            ......
    },
        "queue": [
            ......
    ],
        "models": {
            ......
    }
}

2.1. system 部分

具体数据如下:

system": {
        "name": "OlivOS",
        "init": [
            "OlivOS_logger",
            "OlivOS_account_config",
            "OlivOS_multiLoginUI",
            "OlivOS_account_fix",
            "OlivOS_account_config_save",
            "OlivOS_account_config",
            "OlivOS_nativeWinUIAPI",
            "OlivOS_gocqhttp_lib_exe_model",
            "OlivOS_walleq_lib_exe_model",
            "OlivOS_cwcb_lib_exe_model",
            "OlivOS_hackChat_link",
            "OlivOS_plugin",
            "OlivOS_virtual_terminal_link",
            "OlivOS_flask_post_rx",
            "OlivOS_onebotV12_link",
            "OlivOS_qqGuild_link",
            "OlivOS_discord_link",
            "OlivOS_telegram_poll",
            "OlivOS_fanbook_poll",
            "OlivOS_kaiheila_link",
            "OlivOS_dodo_link",
            "OlivOS_biliLive_link",
            "OlivOS_update_check"
        ],
        "event": {
            "account_edit": [
                "OlivOS_account_config",
                "OlivOS_multiLoginUI",
                "OlivOS_account_fix",
                "OlivOS_account_config_save",
                "OlivOS_account_config",
                "OlivOS_account_config_update"
            ],
            "account_edit_asayc_start": [
                "OlivOS_account_config"
            ],
            "account_edit_asayc_do": [
                "OlivOS_multiLoginUI_asayc"
            ],
            "account_edit_asayc_end": [
                "OlivOS_account_fix",
                "OlivOS_account_config_save",
                "OlivOS_account_config",
                "OlivOS_account_config_update"
            ],
            "account_update": [
                "OlivOS_gocqhttp_lib_exe_model",
                "OlivOS_walleq_lib_exe_model",
                "OlivOS_cwcb_lib_exe_model",
                "OlivOS_hackChat_link",
                "OlivOS_virtual_terminal_link",
                "OlivOS_flask_post_rx",
                "OlivOS_onebotV12_link",
                "OlivOS_qqGuild_link",
                "OlivOS_discord_link",
                "OlivOS_telegram_poll",
                "OlivOS_fanbook_poll",
                "OlivOS_kaiheila_link",
                "OlivOS_dodo_link",
                "OlivOS_biliLive_link"
            ]
        },
        "type_event": {
            "account_update": [
                "gocqhttp_lib_exe_model",
                "walleq_lib_exe_model",
                "cwcb_lib_exe_model",
                "hackChat_link",
                "terminal_link",
                "flask_post_rx",
                "onebotV12_link",
                "qqGuild_link",
                "discord_link",
                "telegram_poll",
                "fanbook_poll",
                "kaiheila_link",
                "dodo_link",
                "biliLive_link"
            ]
        },
        "control_queue": "OlivOS_control_queue",
        "interval": 0.2,
        "proc_mode": "auto"
    },

2.1.1 init 列表

这个列表中定义了当OlivOS开始运行时的初始化顺序,一共可以分为以下几步:


  1. init队列读取前操作
    详见OlivOS/bootAPI.py中,进入大循环前的部分

在这个阶段,主要操作有:

  • 打印欢迎横幅
  • 尝试读取basic.json的内容或使用默认配置
  • 建立所有后续需要用到的进程间通讯队列
  • 解析运行指令参数(目前仅有 --noblock 一个可选参数,使用后会跳过初始化阶段的账户控制gui界面)

  1. 日志初始化

具体包含以下过程

1 - "OlivOS_logger",

首先建立日志进程,在建立过后所有的日志全部由日志模块进行统一输出处理
(在这之前的日志,通过preLoadPrint函数实现,具体目前处理方式就是直接print,后续可能会修改)


2.账号初始化

具体包含以下过程

1 - "OlivOS_account_config",
2 - "OlivOS_multiLoginUI",
3 - "OlivOS_account_fix",
4 - "OlivOS_account_config_save",
5 - "OlivOS_account_config",

这个过程中,每个过程的具体用途如下

序号名称解释
1OlivOS_account_config第一次读取 account.json 中的数据,获取保存的各平台账号密码信息
2OlivOS_multiLoginUI在windows界面下,若–noblock参数未选择,则使用刚刚读取的数据,构建账号管理器
3OlivOS_account_fix检查账号信息,(1)部分平台通过api获取真实bot id信息;(2)gcq中自动修改 device.json 的协议、补齐其中设备信息配置;(3)端口自动分配开启时,检查端口分配情况,避免发生端口冲突
4OlivOS_account_config_save保存覆写 account.json
5OlivOS_account_config重新读取各个平台的账号信息

在以前版本中,还有一个account_config_safe的步骤,用于获取不包含密码的纯账号信息,提供给插件使用(以避免插件对密码等敏感数据的误读)。似乎在最近一次更新basic.json的过程中,这个步骤删除了


  1. windows下的gui界面生成

具体包含以下过程

1 - "OlivOS_nativeWinUIAPI",

这个步骤,指的是在 Windows 系统下,OlivOS的主要界面生成,日志窗口、右下角系统托盘等一系列gui内容都是这次统一进行初始化的。


  1. 导入运行需要的第三方的各种模块

具体包含以下过程

1 - "OlivOS_gocqhttp_lib_exe_model",
2 - "OlivOS_walleq_lib_exe_model",
3 - "OlivOS_cwcb_lib_exe_model",

这个步骤,指的是在 Windows 系统下,启动内置内置QQ、微信等部分平台的第三方客户端

序号名称解释
1OlivOS_gocqhttp_lib_exe_modelgo-cqhttp 大家的老熟人了,如果不是分离部署的话,OlivOS 就是通过这个步骤打开gcq的
2OlivOS_walleq_lib_exe_modelwalleq,类似gcq、mirai的另外一个框架?没具体用过没有发言权
3OlivOS_cwcb_lib_exe_model微信平台框架,做微信机器人的

❓问题:OlivOS_hackChat_link 为何在这时候初始化,而不是在后续各平台过程中初始化。看着不像是调用第三方客户端软件,会不会是为了后续支持私有源搭建做准备?

仑质 OlivOS_hackChat_link的初始化为何会在OlivOS_plugin之前
这其实恰恰是你所遗漏的部分,Hack.Chat平台的协议对接更类似onebotV11中正向websocket的模式,这带来一个明显的逻辑特点,就是收发消息需要在同一个websocket连接中完成,对于OlivOS来说,这与其它的诸如KOOK与Discord的平台相比,它要求从OlivOS_plugin中发起的接口调用(如发送消息、获取用户信息等),经由全局的事件路由重新回到OlivOS_hackChat_link中,再经由相关的SDK转换后从websocket会话中发出。这个过程中的关键在于OlivOS的全局事件路由,OlivOS_plugin将会把事件调用以类似广播的方式发送到主要事件通道中,发送的目标的匹配条件为type为hackChat_link且hash与账号实例一致,在这个过程中如果OlivOS_hackChat_link在此时没有完成启动,则会导致该次调用丢失,这会给插件设计带来额外的成本,并且目前来看,前置这一个模块的加载没有明确的弊端,所以这里决定了这样做。


5.插件托盘初始化,插件载入

具体包含以下过程

1 - "OlivOS_plugin",

这个步骤是正常开发 OlivOS 插件过程中,插件与 OlivOS 的第一次交互。
值得注意的是,插件的初始化阶段其实有两个事件 initinit_after

  • init 事件的运行不保证插件优先级顺序,同时.opk打包状态下资源文件还没有自动解压到data目录下。在那里适合做插件内部的各种环境初始化,但不适合做与外界的交互(也就是说,这个阶段应该都是运行确定的python语句)。
  • init_after 事件的运行顺序严格按照插件优先级,同时确保 opk 打包中的 data 目录已经完成释放;在这个阶段,各个插件的第一轮 init已经完成。
    在这里,适合做对外部内容的处理,比如:对opk内置资源文件的操作、与其他插件尝试联动

  1. OlivOS_hackChat_link之外的各平台初始化

具体包含以下过程

1 - "OlivOS_virtual_terminal_link",
2 - "OlivOS_flask_post_rx",
3 - "OlivOS_onebotV12_link",
4 - "OlivOS_qqGuild_link",
5 - "OlivOS_discord_link",
6 - "OlivOS_telegram_poll",
7 - "OlivOS_fanbook_poll",
8 - "OlivOS_kaiheila_link",
9 - "OlivOS_dodo_link",
10 - "OlivOS_biliLive_link",

这一块就是OlivOS支持的各个平台,没啥可说的,没多支持一个平台就会多一行。

还是多提一嘴,这些模块只有在 account.json 中真正出现了相应账号后才会运行


  1. 新版本检查

具体包含以下过程

1 - "OlivOS_update_check",

检查OlvOS是否存在更新的发行版,在这里仅作检查并不会自动更新

注意:这个是指 OlivOS 框架的更新,和 OlivaDice 等各种件的自动更新无关


2.2. queue 部分

这里定义了所有进程间通讯的队列
个人感觉就是,这里列举了在后续model中使用到的进程间

2.3. models 部分

3. 简单修改实例

3.1. 日志模块(如开启debug信息)

配置文件 183 ~ 197行(位置可能变化):

{
    "OlivOS_logger": {
        "enable": true,
        "name": "OlivOS_logger",
        "type": "logger",
        "interval": 0.002,
        "dead_interval": 1,
        "proc_mode": "auto",
        "rx_queue": "OlivOS_logger_queue",
        "control_queue": "OlivOS_control_queue",
        "mode": [
            "console_color",
            "logfile",
            "native"
        ],
        "fliter": [2, 3, 4, 5]
    }
}

在这里,主要可以修改的是 filter (和mode

3.1.1. 日志输出等级控制

filter 主要控制输出的日志等级(默认在info)

具体日志等级如下:

Log LevelLog TypeLog Color
-1TRACE#666666
0DEBUGgreen
1NOTEblack
2INFOblack
3WARN#E6992C
4ERRORred
5FATALred

Image description

通过修改filter列表,可以打开 DEBUG 等 log 信息,对开发过程还是很有帮助的

3.1.2. 日志输出格式控制

mode 主要控制日志输出的类型
OlivOS目前支持以下几种模式

输出模式解释
console控制台黑白日志输出(上古版本的日志输出,现在默认关闭改为彩色日志输出)
console_color控制台彩色输出,不同等级的日志用不同颜色标注
logfile日志保存到本地文件中,默认位置在 ./logfile 文件夹中
native在windows系统下,在原生tk的gui中渲染日志(就是现在的gui版本日志)

如果需要自己增加对日志的处理逻辑(如自动导入到数据库中),只需要在 OlivOS/diagnoseAPI.py 中简单增加一下自己的处理逻辑后,在 mode 列表中打开这一项你实现的那一项即可,非常简单(确信)

具体源代码,改这里就行:
Image description

3.2. 插件托盘(如控制托盘重载频率,插件运行方式)

配置文件 199 ~ 215行(位置可能变化):

        "OlivOS_plugin": {
            "enable": true,
            "name": "OlivOS_plugin",
            "type": "plugin",
            "interval": 0.002,
            "dead_interval": 1,
            "rx_queue": "OlivOS_rx_queue",
            "tx_queue": [
                "OlivOS_dodobot_rx_queue"
            ],
            "control_queue": "OlivOS_control_queue",
            "logger_proc": "OlivOS_logger",
            "treading_mode": "full",
            "restart_gate": 50000,
            "enable_auto_restart": true,
            "debug": false
        },

3.2.1. 插件重载控制

  • enable_auto_restart 是否开启插件托盘的自动重载功能(true/false),开启后每过一段时间,OlivOS会自动重载所有插件,减少由于个别插件代码质量问题导致的内存泄露等情况,进而使得OlivOS可以在服务器中长期稳定持续运行不变慢
  • restart_gate 插件托盘从的事件次数,默认每经过50000次事件重载一次所有插件。

注意:这里的事件是指插件的Event中可以触发的任何事件,简单而言就是包括私聊、群聊、群通知消息在内的一切消息(也包括来自于OlivOS框架的事件),具体种类详见OlivOS文档
无论有无插件真正处理了这个事件,重载计数器都会增加

通过控制 restart_gate 大小,可以人为实现短周期重载,在开发阶段可以方便的测试插件在自动重载过程中的行为。
同时如果是私有机器人,一年到头没有几条消息,也可以人为降低阈值加快重载频率。

3.2.2. 插件运行模式控制

treading_mode 控制插件运行的模式,默认为"full"

有以下选择:

treading_mode解释
“full”每个OlivOS事件(每条消息)用一个独立的线程处理,(消息之间可以有一定系统调用层次的并行)。每个线程中,消息还是严格按照插件优先级依次调用
“none”所有OlivOS事件(每条消息)都在插件托盘所在的主线程/进程中执行

相关源代码在 OlivOS/pluginAPI.py 中,具体如下图所示
Image description

4. QQ分离部署相关

(有时间有兴趣再写,只是先po一张之前做的分离部署示意图)

Image description

    一楼归楼主

    目前值得讨论的问题:

    [X] 初始化过程中,account_config_safe 为何删除弃用?
    [X] OlivOS_hackChat_link 的初始化为何会在 OlivOS_plugin 之前,而不是在各平台统一初始化的过程中?——猜测:为未来支持私有源搭建做准备?

    感觉这个应该没啥人愿意看吧,算了,就当是写给自己看的笔记了。。。

    目前可以回答的几点:

    1、account_config_safe为何删除弃用
    这是因为在0.11.8(128)版本时引入了动态的账号编辑,该模式允许账号进行动态的修改,这就意味着原先所考虑的在加载阶段就不加载明文密码的设计的前提(只有一次账号的修改,并且后续运行不需要密码数据)已经不成立了,此外,实际上该设计由于开发者完全可以直接去文件系统中读取对应的配置文件,所以其实完全没有任何意义(脱裤子放屁),故而将其移除。

    2、OlivOS_hackChat_link的初始化为何会在OlivOS_plugin之前
    这其实恰恰是你所遗漏的部分,Hack.Chat平台的协议对接更类似onebotV11正向websocket的模式,这带来一个明显的逻辑特点,就是收发消息需要在同一个websocket连接中完成,对于OlivOS来说,这与其它的诸如KOOKDiscord的平台相比,它要求从OlivOS_plugin中发起的接口调用(如发送消息、获取用户信息等),经由全局的事件路由重新回到OlivOS_hackChat_link中,再经由相关的SDK转换后从websocket会话中发出。这个过程中的关键在于OlivOS的全局事件路由,OlivOS_plugin将会把事件调用以类似广播的方式发送到主要事件通道中,发送的目标的匹配条件为typehackChat_linkhash与账号实例一致,在这个过程中如果OlivOS_hackChat_link在此时没有完成启动,则会导致该次调用丢失,这会给插件设计带来额外的成本,并且目前来看,前置这一个模块的加载没有明确的弊端,所以这里决定了这样做。

      说点什么吧...