米读小说

https://m.midukanshu.com

ethereal-essence (13554) 16小时前 下载:343

小说 小说 米读 API H5
米读小说适配鸿蒙端
二维码导入(APP尚未完成该功能)
// @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(/&nbsp;/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);
}
广告