Initial commit: skills library

- 70 skills with code and documentation
- Add .gitignore (ignore __pycache__, output/, temp/, venv/)
- Clean up test intermediates and caches
This commit is contained in:
hmo
2026-04-26 19:27:40 +08:00
commit 04db423416
861 changed files with 210414 additions and 0 deletions
+572
View File
@@ -0,0 +1,572 @@
#!/usr/bin/env node
// CDP Proxy - 通过 HTTP API 操控用户日常 Chrome
// 要求:Chrome 已开启 --remote-debugging-port
// Node.js 22+(使用原生 WebSocket
import http from 'node:http';
import { URL } from 'node:url';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import net from 'node:net';
const PORT = parseInt(process.env.CDP_PROXY_PORT || '3456');
let ws = null;
let cmdId = 0;
const pending = new Map(); // id -> {resolve, timer}
const sessions = new Map(); // targetId -> sessionId
// --- WebSocket 兼容层 ---
let WS;
if (typeof globalThis.WebSocket !== 'undefined') {
// Node 22+ 原生 WebSocket(浏览器兼容 API
WS = globalThis.WebSocket;
} else {
// 回退到 ws 模块
try {
WS = (await import('ws')).default;
} catch {
console.error('[CDP Proxy] 错误:Node.js 版本 < 22 且未安装 ws 模块');
console.error(' 解决方案:升级到 Node.js 22+ 或执行 npm install -g ws');
process.exit(1);
}
}
// --- 自动发现 Chrome 调试端口 ---
async function discoverChromePort() {
// 1. 尝试读 DevToolsActivePort 文件
const possiblePaths = [];
const platform = os.platform();
if (platform === 'darwin') {
const home = os.homedir();
possiblePaths.push(
path.join(home, 'Library/Application Support/Google/Chrome/DevToolsActivePort'),
path.join(home, 'Library/Application Support/Google/Chrome Canary/DevToolsActivePort'),
path.join(home, 'Library/Application Support/Chromium/DevToolsActivePort'),
);
} else if (platform === 'linux') {
const home = os.homedir();
possiblePaths.push(
path.join(home, '.config/google-chrome/DevToolsActivePort'),
path.join(home, '.config/chromium/DevToolsActivePort'),
);
} else if (platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA || '';
possiblePaths.push(
path.join(localAppData, 'Google/Chrome/User Data/DevToolsActivePort'),
path.join(localAppData, 'Chromium/User Data/DevToolsActivePort'),
);
}
for (const p of possiblePaths) {
try {
const content = fs.readFileSync(p, 'utf-8').trim();
const lines = content.split('\n');
const port = parseInt(lines[0]);
if (port > 0 && port < 65536) {
const ok = await checkPort(port);
if (ok) {
// 第二行是带 UUID 的 WebSocket 路径(如 /devtools/browser/xxx-xxx
// 非显式 --remote-debugging-port 启动时,Chrome 可能只接受此路径
const wsPath = lines[1] || null;
console.log(`[CDP Proxy] 从 DevToolsActivePort 发现端口: ${port}${wsPath ? ' (带 wsPath)' : ''}`);
return { port, wsPath };
}
}
} catch { /* 文件不存在,继续 */ }
}
// 2. 扫描常用端口
const commonPorts = [9222, 9229, 9333];
for (const port of commonPorts) {
const ok = await checkPort(port);
if (ok) {
console.log(`[CDP Proxy] 扫描发现 Chrome 调试端口: ${port}`);
return { port, wsPath: null };
}
}
return null;
}
// 用 TCP 探测端口是否监听——避免 WebSocket 连接触发 Chrome 安全弹窗
// WebSocket 探测会被 Chrome 视为调试连接,弹出授权对话框)
function checkPort(port) {
return new Promise((resolve) => {
const socket = net.createConnection(port, '127.0.0.1');
const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 2000);
socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); });
socket.once('error', () => { clearTimeout(timer); resolve(false); });
});
}
function getWebSocketUrl(port, wsPath) {
if (wsPath) return `ws://127.0.0.1:${port}${wsPath}`;
return `ws://127.0.0.1:${port}/devtools/browser`;
}
// --- WebSocket 连接管理 ---
let chromePort = null;
let chromeWsPath = null;
let connectingPromise = null;
async function connect() {
if (ws && (ws.readyState === WS.OPEN || ws.readyState === 1)) return;
if (connectingPromise) return connectingPromise; // 复用进行中的连接
if (!chromePort) {
const discovered = await discoverChromePort();
if (!discovered) {
throw new Error(
'Chrome 未开启远程调试端口。请用以下方式启动 Chrome:\n' +
' macOS: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222\n' +
' Linux: google-chrome --remote-debugging-port=9222\n' +
' 或在 chrome://flags 中搜索 "remote debugging" 并启用'
);
}
chromePort = discovered.port;
chromeWsPath = discovered.wsPath;
}
const wsUrl = getWebSocketUrl(chromePort, chromeWsPath);
if (!wsUrl) throw new Error('无法获取 Chrome WebSocket URL');
return connectingPromise = new Promise((resolve, reject) => {
ws = new WS(wsUrl);
const onOpen = () => {
cleanup();
connectingPromise = null;
console.log(`[CDP Proxy] 已连接 Chrome (端口 ${chromePort})`);
resolve();
};
const onError = (e) => {
cleanup();
connectingPromise = null;
const msg = e.message || e.error?.message || '连接失败';
console.error('[CDP Proxy] 连接错误:', msg);
reject(new Error(msg));
};
const onClose = () => {
console.log('[CDP Proxy] 连接断开');
ws = null;
chromePort = null; // 重置端口缓存,下次连接重新发现
chromeWsPath = null;
sessions.clear();
};
const onMessage = (evt) => {
const data = typeof evt === 'string' ? evt : (evt.data || evt);
const msg = JSON.parse(typeof data === 'string' ? data : data.toString());
if (msg.method === 'Target.attachedToTarget') {
const { sessionId, targetInfo } = msg.params;
sessions.set(targetInfo.targetId, sessionId);
}
if (msg.id && pending.has(msg.id)) {
const { resolve, timer } = pending.get(msg.id);
clearTimeout(timer);
pending.delete(msg.id);
resolve(msg);
}
};
function cleanup() {
ws.removeEventListener?.('open', onOpen);
ws.removeEventListener?.('error', onError);
}
// 兼容 Node 原生 WebSocket 和 ws 模块的事件 API
if (ws.on) {
ws.on('open', onOpen);
ws.on('error', onError);
ws.on('close', onClose);
ws.on('message', onMessage);
} else {
ws.addEventListener('open', onOpen);
ws.addEventListener('error', onError);
ws.addEventListener('close', onClose);
ws.addEventListener('message', onMessage);
}
});
}
function sendCDP(method, params = {}, sessionId = null) {
return new Promise((resolve, reject) => {
if (!ws || (ws.readyState !== WS.OPEN && ws.readyState !== 1)) {
return reject(new Error('WebSocket 未连接'));
}
const id = ++cmdId;
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error('CDP 命令超时: ' + method));
}, 30000);
pending.set(id, { resolve, timer });
ws.send(JSON.stringify(msg));
});
}
async function ensureSession(targetId) {
if (sessions.has(targetId)) return sessions.get(targetId);
const resp = await sendCDP('Target.attachToTarget', { targetId, flatten: true });
if (resp.result?.sessionId) {
sessions.set(targetId, resp.result.sessionId);
return resp.result.sessionId;
}
throw new Error('attach 失败: ' + JSON.stringify(resp.error));
}
// --- 等待页面加载 ---
async function waitForLoad(sessionId, timeoutMs = 15000) {
// 启用 Page 域
await sendCDP('Page.enable', {}, sessionId);
return new Promise((resolve) => {
let resolved = false;
const done = (result) => {
if (resolved) return;
resolved = true;
clearTimeout(timer);
clearInterval(checkInterval);
resolve(result);
};
const timer = setTimeout(() => done('timeout'), timeoutMs);
const checkInterval = setInterval(async () => {
try {
const resp = await sendCDP('Runtime.evaluate', {
expression: 'document.readyState',
returnByValue: true,
}, sessionId);
if (resp.result?.result?.value === 'complete') {
done('complete');
}
} catch { /* 忽略 */ }
}, 500);
});
}
// --- 读取 POST body ---
async function readBody(req) {
let body = '';
for await (const chunk of req) body += chunk;
return body;
}
// --- HTTP API ---
const server = http.createServer(async (req, res) => {
const parsed = new URL(req.url, `http://localhost:${PORT}`);
const pathname = parsed.pathname;
const q = Object.fromEntries(parsed.searchParams);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
try {
// /health 不需要连接 Chrome
if (pathname === '/health') {
const connected = ws && (ws.readyState === WS.OPEN || ws.readyState === 1);
res.end(JSON.stringify({ status: 'ok', connected, sessions: sessions.size, chromePort }));
return;
}
await connect();
// GET /targets - 列出所有页面
if (pathname === '/targets') {
const resp = await sendCDP('Target.getTargets');
const pages = resp.result.targetInfos.filter(t => t.type === 'page');
res.end(JSON.stringify(pages, null, 2));
}
// GET /new?url=xxx - 创建新后台 tab
else if (pathname === '/new') {
const targetUrl = q.url || 'about:blank';
const resp = await sendCDP('Target.createTarget', { url: targetUrl, background: true });
const targetId = resp.result.targetId;
// 等待页面加载
if (targetUrl !== 'about:blank') {
try {
const sid = await ensureSession(targetId);
await waitForLoad(sid);
} catch { /* 非致命,继续 */ }
}
res.end(JSON.stringify({ targetId }));
}
// GET /close?target=xxx - 关闭 tab
else if (pathname === '/close') {
const resp = await sendCDP('Target.closeTarget', { targetId: q.target });
sessions.delete(q.target);
res.end(JSON.stringify(resp.result));
}
// GET /navigate?target=xxx&url=yyy - 导航(自动等待加载)
else if (pathname === '/navigate') {
const sid = await ensureSession(q.target);
const resp = await sendCDP('Page.navigate', { url: q.url }, sid);
// 等待页面加载完成
await waitForLoad(sid);
res.end(JSON.stringify(resp.result));
}
// GET /back?target=xxx - 后退
else if (pathname === '/back') {
const sid = await ensureSession(q.target);
await sendCDP('Runtime.evaluate', { expression: 'history.back()' }, sid);
await waitForLoad(sid);
res.end(JSON.stringify({ ok: true }));
}
// POST /eval?target=xxx - 执行 JS
else if (pathname === '/eval') {
const sid = await ensureSession(q.target);
const body = await readBody(req);
const expr = body || q.expr || 'document.title';
const resp = await sendCDP('Runtime.evaluate', {
expression: expr,
returnByValue: true,
awaitPromise: true,
}, sid);
if (resp.result?.result?.value !== undefined) {
res.end(JSON.stringify({ value: resp.result.result.value }));
} else if (resp.result?.exceptionDetails) {
res.statusCode = 400;
res.end(JSON.stringify({ error: resp.result.exceptionDetails.text }));
} else {
res.end(JSON.stringify(resp.result));
}
}
// POST /click?target=xxx - 点击(body 为 CSS 选择器)
// POST /click?target=xxx — JS 层面点击(简单快速,覆盖大多数场景)
else if (pathname === '/click') {
const sid = await ensureSession(q.target);
const selector = await readBody(req);
if (!selector) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'POST body 需要 CSS 选择器' }));
return;
}
const selectorJson = JSON.stringify(selector);
const js = `(() => {
const el = document.querySelector(${selectorJson});
if (!el) return { error: '未找到元素: ' + ${selectorJson} };
el.scrollIntoView({ block: 'center' });
el.click();
return { clicked: true, tag: el.tagName, text: (el.textContent || '').slice(0, 100) };
})()`;
const resp = await sendCDP('Runtime.evaluate', {
expression: js,
returnByValue: true,
awaitPromise: true,
}, sid);
if (resp.result?.result?.value) {
const val = resp.result.result.value;
if (val.error) {
res.statusCode = 400;
res.end(JSON.stringify(val));
} else {
res.end(JSON.stringify(val));
}
} else {
res.end(JSON.stringify(resp.result));
}
}
// POST /clickAt?target=xxx — CDP 浏览器级真实鼠标点击(算用户手势,能触发文件对话框、绕过反自动化检测)
else if (pathname === '/clickAt') {
const sid = await ensureSession(q.target);
const selector = await readBody(req);
if (!selector) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'POST body 需要 CSS 选择器' }));
return;
}
const selectorJson = JSON.stringify(selector);
const js = `(() => {
const el = document.querySelector(${selectorJson});
if (!el) return { error: '未找到元素: ' + ${selectorJson} };
el.scrollIntoView({ block: 'center' });
const rect = el.getBoundingClientRect();
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, tag: el.tagName, text: (el.textContent || '').slice(0, 100) };
})()`;
const coordResp = await sendCDP('Runtime.evaluate', {
expression: js,
returnByValue: true,
awaitPromise: true,
}, sid);
const coord = coordResp.result?.result?.value;
if (!coord || coord.error) {
res.statusCode = 400;
res.end(JSON.stringify(coord || coordResp.result));
return;
}
await sendCDP('Input.dispatchMouseEvent', {
type: 'mousePressed', x: coord.x, y: coord.y, button: 'left', clickCount: 1
}, sid);
await sendCDP('Input.dispatchMouseEvent', {
type: 'mouseReleased', x: coord.x, y: coord.y, button: 'left', clickCount: 1
}, sid);
res.end(JSON.stringify({ clicked: true, x: coord.x, y: coord.y, tag: coord.tag, text: coord.text }));
}
// POST /setFiles?target=xxx — 给 file input 设置本地文件(绕过文件对话框)
// body: JSON { "selector": "input[type=file]", "files": ["/path/to/file1.png", "/path/to/file2.png"] }
else if (pathname === '/setFiles') {
const sid = await ensureSession(q.target);
const body = JSON.parse(await readBody(req));
if (!body.selector || !body.files) {
res.statusCode = 400;
res.end(JSON.stringify({ error: '需要 selector 和 files 字段' }));
return;
}
// 获取 DOM 节点
await sendCDP('DOM.enable', {}, sid);
const doc = await sendCDP('DOM.getDocument', {}, sid);
const node = await sendCDP('DOM.querySelector', {
nodeId: doc.result.root.nodeId,
selector: body.selector
}, sid);
if (!node.result?.nodeId) {
res.statusCode = 400;
res.end(JSON.stringify({ error: '未找到元素: ' + body.selector }));
return;
}
// 设置文件
await sendCDP('DOM.setFileInputFiles', {
nodeId: node.result.nodeId,
files: body.files
}, sid);
res.end(JSON.stringify({ success: true, files: body.files.length }));
}
// GET /scroll?target=xxx&y=3000 - 滚动
else if (pathname === '/scroll') {
const sid = await ensureSession(q.target);
const y = parseInt(q.y || '3000');
const direction = q.direction || 'down'; // down | up | top | bottom
let js;
if (direction === 'top') {
js = 'window.scrollTo(0, 0); "scrolled to top"';
} else if (direction === 'bottom') {
js = 'window.scrollTo(0, document.body.scrollHeight); "scrolled to bottom"';
} else if (direction === 'up') {
js = `window.scrollBy(0, -${Math.abs(y)}); "scrolled up ${Math.abs(y)}px"`;
} else {
js = `window.scrollBy(0, ${Math.abs(y)}); "scrolled down ${Math.abs(y)}px"`;
}
const resp = await sendCDP('Runtime.evaluate', {
expression: js,
returnByValue: true,
}, sid);
// 等待懒加载触发
await new Promise(r => setTimeout(r, 800));
res.end(JSON.stringify({ value: resp.result?.result?.value }));
}
// GET /screenshot?target=xxx&file=/tmp/x.png - 截图
else if (pathname === '/screenshot') {
const sid = await ensureSession(q.target);
const format = q.format || 'png';
const resp = await sendCDP('Page.captureScreenshot', {
format,
quality: format === 'jpeg' ? 80 : undefined,
}, sid);
if (q.file) {
fs.writeFileSync(q.file, Buffer.from(resp.result.data, 'base64'));
res.end(JSON.stringify({ saved: q.file }));
} else {
res.setHeader('Content-Type', 'image/' + format);
res.end(Buffer.from(resp.result.data, 'base64'));
}
}
// GET /info?target=xxx - 获取页面信息
else if (pathname === '/info') {
const sid = await ensureSession(q.target);
const resp = await sendCDP('Runtime.evaluate', {
expression: 'JSON.stringify({title: document.title, url: location.href, ready: document.readyState})',
returnByValue: true,
}, sid);
res.end(resp.result?.result?.value || '{}');
}
else {
res.statusCode = 404;
res.end(JSON.stringify({
error: '未知端点',
endpoints: {
'/health': 'GET - 健康检查',
'/targets': 'GET - 列出所有页面 tab',
'/new?url=': 'GET - 创建新后台 tab(自动等待加载)',
'/close?target=': 'GET - 关闭 tab',
'/navigate?target=&url=': 'GET - 导航(自动等待加载)',
'/back?target=': 'GET - 后退',
'/info?target=': 'GET - 页面标题/URL/状态',
'/eval?target=': 'POST body=JS表达式 - 执行 JS',
'/click?target=': 'POST body=CSS选择器 - 点击元素',
'/scroll?target=&y=&direction=': 'GET - 滚动页面',
'/screenshot?target=&file=': 'GET - 截图',
},
}));
}
} catch (e) {
res.statusCode = 500;
res.end(JSON.stringify({ error: e.message }));
}
});
// 检查端口是否被占用
function checkPortAvailable(port) {
return new Promise((resolve) => {
const s = net.createServer();
s.once('error', () => resolve(false));
s.once('listening', () => { s.close(); resolve(true); });
s.listen(port, '127.0.0.1');
});
}
async function main() {
// 检查是否已有 proxy 在运行
const available = await checkPortAvailable(PORT);
if (!available) {
// 验证已有实例是否健康
try {
const ok = await new Promise((resolve) => {
http.get(`http://127.0.0.1:${PORT}/health`, { timeout: 2000 }, (res) => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => resolve(d.includes('"ok"')));
}).on('error', () => resolve(false));
});
if (ok) {
console.log(`[CDP Proxy] 已有实例运行在端口 ${PORT},退出`);
process.exit(0);
}
} catch { /* 端口占用但非 proxy,继续报错 */ }
console.error(`[CDP Proxy] 端口 ${PORT} 已被占用`);
process.exit(1);
}
server.listen(PORT, '127.0.0.1', () => {
console.log(`[CDP Proxy] 运行在 http://localhost:${PORT}`);
// 启动时尝试连接 Chrome(非阻塞)
connect().catch(e => console.error('[CDP Proxy] 初始连接失败:', e.message, '(将在首次请求时重试)'));
});
}
// 防止未捕获异常导致进程崩溃
process.on('uncaughtException', (e) => {
console.error('[CDP Proxy] 未捕获异常:', e.message);
});
process.on('unhandledRejection', (e) => {
console.error('[CDP Proxy] 未处理拒绝:', e?.message || e);
});
main();
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# 环境检查 + 确保 CDP Proxy 就绪
# Node.js
if command -v node &>/dev/null; then
NODE_VER=$(node --version 2>/dev/null)
NODE_MAJOR=$(echo "$NODE_VER" | sed 's/v//' | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 22 ] 2>/dev/null; then
echo "node: ok ($NODE_VER)"
else
echo "node: warn ($NODE_VER, 建议升级到 22+)"
fi
else
echo "node: missing — 请安装 Node.js 22+"
exit 1
fi
# Chrome 调试端口 — 先读 DevToolsActivePort,再回退常见端口
if ! CHROME_PORT=$(node -e "
const fs = require('fs');
const path = require('path');
const os = require('os');
const net = require('net');
function checkPort(port) {
return new Promise((resolve) => {
const socket = net.createConnection(port, '127.0.0.1');
const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 2000);
socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); });
socket.once('error', () => { clearTimeout(timer); resolve(false); });
});
}
function activePortFiles() {
const home = os.homedir();
const localAppData = process.env.LOCALAPPDATA || '';
switch (process.platform) {
case 'darwin':
return [
path.join(home, 'Library/Application Support/Google/Chrome/DevToolsActivePort'),
path.join(home, 'Library/Application Support/Google/Chrome Canary/DevToolsActivePort'),
path.join(home, 'Library/Application Support/Chromium/DevToolsActivePort'),
];
case 'linux':
return [
path.join(home, '.config/google-chrome/DevToolsActivePort'),
path.join(home, '.config/chromium/DevToolsActivePort'),
];
case 'win32':
return [
path.join(localAppData, 'Google/Chrome/User Data/DevToolsActivePort'),
path.join(localAppData, 'Chromium/User Data/DevToolsActivePort'),
];
default:
return [];
}
}
(async () => {
for (const filePath of activePortFiles()) {
try {
const lines = fs.readFileSync(filePath, 'utf8').trim().split(/\\r?\\n/).filter(Boolean);
const port = parseInt(lines[0], 10);
if (port > 0 && port < 65536 && await checkPort(port)) {
console.log(port);
process.exit(0);
}
} catch (_) {}
}
for (const port of [9222, 9229, 9333]) {
if (await checkPort(port)) {
console.log(port);
process.exit(0);
}
}
process.exit(1);
})();
" 2>/dev/null); then
echo "chrome: not connected — 请打开 chrome://inspect/#remote-debugging 并勾选 Allow remote debugging"
exit 1
fi
echo "chrome: ok (port $CHROME_PORT)"
# CDP Proxy — 用 /targets 统一判断:返回 JSON 数组即 ready,失败则启动并重试
TARGETS=$(curl -s --connect-timeout 3 "http://127.0.0.1:3456/targets" 2>/dev/null)
if echo "$TARGETS" | grep -q '^\['; then
echo "proxy: ready"
else
# /targets 失败:proxy 未运行或未连接 Chrome,尝试启动(已运行会自动跳过)
echo "proxy: connecting..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
node "$SCRIPT_DIR/cdp-proxy.mjs" > /tmp/cdp-proxy.log 2>&1 &
sleep 2 # 等 proxy 进程就绪
for i in $(seq 1 15); do
# connect-timeout 5s:给 Chrome 授权弹窗留够响应时间,避免超时后重复触发连接
curl -s --connect-timeout 5 --max-time 8 http://localhost:3456/targets 2>/dev/null | grep -q '^\[' && echo "proxy: ready" && exit 0
[ $i -eq 1 ] && echo "⚠️ Chrome 可能有授权弹窗,请点击「允许」后等待连接..."
done
echo "❌ 连接超时,请检查 Chrome 调试设置"
exit 1
fi
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# 根据用户输入匹配站点经验文件
# 用法:match-site.sh "用户输入文本"
# 输出:匹配到的站点经验内容,无匹配则静默
DIR="$(dirname "$0")/../references/site-patterns"
[ ! -d "$DIR" ] && exit 0
[ -z "$1" ] && exit 0
for f in "$DIR"/*.md; do
[ ! -f "$f" ] && continue
domain=$(basename "$f" .md)
# 提取 aliases 行,去掉 yaml 格式字符,逗号分隔转竖线
aliases=$(grep '^aliases:' "$f" | sed 's/^aliases: *//;s/\[//g;s/\]//g;s/, */|/g;s/ *$//')
# 构建匹配模式:domain|alias1|alias2
patterns="$domain"
[ -n "$aliases" ] && patterns="$patterns|$aliases"
# 匹配用户输入(不区分大小写,固定字符串用 fgrep 思路但需要多模式所以用 grep -iE)
if echo "$1" | grep -qiE "$patterns"; then
echo "--- 站点经验: $domain ---"
# 跳过 frontmatter(两个 --- 之间),输出正文
awk 'BEGIN{n=0} /^---$/{n++;next} n>=2{print}' "$f"
echo ""
fi
done