3A小说
https://www.aaawz.cc
ethereal-essence (13554) 16小时前 下载:305
小说 免费 小说 免费小说 API
3A小说网(aaawz.cc),免费小说站,JSON API 书源,正文 AES 加密。
// @uuid 019e1209-09f6-7401-ba45-2c5bd162c276
// @name 3A小说
// @version 1.2.5
// @author Ethereal
// @url https://www.aaawz.cc
// @type novel
// @logo https://www.aaawz.cc/favicon.ico
// @enabled true
// @tags 免费,小说,免费小说,API
// @description 3A小说网(aaawz.cc),免费小说站,JSON API 书源,正文 AES 加密。
// ─── 内置测试 ─────────────────────────────────────────────────────────────
async function TEST(type) {
if (type === '__list__') return ['search', 'explore'];
if (type === 'search') {
var results = await search('斗破苍穹', 1);
if (!results || results.length < 1) return { passed: false, message: '搜索结果为空' };
var found = false;
for (var i = 0; i < results.length; i++) {
if (results[i].author && results[i].author.indexOf('天蚕土豆') !== -1) { found = true; break; }
}
if (!found) return { passed: false, message: '搜索结果中未找到作者包含"天蚕土豆"的条目' };
return { passed: true, message: '搜索"斗破苍穹"返回 ' + results.length + ' 条结果 ✓' };
}
if (type === 'explore') {
var books = await explore(1, '推荐');
if (!books || books.length < 1) return { passed: false, message: '发现页返回为空' };
return { passed: true, message: '发现页返回 ' + books.length + ' 条结果 ✓' };
}
return { passed: false, message: '未知测试类型: ' + type };
}
// ─── 配置 ────────────────────────────────────────────────────────────────
var BASE = 'https://www.aaawz.cc';
var AES_KEY = '123#2^0@0vm@08.b5%$1[A]1&4115s((';
// ─── HTTP(带超时/重试)────────────────────────────────────────────────────
function mergeHeaders(a, b) {
var out = {};
var k;
if (a) for (k in a) out[k] = a[k];
if (b) for (k in b) out[k] = b[k];
return out;
}
function defaultHeaders(extra) {
// 鸿蒙端/部分网络栈在无 UA 时更容易遇到 TLS/EOF 等异常,统一补默认请求头
var base = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.6',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
};
return mergeHeaders(base, extra || {});
}
async function httpGet(url, headers, timeoutSecs) {
try {
return await legado.http.get(url, defaultHeaders(headers));
} catch (e1) {
legado.log('[httpGet] retry once: ' + url + ' err=' + e1);
try {
return await legado.http.get(url, defaultHeaders(headers));
} catch (e2) {
legado.log('[httpGet] fallback to browser.run: ' + url + ' err=' + e2);
// 仅在宿主支持 browser API 时启用兜底,避免鸿蒙端缺少接口导致卡死
if (legado && legado.browser && legado.browser.run) {
return browserFetchText(url, 'GET', '', defaultHeaders(headers), timeoutSecs || 30);
}
throw e2;
}
}
}
async function httpPost(url, body, headers, timeoutSecs) {
try {
return await legado.http.post(url, body, defaultHeaders(headers));
} catch (e1) {
legado.log('[httpPost] retry once: ' + url + ' err=' + e1);
try {
return await legado.http.post(url, body, defaultHeaders(headers));
} catch (e2) {
legado.log('[httpPost] fallback to browser.run: ' + url + ' err=' + e2);
if (legado && legado.browser && legado.browser.run) {
return browserFetchText(url, 'POST', body || '', defaultHeaders(headers), timeoutSecs || 30);
}
throw e2;
}
}
}
function browserFetchText(url, method, body, headers, timeoutSecs) {
// 通过探测 WebView 发起 fetch,规避部分站点对直连 HTTP 客户端的限制
// 注意:这里只返回 text,由上层 parseApiResponse 决定是否需要 LZ 解压/JSON.parse
var u = '' + url;
var m = (method || 'GET').toUpperCase();
var h = headers || {};
var b = body || '';
var to = timeoutSecs || 30;
var code = [
'(async function(){',
' var url=' + JSON.stringify(u) + ';',
' var method=' + JSON.stringify(m) + ';',
' var headers=' + JSON.stringify(h) + ';',
' var body=' + JSON.stringify(b) + ';',
' var ctrl = new AbortController();',
' var t = setTimeout(function(){ try{ ctrl.abort(); }catch(e){} }, ' + (to * 1000) + ');',
' try {',
' var init = { method: method, headers: headers, signal: ctrl.signal };',
' if (method !== "GET" && method !== "HEAD") init.body = body;',
' var resp = await fetch(url, init);',
' return await resp.text();',
' } finally {',
' try{ clearTimeout(t); }catch(e){}',
' }',
'})()',
].join('\n');
// 以主站首页作为上下文更稳(同源、Cookie/UA/指纹更像浏览器)
return legado.browser.run(BASE + '/', code, { visible: false, waitUntil: 'load' });
}
// ─── LZ-String Base64 解压 ─────────────────────────────────────────────
/**
* LZ-String decompressFromBase64 的 ES5 实现(基于官方算法改写为单函数)。
* 3A小说 API 返回的数据通常为 LZ-String Base64 压缩结果。
*/
function decompressFromBase64(input) {
if (input === null || input === undefined) return '';
if (input === '') return null;
var keyStrBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
function getBaseValue(alphabet, character) {
return alphabet.indexOf(character);
}
function _decompress(length, resetValue, getNextValue) {
var dictionary = [];
var next;
var enlargeIn = 4;
var dictSize = 4;
var numBits = 3;
var entry = '';
var result = [];
var i;
var w;
var bits, resb, maxpower, power;
var c;
var data = { val: getNextValue(0), position: resetValue, index: 1 };
for (i = 0; i < 3; i += 1) {
dictionary[i] = i;
}
bits = 0;
maxpower = Math.pow(2, 2);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch (bits) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = String.fromCharCode(bits);
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
c = String.fromCharCode(bits);
break;
case 2:
return '';
default:
return '';
}
dictionary[3] = c;
w = c;
result.push(c);
while (true) {
if (data.index > length) {
return '';
}
bits = 0;
maxpower = Math.pow(2, numBits);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
switch (c = bits) {
case 0:
bits = 0;
maxpower = Math.pow(2, 8);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = String.fromCharCode(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 1:
bits = 0;
maxpower = Math.pow(2, 16);
power = 1;
while (power !== maxpower) {
resb = data.val & data.position;
data.position >>= 1;
if (data.position === 0) {
data.position = resetValue;
data.val = getNextValue(data.index++);
}
bits |= (resb > 0 ? 1 : 0) * power;
power <<= 1;
}
dictionary[dictSize++] = String.fromCharCode(bits);
c = dictSize - 1;
enlargeIn--;
break;
case 2:
return result.join('');
}
if (enlargeIn === 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
if (dictionary[c]) {
entry = dictionary[c];
} else {
if (c === dictSize) {
entry = w + w.charAt(0);
} else {
return '';
}
}
result.push(entry);
// Add w+entry[0] to the dictionary.
dictionary[dictSize++] = w + entry.charAt(0);
enlargeIn--;
w = entry;
if (enlargeIn === 0) {
enlargeIn = Math.pow(2, numBits);
numBits++;
}
}
}
return _decompress(input.length, 32, function (index) {
return getBaseValue(keyStrBase64, input.charAt(index));
});
}
/**
* 解析 API 响应:先 LZ 解压,再 JSON.parse
*/
function parseApiResponse(raw) {
if (!raw) return null;
var text = ('' + raw).replace(/^\s+|\s+$/g, '');
if (!text) return null;
// 优先尝试站点默认的 LZ-String Base64 压缩格式
try {
var json = decompressFromBase64(text);
if (json) return JSON.parse(json);
} catch (e1) {}
// 兼容接口直接返回 JSON 文本(未压缩)
try {
return JSON.parse(text);
} catch (e2) {}
legado.log('[parseApiResponse] 解析失败,返回 null');
return null;
}
// ─── 工具 ────────────────────────────────────────────────────────────────
function normalizeUrl(u) {
if (!u) return '';
var s = ('' + u).replace(/^\s+|\s+$/g, '');
if (!s) return '';
// 兼容协议相对 //cdn.xxx/...
if (s.indexOf('//') === 0) return 'https:' + s;
// 已是绝对地址
if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s;
// 兼容相对路径 /xxx/yyy.jpg
if (s.charAt(0) === '/') return BASE + s;
// 兼容相对路径 xxx/yyy.jpg(无前导 /)
return BASE + '/' + s;
}
function buildCoverUrl(tid, siteid) {
if (!tid) return '';
var tidNum = parseInt(tid, 10);
var folder = tidNum % 100;
// 以网页实际路径规则为准:/bookimg/{siteid}/{tid%100}/{tid}.jpg
return normalizeUrl('/bookimg/' + siteid + '/' + folder + '/' + tid + '.jpg');
}
function isPlaceholderCover(url) {
if (!url) return true;
var u = '' + url;
return u.indexOf('/static/img/img.jpg') !== -1;
}
function pickCoverUrl(imgurl, tid, siteid) {
// 优先相信接口返回的站内封面路径(/bookimg/...);其次再用规则拼
var u = (imgurl || '').toString();
if (u) {
// 站内封面:更稳定、无外链防盗链
if (u.indexOf('/bookimg/') !== -1 || u.indexOf('aaawz.cc') !== -1 || u.indexOf('//') === 0 || u.charAt(0) === '/') {
var c1 = normalizeUrl(u);
if (c1 && !isPlaceholderCover(c1)) return c1;
}
}
var c2 = buildCoverUrl(tid, siteid);
if (c2 && !isPlaceholderCover(c2)) return c2;
// 最后再尝试把 imgurl 当作普通 URL 规范化(可能是相对路径或完整外链)
var c3 = normalizeUrl(u);
if (c3 && !isPlaceholderCover(c3)) return c3;
return '';
}
async function extractCoverFromWeb(tid, siteid) {
if (!tid || !siteid) return '';
var pageUrl = BASE + '/#/book/' + tid + '/' + siteid;
var code = [
'(async function(){',
' function sleep(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }',
' // 等 SPA 渲染',
' for (var i=0;i<20;i++){',
// 该站封面 img 通常具备 onerror 回退到 /static/img/img.jpg 的特征
' var img = document.querySelector(\"img[src*=\\\"/bookimg/\\\"][onerror*=\\\"/static/img/img.jpg\\\"]\")',
' || document.querySelector(\"img[src*=\\\"/bookimg/\\\"]\")',
' || document.querySelector(\"img[onerror*=\\\"/static/img/img.jpg\\\"][src]\")',
' || document.querySelector(\"img[src]\");',
' if (img && img.getAttribute && img.getAttribute(\"src\")) {',
' return img.getAttribute(\"src\");',
' }',
' await sleep(250);',
' }',
' return \"\";',
'})()',
].join('\\n');
try {
var src = await legado.browser.run(pageUrl, code, { visible: false, waitUntil: 'networkidle' });
var url = normalizeUrl(src);
if (url && !isPlaceholderCover(url)) return url;
} catch (e) {
legado.log('[extractCoverFromWeb] failed tid=' + tid + ' siteid=' + siteid + ' err=' + e);
}
return '';
}
function normalizeTextKey(s) {
if (!s) return '';
// 去掉空白与常见分隔符,降低“同名不同写法”导致的重复
return ('' + s)
.replace(/<\/?em>/g, '')
.replace(/\s+/g, '')
.replace(/[·•・\-—_]/g, '')
.toLowerCase();
}
function pickBetterBook(a, b) {
// 返回更“像主源”的那条(尽量保留章节/封面更完整的数据)
function score(x) {
if (!x) return 0;
var s = 0;
if (x.lastChapter) s += 3;
if (x.coverUrl) s += 2;
if (x.author) s += 1;
if (x.name) s += 1;
return s;
}
return score(b) > score(a) ? b : a;
}
// ─── 搜索 ─────────────────────────────────────────────────────────────────
async function search(keyword, page) {
legado.log('[search] keyword=' + keyword + ' page=' + page);
var url = BASE + '/api-search';
var body = 'keyword=' + encodeURIComponent(keyword) + '&page=' + (page || 1) + '&size=10';
var raw = await httpPost(url, body, {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json, text/plain, */*',
'Origin': BASE,
'Referer': BASE + '/',
}, 60);
var resp = parseApiResponse(raw);
if (!resp || !resp.data || !resp.data.books) return [];
var booksArr = resp.data.books;
var map = {};
var books = [];
for (var i = 0; i < booksArr.length; i++) {
var b = booksArr[i];
var name = (b.articlename || '').replace(/<\/?em>/g, '');
var author = (b.author || '').replace(/<\/?em>/g, '');
var item = {
name: name,
author: author,
bookUrl: BASE + '/api-info-' + b.tid + '-' + b.siteid,
coverUrl: pickCoverUrl(b.imgurl, b.tid, b.siteid),
lastChapter: b.lastchapter || '',
kind: '',
};
// 同一本书在不同站点源(siteid)会重复出现:用 “书名+作者” 去重
var key = normalizeTextKey(name) + '|' + normalizeTextKey(author);
if (!key || key === '|') {
books.push(item);
continue;
}
if (!map[key]) {
map[key] = item;
books.push(item);
} else {
var better = pickBetterBook(map[key], item);
// 如更优则替换(保持 books 数组中的同一个引用更新)
if (better !== map[key]) {
map[key].name = better.name;
map[key].author = better.author;
map[key].bookUrl = better.bookUrl;
map[key].coverUrl = better.coverUrl;
map[key].lastChapter = better.lastChapter;
map[key].kind = better.kind;
}
}
}
legado.log('[search] found=' + books.length);
return books;
}
// ─── 书籍详情 ─────────────────────────────────────────────────────────────
async function bookInfo(bookUrl) {
legado.log('[bookInfo] url=' + bookUrl);
var raw = await httpGet(bookUrl, {
'Accept': 'application/json, text/plain, */*',
'Origin': BASE,
'Referer': BASE + '/',
}, 60);
var resp = parseApiResponse(raw);
if (!resp) return { name: '', author: '', coverUrl: '', intro: '', lastChapter: '', kind: '', tocUrl: bookUrl };
// 从 URL 提取 tid 和 siteid
var urlMatch = bookUrl.match(/api-info-(\d+)-(\d+)/);
var tid = urlMatch ? urlMatch[1] : (resp.tid || '');
var siteid = urlMatch ? urlMatch[2] : (resp.siteid || '');
var coverUrl = pickCoverUrl(resp.imgurl, tid, siteid);
if (!coverUrl) {
// 详情页兜底:从网页 DOM 抓真实封面(部分书籍 API 字段缺失/不一致)
coverUrl = await extractCoverFromWeb(tid, siteid);
}
return {
name: resp.articlename || '',
author: resp.author || '',
coverUrl: coverUrl,
intro: resp.intro || '',
lastChapter: resp.lastchapter || '',
kind: '',
tocUrl: BASE + '/api-chapterlist-' + tid + '-' + siteid,
};
}
// ─── 章节列表 ─────────────────────────────────────────────────────────────
async function chapterList(tocUrl) {
legado.log('[chapterList] url=' + tocUrl);
// 兼容:部分端(鸿蒙)可能会把 bookUrl(api-info-*)直接传给 chapterList
// 这里自动转换为真正的目录接口 api-chapterlist-<tid>-<siteid>
var mInfo = (tocUrl || '').match(/api-info-(\d+)-(\d+)/);
if (mInfo) {
tocUrl = BASE + '/api-chapterlist-' + mInfo[1] + '-' + mInfo[2];
legado.log('[chapterList] redirect api-info -> ' + tocUrl);
}
var raw = await httpGet(tocUrl, {
'Accept': 'application/json, text/plain, */*',
'Origin': BASE,
'Referer': BASE + '/',
}, 60);
var resp = parseApiResponse(raw);
if (!resp) return [];
// 再兜底一次:如果拿到的是“详情对象”(仍然是 api-info 的返回),则根据 tid/siteid 再去拉目录
if (!Array.isArray(resp)) {
if (resp.tid && resp.siteid) {
var toc2 = BASE + '/api-chapterlist-' + resp.tid + '-' + resp.siteid;
legado.log('[chapterList] resp is object, refetch toc: ' + toc2);
var raw2 = await httpGet(toc2, { 'Origin': BASE, 'Referer': BASE + '/' }, 60);
resp = parseApiResponse(raw2);
}
}
if (!Array.isArray(resp)) return [];
// 从 URL 提取 tid 和 siteid 用于构造章节内容 URL
var urlMatch = tocUrl.match(/api-chapterlist-(\d+)-(\d+)/);
var tid = urlMatch ? urlMatch[1] : '';
var siteid = urlMatch ? urlMatch[2] : '';
var chapters = [];
for (var i = 0; i < resp.length; i++) {
var ch = resp[i];
chapters.push({
name: ch.title || '',
url: BASE + '/api-chapter-' + tid + '-' + siteid + '-' + ch.cid,
});
}
legado.log('[chapterList] total=' + chapters.length);
return chapters;
}
// ─── 正文 ─────────────────────────────────────────────────────────────────
async function chapterContent(chapterUrl) {
legado.log('[content] url=' + chapterUrl);
var raw = (await httpGet(chapterUrl, {
'Origin': BASE,
'Referer': BASE + '/',
}, 60)).replace(/\s/g, '');
var content = '';
if (chapterUrl.indexOf('-chapter-') !== -1) {
// 正文接口协议:响应为 base64( IV[0..16] || AES-CBC-PKCS7密文 )
// 1. 用 base64ByteSlice 从二进制流中分离 IV 和密文(避免 UTF-8 截断)
// 2. 用 aesDecryptB64Iv 以 base64 格式传入 IV 做标准 AES-CBC 解密
// 3. 解密结果再做 LZ-String Base64 解压得到最终正文
try {
var ivB64 = legado.base64ByteSlice(raw, 0, 16);
var cipherB64 = legado.base64ByteSlice(raw, 16);
var plaintext = await legado.aesDecryptB64Iv(cipherB64, AES_KEY, ivB64, 'CBC');
content = decompressFromBase64(plaintext.replace(/\s/g, ''));
if (!content) throw new Error('LZ 解压结果为空');
} catch (e) {
legado.log('[content] 正文解密失败: ' + e);
content = '';
}
} else {
// 非章节接口(理论上不会走到这里):直接 LZ 解压
try {
content = decompressFromBase64(raw) || '';
} catch (e) {
legado.log('[content] LZ 解压失败: ' + e);
content = '';
}
}
if (!content) {
// 兜底:返回可见错误,避免阅读端一直“排版中”
return '(正文获取失败)\n\n请稍后重试,或切换网络/节点。';
}
// 清理 HTML 标签
content = content.replace(/<br\s*\/?>/gi, '\n');
content = content.replace(/<\/?p[^>]*>/gi, '\n');
content = content.replace(/<[^>]+>/g, '');
content = content.replace(/ /g, ' ');
// 按段落拆分并过滤空行
var lines = content.split('\n');
var paragraphs = [];
for (var i = 0; i < lines.length; i++) {
var text = lines[i].replace(/^\s+|\s+$/g, '');
if (text) {
paragraphs.push(text);
}
}
return paragraphs.join('\n\n');
}
// ─── 发现页 ──────────────────────────────────────────────────────────────
// 接口:/api-list-{page},返回按热度排序的书籍数组,无分类过滤。
async function explore(page, category) {
// category === 'GETALL':返回分类名列表
if (!category || category === 'GETALL') {
return ['推荐'];
}
// category === '推荐':返回热门排行书籍列表(无其他分类)
var p = page || 1;
var url = BASE + '/api-list-' + p;
legado.log('[explore] category=' + category + ' page=' + p + ' url=' + url);
var raw = await httpGet(url, {
'Accept': 'application/json, text/plain, */*',
'Origin': BASE,
'Referer': BASE + '/',
}, 60);
var resp = parseApiResponse(raw);
if (!Array.isArray(resp)) return [];
var books = [];
for (var i = 0; i < resp.length; i++) {
var b = resp[i];
books.push({
name: b.articlename || '',
author: b.author || '',
bookUrl: BASE + '/api-info-' + b.tid + '-' + b.siteid,
coverUrl: pickCoverUrl(b.imgurl, b.tid, b.siteid),
lastChapter: b.lastchapter || '',
kind: '',
});
}
legado.log('[explore] found=' + books.length);
return books;
}