summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormayx <mayx@outlook.com>2026-08-06 09:47:54 +0000
committermayx <mayx@outlook.com>2026-08-06 09:47:54 +0000
commit2ec9345113bad58204178ec4d0c7ffdc132fb140 (patch)
treea6af942caaef14de3c56e381d107fa1c6ccf66ce
parent987c604deaf893cff361a71a10107cb712ae9ab3 (diff)
Update 3 files
- /assets/js/blog-console.js - /_data/other_repo_list.csv - /_data/proxylist.yml
-rw-r--r--_data/other_repo_list.csv58
-rw-r--r--_data/proxylist.yml1
-rw-r--r--assets/js/blog-console.js1105
3 files changed, 706 insertions, 458 deletions
diff --git a/_data/other_repo_list.csv b/_data/other_repo_list.csv
index e8bd2b4..d1702f2 100644
--- a/_data/other_repo_list.csv
+++ b/_data/other_repo_list.csv
@@ -44,8 +44,6 @@ https://git.7o9o.net/mayx/blog
https://git.gupaoedu.cn/mayx/blog
https://git.7milch.com/mayx/blog
https://dreamplacesai.de/mayx/blog
-https://git.lekai.info/mayx/blog
-https://git.novaa.xyz/mayx/blog
https://gitea.sciotech.cn/mayx/blog
https://git.gloje-rinchen-dorjee-rinpoche-buddhist-monastery.org/mayx/blog
https://gitea.micro-stack.org/mayx/blog
@@ -73,13 +71,11 @@ https://www.simpra.org:3000/mayx/blog
https://www.arcbyte.dev/mayx/blog
https://scm.bcorex.e3labs.net/mayx/blog
https://git.tobiasweise.dev/mayx/blog
-https://code.cif.su/mayx/blog
https://git.cynic.moe/mayx/blog
https://git.muellers-software.org/mayx/blog
https://git.cavemanon.xyz/mayx/blog
http://svn.rivastudio.cn/mayx/blog
http://118.178.172.49:3000/mayx/blog
-https://gitea.lasallesaintdenis.com/mayx/blog
http://8.130.135.159:3000/mayx/blog
https://git.dshkabatur.ru/mayx/blog
http://47.103.78.70:3000/mayx/blog
@@ -103,7 +99,6 @@ http://45.55.138.82:3000/mayx/blog_cn
http://118.24.129.148:3000/mayx/blog_cn
http://120.48.141.82:3000/mayx/blog_cn
http://82.156.111.58:3000/mayx/blog_cn
-https://git.cool2645.com/mayx/blog_cn
http://175.27.229.211:3000/mayx/blog_cn
https://git.yidaimingjvn.xyz/mayx/blog_cn
http://39.105.67.143:3000/mayx/blog_cn
@@ -115,10 +110,7 @@ http://58.213.60.6:19000/mayx/blog_cn
http://47.92.113.131:3000/mayx/blog
http://gyc.myds.me:4000/mayx/blog
https://git.graveyard.sh/mayx/blog
-https://git.nathanspackman.com/mayx/blog
-http://git.mynas71.myds.me/mayx/blog
https://git.veraskolivna.net/mayx/blog
-https://git.lifetop.net/mayx/blog
https://gitea.jnyuxia.com/mayx/blog
https://git.danpeak.co.uk/mayx/blog
https://gitea.mynas-lechner.de/mayx/blog
@@ -137,12 +129,10 @@ http://119.96.62.56:3000/mayx/blog_cn
http://43.143.209.246:6300/mayx/blog_cn
http://39.100.183.95:88/mayx/blog_cn
http://www.xshideserver.com:3000/mayx/blog_cn
-http://1.117.67.95:3000/mayx/blog_cn
http://81.70.102.154/mayx/blog_cn
http://36.133.248.69:3088/mayx/blog_cn
http://220.205.16.27:18081/mayx/blog_cn
http://183.204.60.122:10081/mayx/blog_cn
-https://gitea.wuyuank.com/mayx/blog_cn
http://47.106.222.181:20511/mayx/blog_cn
https://gitea.myat4.com/mayx/blog_cn
http://118.24.161.24:3000/mayx/blog_cn
@@ -155,13 +145,10 @@ https://gitea.accept.dev.dbf.nl/mayx/blog
https://git.jhossain.online/mayx/blog
https://barhoum-lab.fr/mayx/blog
https://redev.lol/mayx/blog
-https://papi.tkpups.com/mayx/blog
-https://git.beyond-a-i.org/mayx/blog
http://git.zhmight.com/mayx/blog_cn
http://gitea.snailtrack.cn/mayx/blog_cn
https://code.draussenfunker.de/mayx/blog_cn
https://git.dinsor.co.th/mayx/blog
-http://www.arkproject.top/mayx/blog
http://www.bkandssp.cn:30/mayx/blog
http://gitea.ydxtool.com/mayx/blog
https://gitea.malxte.de/mayx/blog
@@ -177,9 +164,7 @@ http://git.hbg99.com:8080/mayx/blog_cn
https://git.eplg.services/mayx/blog
http://namonba.asuscomm.com:3001/mayx/blog
http://109.199.98.226:3001/mayx/blog
-https://gitea.digitanie.org/mayx/blog
https://git.xleed.com/mayx/blog
-https://qlcodegitserver.online/mayx/blog
https://git.supernets.org/mayx/blog
https://git.digitaltelepresence.com/mayx/blog
https://git.hrfee.pw/mayx/blog
@@ -188,32 +173,24 @@ https://git.kaki87.net/mayx/blog
https://forgejo.vanten-s.com/mayx/blog
http://ysx.myds.me:3005/mayx/blog
http://124.71.169.183:3000/mayx/blog
-https://hiwifi.denq.us:8418/mayx/blog
https://git.msoucy.me/mayx/blog
http://140.120.108.238:49308/mayx/blog
http://git.chilidoginteractive.com:3000/mayx/blog_cn
https://git.thunder-data.cn/mayx/blog_cn
-https://git.ixu.me/mayx/blog_cn
http://git.uhfdemo.com/mayx/blog_cn
http://43.143.142.38:7001/mayx/blog_cn
https://git.wieerwill.dev/mayx/blog
-https://git.etwo.dev/mayx/blog
https://git.panda-number.one/mayx/blog
https://datacortex.pl/mayx/blog
-https://git.trevorbotha.net/mayx/blog
https://gitea.jobiglo.com/mayx/blog_cn
http://118.31.7.2:3000/mayx/blog_cn
http://101.37.147.115:3000/mayx/blog_cn
-https://gitea.originaltech.cn/mayx/blog_cn
https://git.akarmain.ru/mayx/blog_cn
https://git.georgew.dev/mayx/blog
https://gitea.keymantek.com:777/mayx/blog
http://git.realrobots.net/mayx/blog
http://www.souper.ee/mayx/blog
https://git.tirtapakuan.co.id/mayx/blog
-https://tacticallysolved.com/mayx/blog
-https://git.juntekim.com/mayx/blog
-https://git.farmtowntech.com/mayx/blog
https://gitea.viperlance.net/mayx/blog
https://git.woopwoopserver.com/mayx/blog
http://tian-you.top:7020/mayx/blog
@@ -230,7 +207,6 @@ https://git.smart-family.net/mayx/blog
https://git.chiasson.cloud/mayx/blog
https://git.sleepeesoftware.fr/mayx/blog
https://git.everestlinux.org/mayx/blog
-https://git.devinmajor.com/mayx/blog
https://git.netzbyte.com/mayx/blog
https://ataymakhzan.com/mayx/blog
https://repo.qruize.com/mayx/blog
@@ -238,16 +214,13 @@ https://git.xneon.org/mayx/blog
https://git.lamby.gay/mayx/blog
https://donkey.abanbytes.eu/mayx/blog
https://lab.dutt.ch/mayx/blog
-https://git.liestral.io/mayx/blog
https://gitea.dabit.synology.me/mayx/blog
https://git.joinplu.me/mayx/blog
http://otter.top/mayx/blog
http://188.66.0.25:3000/mayx/blog
https://gitea.deliverables.io/mayx/blog
https://git.sevenvm.de/mayx/blog
-https://code.letsbe.solutions/mayx/blog
https://git.mlhub.it/mayx/blog
-https://jsuse.com/mayx/blog
http://23.182.16.150:3000/mayx/blog
https://git.ritonquilol.fr/mayx/blog
https://bis127.vse.cz/mayx/blog
@@ -260,7 +233,6 @@ https://gitea.hpdocker.hpress.de/mayx/blog
https://git.jhammah.com/mayx/blog
https://git.kemomimi.site/mayx/blog
https://gitea.kolesarhome.com/mayx/blog
-http://sexy6688.vip/mayx/blog
https://gitbucket.aint-no.info/mayx/blog
https://gitea.emulab.it/mayx/blog
https://git.wikeo.ovh/mayx/blog
@@ -272,7 +244,6 @@ http://qiubei-git.cn/mayx/blog_cn
http://wangbeibei.xyz:6002/mayx/blog
http://8.141.82.163:20000/mayx/blog_cn
http://152.136.126.252:3000/mayx/blog_cn
-https://home.zhupei.me:3000/mayx/blog_cn
http://gitea.coderpath.com/mayx/blog_cn
https://www.chenisgod.com:3096/mayx/blog_cn
http://81.70.30.91:3000/mayx/blog_cn
@@ -281,30 +252,24 @@ http://8.148.31.14:3000/mayx/blog_cn
http://58.17.14.95:8001/mayx/blog_cn
http://39.99.175.172:8000/mayx/blog_cn
http://39.171.252.63:3000/mayx/blog_cn
-https://git.lucas-michel.fr/mayx/blog_cn
https://git.cdev.su/mayx/blog
https://root-kit.ru/mayx/blog
https://git.gnyra.com/mayx/blog_cn
https://git.newnaturalphilosophy.org/mayx/blog_cn
-https://git.jingchengdl.com/mayx/blog_cn
https://gitea.detr.top/mayx/blog_cn
http://175.178.193.35:3000/mayx/blog_cn
http://frp5.mmszxc.xin:55469/mayx/blog_cn
http://gitea.jb1000.com:88/mayx/blog_cn
https://gitea.liyongzheng.fun/mayx/blog_cn
http://www.22ai.vip/mayx/blog_cn
-http://voodoo.plus/mayx/blog_cn
https://git.extra.eiffel.com/mayx/blog_cn
http://219.151.177.172:3000/mayx/blog_cn
http://62.234.20.54:3000/mayx/blog_cn
https://git.urnas.cn/mayx/blog_cn
https://gitea.4gunn.cn:52443/mayx/blog_cn
-https://gitea.nightdev.cn:9527/mayx/blog_cn
-https://git.hivetasks.com/mayx/blog_cn
https://git.ddns.net/mayx/blog_cn
http://jdunlap.com/mayx/blog
https://git.silica.codes/mayx/blog
-http://xiaolee.space/mayx/blog
https://www.vitelgrid.app/mayx/blog
https://gitruhub.ru/mayx/blog
https://git.linuxposting.xyz/mayx/blog
@@ -314,7 +279,6 @@ http://speedyfox.app/mayx/blog
https://mjjtop.com/mayx/blog
https://git.noosfera.digital/mayx/blog
https://git.birdideas.net/mayx/blog
-https://salamihub.ru/mayx/blog
https://forgejo.wanderingmonster.dev/mayx/blog
http://v.udvip.com/mayx/blog
https://gitea-fs0kwo8kccc4g88g0kk8k88c.gnextd.io/mayx/blog
@@ -322,34 +286,26 @@ https://git.edavmig.ru/mayx/blog
https://gitea.ns5001k.sigma2.no/mayx/blog
https://git.dongshan.tech/mayx/blog
https://fzsimo.com/mayx/blog
-https://www.webetter.co.jp/mayx/blog
https://git.hemangvyas.com/mayx/blog
https://git.zotadevices.ru/mayx/blog
https://gitav.ru/mayx/blog
-https://git.cribdev.com/mayx/blog
https://sapkyy.ru/mayx/blog
https://git.sakuzyo.net/mayx/blog
https://git.zefie.net/mayx/blog
-https://gitea.redpowerfuture.com/mayx/blog
https://git.jinzhao.me/mayx/blog
http://www.365rise.top/mayx/blog
https://git.ifuntanhub.dev/mayx/blog
-https://mahta-tech.com/mayx/blog
-http://kuiwisen.asia/mayx/blog
https://git.aptcloud.ru/mayx/blog
https://gitea.simssoftware.in/mayx/blog
https://gitlab.dev.genai-team.ru/mayx/blog
-https://gitea.garandplg.com/mayx/blog
https://d.roxyipt.com/mayx/blog
https://git.umervtilte.lol/mayx/blog
-https://infrared.xxx/mayx/blog
https://git.bitpak.ru/mayx/blog
https://git.amamedis.de/mayx/blog
https://gitea.neanderhub.com/mayx/blog
https://git.kunstglass.de/mayx/blog
https://git.kry008.xyz/mayx/blog
https://gitea.onfing.ir/mayx/blog
-http://59.57.4.66:3000/mayx/blog_cn
https://gitea.xala.dev/mayx/blog
https://gitea.augeo.dev/mayx/blog_cn
http://36.213.200.127:23000/mayx/blog_cn
@@ -361,4 +317,16 @@ https://git.dglyoo.com/mayx/blog_cn
http://c8202.top:3000/mayx/blog_cn
http://nojava.fun/mayx/blog_cn
http://www.zhima.life/mayx/blog_cn
-https://git.healparts.ru/mayx/blog_cn
+https://git.wending993.top/mayx/blog_cn
+https://gitimn.com/mayx/blog
+http://git.zapthisblog.com/mayx/blog
+https://forgejo.networkx.de/mayx/blog
+https://git.resacachile.cl/mayx/blog
+https://zhanghome.uk/mayx/blog
+https://git.rentakloud.com/mayx/blog
+https://git.solutionsinc.co.uk/mayx/blog
+https://no-idea.life/mayx/blog
+https://www.sosupple.com/mayx/blog
+https://gitea.thomas.rocks/mayx/blog
+https://git.patapouk.fr/mayx/blog
+https://git.vycsucre.gob.ve/mayx/blog
diff --git a/_data/proxylist.yml b/_data/proxylist.yml
index 944b2e7..4d86deb 100644
--- a/_data/proxylist.yml
+++ b/_data/proxylist.yml
@@ -1,6 +1,5 @@
proxies:
- https://blog.mayx.workers.dev/
-- https://mayx.deno.dev/
- https://mayx.val.run/
- https://mayx.azion.app/
- https://yuki.gear.host/
diff --git a/assets/js/blog-console.js b/assets/js/blog-console.js
index da8e373..bf9701f 100644
--- a/assets/js/blog-console.js
+++ b/assets/js/blog-console.js
@@ -1,24 +1,35 @@
/*!
- * blog-console.js — Mayx's Blog DevTools Console API
+ * blog-console.js — Mayx's Blog Console API + WebMCP Tools
*/
(function (global) {
'use strict';
/* =====================================================================
- * 0. 常量与配置
+ * §0. 常量与配置
* ===================================================================== */
var config = {
/** list() 默认每页条数 */
pageSize: 10,
+ /** 工具单页最大条数,避免撑爆 Agent 上下文 */
+ maxPageSize: 50,
/** show() 单次最多渲染的行数,防止超长文章刷屏 */
maxShowLines: 600,
/** 摘要/预览截断长度 */
- previewLength: 120
+ previewLength: 120,
+ /** 工具返回 Markdown 的默认/最大字符数 */
+ readChars: 8000,
+ maxReadChars: 50000,
+ /** grep 最长执行时间(毫秒),防止病态正则卡死页面 */
+ grepTimeBudget: 2000,
+ /** 是否在脚本加载后自动注册 WebMCP 工具 */
+ autoRegister: true,
+ /** 工具名前缀,须符合规范:仅 ASCII 字母数字与 _ - . */
+ toolPrefix: 'blog_'
};
/* =====================================================================
- * 1. 控制台样式(DevTools 用 %c + CSS)
+ * §1. 控制台样式与打印器(DevTools 用 %c + CSS)
* ===================================================================== */
var S = {
@@ -48,44 +59,30 @@
]
};
- /**
- * 分段样式打印器。收集 [文本, CSS] 片段,分批 flush 到 console.log,
- * 既能保证多行样式连续,又避免单条消息过长被浏览器截断。
- */
function Printer(chunkSize) {
this.chunk = chunkSize || 160;
this.fmt = [];
this.css = [];
this.pending = 0;
}
- /**
- * 追加一个带样式的片段(不换行)。
- * @param {string} text 文本内容
- * @param {string} [css] CSS 样式串,省略则使用默认样式
- */
Printer.prototype.push = function (text, css) {
- // 转义 %,防止与 console 的格式化占位符冲突
this.fmt.push('%c' + String(text).replace(/%/g, '%%'));
this.css.push(css || '');
this.pending++;
return this;
};
- /** 追加一行(自动换行),并在片段数超过阈值时 flush。 */
Printer.prototype.line = function (text, css) {
this.push((text === undefined ? '' : text) + '\n', css);
if (this.pending >= this.chunk) this.flush();
return this;
};
- /** 结束当前行。 */
Printer.prototype.br = function () {
this.push('\n', '');
return this;
};
- /** 把已累积的片段输出到控制台。 */
Printer.prototype.flush = function () {
if (!this.fmt.length) return this;
var msg = this.fmt.join('');
- // 去掉行尾多余换行,避免每次 flush 产生空行
msg = msg.replace(/\n$/, '');
console.log.apply(console, [msg].concat(this.css));
this.fmt = [];
@@ -94,7 +91,6 @@
return this;
};
- /** 打印一条分隔标题横幅。 */
function banner(text) {
var p = new Printer();
p.line('── ' + text + ' ' + repeat('─', Math.max(2, 46 - strWidth(text))), S.title);
@@ -103,7 +99,6 @@
function repeat(ch, n) { return n > 0 ? new Array(n + 1).join(ch) : ''; }
- /** 粗略估算显示宽度(中日韩字符按 2 计)。 */
function strWidth(s) {
var w = 0;
for (var i = 0; i < s.length; i++) {
@@ -112,16 +107,19 @@
return w;
}
- /** 打印错误并返回 null,统一失败出口。 */
function fail(msg) {
console.log('%c⚠️ ' + msg, S.err);
return null;
}
/* =====================================================================
- * 2. 宿主环境依赖层
+ * §2. 宿主直读
+ *
+ * 直接读博客页面已提供的宿主对象;不在缺失时做回退。
* ===================================================================== */
+ var doc = global.document;
+
/**
* GitHub Issues 访问用的 Basic 凭据。
* @returns {{headers:object, owner:string, repo:string}}
@@ -145,53 +143,18 @@
});
}
- /**
- * 懒加载站点自带的 SimpleJekyllSearch。
- * 该库只在 /search.html 里被引入,其他页面需要时动态注入同一个文件,
- * 从而保证控制台搜索与页面搜索使用完全一致的匹配/排序逻辑。
- * @returns {Promise<Function>} SimpleJekyllSearch 工厂函数
- * @throws {Error} 脚本加载失败时 reject
- */
- function ensureSimpleJekyllSearch() {
- if (typeof global.SimpleJekyllSearch === 'function') {
- return Promise.resolve(global.SimpleJekyllSearch);
- }
- return new Promise(function (resolve, reject) {
- var s = document.createElement('script');
- s.src = '/assets/js/simple-jekyll-search.min.js';
- s.async = true;
- s.onload = function () {
- if (typeof global.SimpleJekyllSearch === 'function') resolve(global.SimpleJekyllSearch);
- };
- s.onerror = function () {
- reject(new Error('无法加载 SimpleJekyllSearch'));
- };
- document.head.appendChild(s);
- });
- }
-
/* =====================================================================
- * 3. 通用请求工具
+ * §3. 通用请求工具
* ===================================================================== */
- /**
- * 请求 JSON,失败返回 null(不抛异常,便于控制台链式使用)。
- * @param {string} url
- * @param {object} [options] fetch 选项
- * @returns {Promise<any|null>}
- */
+ /** 请求 JSON;网络失败时返回 null,交给上层返回结构化的领域错误。 */
function fetchJSON(url, options) {
return fetch(url, options || {})
.then(function (r) { return r.ok ? r.json() : null; })
.catch(function () { return null; });
}
- /**
- * 请求纯文本,失败返回 null。
- * @param {string} url
- * @param {object} [options] fetch 选项
- * @returns {Promise<string|null>}
- */
+ /** 请求纯文本;网络失败时返回 null。 */
function fetchText(url, options) {
return fetch(url, options || {})
.then(function (r) { return r.ok ? r.text() : null; })
@@ -201,23 +164,18 @@
/** 解码 HTML 实体(search.json 的 title 经过 Liquid escape 过滤器处理)。 */
function unescapeHTML(str) {
if (!str || str.indexOf('&') === -1) return str || '';
- var el = document.createElement('textarea');
+ if (!doc || !doc.createElement) return str;
+ var el = doc.createElement('textarea');
el.innerHTML = str;
return el.value;
}
/* =====================================================================
- * 4. 数据层
+ * §4. 数据层
* ===================================================================== */
var _articles = null;
- /**
- * 规范化 search.json 的一条记录。
- * @param {object} item search.json 原始项
- * @param {number} index 从 0 开始的下标
- * @returns {{num:number,title:string,url:string,date:string,category:string,tags:string[],content:string,excerpt:string,link:string}}
- */
function normalize(item, index) {
var content = item.content || '';
var tags = (item.tags || '')
@@ -238,43 +196,28 @@
};
}
- /**
- * 取得全部文章(含缓存)。
- * 注意:search.json 由 Jekyll 生成时已排除 layout 为 encrypt 的加密文章,
- * 因此这里的序号 num 是「非加密文章」的序号。
- * @param {boolean} [force] 传 true 强制重新拉取
- * @returns {Promise<Array>} 规范化后的文章数组,最新的在前
- */
function getArticles(force) {
if (_articles && !force) return Promise.resolve(_articles);
return loadSearchJSON().then(function (data) {
- _articles = (data || []).map(normalize);
+ if (!data) return null;
+ _articles = data.map(normalize);
return _articles;
});
}
- /**
- * 把各种形式的标识解析为一篇文章。
- * @param {number|string} id 序号(1 起) / 文章 URL / 标题(支持模糊包含匹配)
- * @returns {Promise<object|null>} 命中的文章对象,未命中为 null
- */
function resolve(id) {
return getArticles().then(function (list) {
if (!list || !list.length) return null;
if (id === undefined || id === null || id === '') {
- // 无参时默认取当前页面对应的文章
- return matchByPath(list, global.location.pathname);
+ return matchByPath(list, global.location && global.location.pathname);
}
- // 1) 纯数字序号
var n = parseInt(id, 10);
if (!isNaN(n) && String(n) === String(id).trim()) {
return (n >= 1 && n <= list.length) ? list[n - 1] : null;
}
var s = String(id).trim();
- // 2) 精确 URL / 路径
var byUrl = matchByPath(list, s);
if (byUrl) return byUrl;
- // 3) 标题包含匹配(大小写不敏感)
var low = s.toLowerCase();
var hit = list.filter(function (a) {
return a.title.toLowerCase().indexOf(low) !== -1;
@@ -283,7 +226,6 @@
});
}
- /** 按路径匹配文章(容忍 URL 编码差异与站点前缀)。 */
function matchByPath(list, path) {
if (!path) return null;
var dec = path, p = path;
@@ -296,11 +238,6 @@
return null;
}
- /**
- * 由文章对象推导其原始 Markdown 文件的 raw 地址。
- * @param {object} article
- * @returns {string} raw.githubusercontent.com 上的 .md 地址
- */
function rawUrlOf(article) {
var dateDash = (article.date || '').replace(/\//g, '-');
var last = (article.url || '').split('/').pop();
@@ -309,16 +246,246 @@
return 'https://raw.githubusercontent.com/Mabbs/mabbs.github.io/refs/heads/master/_posts/' + dateDash + '-' + slug + '.md';
}
+ function brief(a, extra) {
+ if (!a) return null;
+ var o = {
+ num: a.num,
+ title: a.title,
+ date: a.date,
+ url: a.url,
+ category: a.category || '',
+ tags: a.tags.slice(),
+ excerpt: a.excerpt,
+ wordCount: a.content.length
+ };
+ if (extra) for (var k in extra) if (extra.hasOwnProperty.call(extra, k)) o[k] = extra[k];
+ return o;
+ }
+
+ function err(code, message, extra) {
+ var r = { ok: false, error: { code: code, message: message } };
+ if (extra) for (var k in extra) if (extra.hasOwnProperty.call(extra, k)) r[k] = extra[k];
+ return r;
+ }
+
+ /** 字符串/正则 → 带 g 标志的 RegExp。 */
+ function toRegExp(pattern, opts) {
+ opts = opts || {};
+ if (pattern instanceof RegExp) {
+ var f = pattern.flags.indexOf('g') === -1 ? pattern.flags + 'g' : pattern.flags;
+ return new RegExp(pattern.source, f);
+ }
+ var src = String(pattern);
+ var flags = opts.flags || 'gi';
+ if (flags.indexOf('g') === -1) flags += 'g';
+ if (!opts.regex) src = src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return new RegExp(src, flags);
+ }
+
+ /** 去掉 Markdown 的 YAML front matter。 */
+ function stripFrontMatter(md) {
+ var lines = md.split('\n');
+ if (!/^---\s*$/.test(lines[0])) return md;
+ for (var i = 1; i < lines.length; i++) {
+ if (/^---\s*$/.test(lines[i])) return lines.slice(i + 1).join('\n').replace(/^\n+/, '');
+ }
+ return md;
+ }
+
+ function clamp(v, min, max, dflt) {
+ var n = parseInt(v, 10);
+ if (isNaN(n)) return dflt;
+ return Math.min(max, Math.max(min, n));
+ }
+
+ /** 子序列模糊匹配(fuzzy 搜索用)。 */
+ function fuzzyMatch(needle, hay) {
+ var i = 0;
+ for (var j = 0; j < hay.length && i < needle.length; j++) {
+ if (hay[j] === needle[i]) i++;
+ }
+ return i === needle.length;
+ }
+
/* =====================================================================
- * 5. Markdown → 控制台样式渲染
+ * §5. 服务层 —— 无副作用的业务逻辑
+ *
+ * 每个方法都返回 { ok: true, ... } 或 { ok:false, error:{code,message} },
+ * 不打印任何东西。控制台层负责渲染,WebMCP 层负责序列化。
+ * 预期内的失败以结构化对象返回,让调用方(Agent/控制台)能自行纠错;
+ * 非预期异常(如宿主对象缺失)自然抛出,不再被吞掉或回退。
+ * ===================================================================== */
+
+ var Service = {};
+
+ Service.list = function (page, pageSize) {
+ page = parseInt(page, 10) || 1;
+ pageSize = clamp(pageSize, 1, config.maxPageSize, config.pageSize);
+ if (page < 1) return Promise.resolve(err('BAD_INPUT', '页码必须是正整数'));
+
+ return getArticles().then(function (list) {
+ if (!list) return err('INDEX_UNAVAILABLE', '无法获取文章列表');
+ var total = list.length;
+ var totalPages = Math.ceil(total / pageSize) || 1;
+ var start = (page - 1) * pageSize;
+ if (start >= total) {
+ return err('OUT_OF_RANGE', '第 ' + page + ' 页没有文章(共 ' + totalPages + ' 页)',
+ { page: page, totalPages: totalPages, total: total });
+ }
+ return {
+ ok: true,
+ page: page,
+ pageSize: pageSize,
+ total: total,
+ totalPages: totalPages,
+ posts: list.slice(start, Math.min(start + pageSize, total))
+ };
+ });
+ };
+
+ Service.get = function (id) {
+ return resolve(id).then(function (a) {
+ if (!a) return err('NOT_FOUND', '未找到文章: ' + (id === undefined ? '(当前页面)' : id));
+ return { ok: true, post: a };
+ });
+ };
+
+ /** 搜索文章:标题/分类/标签/正文的大小写不敏感包含匹配,fuzzy 启用子序列匹配。 */
+ Service.search = function (keyword, opts) {
+ opts = opts || {};
+ var limit = clamp(opts.limit, 1, config.maxPageSize, 10);
+ var fuzzy = !!opts.fuzzy;
+ if (!keyword) return Promise.resolve(err('BAD_INPUT', '请提供关键词'));
+
+ return getArticles().then(function (list) {
+ if (!list) return err('INDEX_UNAVAILABLE', '无法获取文章列表');
+ var low = String(keyword).toLowerCase();
+ var out = list.filter(function (a) {
+ var hay = (a.title + ' ' + a.tags.join(' ') + ' ' + a.category + ' ' + a.content).toLowerCase();
+ return fuzzy ? fuzzyMatch(low, hay) : hay.indexOf(low) !== -1;
+ }).slice(0, limit);
+ return { ok: true, keyword: keyword, fuzzy: fuzzy, count: out.length, posts: out };
+ });
+ };
+
+ /** 正则全文检索(search.json 已内联全文,无需额外请求)。 */
+ Service.grep = function (pattern, opts) {
+ if (!pattern) return Promise.resolve(err('BAD_INPUT', '请提供检索内容'));
+ opts = opts || {};
+ var ctx = clamp(opts.context, 0, 400, 40);
+ var limit = clamp(opts.limit, 1, config.maxPageSize, 20);
+ var re;
+ try {
+ re = toRegExp(pattern, opts);
+ } catch (e) {
+ return Promise.resolve(err('BAD_PATTERN', '无效的正则表达式: ' + e.message));
+ }
+
+ return getArticles().then(function (list) {
+ if (!list) return err('INDEX_UNAVAILABLE', '无法获取文章列表');
+ var out = [], t0 = Date.now(), timedOut = false;
+ for (var i = 0; i < list.length && out.length < limit; i++) {
+ if (Date.now() - t0 > config.grepTimeBudget) { timedOut = true; break; }
+ var a = list[i], m, hits = [];
+ re.lastIndex = 0;
+ while ((m = re.exec(a.content)) !== null && hits.length < 3) {
+ var s = Math.max(0, m.index - ctx);
+ var e = Math.min(a.content.length, m.index + m[0].length + ctx);
+ hits.push((s > 0 ? '…' : '') + a.content.slice(s, e) + (e < a.content.length ? '…' : ''));
+ if (m[0] === '') re.lastIndex++;
+ }
+ if (hits.length) out.push({ article: a, matches: hits });
+ }
+ return { ok: true, pattern: String(re), regex: re, timedOut: timedOut, hits: out };
+ });
+ };
+
+ Service.read = function (id) {
+ return resolve(id).then(function (a) {
+ if (!a) return err('NOT_FOUND', '未找到文章: ' + (id === undefined ? '(当前页面)' : id));
+ var url = rawUrlOf(a);
+ return fetchText(url).then(function (md) {
+ if (md === null) return err('FETCH_FAILED', '无法获取原始 Markdown:' + url, { source: url });
+ return { ok: true, post: a, source: url, markdown: md };
+ });
+ });
+ };
+
+ Service.comments = function (id) {
+ return resolve(id).then(function (a) {
+ if (!a) return err('NOT_FOUND', '未找到文章: ' + (id === undefined ? '(当前页面)' : id));
+ var auth = githubAuth();
+ var label = a.url.replace(/\.html$/, '');
+ var api = 'https://api.github.com/repos/' + auth.owner + '/' + auth.repo +
+ '/issues?labels=' + encodeURIComponent('Gitalk,' + label);
+
+ return fetchJSON(api, { headers: auth.headers }).then(function (issues) {
+ if (!issues || !issues.length) {
+ return { ok: true, post: a, issueUrl: null, comments: [], reason: 'NO_ISSUE' };
+ }
+ return fetchJSON(issues[0].comments_url, { headers: auth.headers }).then(function (cs) {
+ var out = (cs || []).map(function (c) {
+ return {
+ author: (c.user && c.user.login) || 'unknown',
+ date: c.created_at || '',
+ body: c.body || ''
+ };
+ });
+ return {
+ ok: true, post: a, issueUrl: issues[0].html_url,
+ comments: out, reason: out.length ? null : 'NO_COMMENT'
+ };
+ });
+ });
+ });
+ };
+
+ Service.open = function (id, opts) {
+ opts = opts || {};
+ return resolve(id).then(function (a) {
+ if (!a) return err('NOT_FOUND', '未找到文章: ' + (id === undefined ? '(当前页面)' : id));
+ if (opts.newTab) {
+ var w = global.open ? global.open(a.link, '_blank', 'noopener,noreferrer') : null;
+ if (!w) return err('POPUP_BLOCKED', '浏览器阻止了弹出窗口,请允许后重试', { post: a });
+ return { ok: true, post: a, navigated: true, method: 'newTab' };
+ }
+ var method = go(a.url);
+ if (method === 'none') return err('NAVIGATION_UNAVAILABLE', '当前环境无法执行跳转', { post: a });
+ return { ok: true, post: a, navigated: true, method: method };
+ });
+ };
+
+ Service.random = function (opts) {
+ opts = opts || {};
+ var willOpen = opts.open !== false;
+ return getArticles().then(function (list) {
+ if (!list || !list.length) return err('INDEX_UNAVAILABLE', '无法获取文章列表');
+ var a = list[Math.floor(Math.random() * list.length)];
+ var method = willOpen ? go(a.url) : null;
+ return { ok: true, post: a, navigated: willOpen && method !== 'none', method: method };
+ });
+ };
+
+ Service.current = function () {
+ var path = (global.location && global.location.pathname) || '';
+ return getArticles().then(function (list) {
+ var a = list ? matchByPath(list, path) : null;
+ if (!a) return err('NOT_A_POST', '当前页面不是文章页:' + path, { path: path });
+ return { ok: true, post: a, path: path };
+ });
+ };
+
+ Service.about = function () {
+ return fetchText('/humans.txt').then(function (t) {
+ if (t === null) return err('FETCH_FAILED', '无法获取 humans.txt');
+ return { ok: true, url: '/humans.txt', text: t };
+ });
+ };
+
+ /* =====================================================================
+ * §6. Markdown → 控制台样式渲染
* ===================================================================== */
- /**
- * 行内标记解析:`code`、**bold**、*italic*、~~del~~、[text](url)、![alt](url)
- * @param {Printer} p
- * @param {string} text
- * @param {string} baseCss 该行的基础样式
- */
function inline(p, text, baseCss) {
var re = /(`[^`]+`)|(\*\*[^*]+\*\*|__[^_]+__)|(\*[^*\n]+\*|_[^_\n]+_)|(~~[^~]+~~)|(!?\[[^\]]*\]\([^)]*\))/g;
var last = 0, m;
@@ -349,13 +516,6 @@
p.push('\n', '');
}
- /**
- * 渲染整篇 Markdown 到控制台。
- * @param {string} md Markdown 原文
- * @param {object} [opts]
- * @param {number} [opts.maxLines] 最大渲染行数
- * @param {boolean} [opts.frontMatter=true] 是否显示 YAML front matter
- */
function renderMarkdown(md, opts) {
opts = opts || {};
var maxLines = opts.maxLines || config.maxShowLines;
@@ -369,7 +529,6 @@
var raw = lines[i];
var t = raw.replace(/^\s+/, '');
- // YAML front matter
if (i === 0 && /^---\s*$/.test(t)) { inFM = true; if (showFM) p.line(raw, S.dim); rendered++; continue; }
if (inFM) {
if (/^---\s*$/.test(t)) inFM = false;
@@ -377,35 +536,23 @@
continue;
}
- // 围栏代码块
if (/^```/.test(t) || /^~~~/.test(t)) {
- inCode = !inCode;
- p.line(raw, S.dim);
- rendered++;
- continue;
+ inCode = !inCode; p.line(raw, S.dim); rendered++; continue;
}
if (inCode) { p.line(raw, S.code); rendered++; continue; }
if (t === '') { p.line(''); rendered++; continue; }
- // 标题
var h = t.match(/^(#{1,6})\s+/);
- if (h) {
- p.line(raw, S.h[h[1].length] || S.h[6]);
- rendered++;
- continue;
- }
+ if (h) { p.line(raw, S.h[h[1].length] || S.h[6]); rendered++; continue; }
- // 水平分割线
var hr = t.replace(/\s/g, '');
if (/^-{3,}$/.test(hr) || /^\*{3,}$/.test(hr) || /^_{3,}$/.test(hr)) {
p.line(raw, S.hr); rendered++; continue;
}
- // 引用
if (/^>/.test(t)) { p.line(raw, S.quote); rendered++; continue; }
- // 无序列表
if (/^[-*+]\s/.test(t)) {
var ind = raw.match(/^(\s*)/)[1];
p.push(ind + t[0] + ' ', S.ok);
@@ -415,7 +562,6 @@
continue;
}
- // 有序列表
if (/^\d+\.\s/.test(t)) {
var ind2 = raw.match(/^(\s*)/)[1];
var mk = t.match(/^\d+\./)[0];
@@ -438,26 +584,22 @@
}
/* =====================================================================
- * 6. 对外 API
+ * §7. 控制台 API(window.Blog.*)
+ *
+ * 全部保留原有签名与返回值,内部改为调用服务层。
* ===================================================================== */
var Blog = {};
- /** 运行期配置对象,可直接修改 @type {object} */
Blog.config = config;
+ Blog.service = Service;
// ---------------------------------------------------------------- 数据
- /**
- * 按序号 / URL / 标题定位一篇文章并打印其元信息。
- * @param {number|string} [id] 序号(从 1 开始) / 文章 URL / 标题关键字;
- * 省略时取当前页面对应的文章
- * @returns {Promise<object|null>} 文章对象,未找到返回 null
- * @example await Blog.get(1); await Blog.get('/2015/02/23/diary.html'); await Blog.get('日记')
- */
Blog.get = function (id) {
- return resolve(id).then(function (a) {
- if (!a) return fail('未找到文章: ' + id);
+ return Service.get(id).then(function (r) {
+ if (!r.ok) return fail(r.error.message);
+ var a = r.post;
banner('文章 #' + a.num);
var p = new Printer();
p.line(a.title, S.title);
@@ -473,339 +615,129 @@
});
};
- /**
- * 分页列出文章(控制台以表格呈现)。
- * @param {number} [page=1] 页码,从 1 开始
- * @param {number} [pageSize] 每页条数,默认取 Blog.config.pageSize(10)
- * @returns {Promise<Array|null>} 当前页的文章数组
- * @example await Blog.list(2)
- */
Blog.list = function (page, pageSize) {
- page = parseInt(page, 10) || 1;
- pageSize = parseInt(pageSize, 10) || config.pageSize;
- if (page < 1) return Promise.resolve(fail('页码必须是正整数'));
-
- return getArticles().then(function (list) {
- if (!list) return fail('无法获取文章列表');
- var total = list.length;
- var totalPages = Math.ceil(total / pageSize);
- var start = (page - 1) * pageSize;
- if (start >= total) return fail('第 ' + page + ' 页没有文章(共 ' + totalPages + ' 页)');
-
- var slice = list.slice(start, Math.min(start + pageSize, total));
- banner('博客文章列表 · 第 ' + page + '/' + totalPages + ' 页');
+ return Service.list(page, pageSize).then(function (r) {
+ if (!r.ok) return fail(r.error.message);
+ banner('博客文章列表 · 第 ' + r.page + '/' + r.totalPages + ' 页');
var obj = {};
- slice.forEach(function (a) {
- obj[a.num] = { '日期': a.date, '标题': a.title };
- });
-
+ r.posts.forEach(function (a) { obj[a.num] = { '日期': a.date, '标题': a.title }; });
console.table(obj);
- console.log('%c共 ' + total + ' 篇 · Blog.show(id) 读正文 · Blog.open(id) 跳转 · Blog.comment(id) 看评论', S.sub);
- return slice;
+ console.log('%c共 ' + r.total + ' 篇 · Blog.show