QQ 群聊锐评机器人 —— 基于 AstrBot(QQ 官方机器人)+ 大模型,@一下即可锐评群友、问答互动
- @机器人 + 关键词触发:群内 @机器人并说出关键词(如"锐评下"),即可触发大模型生成回复
- 群聊上下文感知:基于最近的群聊记录生成上下文相关的锐评,不是无脑随机回复
- 插件式指令扩展:在
prompt_system.yaml里加配置即可新增同类型指令变体,无需改代码 - 群内动态添加指令:在群里 @机器人即可动态增删自定义指令,无需重启
- 白名单机制:只响应指定群(QQ 官方机器人为
group_openid),安全可控 - 纯内网通信:AstrBot 桥接插件通过本地 HTTP 推送事件,无需公网 IP
- 单二进制部署:编译为单个可执行文件,丢到服务器上就能跑
- 人格系统:每条指令可配 5 维人格(身份/性格/关系/情绪/系统指令),回复有性格但有边界
- @机器人直接问答:@机器人但没匹配到指令时,直接把消息发给大模型回答(不带人格)
- 主动插话:没被 @ 时先由大模型判定要不要接话(默认关闭;有冷却/连续发言上限,按「机器人+群」串行)
- 富消息回复:回复支持引用、@、图片、语音、视频、文件;QQ 官方机器人自动上传媒体并尽量走被动回复
- 成本优化缓存命中:无状态请求 + 前缀缓存,按 token 成本自动扩展/重置窗口,省钱省算力
QQ 官方机器人 ←→ AstrBot(sidecar,只做 IM 网关)←→ Go Bot ←→ LLM API
┌──────────┐ OneBot 12 ┌───────────┐ OneBot 12 ┌──────────────────┐
│ QQ 群 │ ←─────────→ │ AstrBot │ ←──────────→ │ Go Bot │
└──────────┘ (WS/HTTP) │ + 桥接插件 │ 事件/动作 │ (router/LLM/缓存) │
└───────────┘ └────────┬─────────┘
│ OpenAI API
▼
┌──────────────┐
│ LLM 大模型 │
│ (DeepSeek等) │
└──────────────┘
AstrBot 只负责平台接入:收到消息后由桥接插件转成 OneBot 12 事件推给 Go;Go 的
send_message / get_self_info / get_group_member_info 等动作再由插件翻译回平台。
部署方式见 sidecar/README.md。
- QQ 官方机器人(AppID / AppSecret)
- AstrBot 已安装,并装有本仓库的
桥接插件(
qq_official适配器已连上平台) - 一个 OpenAI 兼容的大模型 API Key(DeepSeek、豆包、通义千问等均可)
# Windows:双击 start_main.bat
# Linux/macOS:./start_main.sh
# 脚本会自动完成 3 步:
# 1. 安装前端依赖 + 构建前端
# 2. 拷贝到嵌入目录
# 3. go run ./cmd/good-review 启动服务
# 首次运行会自动创建 config.yaml 并退出
# 编辑 config.yaml 填入你的配置,重新运行即可打包还需要 Git,用于自动提取版本号。
# Windows:双击 build_exe.bat
# Linux/macOS:./build_linux.sh
# 脚本会自动完成 3 步:
# 1. 安装前端依赖 + 构建前端
# 2. 拷贝到嵌入目录
# 3. 交叉编译 4 个平台的可执行文件到 dist/ 目录
#
# 版本号自动从 git tag 提取,无需手动维护:
# 打了 tag v1.0.0 → 文件名带 v1.0.0
# 没打 tag → 文件名带 abc1234(commit hash)
# 有未提交修改 → 文件名带 abc1234-dirty
#
# 输出示例:
# dist/good-review-master-windows-amd64-v1.0.0.exe
# dist/good-review-master-linux-amd64-v1.0.0
# dist/good-review-master-darwin-amd64-v1.0.0 (Intel Mac)
# dist/good-review-master-darwin-arm64-v1.0.0 (Apple Silicon)
# 将对应平台的文件复制到任意目录运行,首次运行会自动创建 config.yaml 并退出
# 编辑 config.yaml 填入你的配置,重新运行即可| 配置项 | 说明 | 示例值 |
|---|---|---|
im.api_base |
所有账号默认的出站地址(实现端动作服务) | http://127.0.0.1:6199 |
im.echo_self_messages |
实现端是否回传机器人自己发的消息(NapCat 会,AstrBot 桥不会);true 时回复靠回传事件入库、本地不再重复记录 | false |
im.accounts |
可选:多实现端时按账号覆盖出站地址 [{id, api_base}];id = 平台名_接收账号 |
一般不用配 |
bot.allow_groups |
允许响应的群(QQ 官方机器人填 group_openid;* 表示全部群;支持逗号分隔字符串或 YAML 列表) |
123456,789012 |
bot.allow_workspace_accounts |
允许自动开通的工作空间 ID:平台名_账号类型_接收账号(如 qq_official_bot_1906xxxx;* 表示全部;支持 YAML 列表或逗号分隔字符串) |
["qq_official_bot_1906xxxx"] |
bot.proactive_enabled |
主动插话:没被 @ 时也先由大模型判定要不要接话 | false |
bot.proactive_cooldown_sec |
主动发言后该群冷却秒数(期间不再判定;0=不冷却) | 90 |
bot.proactive_max_streak |
同一话题最多连续主动发言次数(0=不主动发言) | 5 |
bot.proactive_judge_interval_sec |
两次判定的最小间隔(秒,0=不设;从上次判定开始计时) | 20 |
bot.proactive_judge_timeout_sec |
单次判定超时(秒,超时=不回复;0=沿用 runtime.llm_timeout_sec) |
20 |
bot.proactive_judge_images |
每次判定最多附几张图(0=图片消息不判定) | 1 |
llm.provider |
固定填 openai(兼容所有 OpenAI 格式) |
openai |
llm.api_key |
大模型 API Key(写在 secret.yaml) |
sk-xxx |
llm.api_base |
大模型接口地址 | https://api.deepseek.com |
llm.model_name |
模型名称 | deepseek-v4-flash |
llm.temperature |
锐评风格:1.0=发散 0.5=集中 | 1.2 |
llm.top_p |
核采样参数:越小输出越集中 | 0.95 |
llm.cache_hit_cost |
缓存命中单价(相对值) | 0.033 |
llm.cache_miss_cost |
缓存未命中单价(相对值) | 1.0 |
llm.max_context_tokens |
单次发送大模型的上下文 token 上限(超限强制重置) | 50000 |
llm.image_max |
单次回复最多实际查看的图片张数(0=关闭看图) | 3 |
llm.reply_max_sentences |
最终回复的句数上限(出稿后再排版一次,最多几句;0=关闭排版) | 5 |
llm.reply_rewrite_min_runes |
原稿不足这么多字就不排版(直接发原文);0=不设下限 | 60 |
llm.image_note_max_side |
入库描述用的降采样长边(像素);0=关闭入库描述 | 640 |
llm.image_note_max_pixels |
入库描述图的总像素上限;0=不限制 | 400000 |
llm.image_note_timeout_sec |
单张图描述超时(秒);0=用 runtime.llm_timeout_sec |
15 |
llm.image_note_retries |
描述失败重试次数;0=不重试 | 2 |
llm.image_note_concurrency |
单条消息内并发描述张数;0=按 1 | 3 |
llm.image_local_dirs |
view_image 允许读取的本地图片目录(QQ 官方图片是 AstrBot 本地临时文件) |
["~/.astrbot/data/temp"] |
runtime.llm_send_count |
每次发送的最近消息条数(重置窗口) | 200 |
runtime.max_cache_msg |
环形缓冲条数上限(建议 ≈31×llm_send_count) | 6200 |
runtime.llm_timeout_sec |
大模型超时(秒) | 20 |
runtime.max_msg_rune |
单条消息最大字符数 | 500 |
runtime.reply_send_interval_ms |
分句发送间隔:>0 拆成最多 5 条逐条发;0=合成一条 | 800 |
runtime.im_listen_addr |
AstrBot 推送事件的监听地址 | 127.0.0.1:18090 |
runtime.web_port |
Web 管理面板端口(<=0 禁用) | 8080 |
runtime.web_username |
Web 面板登录用户名 | admin |
runtime.web_password |
Web 面板登录密码(必填;写在 secret.yaml) |
"123456" |
pipeline.* |
三段流水线的批量/凑批/并发/重试/容量/空闲回收/排空;详见 config/config_example.yaml |
默认即可 |
assets.* |
后台补标签、消息描述预算、原图副本与台账保留;详见 config/config_example.yaml |
默认即可 |
pipeline 与 assets 的大部分参数可以在运行时通过 state.db 的 settings 表覆盖(键为 pipeline / assets,值为同名字段 JSON),库里的值优先于 config.yaml,约 30 秒生效。
Gin + JWT + 内嵌 Vue SPA,提供群消息监控页面。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /api/login |
否 | 返回 JWT token(账号密码校验) |
| GET | /api/status |
JWT | 昵称、脱敏 API Key、群数量 |
| GET | /api/groups |
JWT | 各群信息 + 活跃状态 |
| GET | /api/groups/:id |
JWT | 单群缓存消息 |
| POST | /api/logout |
JWT | 登出(无状态 token,占位) |
web_password 必填,登录页面始终需要账号密码。web_port 设为 0 或负数可以完全禁用 Web 面板。
密钥文件:
llm_api_key、web_password、jwt_secret、im_token、mcp_builtin_token、mcp_server_tokens全部写在同目录的secret.yaml(首次运行自动创建,权限 0600、已在.gitignore中)。 优先级为 环境变量 > secret.yaml。可用环境变量:GOOD_REVIEW_LLM_API_KEY、GOOD_REVIEW_WEB_PASSWORD、GOOD_REVIEW_IM_TOKEN。
指令提示词存放在 prompt_system.yaml 中,采用 list 格式,同一类型指令可配多个 keyword + prompt 变体:
cmd:
chat_review: # 形式:发送最近群聊记录给大模型
- keyword: "猫娘"
prompt: |
你是一只可爱的猫娘,在群聊里撒娇。基于群聊记录,选出你觉得最可爱的一位群友,用符合猫娘气质的语气夸夸TA、撒个娇。
persona: # 可选:5 维人格(配了就全必填)
identity: |-
一只软萌的猫娘,粘人爱撒娇,最喜欢被夸,看到可爱的人会忍不住想贴贴。
personality: '["爱撒娇粘人","单纯容易开心","被夸会炸毛","护短但不记仇"]'
relationship: '{"角色":"群里的团宠猫娘","对待方式":"对谁都软乎乎地撒娇"}'
system_prompt: |-
撒娇自然、不过度堆砌语气词;选出最可爱的一位群友夸夸TA。
emotion: '{"情绪底色":"愉悦","情绪反应性":"高","情绪外显度":"外放","恢复速度":"快","表达方式":"撒娇卖萌"}'
rules:
chat_review: | # 共享规则:追加到每条 chat_review 指令的 prompt 末尾
1. 禁止人身攻击和违禁内容
2. 字数控制在 100 字以内
3. 直接输出结果,不额外解释
4. 重点关注最近 10 条消息新增同类型变体只需在对应列表下加一项,无需改 Go 代码。persona 5 字段全必填,渲染在发给大模型的 user 消息末尾(详见核心机制)。
internal 下是内置指令自己的提示词,可直接在 prompt_system.yaml 里修改,热重载生效。前三项按拼进回复 system prompt 的先后排列,后四项是独立调用:
internal.reply_requirement:@ 回复时 system prompt 里的【要求】句internal.proactive_requirement:主动接话时替代上一条的【要求】句internal.focus_mention/internal.focus_last/internal.focus_context:追加在 system prompt 末尾的回复焦点提示;群内被点名固定用focus_mention(只回最后一条找机器人的消息),私聊用focus_last,主动接话按裁判给出的focus二选一internal.proactive_judge:没有被 @ 时,判断“这条消息要不要主动回复”的裁判提示词;输出reply / long_form / focus / reason(最后一条 @ 的是别人、不是机器人时一律不回)internal.add_persona:#添加人格生成 prompt + persona 时发给大模型的系统提示词internal.reply_rewrite:模型出稿后,把回复发回来改写成最多 N 句短句(N 见llm.reply_max_sentences)的排版提示词;请求里会附上机器人身份、生成回复时用的群聊记录和这条回复本身,并要求保持原回复的语气internal.image_describe:图片入库时生成与聊天语境无关的内容摘要(类型 / 主题 / 信息量 / 是否有文字),写入全局图库image_notes,供候选列表判断是否值得看原图
在群里 @机器人 发送以下格式,即可动态添加指令到 prompt_custom.yaml,重启后依然生效:
@机器人 #添加人格(人格名)指令类型(指令类型)大模型想提示词(要点)
示例:
@机器人 #添加人格(雌小鬼锐评下)指令类型(chat_review)大模型想提示词(嘴臭的雌小鬼,毒舌,喜欢说老登)
添加后立即生效,无需重启。动态添加的指令保存在 prompt_custom.yaml,与 prompt_system.yaml 分离。
通过群内指令只能增删 prompt_custom.yaml 中的自定义指令,无法修改或删除 prompt_system.yaml 中的系统指令和内置指令(如 帮助、添加人格)。
good-review-master/
├── cmd/
│ └── good-review/
│ └── main.go # 入口:日志初始化、首跑提示、app.Run + 退出码
├── app/ # 组合根:装配全部组件(App)、启动与优雅关闭
├── go.mod / go.sum # Go 模块依赖
├── config.yaml # 运行时配置(gitignore)
├── prompt_system.yaml # 系统提示词配置(gitignore)
├── prompt_custom.yaml # 动态添加的提示词(gitignore,程序自动创建)
├── secret.yaml # 密钥(API Key / 登录密码 / token;gitignore,权限 0600)
├── start_main.bat / .sh # 开发启动脚本(含前端构建)
├── build_exe.bat / .sh # 编译打包脚本(交叉编译 4 平台)
├── version/
│ └── version.go # 版本号注入(编译时由 -ldflags 写入)
├── apppath/
│ └── apppath.go # 可执行文件同目录路径解析
├── pool/
│ └── pool.go # 通用协程池(固定 worker,有界队列)
├── config/
│ ├── config.go # 运行时配置加载(config.yaml → struct)
│ ├── config_example.yaml # 内置配置模板(编译时嵌入 exe)
│ ├── prompt_system_example.yaml # 内置提示词模板(编译时嵌入 exe)
│ ├── embed.go # //go:embed 模板嵌入
│ ├── init.go # 首次运行自动创建配置文件
│ └── prompt.go # 提示词配置加载+热重载+增删改
├── cache/
│ └── cache.go # 环形缓冲区(零拷贝写入,O(1) 去重)
├── llm/
│ └── llm.go # 大模型客户端(go-openai SDK,连接池,类型安全)
├── async/
│ └── async.go # 安全 goroutine 管理器(基于 pool + ctx 自动传播 + panic recover)
├── logutil/
│ └── logger.go # 日志(zap + lumberjack,20MB 切割,30 天保留)
├── onebot/
│ ├── client.go # OneBot 动作客户端(resty,指向 AstrBot 桥接代理)
│ ├── cqimage.go # CQ 码解析(图片提取、引用/文件等占位、入库截断)
│ └── types.go # API 数据类型定义
├── im/
│ └── server.go # AstrBot 事件入口(鉴权、解码、入队)
├── bot/
│ ├── ingest.go # 事件消费循环 + 启动历史回填(context 支持优雅退出)
│ └── handler.go # 消息处理:白名单 → @检测 → 指令路由
├── router/ # 指令系统:前缀树匹配 + 分发(原 cmd/,包名改为 router)
│ ├── command.go # Router + 前缀树(trie)路由匹配 + 未匹配兜底 + 安全 goroutine
│ ├── internal_cmd.go # 内部指令(添加人格、删除人格、添加/删除指令规则、帮助)
│ ├── chat_review.go # chat_review 异步处理函数(缓存窗口 + 组装消息)
│ ├── chat_window.go # 缓存窗口决策:扩展 vs 重置(纯函数,可单测穷举)
│ └── persona_render.go # 人格渲染(5 维 → user 消息末尾片段)
└── web/
├── server/ # Gin Web 管理面板后端
│ ├── server.go # Gin 引擎、API 路由、SPA 回退、优雅关闭
│ ├── handlers.go # API 处理函数(login/status/groups/messages)
│ ├── auth.go # JWT 签发与校验(HS256,24h 过期)
│ ├── middleware.go # 中间件(日志、panic 恢复、CORS、JWT 鉴权)
│ └── embed.go # //go:embed static/frontend
└── frontend/ # uni-app Vue 3 SPA
└── src/ # 页面:登录、群列表、消息详情
main → app, config, apppath, logutil, version
app → config, llm, onebot, router, bot, web/server, mcpclient, mcpserver, logutil, internal/testutil
bot → config, cache, onebot, router
router → config, cache, llm, onebot, async
web/server → config, logutil, onebot, cache, version
async → logutil, pool
pool → (无内部依赖:仅标准库 sync)
onebot → (无内部依赖)
cache → (无内部依赖)
llm → (无内部依赖)
config → apppath, logutil
logutil → apppath
apppath → (无内部依赖)
version → (无内部依赖)
app 是组合根,只有 main 引它;组件需要别的组件时在 app 里注入,不要跨包直接 import。
IM ingress POST /im/event/{account} → 按 {platform}_{account} 开通/加载工作空间
→ 队列 (bot/ingest.go)
→ 去重:O(1) message_id 集合
→ 历史来自共享 state.db(按 workspace_id 过滤,无平台历史接口)
→ ProcessMessage (bot/handler.go)
→ 白名单检查
→ 截断到最大长度
→ 存入环形缓冲区(零拷贝写入)
→ @机器人检测(mention.user_id == self.user_id + 昵称兜底)
→ 前缀树匹配 → 命中指令:调用该指令 handler(带该指令人格)
→ 未命中:兜底直接发大模型回答(不带人格)
在 chat_review 列表下新增一项即可:
cmd:
chat_review:
- keyword: "雌小鬼锐评下"
prompt: |
你是嘴臭的雌小鬼...三步完成:
1. 在 prompt_system.yaml 的 cmd: 下添加新类型配置
cmd:
weather:
- keyword: "天气"
prompt: "你是天气助手..."2. 在 router/ 下新建 handler 文件(如 weather.go),handler 为 Router 的方法,签名固定为 (event, groupID, systemPrompt, keywordPrompt, mentionerNick, extra, persona):
func (r *Router) weatherHandler(event onebot.Event, groupID, systemPrompt, keywordPrompt, mentionerNick, extra, persona string) {
r.Go(func(ctx context.Context) error {
// 异步安全启动:ctx 自动继承 shutdown 信号(Ctrl+C 可取消进行中的 LLM 调用)
// systemPrompt = 机器人身份(模型名+昵称)+ 该指令共享规则
// keywordPrompt = 该指令的 prompt;persona = 5 维人格渲染(未配则空串)
// 组装 user 消息:聊天记录 + @者信息 + 关键词 prompt + 人格
userMsg := buildUserMsg(chatLog, mentionerNick, keywordPrompt, extra, persona)
reply, err := r.llmClient.Review(ctx, userMsg, systemPrompt)
...
return nil
})
}3. 在 router/command.go 的 NewRouter() 中注册到 handlerMap:
r.handlerMap = map[string]HandlerFunc{
"chat_review": r.chatReview,
"weather": r.weatherHandler, // 新增这一行
}- 本机或云服务器均可,无需公网 IP
- 编译为单二进制文件(前端页面一并嵌入),丢上去就跑
- 首次启动自动创建配置文件:exe 运行时若同目录没有
config.yaml和prompt_system.yaml,自动从内置模板生成,编辑后重新运行即可 - 推荐使用
systemd(Linux)或任务计划程序(Windows)设为开机自启
三层测试体系:
- 单元测试:
router/chat_window_test.go表格驱动穷举缓存窗口决策(扩展 vs 重置)各分支;决策函数为纯函数、显式传参、无全局依赖。 - 测试模式基建:
GOOD_REVIEW_TEST=1启动时用 FakeLLM(固定回复并记录每次调用)替代真实大模型、AstrBot 桥接指向配置地址,并注册/api/debug/*自测接口(inject/reset/state/trigger)——仅测试模式可达,生产模式会被 SPA fallback 回成页面 HTML。 - E2E:Playwright API 层自测(
requestfixture,不启动浏览器),直打真实测试二进制的 HTTP 接口:
cd tests/e2e && pnpm test- 覆盖:登录鉴权、群数据、消息数据、bot 全流程(注入→触发锐评→断言回复)、缓存命中成本(扩展 vs 重置窗口)。
- 进程内缓存/锚点是全局的,用例串行(
workers: 1),靠/api/debug/reset隔离;trigger同步等待 handler 收尾(锚点更新)再返回,异步副作用不跨用例泄漏。 - 详情见 docs/testing.md。
本项目围绕「并发安全 + 优雅退出 + 可测试性」设计了以下机制:
- 协程池(
pool/):固定 worker(默认runtime.NumCPU()*2)+ 有界任务队列;Submit非阻塞,队列满返回false由上层处理背压;Shutdown优雅排空。纯标准库,无内部依赖。 - 安全 goroutine 管理器(
async/):async.Group封装协程池,context 自动继承(Ctrl+C 可取消进行中的 LLM 调用)、panic recover 防止单任务 panic 拖垮进程;Wait()先取消再排空。 - 单写者架构:事件消费循环是唯一写缓存的 goroutine,配合
RWMutex,写侧几乎无竞争。
app.New 用 signal.NotifyContext 捕获 SIGINT/SIGTERM,三层消费同一个生命周期 ctx(app.App.ctx):
信号 → ctx 取消 → 事件消费循环 select ctx.Done() 正常退出
→ IM 入口 Shutdown(停止接收新事件)
→ webSrv.Shutdown(ctx)(10s 超时排空请求)
→ router.Wait() 等所有 in-flight goroutine(LLM 调用可被取消)
启动与关闭的顺序集中在 app 包:app.New 只负责装配(main.go 只做日志初始化、首跑提示和退出码),app.Run() 阻塞到信号后按上面的顺序关闭。
每次发大模型前按 token 成本二选一(router/chat_window.go):
- 扩展窗口(缓存命中):锚点
LLMAnchor{Start, LastSent}定位上次发送窗口,成本 =命中前缀 × cache_hit_cost + 新增 × cache_miss_cost; - 重置窗口:最近
llm_send_count条,成本 =全部 × cache_miss_cost。
三者全满足才扩展(锚点可用 + 扩展更便宜 + 未超 max_context_tokens 护栏),否则重置;重置窗口超护栏时从旧到新逐条截断。token 用启发式估算(cache/token.go:CJK 每字 1 token,ASCII 每 4 字符 1 token),不建字符串、不分配。
- 无状态请求:每次调用大模型(
llm.Review)都是全新的system + user单次请求,不维护任何服务端会话。"对话连续感"由机器人把该群聊天记录拼进 user 消息实现——请求无状态,但前缀连续。 - 前缀缓存命中:DeepSeek 对"从第 0 个 token 完全一致"的请求前缀自动做磁盘缓存(内容寻址、每用户独立、best-effort),命中部分按
cache_hit_cost计费并复用计算。扩展窗口时本次请求头部与上次完全一致 → 命中前面一大段,只对新增消息计未命中。 - 按群隔离:环形缓存与扩展锚点都按
groupID键控,每个群独立维护聊天记录与窗口——不同群前缀各异、各占各的缓存单元,互不挤占(内容寻址,非"一条缓存被覆盖")。 - 不破坏命中的关键:人格渲染放 user 消息末尾(见下),让
system + 以下是群聊记录 + 聊天记录前缀在换指令/换人格时保持稳定;只有单群自己前缀变化(被迫重置、长期沉默缓存被清)才会丢命中。
每条指令可配 5 维人格(config/prompt.go 的 Persona),字段全必填:
| 字段 | 类型 | 说明 |
|---|---|---|
identity |
文本 | 身份背景(名字/年龄/职业/经历,具体可信) |
personality |
JSON 数组 | 性格特质(行为化描述) |
relationship |
JSON 对象 | 与群友的关系(角色 / 对待方式) |
system_prompt |
文本 | 人格级系统指令(行为边界 / 输出要求) |
emotion |
JSON 对象 | 情绪维度(维度 → 取值,人格核心) |
渲染位置:RenderPersona(router/persona_render.go)把人格渲染成 system prompt 末尾的片段(身份头之后),user 消息(聊天记录)里不携带人格块。
无状态机:不维护人格状态,对话窗口即隐式状态,LLM 是维度变化与回答的最终解释者。
- 环形缓冲缓存(
cache/):定长数组 + 写指针,满了循环覆盖,零拷贝写入;msgIDSetO(1) 去重;GetAll()两段拼接保持时间序。 - 前缀树指令路由(
router/command.go):trieMatch逐字符返回最长匹配前缀(「锐评下」优先于「锐评」),O(k) 与路由总数无关;用户指令从 YAML 动态 rebuild,内部指令持久保留。
- 显式依赖注入,零
init()副作用:所有组件在main()自上而下构造。 - 防御性:白名单群、消息按 rune 截断、@检测以标准 mention 为主并用唯一昵称兜底、配置缺失自动从模板生成并退出。
- 可观测性:zap + lumberjack 双输出(控制台 + 文件 20MB 轮转、30 天保留、gzip)。
- Web 面板:
//go:embed内嵌 SPA、JWT(HS256) 鉴权、无免密模式。