/ Update
4 min
No categories available
No tags available
Hugo 代码执行
Without further ado
val views
|
comments
一个 Hugo 构建阶段执行各种代码的小方法。
缘起
Life finds a way.
这句话用来形容 AI 沙箱权限逃逸,倒也挺合适的。最近我也遇到了一个类似的情况。
我在查阅文档的时候,偶然发现了 Hugo 有个配置项,是对二进制名单限制,还以为能执行任意二进制了。但查阅相关资料和尝试之后才发现,并不是那么一回事。那个名单是只能删,不能增的一个名单。或者说增加的可执行二进制文件,没有调用的方法。
不过我在向 Deepseek 提问的时候,AI 向我提供了一个方法,简单来说,就是搭建一个 RPC 服务,Hugo 的构建阶段使用 HTTP 请求。
这倒是一个我没想到方法,我之前的思路就是在 markdown 文件上进行操作,解析 markdown 文件,然后读取修改数据等。很繁琐,很灵活,拓展性很强。相对来说,这套 RPC 方案倒是很好上手。能玩出的花样也不少。
实现
服务端使用 Bun,提供 js 执行器。
加一个 render-codeblock-x.html ,将要执行的代码放在 markdown 的代码块里,然后构建阶段发送 HTTP 请求,获取 js 执行后的标准输出内容并处理。
下面这段代码实现了一个简单的 HTTP 请求发送器,使用了 Hugo 的缓存机制,避免重复执行相同的代码。并且使用 MD5 和时间戳击穿缓存。返回结果直接展示,没有什么额外的步骤。
{{- $code := trim .Inner "\n\r " -}}
{{- $url := "http://localhost:3333/run" -}}
{{ $cacheKey := print $url (now.Format "2006-01-02-150405") }}
{{- $opts := dict
"method" "POST"
"body" $code
"headers" (dict "Content-Type" "text/plain")
"maxAge" "0s"
"key" $cacheKey
-}}
{{ with try (resources.GetRemote $url $opts) }}
{{ with .Err }}
{{ errorf "❌ 无法连接代码执行服务 (http://localhost:3333)\n%s" . }}
{{ else with .Value }}
<div class="astro-code astro-code-themes chroma" style="overflow-x: auto;" data-language="x">
<pre style="padding: 1rem" class="border">{{- $code -}}</pre>
<pre style="padding: 1rem" >{{- .Content -}}</pre>
</div>
{{ else }}
{{ errorf "Unable to get remote resource %q" $url }}
{{ end }}
{{ end }}html
js 执行器让 AI 写了两个版本
独立进程版本
// scripts/run-js-server.js
Bun.serve({
port: 3333,
async fetch(req) {
const url = new URL(req.url);
console.log("Run ", req.url);
// 只接受 /run 的 POST 请求
if (url.pathname !== "/run" || req.method !== "POST") {
return new Response("Not found", { status: 404 });
}
try {
const code = await req.text();
console.log(code+"\n------------------\n");
const proc = Bun.spawn(["bun", "-e", code], {
stdout: "pipe",
stderr: "pipe",
});
const output = await new Response(proc.stdout).text();
const errOutput = await new Response(proc.stderr).text();
await proc.exited;
// 无论成功或失败,都返回 200,把完整信息放在响应体里
let body;
if (proc.exitCode === 0) {
body = output;
} else {
body = `【执行失败,退出码: ${proc.exitCode}】\n\n标准输出:\n${output}\n\n错误输出:\n${errOutput}`;
}
return new Response(body, { status: 200 });
} catch (e) {
return new Response(`Bun 服务内部错误: ${e.message}`, { status: 200 });
}
},
});
console.log("Bun JS runner on http://localhost:3333");js
复用进程版本
还可以搞成 VM 版本
// scripts/run-js-server.js
import { spawn } from 'bun';
// 启动持久化 worker 子进程
const worker = spawn({
cmd: ['bun', 'run', 'scripts/js-worker.js'],
stdin: 'pipe',
stdout: 'pipe',
stderr: 'inherit', // worker 自身错误直接输出到控制台
});
const pending = new Map(); // id -> { resolve, reject }
let nextId = 1;
// 处理 worker 输出的每一行 JSON 响应
(async () => {
const reader = worker.stdout.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按行分割
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 最后一个可能不完整,保留
for (const line of lines) {
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
const { id, result, error } = response;
if (pending.has(id)) {
const { resolve } = pending.get(id);
pending.delete(id);
resolve({ result, error });
}
} catch (e) {
console.error('无法解析 worker 输出:', line);
}
}
}
})();
// 向 worker 发送代码并等待结果
function executeCode(code) {
return new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
// 发送 JSON 命令
const request = JSON.stringify({ id, code }) + '\n';
console.log(request);
worker.stdin.write(request);
// 设置超时(可选)
setTimeout(() => {
if (pending.has(id)) {
pending.delete(id);
reject(new Error('执行超时'));
}
}, 10000); // 10秒超时
});
}
// HTTP 服务
Bun.serve({
port: 3333,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname !== '/run' || req.method !== 'POST') {
return new Response('Not found', { status: 404 });
}
try {
const code = await req.text();
const { result, error } = await executeCode(code);
if (error) {
return new Response(`执行错误: ${error}\n\n${result}`, { status: 200 });
}
return new Response(result, { status: 200 });
} catch (e) {
return new Response(`Bun 服务内部错误: ${e.message}`, { status: 200 });
}
},
});
console.log('Bun JS runner (复用进程模式) 已在 http://localhost:3333 启动');js
// scripts/js-worker.js
// 从 stdin 逐行读取 JSON 命令:{ "id": 唯一标识, "code": "要执行的 JS 代码" }
// 执行后向 stdout 输出 JSON 结果:{ "id": 相同标识, "result": 标准输出, "error": 错误信息 }
for await (const line of console) {
let request;
try {
request = JSON.parse(line);
} catch {
// 忽略非法 JSON
continue;
}
const { id, code } = request;
if (!id || typeof code !== 'string') continue;
let result = '';
let error = null;
// 捕获 console.log 输出
const originalLog = console.log;
const logs = [];
console.log = (...args) => {
logs.push(args.map(String).join(' '));
};
try {
// 使用 new Function 执行,可以访问全局作用域,但更安全一点
// 也可用 eval,但 new Function 不直接访问局部变量
const fn = new Function(code);
const returnValue = fn();
// 如果有返回值且不是 undefined,也加入到输出
if (returnValue !== undefined) {
logs.push(String(returnValue));
}
result = logs.join('\n');
} catch (e) {
error = e instanceof Error ? e.message : String(e);
result = logs.join('\n');
} finally {
console.log = originalLog;
}
// 返回 JSON 响应
process.stdout.write(JSON.stringify({ id, result, error }) + '\n');
}js
总结
这个小玩意大概不会出现在我博客的构建流程中。但是不得不说,如果想实现花里胡哨的功能,这确实是一个方法。比如:
- 服务端程序使用 NPM 各种包生成一堆 HTML 插入到 Hugo 中
- 调用各种二进制文件处理点什么
- 使用 AI 生成摘要,放在可以被 Hugo 使用的 data 文件夹。在构建阶段检测没有摘要就调用 AI 生成
- 生成文章的高频词,自动生成标签,用于 RAG 的嵌入向量,文章推荐等等
- 生成文章封面,程序自动修改文章封面 URL