Module:Lorebook/Debug: Difference between revisions
Appearance
上传带详细调试输出的 Lorebook 模块 |
Fix context trigger logic and SMW query - same fixes as main module |
||
Line 22: | Line 22: | ||
local function containsAny(haystack, needles) | local function containsAny(haystack, needles) | ||
local s = | local s = haystack:lower() | ||
for _, n in ipairs(needles) do | for _, n in ipairs(needles) do | ||
if n ~= '' then | if n ~= '' then | ||
local escaped = | local needle = n:lower() | ||
-- Escape special pattern characters | |||
local escaped = needle:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%1') | |||
-- Try multiple matching strategies | |||
-- 1. Exact match | |||
if s == needle then | |||
return true | |||
end | |||
-- 2. Word boundary patterns | |||
local patterns = { | local patterns = { | ||
' | '^' .. escaped .. '%W', -- At start followed by non-word | ||
' | '%W' .. escaped .. '%W', -- Surrounded by non-word chars | ||
'%W' .. escaped .. '$', -- Preceded by non-word at end | |||
' | '^' .. escaped .. '$' -- Exact match (redundant but safe) | ||
'^' .. escaped .. '$' | |||
} | } | ||
for _, pattern in ipairs(patterns) do | for _, pattern in ipairs(patterns) do | ||
if s:find(pattern) then | if s:find(pattern) then | ||
return true | return true | ||
end | end | ||
end | |||
-- 3. Simple substring match as last resort | |||
if s:find(escaped, 1, true) then | |||
return true | |||
end | end | ||
end | end | ||
Line 71: | Line 86: | ||
local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world) | local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world) | ||
local res = mw.smw.ask(query, { | local res = mw.smw.ask(query, { | ||
['?Plist'] = 'plist', | ['?Plist'] = 'plist', | ||
['?AliChat'] = 'alichat', | ['?AliChat'] = 'alichat', | ||
Line 89: | Line 103: | ||
for i, row in ipairs(res) do | for i, row in ipairs(res) do | ||
normalized[i] = { | normalized[i] = { | ||
page = normalizeValue(row[1]), | page = normalizeValue(row[1]), -- First element is the page title when mainlabel is not '-' | ||
plist = normalizeValue(row.plist), | plist = normalizeValue(row.plist), | ||
alichat = normalizeValue(row.alichat), | alichat = normalizeValue(row.alichat), |
Revision as of 18:29, 17 October 2025
Documentation for this module may be created at Module:Lorebook/Debug/doc
local p = {}
-- 调试模式标志
local DEBUG = true
local function debug_log(msg)
if DEBUG then
return "<!-- DEBUG: " .. msg .. " -->\n"
end
return ""
end
local function splitCSV(s)
if not s or s == '' then return {} end
local t = {}
for token in mw.text.gsplit(s, ',', true) do
token = mw.text.trim(token)
if token ~= '' then table.insert(t, token:lower()) end
end
return t
end
local function containsAny(haystack, needles)
local s = haystack:lower()
for _, n in ipairs(needles) do
if n ~= '' then
local needle = n:lower()
-- Escape special pattern characters
local escaped = needle:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%1')
-- Try multiple matching strategies
-- 1. Exact match
if s == needle then
return true
end
-- 2. Word boundary patterns
local patterns = {
'^' .. escaped .. '%W', -- At start followed by non-word
'%W' .. escaped .. '%W', -- Surrounded by non-word chars
'%W' .. escaped .. '$', -- Preceded by non-word at end
'^' .. escaped .. '$' -- Exact match (redundant but safe)
}
for _, pattern in ipairs(patterns) do
if s:find(pattern) then
return true
end
end
-- 3. Simple substring match as last resort
if s:find(escaped, 1, true) then
return true
end
end
end
return false
end
local function anyMatch(haystack, primary, _secondary)
return containsAny(haystack, primary)
end
local function andMatch(haystack, primary, secondary)
return containsAny(haystack, primary) and containsAny(haystack, secondary)
end
local function notMatch(haystack, primary, secondary)
return containsAny(haystack, primary) and not containsAny(haystack, secondary)
end
local logicDispatch = {
ANY = anyMatch,
AND = andMatch,
NOT = notMatch,
}
local function normalizeValue(val)
if type(val) == 'table' then
return val[1] or ''
end
return val or ''
end
local function getAllConcepts(world)
local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world)
local res = mw.smw.ask(query, {
['?Plist'] = 'plist',
['?AliChat'] = 'alichat',
['?Primary keys'] = 'primary',
['?Secondary keys'] = 'secondary',
['?Logic'] = 'logic',
['?Key mode'] = 'mode',
['?Parent concept'] = 'parent',
['?Non-recursable'] = 'nonrec',
['?Placement'] = 'placement',
limit = 999
})
if not res then return {} end
local normalized = {}
for i, row in ipairs(res) do
normalized[i] = {
page = normalizeValue(row[1]), -- First element is the page title when mainlabel is not '-'
plist = normalizeValue(row.plist),
alichat = normalizeValue(row.alichat),
primary = normalizeValue(row.primary),
secondary = normalizeValue(row.secondary),
logic = normalizeValue(row.logic),
mode = normalizeValue(row.mode),
parent = normalizeValue(row.parent),
nonrec = normalizeValue(row.nonrec),
placement = normalizeValue(row.placement)
}
end
return normalized
end
local function indexByTitle(rows)
local idx = {}
for _, r in ipairs(rows) do
if r.page and r.page ~= '' then
idx[r.page] = r
end
end
return idx
end
local function shouldTrigger(row, haystack)
local mode = (row.mode or 'conditional'):upper()
if mode == 'DISABLED' then return false end
if mode == 'CONSTANT' then return true end
local logic = (row.logic or 'ANY'):upper()
local f = logicDispatch[logic] or anyMatch
local primary = splitCSV(row.primary)
local secondary = splitCSV(row.secondary)
return f(haystack, primary, secondary)
end
local function walkRecursion(rows, idx, startPages)
local children = {}
for _, r in ipairs(rows) do
local parent = r.parent
if parent and parent ~= '' then
children[parent] = children[parent] or {}
table.insert(children[parent], r.page)
end
end
local visited = {}
local stack = {}
for _, ptitle in ipairs(startPages) do table.insert(stack, ptitle) end
local ordered = {}
while #stack > 0 do
local cur = table.remove(stack)
if not visited[cur] then
visited[cur] = true
table.insert(ordered, cur)
local row = idx[cur]
if row and (row.nonrec or ''):lower() ~= 'yes' then
for _, c in ipairs(children[cur] or {}) do
table.insert(stack, c)
end
end
end
end
return ordered
end
local function buildInjection(rows, triggered)
local plistChunks, chatChunks = {}, {}
for _, title in ipairs(triggered) do
local r
for _, row in ipairs(rows) do if row.page == title then r = row break end end
if r then
if r.plist and r.plist ~= '' then table.insert(plistChunks, r.plist) end
if r.alichat and r.alichat ~= '' then table.insert(chatChunks, r.alichat) end
end
end
local text = ''
if #plistChunks > 0 then
text = text .. table.concat(plistChunks, '\n') .. '\n'
end
if #chatChunks > 0 then
text = text .. table.concat(chatChunks, '\n')
end
return text
end
function p.inject(frame)
local world = frame.args.world or 'Farlandia'
local haystack = frame.args.context or ''
local output = debug_log("Starting inject function")
output = output .. debug_log("World: " .. world)
output = output .. debug_log("Context: " .. haystack)
local rows = getAllConcepts(world)
output = output .. debug_log("Found " .. #rows .. " concepts")
if #rows == 0 then
output = output .. "\n'''⚠️ No concepts found for world: " .. world .. "'''\n\n"
output = output .. "Possible issues:\n"
output = output .. "* Concepts not created yet\n"
output = output .. "* Concepts not using {{Concept}} template\n"
output = output .. "* SMW properties not set correctly\n"
output = output .. "* SMW data cache needs refresh (re-save concept pages)\n\n"
output = output .. "Debug query: <code><nowiki>{{#ask: [[Category:Concept]] [[Belongs to world::" .. world .. "]] }}</nowiki></code>\n"
return output
end
-- 显示找到的概念列表(调试)
if DEBUG then
output = output .. "\n'''Found concepts:'''\n"
for i, r in ipairs(rows) do
output = output .. "* " .. (r.page or "unnamed") ..
" (mode: " .. (r.mode or "conditional") ..
", primary: " .. (r.primary or "none") .. ")\n"
end
output = output .. "\n"
end
local idx = indexByTitle(rows)
local start = {}
for _, r in ipairs(rows) do
if shouldTrigger(r, haystack) then
table.insert(start, r.page)
output = output .. debug_log("Triggered: " .. r.page)
end
end
output = output .. debug_log("Triggered " .. #start .. " concepts")
if #start == 0 then
output = output .. "\n'''ℹ️ No concepts triggered for context: " .. haystack .. "'''\n\n"
output = output .. "Available trigger keys:\n"
for _, r in ipairs(rows) do
if r.primary and r.primary ~= '' then
output = output .. "* " .. r.page .. ": <code>" .. r.primary .. "</code>\n"
end
end
return output
end
local triggered = walkRecursion(rows, idx, start)
output = output .. debug_log("After recursion: " .. #triggered .. " concepts")
local result = buildInjection(rows, triggered)
if DEBUG then
output = output .. "\n'''Injected concepts:'''\n"
for _, t in ipairs(triggered) do
output = output .. "* " .. t .. "\n"
end
output = output .. "\n'''=== Output ==='''\n<pre>\n"
end
output = output .. result
if DEBUG then
output = output .. "\n</pre>\n"
end
return output
end
function p.export(frame)
local world = frame.args.world or 'Farlandia'
local rows = getAllConcepts(world)
local entries = {}
for _, r in ipairs(rows) do
table.insert(entries, {
title = r.page,
plist = r.plist or '',
alichat = r.alichat or '',
primary = r.primary or '',
secondary = r.secondary or '',
logic = (r.logic or 'ANY'),
mode = (r.mode or 'conditional'),
parent = r.parent or '',
nonrecursable = r.nonrec or '',
placement = r.placement or ''
})
end
return mw.text.jsonEncode(entries)
end
return p