local M = {} local function trim(value) return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "") end local function is_array(value) if type(value) ~= "table" then return false end local count = 0 for key in pairs(value) do if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then return false end count = count + 1 end return count == #value end function M.sanitize_output(value, limit) local text = tostring(value or "") text = text:gsub("\27%[[%d;?]*[ -/]*[@-~]", "") text = text:gsub("([Tt][Oo][Kk][Ee][Nn]%s*[:=]%s*)[^%s,;]+", "%1[REDACTED]") text = text:gsub("([Aa][Pp][Ii][_-]?[Kk][Ee][Yy]%s*[:=]%s*)[^%s,;]+", "%1[REDACTED]") text = text:gsub("[%z\1-\8\11\12\14-\31\127]", "") return trim(text):sub(1, limit or 300) end local function extract_json_object(text) local start_at = text:find("{", 1, true) while start_at do local depth, in_string, escaped = 0, false, false for i = start_at, #text do local char = text:sub(i, i) if in_string then if escaped then escaped = false elseif char == "\\" then escaped = true elseif char == '"' then in_string = false end elseif char == '"' then in_string = true elseif char == "{" then depth = depth + 1 elseif char == "}" then depth = depth - 1 if depth == 0 then return text:sub(start_at, i) end end end start_at = text:find("{", start_at + 1, true) end return nil end function M.valid_request_id(value) return type(value) == "string" and value ~= "" and value:match("^[a-zA-Z0-9_-]+$") ~= nil end function M.parse_list(output, decode) if type(output) ~= "string" or trim(output) == "" then return nil, "DEVICES_EMPTY_OUTPUT", "设备列表命令没有返回数据" end local json_text = extract_json_object(output) if not json_text then return nil, "DEVICES_INVALID_JSON", "设备列表不是有效 JSON" end local ok, data = pcall(decode, json_text) if not ok or type(data) ~= "table" then return nil, "DEVICES_INVALID_JSON", "设备列表 JSON 解析失败" end if not is_array(data.pending) or not is_array(data.paired) then return nil, "DEVICES_SCHEMA_MISMATCH", "设备列表缺少 pending/paired 数组" end for _, item in ipairs(data.pending) do if type(item) ~= "table" or not M.valid_request_id(item.requestId) then return nil, "DEVICES_SCHEMA_MISMATCH", "待配对设备包含无效的 requestId" end if type(item.deviceId) ~= "string" or item.deviceId == "" then return nil, "DEVICES_SCHEMA_MISMATCH", "待配对设备包含无效的 deviceId" end end for _, item in ipairs(data.paired) do if type(item) ~= "table" then return nil, "DEVICES_SCHEMA_MISMATCH", "已配对设备包含无效条目" end end return data end function M.find_pending(data, request_id) for _, item in ipairs(data and data.pending or {}) do if item.requestId == request_id then return item end end return nil end -- luci.jsonc 会把普通的空 Lua table 编码成 {}。解析一个真正的 JSON 数组 -- 可取得其内部数组标记,确保 API 的空列表稳定输出为 []。 function M.json_array(value, parse_json) if type(value) == "table" and next(value) ~= nil then return value end if type(parse_json) == "function" then local ok, parsed = pcall(parse_json, "[]") if ok and type(parsed) == "table" then return parsed end end return type(value) == "table" and value or {} end function M.confirm_approval(before_item, after_data) if type(before_item) ~= "table" or type(after_data) ~= "table" then return false end local device_id = before_item.deviceId if type(device_id) ~= "string" or device_id == "" then return false end for _, item in ipairs(after_data.paired or {}) do if type(item) == "table" and item.deviceId == device_id then return true end end return false end function M.evaluate_approval(exit_code, before_item, after_data, after_error, detail) if exit_code ~= 0 then return "error", exit_code == 124 and "APPROVAL_TIMEOUT" or "APPROVAL_CLI_FAILED", detail ~= "" and detail or "批准失败" end if not after_data then return "unconfirmed", after_error or "APPROVAL_UNCONFIRMED", "批准命令已完成,但无法确认设备状态" end if M.confirm_approval(before_item, after_data) then return "ok", nil, detail ~= "" and detail or "设备配对已确认" end if M.find_pending(after_data, before_item.requestId) then return "error", "APPROVAL_NOT_APPLIED", "批准命令未使设备进入已配对列表" end return "unconfirmed", "APPROVAL_UNCONFIRMED", "请求已不在待批准列表,但无法确认对应设备已配对" end return M