# Hugo 代码执行

> [Hugo 代码执行](https://www.ftls.xyz/posts/2026-07-01-hugo-exec/)
> Penned by [恐咖兵糖](https://www.ftls.xyz/) on 2026-07-01


一个 Hugo 构建阶段执行各种代码的小方法。

<!--more-->

## 缘起

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 和时间戳击穿缓存。返回结果直接展示，没有什么额外的步骤。

```html
{{- $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 }}

```


js 执行器让 AI 写了两个版本

### 独立进程版本

```js
// 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");
```

### 复用进程版本

还可以搞成 VM 版本

```js
// 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');
}

```

## 总结

这个小玩意大概不会出现在我博客的构建流程中。但是不得不说，如果想实现花里胡哨的功能，这确实是一个方法。比如：

- 服务端程序使用 NPM 各种包生成一堆 HTML 插入到 Hugo 中
- 调用各种二进制文件处理点什么
- 使用 AI 生成摘要，放在可以被 Hugo 使用的 data 文件夹。在构建阶段检测没有摘要就调用 AI 生成
- 生成文章的高频词，自动生成标签，用于 RAG 的嵌入向量，文章推荐等等
- 生成文章封面，程序自动修改文章封面 URL
