米读小说
https://m.midukanshu.com
ethereal-essence (13554) 16小时前 下载:343
小说 小说 米读 API H5
米读小说适配鸿蒙端
// @uuid 019e1210-e7cc-75c7-ab8e-c6ff441bcaf1
// @name 米读小说
// @version 1.1.0
// @updateUrl http://aliyun.18638642193.cn/api/sources/user:1e280850-2b5c-4e12-8a0d-0cfc2f1fe9d5/download
// @author Ethereal
// @url https://m.midukanshu.com
// @logo https://m.midukanshu.com/favicon.ico
// @type novel
// @enabled true
// @tags 小说,米读,API,H5
// @description 米读小说适配鸿蒙端
var API_BASE = "https://api.midukanshu.com";
var API_SAAS = "https://saasapi.midukanshu.com";
var MIDU_SAAS_SIGN_KEY = "T^xS0x31XL%wEowC";
var H5_BASE = "https://m.midukanshu.com";
var STATIC_BASE = "https://book.midukanshu.com";
var EXPLORE_FIXED_CATEGORIES = [
"猜你喜欢",
"玄幻",
"都市",
"仙侠",
"武侠",
"历史",
"军事",
"科幻",
"游戏",
"悬疑",
"同人次元",
"古代言情",
"现代言情",
"轻小说"
];
/** CLI / 内置单测入口(Legado booksource-test) */
async function TEST(type) {
if (type === "__list__") return ["search", "explore"];
if (type === "search") {
var list = await search("斗破苍穹", 1);
if (!list || list.length < 1) {
return { passed: false, message: "搜索无结果" };
}
return { passed: true, message: "搜索返回 " + list.length + " 条" };
}
if (type === "explore") {
var rows = await explore(1, "猜你喜欢");
if (!rows || rows.length < 1) {
return { passed: false, message: "发现页为空" };
}
return { passed: true, message: "发现页「猜你喜欢」" + rows.length + " 条" };
}
return { passed: false, message: "未知测试类型: " + type };
}
/**
* HarmonyOS NETSTACK:请求 URL 字符串含未编码非 ASCII(及部分非法 % 序列)会抛 BusinessError 401 Parameter error。
* 对 http(s) 的 path / query / hash 分段归一化;decode 失败则仅 encode,避免残留 Unicode。
*/
function pctEncodeSeg(seg) {
if (!seg) return seg;
try {
return encodeURIComponent(decodeURIComponent(seg));
} catch (e) {
return encodeURIComponent(seg);
}
}
function asciiRequestUrl(url) {
if (url == null || url === "") return url;
var u = String(url);
if (u.indexOf("http://") !== 0 && u.indexOf("https://") !== 0) return u;
var hashIndex = u.indexOf("#");
var main = hashIndex === -1 ? u : u.slice(0, hashIndex);
var hash = hashIndex === -1 ? "" : u.slice(hashIndex);
var qIndex = main.indexOf("?");
var base = qIndex === -1 ? main : main.slice(0, qIndex);
var query = qIndex === -1 ? "" : main.slice(qIndex);
var schemeIdx = base.indexOf("://");
if (schemeIdx === -1) {
throw new Error("asciiRequestUrl: missing scheme");
}
var afterScheme = base.slice(schemeIdx + 3);
var pathStartInAuth = afterScheme.indexOf("/");
var authority;
var path;
if (pathStartInAuth === -1) {
authority = afterScheme;
path = "/";
} else {
authority = afterScheme.slice(0, pathStartInAuth);
path = afterScheme.slice(pathStartInAuth);
}
var segs = path.split("/");
var i;
for (i = 0; i < segs.length; i++) {
if (!segs[i]) continue;
segs[i] = pctEncodeSeg(segs[i]);
}
path = segs.join("/");
if (path === "") path = "/";
var newQuery = query;
if (query && query.length > 1) {
var qp = query.slice(1).split("&");
var out = [];
var needQ = false;
for (i = 0; i < qp.length; i++) {
if (!qp[i]) continue;
var eq = qp[i].indexOf("=");
var k = eq === -1 ? qp[i] : qp[i].slice(0, eq);
var v = eq === -1 ? "" : qp[i].slice(eq + 1);
if (/[^\u0000-\u007F]/.test(k) || /[^\u0000-\u007F]/.test(v)) {
needQ = true;
}
try {
k = encodeURIComponent(decodeURIComponent(k.split("+").join(" ")));
} catch (e1) {
k = encodeURIComponent(k);
}
try {
v = encodeURIComponent(decodeURIComponent(v.split("+").join(" ")));
} catch (e2) {
v = encodeURIComponent(v);
}
out.push(k + "=" + v);
}
newQuery = "?" + out.join("&");
if (!needQ && !/[^\u0000-\u007F]/.test(query)) {
newQuery = query;
}
}
var newHash = hash;
if (hash && hash.length > 1 && /[^\u0000-\u007F]/.test(hash)) {
var frag = hash.slice(1);
var eq2 = frag.indexOf("=");
if (eq2 !== -1) {
var hk = frag.slice(0, eq2);
var hv = frag.slice(eq2 + 1);
try {
newHash = "#" + hk + "=" + encodeURIComponent(decodeURIComponent(hv));
} catch (e3) {
newHash = "#" + hk + "=" + encodeURIComponent(hv);
}
} else {
try {
newHash = "#" + encodeURIComponent(decodeURIComponent(frag));
} catch (e4) {
newHash = "#" + encodeURIComponent(frag);
}
}
}
var outUrl = base.slice(0, schemeIdx + 3) + authority + path + newQuery + newHash;
if (/[^\u0000-\u007F]/.test(outUrl)) {
legado.log("[asciiRequestUrl] normalize non-ASCII");
}
return outUrl;
}
/** 凡传入 NETSTACK 的 http(s) URL(含封面)均走 asciiRequestUrl;空串不发起请求,避免鸿蒙对空 URL 报 Parameter error */
function harmonyHttpUrl(url) {
if (url == null || url === "") return undefined;
var s = String(url).trim();
if (s === "") return undefined;
if (s.indexOf("http://") !== 0 && s.indexOf("https://") !== 0) return s;
return asciiRequestUrl(s);
}
/** 自定义 midu://book 查询串(encodeURIComponent 键值) */
function packQuery(obj) {
var parts = [];
var key;
for (key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
if (obj[key] === undefined || obj[key] === null) continue;
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(obj[key])));
}
return parts.join("&");
}
/** 解析 ?query 或整段查询串(兼容 midu://book?… 与 https reader?…) */
function parsePackedUrl(url) {
var idx = url.indexOf("?");
var query = idx === -1 ? url : url.slice(idx + 1);
var parts = query.split("&");
var result = {};
var i;
for (i = 0; i < parts.length; i++) {
var kv = parts[i].split("=");
var k = decodeURIComponent(kv[0] || "");
var value = decodeURIComponent(kv.slice(1).join("=") || "");
result[k] = value;
}
return result;
}
/** 展开 reader 页上打包的书架元数据(与真实 H5 参数共存,阅读页忽略未知键) */
function mergeMiduPackedMeta(meta) {
var packed = meta["x-midu-book"];
if (packed == null || packed === "") {
return meta;
}
var inner = parsePackedUrl("?" + packed);
var k;
for (k in inner) {
if (Object.prototype.hasOwnProperty.call(inner, k)) {
meta[k] = inner[k];
}
}
return meta;
}
/** x-midu-book 内缩短简介,避免整条 bookUrl/tocUrl 超宿主上限被截断在 bookSource 处 */
function packShelfMetaForUrl(item) {
var intro = item.intro;
if (intro != null && String(intro).length > 220) {
intro = String(intro).slice(0, 220);
}
return packQuery({
bookId: item.bookId,
name: item.name,
author: item.author,
coverUrl: item.coverUrl,
intro: intro,
kind: item.kind,
lastChapter: item.lastChapter,
source: item.source,
chapterNum: item.chapterNum
});
}
/** H5 阅读页真实会带很长 bookSource=追踪串,Legado/鸿蒙传 tocUrl 易被截断;此处不传追踪串,仅用 bookId 打开阅读页 */
function readerUrlMinimal(bookId, extraPackEncoded) {
var q = "bookId=" + encodeURIComponent(bookId) +
"&bookSource=" + encodeURIComponent("") +
"&nodeId=undefined";
if (extraPackEncoded != null && extraPackEncoded !== "") {
q += "&x-midu-book=" + extraPackEncoded;
}
return harmonyHttpUrl(H5_BASE + "/novel/reader.html?" + q);
}
function buildBookUrl(item) {
var pack = packShelfMetaForUrl(item);
return readerUrlMinimal(item.bookId, encodeURIComponent(pack));
}
function buildReaderUrl(meta) {
return readerUrlMinimal(meta.bookId, null);
}
function buildChapterUrl(bookId, chapterId, title) {
var u = STATIC_BASE + "/book/chapter/master/" + bookId + "_" + chapterId + ".txt" +
"#title=" + encodeURIComponent(title);
return harmonyHttpUrl(u);
}
function parseChapterUrl(chapterUrl) {
var hashIndex = chapterUrl.indexOf("#");
var pureUrl = hashIndex === -1 ? chapterUrl : chapterUrl.slice(0, hashIndex);
var hash = hashIndex === -1 ? "" : chapterUrl.slice(hashIndex + 1);
var title = "";
if (hash.indexOf("title=") === 0) {
title = decodeURIComponent(hash.slice(6));
}
return {
pureUrl: pureUrl,
title: title
};
}
/**
* 搜索接口为扁平结构;推荐 / 运营位常包一层 bookData,且可能无 chapterNum。
*/
function flattenBookRaw(raw) {
if (raw == null || typeof raw !== "object") {
throw new Error("flattenBookRaw: raw");
}
if (raw.bookData != null && typeof raw.bookData === "object") {
return raw.bookData;
}
return raw;
}
function stripHtml(s) {
return String(s || "").replace(/<[^>]*>/g, "").replace(/ /g, " ").trim();
}
function mapSearchItem(raw) {
var r = flattenBookRaw(raw);
var bookId = r.book_id != null && r.book_id !== "" ? r.book_id : r.id;
if (bookId == null || bookId === "") {
throw new Error("mapSearchItem: book_id");
}
var title = r.title != null ? r.title : stripHtml(r.emTitle);
if (title == null || title === "") {
throw new Error("mapSearchItem: title");
}
var cn = r.chapterNum != null ? r.chapterNum : r.chapter_num;
var lastChapter = cn != null && String(cn) !== "" ? ("共" + cn + "章") : "";
var source = r.source;
if (source == null) {
source = "";
} else if (typeof source !== "string") {
source = String(source);
}
var cover = r.cover != null && r.cover !== "" ? r.cover : r.coverUrl;
if ((cover == null || cover === "") && r.coverImage != null && typeof r.coverImage === "object") {
cover = r.coverImage.thumbnail || r.coverImage.original || r.coverImage.thumbnail_webp || "";
}
return {
bookId: bookId,
name: title,
author: r.author,
coverUrl: cover,
intro: r.description != null ? r.description : stripHtml(r.emDescription),
kind: r.category,
lastChapter: lastChapter,
source: source,
chapterNum: cn != null ? cn : ""
};
}
async function search(keyword, page) {
legado.log("[search] keyword=" + keyword + " page=" + page);
var q = String(keyword).trim();
if (q === "") {
throw new Error("search: empty keyword");
}
var pageNum = Number(page);
if (pageNum !== Math.floor(pageNum) || pageNum < 1) {
throw new Error("search: invalid page");
}
// H5 search.vue:getSearchList({ keyword, page }),page 从 0 起与首屏一致(Legado 页码从 1 起减 1)。
var pageIndex = pageNum - 1;
var body = "keyword=" + encodeURIComponent(q) +
"&page=" + encodeURIComponent(pageIndex);
var headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": H5_BASE + "/novel/search.html",
"Origin": H5_BASE
};
var text = await legado.http.post(harmonyHttpUrl(API_BASE + "/fiction/search/search"), body, headers);
var list = JSON.parse(text).data;
var books = [];
var i;
for (i = 0; i < list.length; i++) {
var row = list[i];
// data 中夹杂 itemType=5 等运营卡片(无 book_id),与真实书籍混排,须跳过。
if (!row || row.book_id == null || row.book_id === "") {
continue;
}
var item = mapSearchItem(row);
books.push({
name: item.name,
author: item.author,
bookUrl: buildBookUrl(item),
coverUrl: harmonyHttpUrl(item.coverUrl) || "",
kind: item.kind,
latestChapter: item.lastChapter
});
}
legado.log("[search] pageIndex=" + pageIndex + " total=" + books.length);
return books;
}
var MIDU_CONFIG_NS = "midu_novel";
/** 鸿蒙/日志里常见把 bookId 显示或存储成 bookld(I→l);并从截断 URL 中用 32 位 hex 兜底 */
function normalizeBookMeta(meta, rawUrl) {
if (meta.bookId == null || meta.bookId === "") {
if (meta.bookld != null && meta.bookld !== "") {
meta.bookId = meta.bookld;
}
}
if ((meta.bookId == null || meta.bookId === "") && rawUrl != null) {
var hex = /([0-9a-f]{32})/i.exec(String(rawUrl));
if (hex) {
meta.bookId = hex[1];
}
}
return meta;
}
async function bookInfo(bookUrl) {
var meta = normalizeBookMeta(mergeMiduPackedMeta(parsePackedUrl(bookUrl)), bookUrl);
legado.log("[bookInfo] bookUrl len=" + String(bookUrl).length + " bookId=" + meta.bookId);
if (meta.bookId == null || meta.bookId === "") {
throw new Error("bookInfo: bookId");
}
if (typeof meta.source !== "string") {
throw new Error("bookInfo: source");
}
legado.config.write(MIDU_CONFIG_NS, "last_book_id", meta.bookId);
return {
name: meta.name,
author: meta.author,
bookUrl: bookUrl,
coverUrl: harmonyHttpUrl(meta.coverUrl) || "",
intro: meta.intro,
kind: meta.kind,
latestChapter: meta.lastChapter,
tocUrl: buildReaderUrl(meta)
};
}
/** 从阅读 URL 取 bookId:兼容 bookId= / bookld=(宿主误写)、大小写、截断后仅剩 hex */
function extractBookIdFromReaderUrl(url) {
if (url == null || url === "") return "";
var s = String(url);
var patterns = [
/[?&]bookId=([^&#]+)/i,
/[?&]bookld=([^&#]+)/i
];
var i;
for (i = 0; i < patterns.length; i++) {
var m = patterns[i].exec(s);
if (m && m[1]) {
try {
return decodeURIComponent(m[1]);
} catch (e) {
return m[1];
}
}
}
var hex = /([0-9a-f]{32})/i.exec(s);
if (hex) {
return hex[1];
}
return "";
}
/** 与阅读页 hash_id 一致:32 位 hex;具备时宿主签名即可拉目录,不必起 WebView */
function isMiduContentBookId(id) {
return /^[0-9a-f]{32}$/i.test(String(id == null ? "" : id).trim());
}
/** 从 start 起截取第一个完整 JSON 对象或数组(忽略其后粘连字符),不依赖正则 */
function sliceFirstCompleteJson(s, start) {
var stack = [];
var inStr = false;
var esc = false;
var i;
var first = s.charAt(start);
if (first !== "{" && first !== "[") {
return null;
}
if (first === "{") {
stack.push("}");
} else {
stack.push("]");
}
for (i = start + 1; i < s.length; i++) {
var c = s.charAt(i);
if (inStr) {
if (esc) {
esc = false;
} else if (c === "\\") {
esc = true;
} else if (c === "\"") {
inStr = false;
}
continue;
}
if (c === "\"") {
inStr = true;
continue;
}
if (c === "{") {
stack.push("}");
continue;
}
if (c === "[") {
stack.push("]");
continue;
}
if (c === "}" || c === "]") {
if (stack.length === 0) {
return null;
}
var want = stack[stack.length - 1];
if (c !== want) {
return null;
}
stack.pop();
if (stack.length === 0) {
return s.slice(start, i + 1);
}
}
}
return null;
}
function findFirstJsonBracketIndex(s) {
var k;
for (k = 0; k < s.length; k++) {
var ch = s.charAt(k);
if (ch === "{" || ch === "[") {
return k;
}
}
return -1;
}
/**
* 接口常返回 null{...}、多余前缀/后缀;strict JSON.parse 报 position 4。
* Boa 环境避免 String#search 正则,只用索引 + 括号栈截取一段再 parse。
*/
function parseApiJsonLoose(text, logTag) {
var s = String(text == null ? "" : text).trim();
if (s.length === 0) {
throw new Error((logTag || "json") + ": empty body");
}
if (s.charCodeAt(0) === 0xfeff) {
s = s.slice(1).trim();
}
var c0 = s.charAt(0);
if (c0 === "{" || c0 === "[") {
try {
return JSON.parse(s);
} catch (e0) {
var sub0 = sliceFirstCompleteJson(s, 0);
if (sub0) {
return JSON.parse(sub0);
}
legado.log((logTag || "json") + " head=" + s.slice(0, 200));
throw e0;
}
}
var start = findFirstJsonBracketIndex(s);
if (start < 0) {
legado.log((logTag || "json") + " head=" + s.slice(0, 200));
throw new Error((logTag || "json") + ": no JSON start");
}
var sub = sliceFirstCompleteJson(s, start);
if (!sub) {
legado.log((logTag || "json") + " from=" + start + " head=" + s.slice(start, start + 200));
throw new Error((logTag || "json") + ": unbalanced JSON");
}
return JSON.parse(sub);
}
/** 从 chapterList 接口 JSON 中取出章节数组(多种嵌套) */
function extractChapterRowsFromPayload(json) {
var data = json.data != null ? json.data : json;
if (Array.isArray(data)) return data;
if (!data || typeof data !== "object") return [];
if (Array.isArray(data.list)) return data.list;
if (Array.isArray(data.chapters)) return data.chapters;
if (Array.isArray(data.chapterList)) return data.chapterList;
if (Array.isArray(data.chapter_list)) return data.chapter_list;
if (Array.isArray(data.rows)) return data.rows;
var vols = data.volumeList || data.volumes || data.volume_list;
if (Array.isArray(vols)) {
var out = [];
var vi;
for (vi = 0; vi < vols.length; vi++) {
var vol = vols[vi];
var arr = vol && (vol.chapters || vol.chapterList || vol.list || vol.chapter_list);
if (Array.isArray(arr)) {
var ci;
for (ci = 0; ci < arr.length; ci++) {
out.push(arr[ci]);
}
}
}
if (out.length > 0) return out;
}
legado.log("[chapterList] api data keys=" + Object.keys(data).join(","));
return [];
}
/** UTF-8 字符串 → md5 小写 hex(与 reader webpack crypto 一致,供 saas 签名) */
function md5Utf8Hex(s) {
function rotl(x, c) {
return (x << c) | (x >>> (32 - c));
}
function add32(a, b) {
var l = (a & 65535) + (b & 65535);
var m = (a >> 16) + (b >> 16) + (l >> 16);
return (m << 16) | (l & 65535);
}
function cmn(q, a, b, x, s, t) {
return add32(rotl(add32(add32(a, q), add32(x, t)), s), b);
}
function ff(a, b, c, d, x, s, t) {
return cmn((b & c) | (~b & d), a, b, x, s, t);
}
function gg(a, b, c, d, x, s, t) {
return cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function hh(a, b, c, d, x, s, t) {
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii(a, b, c, d, x, s, t) {
return cmn(c ^ (b | ~d), a, b, x, s, t);
}
function blocksFromUtf8(str) {
var esc = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (_, h) {
return String.fromCharCode(parseInt(h, 16));
});
var n = esc.length;
var bl = (((n + 8) >> 6) + 1) << 4;
var i;
var w = [];
for (i = 0; i < bl; i++) {
w[i] = 0;
}
for (i = 0; i < n; i++) {
w[i >> 2] |= esc.charCodeAt(i) << ((i % 4) << 3);
}
w[i >> 2] |= 128 << ((i % 4) << 3);
w[bl - 2] = n * 8;
return w;
}
function md51(w) {
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
var i;
for (i = 0; i < w.length; i += 16) {
var oa = a;
var ob = b;
var oc = c;
var od = d;
a = ff(a, b, c, d, w[i + 0], 7, -680876936);
d = ff(d, a, b, c, w[i + 1], 12, -389564586);
c = ff(c, d, a, b, w[i + 2], 17, 606105819);
b = ff(b, c, d, a, w[i + 3], 22, -1044525330);
a = ff(a, b, c, d, w[i + 4], 7, -176418897);
d = ff(d, a, b, c, w[i + 5], 12, 1200080426);
c = ff(c, d, a, b, w[i + 6], 17, -1473231341);
b = ff(b, c, d, a, w[i + 7], 22, -45705983);
a = ff(a, b, c, d, w[i + 8], 7, 1770035416);
d = ff(d, a, b, c, w[i + 9], 12, -1958414417);
c = ff(c, d, a, b, w[i + 10], 17, -42063);
b = ff(b, c, d, a, w[i + 11], 22, -1990404162);
a = ff(a, b, c, d, w[i + 12], 7, 1804603682);
d = ff(d, a, b, c, w[i + 13], 12, -40341101);
c = ff(c, d, a, b, w[i + 14], 17, -1502002290);
b = ff(b, c, d, a, w[i + 15], 22, 1236535329);
a = gg(a, b, c, d, w[i + 1], 5, -165796510);
d = gg(d, a, b, c, w[i + 6], 9, -1069501632);
c = gg(c, d, a, b, w[i + 11], 14, 643717713);
b = gg(b, c, d, a, w[i + 0], 20, -373897302);
a = gg(a, b, c, d, w[i + 5], 5, -701558691);
d = gg(d, a, b, c, w[i + 10], 9, 38016083);
c = gg(c, d, a, b, w[i + 15], 14, -660478335);
b = gg(b, c, d, a, w[i + 4], 20, -405537848);
a = gg(a, b, c, d, w[i + 9], 5, 568446438);
d = gg(d, a, b, c, w[i + 14], 9, -1019803690);
c = gg(c, d, a, b, w[i + 3], 14, -187363961);
b = gg(b, c, d, a, w[i + 8], 20, 1163531501);
a = gg(a, b, c, d, w[i + 13], 5, -1444681467);
d = gg(d, a, b, c, w[i + 2], 9, -51403784);
c = gg(c, d, a, b, w[i + 7], 14, 1735328473);
b = gg(b, c, d, a, w[i + 12], 20, -1926607734);
a = hh(a, b, c, d, w[i + 5], 4, -378558);
d = hh(d, a, b, c, w[i + 8], 11, -2022574463);
c = hh(c, d, a, b, w[i + 11], 16, 1839030562);
b = hh(b, c, d, a, w[i + 14], 23, -35309556);
a = hh(a, b, c, d, w[i + 1], 4, -1530992060);
d = hh(d, a, b, c, w[i + 4], 11, 1272893353);
c = hh(c, d, a, b, w[i + 7], 16, -155497632);
b = hh(b, c, d, a, w[i + 10], 23, -1094730640);
a = hh(a, b, c, d, w[i + 13], 4, 681279174);
d = hh(d, a, b, c, w[i + 0], 11, -358537222);
c = hh(c, d, a, b, w[i + 3], 16, -722521979);
b = hh(b, c, d, a, w[i + 6], 23, 76029189);
a = hh(a, b, c, d, w[i + 9], 4, -640364487);
d = hh(d, a, b, c, w[i + 12], 11, -421815835);
c = hh(c, d, a, b, w[i + 15], 16, 530742520);
b = hh(b, c, d, a, w[i + 2], 23, -995338651);
a = ii(a, b, c, d, w[i + 0], 6, -198630844);
d = ii(d, a, b, c, w[i + 7], 10, 1126891415);
c = ii(c, d, a, b, w[i + 14], 15, -1416354905);
b = ii(b, c, d, a, w[i + 5], 21, -57434055);
a = ii(a, b, c, d, w[i + 12], 6, 1700485571);
d = ii(d, a, b, c, w[i + 3], 10, -1894986606);
c = ii(c, d, a, b, w[i + 10], 15, -1051523);
b = ii(b, c, d, a, w[i + 1], 21, -2054922799);
a = ii(a, b, c, d, w[i + 8], 6, 1873313359);
d = ii(d, a, b, c, w[i + 15], 10, -30611744);
c = ii(c, d, a, b, w[i + 6], 15, -1560198380);
b = ii(b, c, d, a, w[i + 13], 21, 1309151649);
a = ii(a, b, c, d, w[i + 4], 6, -145523070);
d = ii(d, a, b, c, w[i + 11], 10, -1120210379);
c = ii(c, d, a, b, w[i + 2], 15, 718787259);
b = ii(b, c, d, a, w[i + 9], 21, -343485551);
a = add32(a, oa);
b = add32(b, ob);
c = add32(c, oc);
d = add32(d, od);
}
return [a, b, c, d];
}
function hex32(x) {
var hex = "0123456789abcdef";
var out = "";
var i;
for (i = 0; i < 4; i++) {
var v = (x >>> (i * 8)) & 255;
out += hex.charAt(v >> 4) + hex.charAt(v & 15);
}
return out;
}
var h = md51(blocksFromUtf8(String(s)));
return hex32(h[0]) + hex32(h[1]) + hex32(h[2]) + hex32(h[3]);
}
/** reader 模块 a7c4:对 md5 hex 串逐字节 ^5 再展开为小写 hex,作为 sign 字段 */
function miduXorHexEncodeMD5Hex(md5HexStr) {
var digitChars = "0123456789abcdef";
var t = md5HexStr.split("").map(function (ch) {
return ch.charCodeAt(0);
});
var n = t.length;
var i;
for (i = 0; i < n; i++) {
t[i] = 5 ^ t[i];
}
var o = [];
var ui = 0;
var u = 0;
var c = 0;
while (true) {
var sb = 255 & t[ui];
c = u + 1;
o[u] = digitChars.charCodeAt(sb >> 4);
u = c + 1;
o[c] = digitChars.charCodeAt(15 & sb);
ui += 1;
if (ui >= n) {
break;
}
}
return o
.map(function (code) {
return String.fromCharCode(code);
})
.join("");
}
function miduSaasParamStrForSign(obj) {
var keys = Object.keys(obj).sort();
var parts = [];
var ki;
for (ki = 0; ki < keys.length; ki++) {
parts.push("" + keys[ki] + "=" + obj[keys[ki]]);
}
return parts.join("&") + "&key=" + MIDU_SAAS_SIGN_KEY;
}
/** 过滤无效值 → 字符串 → sign → EncStr(正文目录仅此一路) */
function miduSaasParseParamsEnc(rawMerged) {
var n = {};
var keys = Object.keys(rawMerged);
var i;
for (i = 0; i < keys.length; i++) {
var k = keys[i];
var v = rawMerged[k];
if ([void 0, "undefined", null, "null"].indexOf(v) !== -1) {
continue;
}
n[k] = v + "";
}
n.sign = miduXorHexEncodeMD5Hex(md5Utf8Hex(miduSaasParamStrForSign(n)));
var jsonStr = JSON.stringify(n);
var b64;
if (typeof btoa === "function") {
try {
b64 = btoa(unescape(encodeURIComponent(jsonStr)));
} catch (e1) {
b64 = "";
}
}
if (!b64) {
var esc = encodeURIComponent(jsonStr).replace(/%([0-9A-F]{2})/g, function (_, h) {
return String.fromCharCode(parseInt(h, 16));
});
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
b64 = "";
var j;
for (j = 0; j < esc.length; j += 3) {
var c1 = esc.charCodeAt(j);
var c2 = j + 1 < esc.length ? esc.charCodeAt(j + 1) : 0;
var c3 = j + 2 < esc.length ? esc.charCodeAt(j + 2) : 0;
var e1 = c1 >> 2;
var e2 = ((c1 & 3) << 4) | (c2 >> 4);
var e3 = ((c2 & 15) << 2) | (c3 >> 6);
var e4 = c3 & 63;
b64 += tab.charAt(e1) + tab.charAt(e2) + (j + 1 < esc.length ? tab.charAt(e3) : "=") + (j + 2 < esc.length ? tab.charAt(e4) : "=");
}
}
return { EncStr: b64 };
}
/** 与 reader getApp 一致 */
function miduSaasAppFromTocQuery(q) {
var raw = q.platform_name || q.source || "Mdqtt.saas";
raw = String(raw);
if (raw.indexOf("Md") !== 0) {
return "Mdqtt.saas";
}
return raw;
}
/** 与 reader getDcVal:持久化 dc,作匿名 tuid(query tuid 优先) */
function miduSaasEnsureTuid(tocUrl) {
var q = parsePackedUrl(tocUrl.indexOf("?") === -1 ? tocUrl : tocUrl.slice(tocUrl.indexOf("?") + 1));
if (q.tuid != null && q.tuid !== "") {
return String(q.tuid);
}
var dcQ = q.dc;
if (dcQ != null && dcQ !== "") {
return String(dcQ);
}
var cached = legado.config.read(MIDU_CONFIG_NS, "saas_dc");
if (cached != null && cached !== "") {
return String(cached);
}
var appDef = miduSaasAppFromTocQuery(q);
var rnd = "";
try {
rnd = Math.random().toString(36).substr(2);
} catch (e0) {
rnd = String(Date.now());
}
var dc = rnd + "_" + appDef;
legado.config.write(MIDU_CONFIG_NS, "saas_dc", dc);
return dc;
}
/** 与 reader signParams+_getParams 合并结果等价(宿主无 AppBridge userInfo 时的最小字段集),目录均为 ASCII → 固定 EncStr */
function miduSaasBuildChapterListSignedBody(bookId, tocUrl) {
var qStr = tocUrl.indexOf("?") === -1 ? tocUrl : tocUrl.slice(tocUrl.indexOf("?") + 1);
var q = parsePackedUrl(qStr);
var app = miduSaasAppFromTocQuery(q);
var tuid = miduSaasEnsureTuid(tocUrl);
var ts = String(Math.floor(Date.now() / 1000));
var nonce = String(Math.floor(Math.random() * 9000 + 1000));
var merged = {
app: app,
version: 300,
nonce: nonce,
time: ts,
headerQueryTime: ts,
hash_id: bookId,
tuid: tuid,
member_id: q.member_id,
token: q.token,
tk: q.tk,
app_source: q.source || q.qtt_source || ""
};
var signed = miduSaasParseParamsEnc(merged);
var formPairs = [];
var fk;
for (fk in signed) {
if (Object.prototype.hasOwnProperty.call(signed, fk)) {
formPairs.push(encodeURIComponent(fk) + "=" + encodeURIComponent(signed[fk]));
}
}
return formPairs.join("&");
}
/** 网关直返 HTML 404(非 JSON),避免当作 JSON 解析 */
function isGateway404Html(text) {
var s = String(text == null ? "" : text).trim();
if (s.length === 0) return false;
var low = s.toLowerCase();
return low.indexOf("404") !== -1 && (low.indexOf("not found") !== -1 || low.indexOf("nosuchkey") !== -1);
}
async function fetchChapterRowsFromContentApi(bookId, tocUrlForSign) {
var url = harmonyHttpUrl(API_SAAS + "/content/chapterList");
var headersForm = {
"Content-Type": "application/x-www-form-urlencoded",
Referer: H5_BASE + "/novel/reader.html",
Origin: H5_BASE,
Accept: "application/json, text/plain, */*"
};
var bodySigned = miduSaasBuildChapterListSignedBody(bookId, tocUrlForSign || "");
legado.log("[chapterList] saasapi EncStr POST len=" + String(bodySigned).length);
var text = await legado.http.post(url, bodySigned, headersForm);
if (isGateway404Html(text)) {
throw new Error("chapterList: saasapi /content 404(请检查域名与路径)");
}
var json = parseApiJsonLoose(text, "chapterListApi");
if (json != null && json.code != null && json.code !== 0) {
legado.log("[chapterList] api biz code=" + json.code + " msg=" + (json.message || json.msg || ""));
if (json.code === -102) {
throw new Error("chapterList: 签名错误(-102),参数或密钥与站点不一致");
}
throw new Error("chapterList: api code " + json.code);
}
var dataBlk = json != null && json.data != null ? json.data : json;
if (dataBlk && typeof dataBlk.url === "string" && dataBlk.url.indexOf("http") === 0) {
legado.log("[chapterList] api data.url fetch catalog");
var cdnText = await legado.http.get(harmonyHttpUrl(dataBlk.url));
var cdnJson = parseApiJsonLoose(cdnText, "chapterListCdn");
var fromCdn = extractChapterRowsFromPayload(cdnJson);
if (fromCdn && fromCdn.length > 0) {
return fromCdn;
}
if (Array.isArray(cdnJson)) {
return cdnJson;
}
}
var rows = extractChapterRowsFromPayload(json);
if (!rows || rows.length === 0) {
throw new Error("chapterList: api empty rows");
}
return rows;
}
/** 无 32 位 bookId 时兜底:WebView 内 Vuex → Performance → Vue 状态(无签名 fetch,恒 -102 已删) */
function extractChapterListRawByBrowser(readerUrl) {
var bidHex = String(extractBookIdFromReaderUrl(readerUrl) || "").replace(/[^0-9a-fA-F]/g, "");
if (bidHex.length !== 32) {
bidHex = "";
}
return legado.browser.run(
harmonyHttpUrl(readerUrl),
[
"var BID='" + bidHex + "';",
"function pickCatalogUrl(es, bid) {",
" var i, name;",
" if (bid && bid.length === 32) {",
" for (i = 0; i < es.length; i++) {",
" name = es[i].name || '';",
" if (name.indexOf(bid) !== -1 && (name.indexOf('.json') !== -1 || name.toLowerCase().indexOf('json') !== -1)) { return name; }",
" }",
" }",
" for (i = 0; i < es.length; i++) {",
" name = es[i].name || '';",
" if (name.indexOf('chapter_list') !== -1 && name.indexOf('.json') !== -1) { return name; }",
" }",
" for (i = 0; i < es.length; i++) {",
" name = es[i].name || '';",
" if (name.indexOf('/book/chapter_list/') !== -1) { return name; }",
" }",
" for (i = 0; i < es.length; i++) {",
" name = es[i].name || '';",
" if ((name.indexOf('book.midukanshu.com') !== -1 || name.indexOf('midu-book') !== -1 || name.indexOf('img.midukanshu.com') !== -1 || name.indexOf('aliyuncs.com') !== -1 || name.indexOf('saasapi') !== -1) && name.indexOf('.json') !== -1 && (name.indexOf('chapter') !== -1 || name.indexOf('catalog') !== -1 || name.indexOf('list') !== -1)) { return name; }",
" }",
" return '';",
"}",
"function appVm(el) {",
" if (!el) { return null; }",
" if (el.__vue__) { return el.__vue__; }",
" var c = el.firstElementChild;",
" if (c && c.__vue__) { return c.__vue__; }",
" return null;",
"}",
"function tryVueChapterList() {",
" try {",
" var el = document.querySelector('#app');",
" var vm = appVm(el);",
" if (!vm) { return ''; }",
" var store = vm.$store || (vm.$root && vm.$root.$store);",
" if (!store || !store.state) { return ''; }",
" var st = store.state['$_reader'];",
" if (st && st.chapterList && st.chapterList.length) { return JSON.stringify(st.chapterList); }",
" var k, st2;",
" for (k in store.state) {",
" st2 = store.state[k];",
" if (st2 && st2.chapterList && st2.chapterList.length) { return JSON.stringify(st2.chapterList); }",
" }",
" } catch (e0) {}",
" return '';",
"}",
"async function tryVuexDispatchCatalog() {",
" if (!BID || BID.length !== 32) { return ''; }",
" await new Promise(function (r) { setTimeout(r, 800); });",
" var maxTry = 22;",
" var ti;",
" for (ti = 0; ti < maxTry; ti++) {",
" await new Promise(function (r) { setTimeout(r, 400); });",
" try {",
" var el2 = document.querySelector('#app');",
" var vm2 = el2 && (el2.__vue__ || (el2.firstElementChild && el2.firstElementChild.__vue__));",
" var store2 = vm2 && (vm2.$store || (vm2.$root && vm2.$root.$store));",
" if (!store2 || typeof store2.dispatch !== 'function') { continue; }",
" var rows = await store2.dispatch('$_reader/getChapterList', { hash_id: BID });",
" if (rows && rows.length) { return JSON.stringify(rows); }",
" } catch (eDis) {}",
" }",
" return '';",
"}",
"var viaVuex = await tryVuexDispatchCatalog();",
"if (viaVuex) { return viaVuex; }",
"var deadline = Date.now() + 12000;",
"while (Date.now() < deadline) {",
" var es = performance.getEntriesByType('resource');",
" var u = pickCatalogUrl(es, BID);",
" if (u) { return u; }",
" var v = tryVueChapterList();",
" if (v) { return v; }",
" await new Promise(function (r) { setTimeout(r, 250); });",
"}",
"return '';"
].join("\n"),
{ visible: false, waitUntil: "load", timeoutSecs: 50 }
);
}
function rowsToChapters(rows, fallbackBookId) {
var chapters = [];
var i;
for (i = 0; i < rows.length; i++) {
var row = rows[i];
if (!row) continue;
var cid = row.chapterId != null ? row.chapterId : row.chapter_id;
var bid = row.bookId != null ? row.bookId : row.book_id;
if ((bid == null || bid === "") && fallbackBookId) bid = fallbackBookId;
var title = row.title != null ? row.title : row.chapterTitle;
if (title == null && row.name != null) {
title = row.name;
}
if (title == null && row.no != null) {
title = "第" + row.no + "章";
}
if (cid == null || title == null || bid == null || bid === "") continue;
chapters.push({
name: title,
url: buildChapterUrl(bid, cid, title)
});
}
return chapters;
}
async function chapterList(tocUrl) {
var tocLen = tocUrl == null ? 0 : String(tocUrl).length;
var bookIdGuess = extractBookIdFromReaderUrl(tocUrl);
if (!bookIdGuess) {
bookIdGuess = legado.config.read(MIDU_CONFIG_NS, "last_book_id") || "";
}
legado.log("[chapterList] tocUrl len=" + tocLen + " bookId=" + bookIdGuess);
var rows = [];
var bidForRows = bookIdGuess;
if (isMiduContentBookId(bidForRows)) {
legado.log("[chapterList] fast path signed saasapi skip browser");
rows = await fetchChapterRowsFromContentApi(bidForRows, tocUrl);
legado.log("[chapterList] api rows=" + rows.length);
} else {
function applyBrowserRaw(browserOut) {
if (!browserOut || typeof browserOut !== "string") {
return Promise.resolve();
}
var bo = browserOut.trim();
if (bo.indexOf("http://") === 0 || bo.indexOf("https://") === 0) {
legado.log("[chapterList] catalogUrl=" + bo.slice(0, Math.min(120, bo.length)));
return Promise.resolve(legado.http.get(harmonyHttpUrl(bo))).then(function (textBr) {
var sniffParsed = parseApiJsonLoose(textBr, "chapterListSniff");
rows = Array.isArray(sniffParsed) ? sniffParsed : extractChapterRowsFromPayload(sniffParsed);
});
}
if (bo.charAt(0) === "[" || bo.charAt(0) === "{") {
legado.log("[chapterList] vue chapterList json len=" + bo.length);
var vueParsed = parseApiJsonLoose(bo, "chapterListVue");
rows = Array.isArray(vueParsed) ? vueParsed : extractChapterRowsFromPayload(vueParsed);
}
return Promise.resolve();
}
var rawBrowser = extractChapterListRawByBrowser(tocUrl);
var browserOut = await Promise.resolve(rawBrowser);
legado.log(
"[chapterList] browser raw type=" +
typeof browserOut +
" len=" +
(browserOut != null && typeof browserOut === "string" ? browserOut.length : 0)
);
await applyBrowserRaw(browserOut);
if (!rows || rows.length === 0) {
legado.log("[chapterList] browser empty try content chapterList");
}
if (!rows || rows.length === 0) {
bidForRows = extractBookIdFromReaderUrl(tocUrl);
if (!bidForRows) {
bidForRows = legado.config.read(MIDU_CONFIG_NS, "last_book_id") || "";
}
if (!bidForRows) {
throw new Error("chapterList: bookId");
}
rows = await fetchChapterRowsFromContentApi(bidForRows, tocUrl);
legado.log("[chapterList] api rows=" + rows.length);
}
}
var chapters = rowsToChapters(rows, bidForRows);
if (!chapters || chapters.length === 0) {
throw new Error("chapterList: empty chapters");
}
legado.log("[chapterList] total=" + chapters.length);
return chapters;
}
async function chapterContent(chapterUrl) {
var info = parseChapterUrl(chapterUrl);
legado.log("[content] url=" + info.pureUrl);
var text = await legado.http.get(harmonyHttpUrl(info.pureUrl));
return text.replace(/\r/g, "").trim();
}
async function fetchExplorePayload() {
// H5:Vuex 里 getHotSearch() → POST /fiction/recommend/searchPage,发生在书城首页而非搜索页。
var url = API_BASE + "/fiction/recommend/searchPage";
var headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": H5_BASE + "/novel/index.html",
"Origin": H5_BASE
};
var text;
if (typeof fetch === "function") {
var res = await fetch(url, { method: "POST", headers: headers, body: "" });
text = await res.text();
} else {
text = await legado.http.post(harmonyHttpUrl(url), "", headers);
}
return JSON.parse(text).data;
}
function mapRawBookToBookItem(raw) {
var item = mapSearchItem(raw);
return {
name: item.name,
author: item.author,
bookUrl: buildBookUrl(item),
coverUrl: harmonyHttpUrl(item.coverUrl) || "",
kind: item.kind,
latestChapter: item.lastChapter
};
}
async function explore(page, category) {
legado.log("[explore] page=" + page + " category=" + category);
var pageNum = Number(page);
if (pageNum !== Math.floor(pageNum) || pageNum < 1) {
throw new Error("explore: invalid page");
}
if (category == null || category === "") {
throw new Error("explore: category");
}
if (category === "GETALL") {
return EXPLORE_FIXED_CATEGORIES.slice(0);
}
if (category === "猜你喜欢") {
var payload = await fetchExplorePayload();
var list = payload.recommendNode[0].nodeData.books;
var books = [];
var j;
for (j = 0; j < list.length; j++) {
books.push(mapRawBookToBookItem(list[j]));
}
legado.log("[explore] recommend books=" + books.length);
return books;
}
legado.log("[explore] category search: " + category);
return search(category, pageNum);
}