跳到主要内容

常见坑与最佳实践

最后一章。把前面学的串起来,集中讲卷娘 Lua 插件开发里最容易踩的坑和推荐的写法。学完这一章,你就能读懂 plugins/redrock_* 的真实代码了。

坑 1:数组从 1 开始,不是 0

local t = { "a", "b" }
print(t[0]) -- nil!不存在
print(t[1]) -- "a"

坑 2:# 取长度在"空洞数组"上不可靠

如果数组中间有 nil# 的结果是未定义的:

local t = { "a", nil, "c" }
print(#t) -- 可能是 1 或 3,不可靠!

建议:数组里不要留 nil 空洞。要删元素用 table.remove,不要直接置 nil

坑 3:不等于是 ~= 不是 !=

if a ~= b then -- 正确
-- if a != b -- 错误,Lua 没有 !=

坑 4:没有 +++=// 等运算符

Lua 5.1 没有这些,自增要手写:

i = i + 1

坑 5:and / or 返回的是操作数

local x = false or "默认" -- "默认"
local y = 0 or "默认" -- 0(0 是真值!)

注意 Lua 里只有 nilfalse 是假值0、空字符串 "" 都是真值!所以 0 or "默认" 得到 0

坑 6:全局变量污染

卷娘每个插件虽然是独立 Lua 状态,但全局变量仍是共享命名空间。务必用 local

local jn = require("jn") -- 正确,局部
-- jn = require("jn") -- 错误,污染全局

坑 7:异步 API 要配合回调,不要阻塞

卷娘里可能阻塞的 API(HTTP、T2I、沙箱、LLM)都有 _async 异步版本。在事件循环里不要用同步版做慢操作,否则会卡住整个插件:

-- ❌ 同步:会阻塞
local body = jn.http.get("https://example.com/api")

-- ✅ 异步:立即返回,完成时回调 on_http_response
jn.http.get_async("https://example.com/api", { my_ctx = "value" })

function on_http_response(req_id, ctx, result, err)
if err then jn.log.warn("失败: " .. err) return end
jn.log.info("拿到 " .. result.body)
end

详见 插件 API 参考 的异步章节。

坑 8:缓存/状态要先判空

从缓存或配置读出来的值可能是 nil,取用前先给默认值:

local reply = jn.cache.get("last_reply") or "默认回复"
local groups = jn.config.get("groups") or {}

最佳实践 1:用多返回值表达命令结果

命令 handler 返回 (consumed, reply),事件回调返回 (consumed, skip_reply)。理解并善用:

jn.command.register("hi", function(args, event)
return true, "你好!" -- 消费了 /hi,回复"你好!"
end)

最佳实践 2:状态按群/用户隔离

游戏、计数器这类状态要按群隔离,别搞成全局共享。用"表 + 元表"或嵌套表:

local sessions = {} -- group_id -> { 状态 }

function get_session(gid)
if not sessions[gid] then
sessions[gid] = { score = 0, wrong = 0 }
end
return sessions[gid]
end

需要跨重启保存的状态,用 jn.cache(Redis)或 jn.database(Postgres),别只存在模块级变量里。

最佳实践 3:命令优先于 on_message 监听

能用 jn.command.register 的命令式交互,就别用 on_message 手写解析。命令命中后自动不进 on_message,更清晰:

-- ✅ 推荐:注册命令
jn.command.register({"game", "start"}, function(args, event) ... end)

-- ❌ 不推荐:在 on_message 里手写 if 解析 "/game start"

最佳实践 4:词库/配置用数据文件,别硬编码

助手式插件把可配置的东西放 config.yaml(Web 面板可改),词库放 words/*.txt*.csv,用 jn.config.get 读取:

local enabled = jn.config.get("enabled") -- 开关
local reply = jn.config.get("reply_text") or "默认"

最佳实践 5:日志打清楚,方便排查

jn.log.info("收到命令,参数=" .. table.concat(args, ","))
jn.log.warn("配置缺失,使用默认值")
jn.log.error("HTTP 请求失败: " .. tostring(err))

日志会进 stdout 和前端 SSE 流(带 [plugin:<name>] 前缀),是排查问题的主要手段。

最佳实践 6:高危操作校验权限

禁言、踢人等操作前,检查 event.admins 或用命令的权限控制:

local function is_member(list, value)
for _, v in ipairs(list or {}) do
if tostring(v) == tostring(value) then return true end
end
return false
end

function is_admin(event)
return is_member(event.admins, event.user_id)
end

(示意:event.admins 是系统管理员 QQ 号列表,见 插件 API 参考。)

一个综合范例:点歌词条插件

把本章要点综合成一个完整插件:

local jn = require("jn")

-- 本地词条表(也可从文件/配置读)
local SONGS = {
{ keyword = "晴天", lyric = "故事的小黄花,从出生那年就飘着" },
{ keyword = "稻香", lyric = "还记得你说家是唯一的城堡" },
}

-- 按关键词查词条
local function find_song(keyword)
for _, song in ipairs(SONGS) do
if song.keyword == keyword then
return song
end
end
return nil
end

-- 命令:/歌词 <关键词>
jn.command.register({"lyric"}, function(args, event)
local keyword = args[1]
if not keyword then
return true, "用法:/歌词 <关键词>"
end
local song = find_song(keyword)
if not song then
return true, "没找到「" .. keyword .. "」的歌词~"
end
return true, song.keyword .. ":" .. song.lyric
end, { description = "查歌词", usage = "/歌词 <关键词>" })

这个插件用到了:local 变量、表、ipairs 遍历、函数、多返回值、jn.command.register。学完本章,你就能写出来了。

下一步