[GH-ISSUE #354] 自荐 bun-bot,一个自我认知为 **Bun.js** 运行时的 agent,给大模型最基础的能力,让它自己探索环境改造世界 #115

Closed
opened 2026-10-26 09:45:38 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @BoltDoggy on GitHub (Aug 6, 2026).
Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-agent/issues/354

代码仓库: https://github.com/BoltDoggy/bun-bot
设计理念博客: https://juejin.cn/post/7670016345690832947

一句话介绍: 给大模型最基础的能力,让它自己探索环境改造世界
起步原理:

  1. 使用 bunjs 直接调用 deepseek chat api,3 行代码完成模型调用
  2. 让它认知自己就是 bunjs,只提供一个 function call tools: run_script,使用 bunjs 调用模型输出的 js 脚本,而无需提供读写文件、shell 调用、网络请求等 tools
  3. 不提供 TUI,而是通过 shell 调用,基于目录的记忆,每次运行命令输入的提示词类似在 tui 中的对话

构建产物内置 bunjs 运行时,因此用户无 bunjs 环境也能安全运行


你可以提出你的挑战,我会用最少的代码来实现支持

案例一
这里提供一个运行案例,以下是 commit 71b9c34 在 shell 中运行输出的所有日志。

❯ bun run index.ts "你如何控制浏览器"

--- [run_script] ---
import { readdirSync, existsSync, readFileSync } from "fs";
// 读取记忆
const memPath = ".bunbot/memory.md";
if (existsSync(memPath)) {
  console.log("=== memory.md ===");
  console.log(readFileSync(memPath, "utf8"));
} else {
  console.log("(memory.md 不存在)");
}
// 目录树
console.log("\n=== 目录结构 ===");
function tree(dir, prefix = "") {
  for (const name of readdirSync(dir, { withFileTypes: true })) {
    if (name.name.startsWith("node_modules") || name.name.startsWith(".git")) continue;
    console.log(prefix + name.name + (name.isDirectory() ? "/" : ""));
    if (name.isDirectory()) tree(dir + "/" + name.name, prefix + "  ");
  }
}
tree(".");
--------------------
{"stdout":"=== memory.md ===\n### 2025-06-27 拆分 AGENTS.md 与 BUN_BOT.md\n- 目标:让 AGENTS.md 保持通用(可被其他 AGENT 复用),新增 BUN_BOT.md 承载 run_script 相关约定,并让 index.ts 同时加载两个文件。\n- 结论:已完成。AGENTS.md 仅保留项目介绍/运行方式/工程约定;BUN_BOT.md 含 run_script 工具说明与记忆协议;index.ts 的 loadAgentsContext 泛化为 loadContextFile,依次加载 AGENTS.md + BUN_BOT.md 注入系统提示词。\n- 经验/教训:加载顺序为 AGENTS.md(通用)在前、BUN_BOT.md(专属)在后,后者可补充/覆盖前者;交叉引用说明可保留在 AGENTS.md,不算约定内容。\n- 待办/下一步:无\n### 2025-06-27 补充\"操作顺序\"约定\n- 目标:把\"写文件前必须先读文件,读文件前必须先了解目录树\"固化到 BUN_BOT.md。\n- 结论:已在 BUN_BOT.md 中\"## 工具\"之前插入\"## 操作顺序\"章节,含两条规则。\n- 经验/教训:BUN_BOT.md 会被注入系统提示词,操作顺序类约定放这里对代理最有效;插入章节时先检查是否已存在,避免重复。\n- 待办/下一步:无\n\n\n### 2025-06-27 查看 skill-template 技能\n- 目标:查看 .agents/skills 技能清单并总结 skill-template 的用途与目录结构。\n- 结论:技能清单仅 skill-template(目录型);用途是指导创建新技能/了解技能格式;规定两种形态:目录型(<技能名>/SKILL.md + 可选 scripts/)与单文件型(<技能名>.md);SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写详细步骤。\n- 经验/教训:技能是\"按需加载\",清单已注入系统提示词,全文才需要 run_script 读取。\n- 待办/下一步:无\n\n\n### 2025-06-27 实现对 .agents/skills 技能支持\n- 目标:让 bun-bot 启动时扫描 .agents/skills/ 并把技能清单注入系统提示词。\n- 结论:已完成。新增 skills.ts(loadSkills/parseFrontmatter);index.ts 在 AGENTS.md+BUN_BOT.md 之后拼接技能清单;BUN_BOT.md 增加\"技能\"章节;附带示例技能 .agents/skills/skill-template/SKILL.md;端到端验证 agent 会按需读取 SKILL.md。\n- 经验/教训:①Bun 1.3.14 无 Bun.readdir/Bun.glob,扫描目录用 new Bun.Glob(\"**/*.md\").scanSync({cwd}),目录不存在抛 ENOENT 需 try/catch;②frontmatter 用 Bun.YAML.parse,坏 YAML 抛错需兜底;③只注入技能清单、全文由 agent 按需 run_script 读取,避免撑爆上下文;④run_script 在 tmpdir 运行,import 项目模块须用绝对路径 process.cwd() 拼接。\n- 待办/下一步:无\n\n\n\n### 2026-08-06 .bunbot 加入 .gitignore\n- 目标:让 .bunbot 记忆目录不被 git 跟踪。\n- 结论:已在 .gitignore 末尾追加 \"# bun-bot memory\" + \".bunbot/\";git check-ignore 验证生效,git status 无相关条目。\n- 经验/教训:.bunbot 含 memory.md 与 .keep,均为本地记忆状态不应提交;因从未被 git 跟踪,仅加 gitignore 即可,无需 git rm --cached。\n- 待办/下一步:无\n\n### 2026-08-06 查看 skill-template 技能(第二次)\n- 目标:用户触发 /skill-template,查看技能用途与格式。\n- 结论:skill-template 是目录型技能,仅含 SKILL.md,用于指导创建新技能或了解 .agents/skills 规范;规定两种形态(目录型/单文件型),SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写具体步骤。\n- 经验/教训:触发技能时若用户无额外子任务,先展示技能用途与目录规范即可,无需改动任何文件。\n- 待办/下一步:无\n### 2026-08-06 提交拆分 AGENTS.md/BUN_BOT.md 与技能支持\n- 目标:把暂存的 6 个文件改动提交到 git。\n- 结论:已提交 d597e5a(feat: 拆分 AGENTS.md/BUN_BOT.md,支持 .agents/skills 技能按需加载),6 files changed, 167 insertions(+), 15 deletions(-)。\n- 经验/教训:①Bun 1.3.14 的 Bun.spawn 无 .output() 方法,用 Bun.spawnSync 并 decode stdout/stderr;②提交前先看 git status/diff --cached,commit message 沿用 conventional commits 风格(feat:)。\n- 待办/下一步:无\n\n### 2026-08-06 约定:/skill-name 开头的指令 = 执行该技能\n- 目标:明确用户指令以 /skill-name 开头时的含义。\n- 结论:以 /skill-name 开头的用户指令,是要求**执行**该技能(按其 SKILL.md 的指令完成实际任务),不是只读技能定义/展示用途;本条修正此前\"触发技能时仅展示用途\"的记录。\n- 经验/教训:用户说 /xxx 时按\"执行动作\"理解,先 run_script 读取对应技能文件全文,再按其指令落地任务。\n- 待办/下一步:无\n\n### 2026-08-06 把 /技能名 指令语义固化进 BUN_BOT.md\n- 目标:让\"用户指令以 /技能名 开头 = 执行该技能\"这条约定不只存在于记忆里,而是注入系统提示词长期生效。\n- 结论:已在 BUN_BOT.md 的\"技能(.agents/skills)\"章节新增一条:/技能名 开头表示要求执行该技能(先读 SKILL.md 全文再落地任务),不得只展示用途/格式。\n- 经验/教训:行为类约定应优先固化到 BUN_BOT.md(会被注入系统提示词),记忆只是临时载体;写入前先读目标文件确认无重复条目(用 includes 检查锚点)。\n- 待办/下一步:无\n\n### 2026-08-06 触发 /skill-template(用户未指定新技能)\n- 目标:执行 skill-template 技能,查看当前技能目录与 SKILL.md 规范。\n- 结论:.agents/skills 下仅 skill-template(目录型,SKILL.md 27 行);已向用户展示两种形态与 SKILL.md 三条写法规范(frontmatter name+description、description 写触发场景、正文中文详细步骤);用户未指定新技能名,未改动任何文件,等待用户给出技能名与用途。\n- 经验/教训:触发 /skill-template 但无子任务时,用 run_script 实际确认目录结构后展示规范并询问创建目标即可,不擅自造文件。\n- 待办/下一步:待用户提供新技能名称与用途后按其落地 SKILL.md。\n\n### 2026-08-06 全局安装 bun-bot(bun link 可在任意目录使用)\n- 目标:把 bun-bot 全局安装,使其他目录也能直接调用。\n- 结论:①index.ts 加 shebang `#!/usr/bin/env bun` 并新增 `ROOT = dirname(import.meta.path)`,loadContextFile/loadSkills 均基于 ROOT 定位项目文件,摆脱 cwd 依赖;②skills.ts 的 loadSkills 接受 root 参数,技能 path 输出绝对路径;③index.ts 新增 loadEnvFromRoot():cwd 无 .env 时兜底读项目根 .env(不覆盖已有环境变量);④package.json 加 `\"bin\": {\"bun-bot\": \"./index.ts\"}`;⑤执行 `bun link` 注册全局命令(符号链接到 ~/.bun/bin/bun-bot,改代码即时生效);⑥README 增加全局安装章节。\n- 经验/教训:bun link 生成的是符号链接而非复制,相对 import ./skills.ts 依旧可用;模板字符串里嵌 ``` 代码块会与反引号冲突,用数组 join 代替;从其他目录运行 `bun-bot` 无任务时输出用法提示,说明 .env 兜底已生效。\n- 待办/下一步:无\n\n### 2026-08-06 移除 skills.ts(技能加载交由 bun-bot 宿主)\n- 目\n... (截断)","stderr":"","exitCode":0}


--- [run_script] ---
import { readFileSync, statSync } from "fs";
const mem = readFileSync(".bunbot/memory.md", "utf8");
const lines = mem.split("\n");
console.log("总行数:", lines.length);
// 打印最近的部分(尾部 60 行)
console.log("=== 尾部 60 行 ===");
console.log(lines.slice(-60).join("\n"));
// 看看 index.ts 是否提到浏览器
const idx = readFileSync("index.ts", "utf8");
console.log("\n=== index.ts 中是否含 browser/cdp/playwright/puppeteer ===");
for (const kw of ["browser", "cdp", "playwright", "puppeteer", "浏览器"]) {
  console.log(kw, "=>", idx.toLowerCase().includes(kw.toLowerCase()));
}
--------------------
{"stdout":"总行数: 93\n=== 尾部 60 行 ===\n### 2026-08-06 查看 skill-template 技能(第二次)\n- 目标:用户触发 /skill-template,查看技能用途与格式。\n- 结论:skill-template 是目录型技能,仅含 SKILL.md,用于指导创建新技能或了解 .agents/skills 规范;规定两种形态(目录型/单文件型),SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写具体步骤。\n- 经验/教训:触发技能时若用户无额外子任务,先展示技能用途与目录规范即可,无需改动任何文件。\n- 待办/下一步:无\n### 2026-08-06 提交拆分 AGENTS.md/BUN_BOT.md 与技能支持\n- 目标:把暂存的 6 个文件改动提交到 git。\n- 结论:已提交 d597e5a(feat: 拆分 AGENTS.md/BUN_BOT.md,支持 .agents/skills 技能按需加载),6 files changed, 167 insertions(+), 15 deletions(-)。\n- 经验/教训:①Bun 1.3.14 的 Bun.spawn 无 .output() 方法,用 Bun.spawnSync 并 decode stdout/stderr;②提交前先看 git status/diff --cached,commit message 沿用 conventional commits 风格(feat:)。\n- 待办/下一步:无\n\n### 2026-08-06 约定:/skill-name 开头的指令 = 执行该技能\n- 目标:明确用户指令以 /skill-name 开头时的含义。\n- 结论:以 /skill-name 开头的用户指令,是要求**执行**该技能(按其 SKILL.md 的指令完成实际任务),不是只读技能定义/展示用途;本条修正此前\"触发技能时仅展示用途\"的记录。\n- 经验/教训:用户说 /xxx 时按\"执行动作\"理解,先 run_script 读取对应技能文件全文,再按其指令落地任务。\n- 待办/下一步:无\n\n### 2026-08-06 把 /技能名 指令语义固化进 BUN_BOT.md\n- 目标:让\"用户指令以 /技能名 开头 = 执行该技能\"这条约定不只存在于记忆里,而是注入系统提示词长期生效。\n- 结论:已在 BUN_BOT.md 的\"技能(.agents/skills)\"章节新增一条:/技能名 开头表示要求执行该技能(先读 SKILL.md 全文再落地任务),不得只展示用途/格式。\n- 经验/教训:行为类约定应优先固化到 BUN_BOT.md(会被注入系统提示词),记忆只是临时载体;写入前先读目标文件确认无重复条目(用 includes 检查锚点)。\n- 待办/下一步:无\n\n### 2026-08-06 触发 /skill-template(用户未指定新技能)\n- 目标:执行 skill-template 技能,查看当前技能目录与 SKILL.md 规范。\n- 结论:.agents/skills 下仅 skill-template(目录型,SKILL.md 27 行);已向用户展示两种形态与 SKILL.md 三条写法规范(frontmatter name+description、description 写触发场景、正文中文详细步骤);用户未指定新技能名,未改动任何文件,等待用户给出技能名与用途。\n- 经验/教训:触发 /skill-template 但无子任务时,用 run_script 实际确认目录结构后展示规范并询问创建目标即可,不擅自造文件。\n- 待办/下一步:待用户提供新技能名称与用途后按其落地 SKILL.md。\n\n### 2026-08-06 全局安装 bun-bot(bun link 可在任意目录使用)\n- 目标:把 bun-bot 全局安装,使其他目录也能直接调用。\n- 结论:①index.ts 加 shebang `#!/usr/bin/env bun` 并新增 `ROOT = dirname(import.meta.path)`,loadContextFile/loadSkills 均基于 ROOT 定位项目文件,摆脱 cwd 依赖;②skills.ts 的 loadSkills 接受 root 参数,技能 path 输出绝对路径;③index.ts 新增 loadEnvFromRoot():cwd 无 .env 时兜底读项目根 .env(不覆盖已有环境变量);④package.json 加 `\"bin\": {\"bun-bot\": \"./index.ts\"}`;⑤执行 `bun link` 注册全局命令(符号链接到 ~/.bun/bin/bun-bot,改代码即时生效);⑥README 增加全局安装章节。\n- 经验/教训:bun link 生成的是符号链接而非复制,相对 import ./skills.ts 依旧可用;模板字符串里嵌 ``` 代码块会与反引号冲突,用数组 join 代替;从其他目录运行 `bun-bot` 无任务时输出用法提示,说明 .env 兜底已生效。\n- 待办/下一步:无\n\n### 2026-08-06 移除 skills.ts(技能加载交由 bun-bot 宿主)\n- 目标:删除 skills.ts——\"加载 skills 是 BUN_BOT 提供的能力\",技能清单由 bun-bot 启动时扫描注入系统提示词,项目代码无需重复实现。\n- 结论:已完成。①skills.ts 删除(git 显示 D skills.ts);②index.ts 移除 loadSkills import,projectContext 改为只拼 AGENTS.md + BUN_BOT.md,注释注明技能清单由宿主注入;③README.md 同步:全局安装段第 42 行去掉 .agents/skills、项目结构删除 skills.ts 行;④bun build 编译通过、无参数运行正常(打印用法提示);⑤BUN_BOT.md 无需改动,其\"bun-bot 自动扫描\"表述本就是宿主行为。\n- 经验/教训:宿主注入的系统提示词已含\"可用技能\"清单段,项目内 skills.ts 是重复实现;删除模块后 grep 残留仅剩 BUN_BOT.md/README/技能自身等合理描述。\n- 待办/下一步:工作区仍留有上次\"全局安装\"会话的未提交改动(index.ts shebang/ROOT/loadEnvFromRoot、package.json bin、README 全局安装章节),如需提交请先确认。\n\n### 2026-08-06 移除 loadEnvFromRoot(env 交由使用方注入)\n- 目标:回应用户反馈\"env 无需从 bun-bot 源码目录读取,使用方可通过环境变量注入,该实现会污染代码库\"。\n- 结论:已删除 index.ts 中的 loadEnvFromRoot() 函数与调用(连带其注释);README 全局安装章节的\"兜底读项目根目录 .env\"改为\"由使用方 export 或在运行目录写 .env(Bun 自动加载)\"。ROOT 保留(loadContextFile 仍靠它定位 AGENTS.md/BUN_BOT.md)。\n- 经验/教训:①bun-bot 只是\"读自身项目文件\"(AGENTS/BUN_BOT)是合理的,但从自身源码目录偷偷读 .env 注入环境变量属于隐式副作用,会污染调用方环境;②.env 加载是 Bun 的宿主行为(自动加载 cwd 的 .env),项目代码不应重复实现兜底;③bun build 编译通过、无 key 报错、有 key 无任务打印用法提示,行为均符合预期。\n- 待办/下一步:工作区仍留有此前未提交改动(index.ts/README.md/package.json/BUN_BOT.md 等),如需提交请先确认。\n\n### 2026-08-06 上下文改为读取 cwd 的 AGENTS.md/BUN_BOT.md\n- 目标:回应用户反馈\"上下文也应该是实际工作目录的 AGENTS.md 和 BUN_BOT.md,不应该读取源码目录下的\"(延续此前 .env 的反馈:bun-bot 不应把源码目录文件强加给使用方)。\n- 结论:已完成。①index.ts 删除 ROOT 常量与 dirname 导入(ROOT 仅用于 loadContextFile);②loadContextFile 改为 join(process.cwd(), fileName),注释与注入标题同步改为\"当前工作目录\";③README 全局安装段第 42 行改为\"按 cwd 定位、不读源码目录同名文件\";④bun build 编译通过;⑤临时目录端到端验证:读到的是该目录的 AGENTS.md/BUN_BOT.md。\n- 经验/教训:①约定文件属于\"使用方项目\"而非 bun-bot 自身,应跟随运行目录;②改动前先 grep ROOT/dirname 确认无其他依赖;③行为变化提示:在无 BUN_BOT.md 的目录运行 bun-bot 时,记忆协议/操作顺序等专属约定不会注入(除非使用方在自己的运行目录放 BUN_BOT.md)。\n- 待办/下一步:无\n\n\n### 2026-08-06 git commit --amend(并入多会话累积改动)\n- 目标:把暂存的\n... (截断)","stderr":"","exitCode":0}


--- [run_script] ---
import { spawnSync } from "bun";

// 1. 探测系统里有哪些浏览器可执行文件
const candidates = [
  "google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
  "chrome", "firefox", "msedge", "brave-browser",
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
  "/usr/bin/google-chrome", "/snap/bin/chromium",
];
for (const c of candidates) {
  const r = spawnSync(["which", c], { stdout: "pipe", stderr: "pipe" });
  if (r.exitCode === 0) console.log("找到:", r.stdout.toString().trim());
}
// 也试试直接找
const r = spawnSync(["bash", "-lc", "command -v google-chrome chromium chromium-browser chrome 2>/dev/null; ls /Applications 2>/dev/null | grep -iE 'chrome|chromium|edge|brave'"], { stdout: "pipe", stderr: "pipe" });
console.log("\n=== bash 探测 ===");
console.log(r.stdout.toString().trim() || "(无)");
console.log("exit:", r.exitCode);

// 2. Bun 版本 & 是否支持 WebSocket(Bun 原生支持)
console.log("\nBun:", (await Bun.version) ?? process.version);
console.log("typeof WebSocket:", typeof WebSocket);
--------------------
{"stdout":"找到: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n\n=== bash 探测 ===\nGoogle Chrome.app\nexit: 0\n\nBun: 1.3.14\ntypeof WebSocket: function\n","stderr":"","exitCode":0}

环境里有 Google Chrome,Bun 原生支持 WebSocket——我来实际演示一把:通过 **CDP(Chrome DevTools Protocol)** 从零控制一个无头浏览器,不依赖任何 npm 包。

--- [run_script] ---
import { spawn } from "bun";

const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const PORT = 9333;
const userData = `/tmp/bun-cdp-${Date.now()}`;

// 1) 启动无头 Chrome,开启远程调试端口
const proc = spawn({
  cmd: [CHROME, "--headless=new", `--remote-debugging-port=${PORT}`,
        `--user-data-dir=${userData}`, "--no-first-run", "--disable-gpu", "about:blank"],
  stdout: "pipe", stderr: "pipe",
});

async function waitForPort(timeoutMs = 10000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    try {
      const res = await fetch(`http://127.0.0.1:${PORT}/json/version`);
      if (res.ok) return await res.json();
    } catch {}
    await Bun.sleep(200);
  }
  throw new Error("CDP 端口未就绪");
}

try {
  const ver = await waitForPort();
  console.log("✅ 浏览器已启动:", ver.Browser);

  // 2) 通过 CDP HTTP API 新建一个标签页
  const tab = await (await fetch(`http://127.0.0.1:${PORT}/json/new?about:blank`, { method: "PUT" })).json();
  console.log("✅ 新建页面:", tab.id, "| ws:", tab.webSocketDebuggerUrl);

  // 3) 封装 WebSocket 上的 CDP 命令
  const ws = new WebSocket(tab.webSocketDebuggerUrl);
  await new Promise((res, rej) => { ws.addEventListener("open", res); ws.addEventListener("error", rej); });

  let seq = 0;
  const pending = new Map();
  ws.addEventListener("message", (ev) => {
    const msg = JSON.parse(ev.data);
    if (msg.id && pending.has(msg.id)) {
      const { resolve, reject } = pending.get(msg.id);
      pending.delete(msg.id);
      msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
    }
  });
  const cdp = (method, params = {}) => new Promise((resolve, reject) => {
    const id = ++seq; pending.set(id, { resolve, reject });
    ws.send(JSON.stringify({ id, method, params }));
  });

  // 4) 导航到 example.com 并等它加载完
  await cdp("Page.enable");
  await cdp("Page.navigate", { url: "https://example.com" });
  for (let i = 0; i < 20; i++) {
    const r = await cdp("Runtime.evaluate", { expression: "document.readyState", returnByValue: true });
    if (r.result.value === "complete") break;
    await Bun.sleep(250);
  }
  console.log("✅ 页面加载完成,标题:", (await cdp("Runtime.evaluate", { expression: "document.title", returnByValue: true })).result.value);

  // 5) 在页面里执行 JS(读取 DOM、修改内容)
  const info = await cdp("Runtime.evaluate", {
    expression: `(() => {
      const h1 = document.querySelector("h1")?.textContent ?? "";
      document.title = "被 Bun 的 CDP 改写了";
      return { h1, links: document.querySelectorAll("a").length, now: Date.now() };
    })()`,
    returnByValue: true,
  });
  console.log("✅ 页面内 JS 执行结果:", JSON.stringify(info.result.value));

  // 6) 截图,直观证明浏览器真的被控制了
  const shot = await cdp("Page.captureScreenshot", { format: "png" });
  await Bun.write("/tmp/bun-cdp-shot.png", Buffer.from(shot.data, "base64"));
  const st = Bun.file("/tmp/bun-cdp-shot.png");
  console.log("✅ 截图已保存 /tmp/bun-cdp-shot.png,大小:", st.size, "bytes");

  // 7) 优雅关闭浏览器
  await cdp("Browser.close").catch(() => {});
  console.log("✅ 已关闭浏览器");
} finally {
  proc.kill();
}
--------------------
{"stdout":"✅ 浏览器已启动: Chrome/151.0.7922.76\n✅ 新建页面: 9DFE130399E8DA40E4E3E3C16F444046 | ws: ws://127.0.0.1:9333/devtools/page/9DFE130399E8DA40E4E3E3C16F444046\n✅ 页面加载完成,标题: Example Domain\n✅ 页面内 JS 执行结果: {\"h1\":\"Example Domain\",\"links\":1,\"now\":1786067802175}\n✅ 截图已保存 /tmp/bun-cdp-shot.png,大小: 15471 bytes\n✅ 已关闭浏览器\n","stderr":"","exitCode":0}

演示成功。先把这个结论记入记忆,再回答你。

--- [run_script] ---
import { readFileSync, appendFileSync, writeFileSync, existsSync } from "fs";
const p = ".bunbot/memory.md";
const entry = `

### 2026-08-06 问答:如何控制浏览器(CDP 演示)
- 目标:回答用户"你如何控制浏览器"。
- 结论:bun-bot 本身无浏览器工具,但 run_script 可用 Bun 原生能力控制浏览器:Bun.spawn 启动 headless Chrome(--remote-debugging-port + --user-data-dir=/tmp 隔离)→ CDP HTTP API(/json/new)建标签页 → WebSocket 连接 webSocketDebuggerUrl 发 {id,method,params} 命令(Page.navigate / Runtime.evaluate / Page.captureScreenshot / Browser.close)→ 完成导航、执行 JS、截图、关闭。已在本机实测成功(Chrome 151,截图 15KB)。
- 经验/教训:①零第三方依赖即可实现,符合项目"禁止运行时第三方依赖"约定;②Bun 原生支持 WebSocket 与 fetch,无需装 puppeteer/playwright;③CDP 消息需自管理 id→Promise 映射;④等端口就绪用轮询 fetch /json/version;⑤headless Chrome 必须 --user-data-dir 隔离避免与用户浏览器冲突。
- 待办/下一步:无
`;
const lines = readFileSync(p, "utf8").split("\n");
console.log("追加前行数:", lines.length);
appendFileSync(p, entry);
console.log("已追加,现总行数:", readFileSync(p, "utf8").split("\n").length);
--------------------
{"stdout":"追加前行数: 93\n已追加,现总行数: 100\n","stderr":"","exitCode":0}

## 回答

**我(bun-bot)本身没有内置的浏览器工具**——我唯一的工具是 `run_script`(写 JS/TS 交给 Bun 执行)。但正因如此,控制浏览器对我来说反而是件很自然的事,因为 **Bun 原生就具备全部所需能力**(`Bun.spawn`、`fetch`、`WebSocket`),一条第三方依赖都不用装。

### 刚才的实际演示(本机 Chrome 已跑通)

我用原生 **CDP(Chrome DevTools Protocol)** 走了一遍完整链路:

1. **启动无头浏览器**:`Bun.spawn` 拉起 Chrome,参数 `--headless=new --remote-debugging-port=9333 --user-data-dir=/tmp/...`(必须隔离用户数据目录,避免和你的正常浏览器冲突)
2. **建标签页**:调 CDP 的 HTTP 接口 `PUT /json/new`,拿到 `webSocketDebuggerUrl`
3. **连 WebSocket 发命令**:Bun 原生支持 WebSocket,按 `{id, method, params}` 格式收发消息,自管 id→Promise 映射
4. **真实控制了页面**:
   - `Page.navigate` → 打开 example.com,轮询 `document.readyState` 等加载完成
   - `Runtime.evaluate` → 在页面里执行 JS,读出 `h1`、链接数,还把标题改成了"被 Bun 的 CDP 改写了"
   - `Page.captureScreenshot` → 截图保存到 `/tmp/bun-cdp-shot.png`(15KB,浏览器真的渲染了)
5. **优雅关闭**:`Browser.close`

### 各种方式的取舍

| 方式 | 适合场景 | 本项目可行性 |
|---|---|---|
| **原生 CDP**(刚才演示的) | 需要真实渲染、点击、截图、执行页面 JS | ✅ 零依赖,最贴合本项目"禁止运行时第三方依赖"的约定 |
| Playwright / Puppeteer | 测试框架、选择器 API 更友好 | ⚠️ 是 npm 包,本项目约定禁止,但可用 `Bun.spawn` 调外部已装的 CLI |
| `Bun.fetch` | 只抓 HTML/API 数据,不需要 JS 渲染 | ✅ 最简单,多数抓取场景够用 |

如果你有具体目标——比如"抓某个需要 JS 渲染的页面"、"登录后下载文件"、"做截图巡检"——告诉我场景,我可以把上面这套封装成一个 `.agents/skills/` 技能,让你以后 `/浏览器` 就能直接调用。

/tmp/bun-cdp-shot.png:

Image

案例二
基于纯 skill 实现连接 figma mcp,commit 56c81b0

  1. run_script tool 会基于 Bunjs 启动一个回调服务
  2. figma 登录成功后回调本地 Bunjs 服务,鉴权 token 写入本地临时文件
  3. run_script tool 调用 figma http mcp 服务携带鉴权信息

无需本地安装配置 mcp

Originally created by @BoltDoggy on GitHub (Aug 6, 2026). Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-agent/issues/354 代码仓库: https://github.com/BoltDoggy/bun-bot 设计理念博客: https://juejin.cn/post/7670016345690832947 一句话介绍: 给大模型最基础的能力,让它自己探索环境改造世界 起步原理: 1. 使用 bunjs 直接调用 deepseek chat api,3 行代码完成模型调用 2. 让它认知自己就是 bunjs,只提供一个 function call tools: run_script,使用 bunjs 调用模型输出的 js 脚本,而无需提供读写文件、shell 调用、网络请求等 tools 3. 不提供 TUI,而是通过 shell 调用,基于目录的记忆,每次运行命令输入的提示词类似在 tui 中的对话 --- 构建产物内置 bunjs 运行时,因此用户无 bunjs 环境也能安全运行 --- **你可以提出你的挑战,我会用最少的代码来实现支持** **案例一** 这里提供一个运行案例,以下是 [commit 71b9c34](https://github.com/BoltDoggy/bun-bot/commit/71b9c3400a08fd17e4e6cb32d3645501cd83c74c) 在 shell 中运行输出的所有日志。 ``` ❯ bun run index.ts "你如何控制浏览器" --- [run_script] --- import { readdirSync, existsSync, readFileSync } from "fs"; // 读取记忆 const memPath = ".bunbot/memory.md"; if (existsSync(memPath)) { console.log("=== memory.md ==="); console.log(readFileSync(memPath, "utf8")); } else { console.log("(memory.md 不存在)"); } // 目录树 console.log("\n=== 目录结构 ==="); function tree(dir, prefix = "") { for (const name of readdirSync(dir, { withFileTypes: true })) { if (name.name.startsWith("node_modules") || name.name.startsWith(".git")) continue; console.log(prefix + name.name + (name.isDirectory() ? "/" : "")); if (name.isDirectory()) tree(dir + "/" + name.name, prefix + " "); } } tree("."); -------------------- {"stdout":"=== memory.md ===\n### 2025-06-27 拆分 AGENTS.md 与 BUN_BOT.md\n- 目标:让 AGENTS.md 保持通用(可被其他 AGENT 复用),新增 BUN_BOT.md 承载 run_script 相关约定,并让 index.ts 同时加载两个文件。\n- 结论:已完成。AGENTS.md 仅保留项目介绍/运行方式/工程约定;BUN_BOT.md 含 run_script 工具说明与记忆协议;index.ts 的 loadAgentsContext 泛化为 loadContextFile,依次加载 AGENTS.md + BUN_BOT.md 注入系统提示词。\n- 经验/教训:加载顺序为 AGENTS.md(通用)在前、BUN_BOT.md(专属)在后,后者可补充/覆盖前者;交叉引用说明可保留在 AGENTS.md,不算约定内容。\n- 待办/下一步:无\n### 2025-06-27 补充\"操作顺序\"约定\n- 目标:把\"写文件前必须先读文件,读文件前必须先了解目录树\"固化到 BUN_BOT.md。\n- 结论:已在 BUN_BOT.md 中\"## 工具\"之前插入\"## 操作顺序\"章节,含两条规则。\n- 经验/教训:BUN_BOT.md 会被注入系统提示词,操作顺序类约定放这里对代理最有效;插入章节时先检查是否已存在,避免重复。\n- 待办/下一步:无\n\n\n### 2025-06-27 查看 skill-template 技能\n- 目标:查看 .agents/skills 技能清单并总结 skill-template 的用途与目录结构。\n- 结论:技能清单仅 skill-template(目录型);用途是指导创建新技能/了解技能格式;规定两种形态:目录型(<技能名>/SKILL.md + 可选 scripts/)与单文件型(<技能名>.md);SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写详细步骤。\n- 经验/教训:技能是\"按需加载\",清单已注入系统提示词,全文才需要 run_script 读取。\n- 待办/下一步:无\n\n\n### 2025-06-27 实现对 .agents/skills 技能支持\n- 目标:让 bun-bot 启动时扫描 .agents/skills/ 并把技能清单注入系统提示词。\n- 结论:已完成。新增 skills.ts(loadSkills/parseFrontmatter);index.ts 在 AGENTS.md+BUN_BOT.md 之后拼接技能清单;BUN_BOT.md 增加\"技能\"章节;附带示例技能 .agents/skills/skill-template/SKILL.md;端到端验证 agent 会按需读取 SKILL.md。\n- 经验/教训:①Bun 1.3.14 无 Bun.readdir/Bun.glob,扫描目录用 new Bun.Glob(\"**/*.md\").scanSync({cwd}),目录不存在抛 ENOENT 需 try/catch;②frontmatter 用 Bun.YAML.parse,坏 YAML 抛错需兜底;③只注入技能清单、全文由 agent 按需 run_script 读取,避免撑爆上下文;④run_script 在 tmpdir 运行,import 项目模块须用绝对路径 process.cwd() 拼接。\n- 待办/下一步:无\n\n\n\n### 2026-08-06 .bunbot 加入 .gitignore\n- 目标:让 .bunbot 记忆目录不被 git 跟踪。\n- 结论:已在 .gitignore 末尾追加 \"# bun-bot memory\" + \".bunbot/\";git check-ignore 验证生效,git status 无相关条目。\n- 经验/教训:.bunbot 含 memory.md 与 .keep,均为本地记忆状态不应提交;因从未被 git 跟踪,仅加 gitignore 即可,无需 git rm --cached。\n- 待办/下一步:无\n\n### 2026-08-06 查看 skill-template 技能(第二次)\n- 目标:用户触发 /skill-template,查看技能用途与格式。\n- 结论:skill-template 是目录型技能,仅含 SKILL.md,用于指导创建新技能或了解 .agents/skills 规范;规定两种形态(目录型/单文件型),SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写具体步骤。\n- 经验/教训:触发技能时若用户无额外子任务,先展示技能用途与目录规范即可,无需改动任何文件。\n- 待办/下一步:无\n### 2026-08-06 提交拆分 AGENTS.md/BUN_BOT.md 与技能支持\n- 目标:把暂存的 6 个文件改动提交到 git。\n- 结论:已提交 d597e5a(feat: 拆分 AGENTS.md/BUN_BOT.md,支持 .agents/skills 技能按需加载),6 files changed, 167 insertions(+), 15 deletions(-)。\n- 经验/教训:①Bun 1.3.14 的 Bun.spawn 无 .output() 方法,用 Bun.spawnSync 并 decode stdout/stderr;②提交前先看 git status/diff --cached,commit message 沿用 conventional commits 风格(feat:)。\n- 待办/下一步:无\n\n### 2026-08-06 约定:/skill-name 开头的指令 = 执行该技能\n- 目标:明确用户指令以 /skill-name 开头时的含义。\n- 结论:以 /skill-name 开头的用户指令,是要求**执行**该技能(按其 SKILL.md 的指令完成实际任务),不是只读技能定义/展示用途;本条修正此前\"触发技能时仅展示用途\"的记录。\n- 经验/教训:用户说 /xxx 时按\"执行动作\"理解,先 run_script 读取对应技能文件全文,再按其指令落地任务。\n- 待办/下一步:无\n\n### 2026-08-06 把 /技能名 指令语义固化进 BUN_BOT.md\n- 目标:让\"用户指令以 /技能名 开头 = 执行该技能\"这条约定不只存在于记忆里,而是注入系统提示词长期生效。\n- 结论:已在 BUN_BOT.md 的\"技能(.agents/skills)\"章节新增一条:/技能名 开头表示要求执行该技能(先读 SKILL.md 全文再落地任务),不得只展示用途/格式。\n- 经验/教训:行为类约定应优先固化到 BUN_BOT.md(会被注入系统提示词),记忆只是临时载体;写入前先读目标文件确认无重复条目(用 includes 检查锚点)。\n- 待办/下一步:无\n\n### 2026-08-06 触发 /skill-template(用户未指定新技能)\n- 目标:执行 skill-template 技能,查看当前技能目录与 SKILL.md 规范。\n- 结论:.agents/skills 下仅 skill-template(目录型,SKILL.md 27 行);已向用户展示两种形态与 SKILL.md 三条写法规范(frontmatter name+description、description 写触发场景、正文中文详细步骤);用户未指定新技能名,未改动任何文件,等待用户给出技能名与用途。\n- 经验/教训:触发 /skill-template 但无子任务时,用 run_script 实际确认目录结构后展示规范并询问创建目标即可,不擅自造文件。\n- 待办/下一步:待用户提供新技能名称与用途后按其落地 SKILL.md。\n\n### 2026-08-06 全局安装 bun-bot(bun link 可在任意目录使用)\n- 目标:把 bun-bot 全局安装,使其他目录也能直接调用。\n- 结论:①index.ts 加 shebang `#!/usr/bin/env bun` 并新增 `ROOT = dirname(import.meta.path)`,loadContextFile/loadSkills 均基于 ROOT 定位项目文件,摆脱 cwd 依赖;②skills.ts 的 loadSkills 接受 root 参数,技能 path 输出绝对路径;③index.ts 新增 loadEnvFromRoot():cwd 无 .env 时兜底读项目根 .env(不覆盖已有环境变量);④package.json 加 `\"bin\": {\"bun-bot\": \"./index.ts\"}`;⑤执行 `bun link` 注册全局命令(符号链接到 ~/.bun/bin/bun-bot,改代码即时生效);⑥README 增加全局安装章节。\n- 经验/教训:bun link 生成的是符号链接而非复制,相对 import ./skills.ts 依旧可用;模板字符串里嵌 ``` 代码块会与反引号冲突,用数组 join 代替;从其他目录运行 `bun-bot` 无任务时输出用法提示,说明 .env 兜底已生效。\n- 待办/下一步:无\n\n### 2026-08-06 移除 skills.ts(技能加载交由 bun-bot 宿主)\n- 目\n... (截断)","stderr":"","exitCode":0} --- [run_script] --- import { readFileSync, statSync } from "fs"; const mem = readFileSync(".bunbot/memory.md", "utf8"); const lines = mem.split("\n"); console.log("总行数:", lines.length); // 打印最近的部分(尾部 60 行) console.log("=== 尾部 60 行 ==="); console.log(lines.slice(-60).join("\n")); // 看看 index.ts 是否提到浏览器 const idx = readFileSync("index.ts", "utf8"); console.log("\n=== index.ts 中是否含 browser/cdp/playwright/puppeteer ==="); for (const kw of ["browser", "cdp", "playwright", "puppeteer", "浏览器"]) { console.log(kw, "=>", idx.toLowerCase().includes(kw.toLowerCase())); } -------------------- {"stdout":"总行数: 93\n=== 尾部 60 行 ===\n### 2026-08-06 查看 skill-template 技能(第二次)\n- 目标:用户触发 /skill-template,查看技能用途与格式。\n- 结论:skill-template 是目录型技能,仅含 SKILL.md,用于指导创建新技能或了解 .agents/skills 规范;规定两种形态(目录型/单文件型),SKILL.md 需 YAML frontmatter 声明 name+description,正文中文写具体步骤。\n- 经验/教训:触发技能时若用户无额外子任务,先展示技能用途与目录规范即可,无需改动任何文件。\n- 待办/下一步:无\n### 2026-08-06 提交拆分 AGENTS.md/BUN_BOT.md 与技能支持\n- 目标:把暂存的 6 个文件改动提交到 git。\n- 结论:已提交 d597e5a(feat: 拆分 AGENTS.md/BUN_BOT.md,支持 .agents/skills 技能按需加载),6 files changed, 167 insertions(+), 15 deletions(-)。\n- 经验/教训:①Bun 1.3.14 的 Bun.spawn 无 .output() 方法,用 Bun.spawnSync 并 decode stdout/stderr;②提交前先看 git status/diff --cached,commit message 沿用 conventional commits 风格(feat:)。\n- 待办/下一步:无\n\n### 2026-08-06 约定:/skill-name 开头的指令 = 执行该技能\n- 目标:明确用户指令以 /skill-name 开头时的含义。\n- 结论:以 /skill-name 开头的用户指令,是要求**执行**该技能(按其 SKILL.md 的指令完成实际任务),不是只读技能定义/展示用途;本条修正此前\"触发技能时仅展示用途\"的记录。\n- 经验/教训:用户说 /xxx 时按\"执行动作\"理解,先 run_script 读取对应技能文件全文,再按其指令落地任务。\n- 待办/下一步:无\n\n### 2026-08-06 把 /技能名 指令语义固化进 BUN_BOT.md\n- 目标:让\"用户指令以 /技能名 开头 = 执行该技能\"这条约定不只存在于记忆里,而是注入系统提示词长期生效。\n- 结论:已在 BUN_BOT.md 的\"技能(.agents/skills)\"章节新增一条:/技能名 开头表示要求执行该技能(先读 SKILL.md 全文再落地任务),不得只展示用途/格式。\n- 经验/教训:行为类约定应优先固化到 BUN_BOT.md(会被注入系统提示词),记忆只是临时载体;写入前先读目标文件确认无重复条目(用 includes 检查锚点)。\n- 待办/下一步:无\n\n### 2026-08-06 触发 /skill-template(用户未指定新技能)\n- 目标:执行 skill-template 技能,查看当前技能目录与 SKILL.md 规范。\n- 结论:.agents/skills 下仅 skill-template(目录型,SKILL.md 27 行);已向用户展示两种形态与 SKILL.md 三条写法规范(frontmatter name+description、description 写触发场景、正文中文详细步骤);用户未指定新技能名,未改动任何文件,等待用户给出技能名与用途。\n- 经验/教训:触发 /skill-template 但无子任务时,用 run_script 实际确认目录结构后展示规范并询问创建目标即可,不擅自造文件。\n- 待办/下一步:待用户提供新技能名称与用途后按其落地 SKILL.md。\n\n### 2026-08-06 全局安装 bun-bot(bun link 可在任意目录使用)\n- 目标:把 bun-bot 全局安装,使其他目录也能直接调用。\n- 结论:①index.ts 加 shebang `#!/usr/bin/env bun` 并新增 `ROOT = dirname(import.meta.path)`,loadContextFile/loadSkills 均基于 ROOT 定位项目文件,摆脱 cwd 依赖;②skills.ts 的 loadSkills 接受 root 参数,技能 path 输出绝对路径;③index.ts 新增 loadEnvFromRoot():cwd 无 .env 时兜底读项目根 .env(不覆盖已有环境变量);④package.json 加 `\"bin\": {\"bun-bot\": \"./index.ts\"}`;⑤执行 `bun link` 注册全局命令(符号链接到 ~/.bun/bin/bun-bot,改代码即时生效);⑥README 增加全局安装章节。\n- 经验/教训:bun link 生成的是符号链接而非复制,相对 import ./skills.ts 依旧可用;模板字符串里嵌 ``` 代码块会与反引号冲突,用数组 join 代替;从其他目录运行 `bun-bot` 无任务时输出用法提示,说明 .env 兜底已生效。\n- 待办/下一步:无\n\n### 2026-08-06 移除 skills.ts(技能加载交由 bun-bot 宿主)\n- 目标:删除 skills.ts——\"加载 skills 是 BUN_BOT 提供的能力\",技能清单由 bun-bot 启动时扫描注入系统提示词,项目代码无需重复实现。\n- 结论:已完成。①skills.ts 删除(git 显示 D skills.ts);②index.ts 移除 loadSkills import,projectContext 改为只拼 AGENTS.md + BUN_BOT.md,注释注明技能清单由宿主注入;③README.md 同步:全局安装段第 42 行去掉 .agents/skills、项目结构删除 skills.ts 行;④bun build 编译通过、无参数运行正常(打印用法提示);⑤BUN_BOT.md 无需改动,其\"bun-bot 自动扫描\"表述本就是宿主行为。\n- 经验/教训:宿主注入的系统提示词已含\"可用技能\"清单段,项目内 skills.ts 是重复实现;删除模块后 grep 残留仅剩 BUN_BOT.md/README/技能自身等合理描述。\n- 待办/下一步:工作区仍留有上次\"全局安装\"会话的未提交改动(index.ts shebang/ROOT/loadEnvFromRoot、package.json bin、README 全局安装章节),如需提交请先确认。\n\n### 2026-08-06 移除 loadEnvFromRoot(env 交由使用方注入)\n- 目标:回应用户反馈\"env 无需从 bun-bot 源码目录读取,使用方可通过环境变量注入,该实现会污染代码库\"。\n- 结论:已删除 index.ts 中的 loadEnvFromRoot() 函数与调用(连带其注释);README 全局安装章节的\"兜底读项目根目录 .env\"改为\"由使用方 export 或在运行目录写 .env(Bun 自动加载)\"。ROOT 保留(loadContextFile 仍靠它定位 AGENTS.md/BUN_BOT.md)。\n- 经验/教训:①bun-bot 只是\"读自身项目文件\"(AGENTS/BUN_BOT)是合理的,但从自身源码目录偷偷读 .env 注入环境变量属于隐式副作用,会污染调用方环境;②.env 加载是 Bun 的宿主行为(自动加载 cwd 的 .env),项目代码不应重复实现兜底;③bun build 编译通过、无 key 报错、有 key 无任务打印用法提示,行为均符合预期。\n- 待办/下一步:工作区仍留有此前未提交改动(index.ts/README.md/package.json/BUN_BOT.md 等),如需提交请先确认。\n\n### 2026-08-06 上下文改为读取 cwd 的 AGENTS.md/BUN_BOT.md\n- 目标:回应用户反馈\"上下文也应该是实际工作目录的 AGENTS.md 和 BUN_BOT.md,不应该读取源码目录下的\"(延续此前 .env 的反馈:bun-bot 不应把源码目录文件强加给使用方)。\n- 结论:已完成。①index.ts 删除 ROOT 常量与 dirname 导入(ROOT 仅用于 loadContextFile);②loadContextFile 改为 join(process.cwd(), fileName),注释与注入标题同步改为\"当前工作目录\";③README 全局安装段第 42 行改为\"按 cwd 定位、不读源码目录同名文件\";④bun build 编译通过;⑤临时目录端到端验证:读到的是该目录的 AGENTS.md/BUN_BOT.md。\n- 经验/教训:①约定文件属于\"使用方项目\"而非 bun-bot 自身,应跟随运行目录;②改动前先 grep ROOT/dirname 确认无其他依赖;③行为变化提示:在无 BUN_BOT.md 的目录运行 bun-bot 时,记忆协议/操作顺序等专属约定不会注入(除非使用方在自己的运行目录放 BUN_BOT.md)。\n- 待办/下一步:无\n\n\n### 2026-08-06 git commit --amend(并入多会话累积改动)\n- 目标:把暂存的\n... (截断)","stderr":"","exitCode":0} --- [run_script] --- import { spawnSync } from "bun"; // 1. 探测系统里有哪些浏览器可执行文件 const candidates = [ "google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome", "firefox", "msedge", "brave-browser", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", "/usr/bin/google-chrome", "/snap/bin/chromium", ]; for (const c of candidates) { const r = spawnSync(["which", c], { stdout: "pipe", stderr: "pipe" }); if (r.exitCode === 0) console.log("找到:", r.stdout.toString().trim()); } // 也试试直接找 const r = spawnSync(["bash", "-lc", "command -v google-chrome chromium chromium-browser chrome 2>/dev/null; ls /Applications 2>/dev/null | grep -iE 'chrome|chromium|edge|brave'"], { stdout: "pipe", stderr: "pipe" }); console.log("\n=== bash 探测 ==="); console.log(r.stdout.toString().trim() || "(无)"); console.log("exit:", r.exitCode); // 2. Bun 版本 & 是否支持 WebSocket(Bun 原生支持) console.log("\nBun:", (await Bun.version) ?? process.version); console.log("typeof WebSocket:", typeof WebSocket); -------------------- {"stdout":"找到: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n\n=== bash 探测 ===\nGoogle Chrome.app\nexit: 0\n\nBun: 1.3.14\ntypeof WebSocket: function\n","stderr":"","exitCode":0} 环境里有 Google Chrome,Bun 原生支持 WebSocket——我来实际演示一把:通过 **CDP(Chrome DevTools Protocol)** 从零控制一个无头浏览器,不依赖任何 npm 包。 --- [run_script] --- import { spawn } from "bun"; const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; const PORT = 9333; const userData = `/tmp/bun-cdp-${Date.now()}`; // 1) 启动无头 Chrome,开启远程调试端口 const proc = spawn({ cmd: [CHROME, "--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${userData}`, "--no-first-run", "--disable-gpu", "about:blank"], stdout: "pipe", stderr: "pipe", }); async function waitForPort(timeoutMs = 10000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const res = await fetch(`http://127.0.0.1:${PORT}/json/version`); if (res.ok) return await res.json(); } catch {} await Bun.sleep(200); } throw new Error("CDP 端口未就绪"); } try { const ver = await waitForPort(); console.log("✅ 浏览器已启动:", ver.Browser); // 2) 通过 CDP HTTP API 新建一个标签页 const tab = await (await fetch(`http://127.0.0.1:${PORT}/json/new?about:blank`, { method: "PUT" })).json(); console.log("✅ 新建页面:", tab.id, "| ws:", tab.webSocketDebuggerUrl); // 3) 封装 WebSocket 上的 CDP 命令 const ws = new WebSocket(tab.webSocketDebuggerUrl); await new Promise((res, rej) => { ws.addEventListener("open", res); ws.addEventListener("error", rej); }); let seq = 0; const pending = new Map(); ws.addEventListener("message", (ev) => { const msg = JSON.parse(ev.data); if (msg.id && pending.has(msg.id)) { const { resolve, reject } = pending.get(msg.id); pending.delete(msg.id); msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result); } }); const cdp = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); }); // 4) 导航到 example.com 并等它加载完 await cdp("Page.enable"); await cdp("Page.navigate", { url: "https://example.com" }); for (let i = 0; i < 20; i++) { const r = await cdp("Runtime.evaluate", { expression: "document.readyState", returnByValue: true }); if (r.result.value === "complete") break; await Bun.sleep(250); } console.log("✅ 页面加载完成,标题:", (await cdp("Runtime.evaluate", { expression: "document.title", returnByValue: true })).result.value); // 5) 在页面里执行 JS(读取 DOM、修改内容) const info = await cdp("Runtime.evaluate", { expression: `(() => { const h1 = document.querySelector("h1")?.textContent ?? ""; document.title = "被 Bun 的 CDP 改写了"; return { h1, links: document.querySelectorAll("a").length, now: Date.now() }; })()`, returnByValue: true, }); console.log("✅ 页面内 JS 执行结果:", JSON.stringify(info.result.value)); // 6) 截图,直观证明浏览器真的被控制了 const shot = await cdp("Page.captureScreenshot", { format: "png" }); await Bun.write("/tmp/bun-cdp-shot.png", Buffer.from(shot.data, "base64")); const st = Bun.file("/tmp/bun-cdp-shot.png"); console.log("✅ 截图已保存 /tmp/bun-cdp-shot.png,大小:", st.size, "bytes"); // 7) 优雅关闭浏览器 await cdp("Browser.close").catch(() => {}); console.log("✅ 已关闭浏览器"); } finally { proc.kill(); } -------------------- {"stdout":"✅ 浏览器已启动: Chrome/151.0.7922.76\n✅ 新建页面: 9DFE130399E8DA40E4E3E3C16F444046 | ws: ws://127.0.0.1:9333/devtools/page/9DFE130399E8DA40E4E3E3C16F444046\n✅ 页面加载完成,标题: Example Domain\n✅ 页面内 JS 执行结果: {\"h1\":\"Example Domain\",\"links\":1,\"now\":1786067802175}\n✅ 截图已保存 /tmp/bun-cdp-shot.png,大小: 15471 bytes\n✅ 已关闭浏览器\n","stderr":"","exitCode":0} 演示成功。先把这个结论记入记忆,再回答你。 --- [run_script] --- import { readFileSync, appendFileSync, writeFileSync, existsSync } from "fs"; const p = ".bunbot/memory.md"; const entry = ` ### 2026-08-06 问答:如何控制浏览器(CDP 演示) - 目标:回答用户"你如何控制浏览器"。 - 结论:bun-bot 本身无浏览器工具,但 run_script 可用 Bun 原生能力控制浏览器:Bun.spawn 启动 headless Chrome(--remote-debugging-port + --user-data-dir=/tmp 隔离)→ CDP HTTP API(/json/new)建标签页 → WebSocket 连接 webSocketDebuggerUrl 发 {id,method,params} 命令(Page.navigate / Runtime.evaluate / Page.captureScreenshot / Browser.close)→ 完成导航、执行 JS、截图、关闭。已在本机实测成功(Chrome 151,截图 15KB)。 - 经验/教训:①零第三方依赖即可实现,符合项目"禁止运行时第三方依赖"约定;②Bun 原生支持 WebSocket 与 fetch,无需装 puppeteer/playwright;③CDP 消息需自管理 id→Promise 映射;④等端口就绪用轮询 fetch /json/version;⑤headless Chrome 必须 --user-data-dir 隔离避免与用户浏览器冲突。 - 待办/下一步:无 `; const lines = readFileSync(p, "utf8").split("\n"); console.log("追加前行数:", lines.length); appendFileSync(p, entry); console.log("已追加,现总行数:", readFileSync(p, "utf8").split("\n").length); -------------------- {"stdout":"追加前行数: 93\n已追加,现总行数: 100\n","stderr":"","exitCode":0} ## 回答 **我(bun-bot)本身没有内置的浏览器工具**——我唯一的工具是 `run_script`(写 JS/TS 交给 Bun 执行)。但正因如此,控制浏览器对我来说反而是件很自然的事,因为 **Bun 原生就具备全部所需能力**(`Bun.spawn`、`fetch`、`WebSocket`),一条第三方依赖都不用装。 ### 刚才的实际演示(本机 Chrome 已跑通) 我用原生 **CDP(Chrome DevTools Protocol)** 走了一遍完整链路: 1. **启动无头浏览器**:`Bun.spawn` 拉起 Chrome,参数 `--headless=new --remote-debugging-port=9333 --user-data-dir=/tmp/...`(必须隔离用户数据目录,避免和你的正常浏览器冲突) 2. **建标签页**:调 CDP 的 HTTP 接口 `PUT /json/new`,拿到 `webSocketDebuggerUrl` 3. **连 WebSocket 发命令**:Bun 原生支持 WebSocket,按 `{id, method, params}` 格式收发消息,自管 id→Promise 映射 4. **真实控制了页面**: - `Page.navigate` → 打开 example.com,轮询 `document.readyState` 等加载完成 - `Runtime.evaluate` → 在页面里执行 JS,读出 `h1`、链接数,还把标题改成了"被 Bun 的 CDP 改写了" - `Page.captureScreenshot` → 截图保存到 `/tmp/bun-cdp-shot.png`(15KB,浏览器真的渲染了) 5. **优雅关闭**:`Browser.close` ### 各种方式的取舍 | 方式 | 适合场景 | 本项目可行性 | |---|---|---| | **原生 CDP**(刚才演示的) | 需要真实渲染、点击、截图、执行页面 JS | ✅ 零依赖,最贴合本项目"禁止运行时第三方依赖"的约定 | | Playwright / Puppeteer | 测试框架、选择器 API 更友好 | ⚠️ 是 npm 包,本项目约定禁止,但可用 `Bun.spawn` 调外部已装的 CLI | | `Bun.fetch` | 只抓 HTML/API 数据,不需要 JS 渲染 | ✅ 最简单,多数抓取场景够用 | 如果你有具体目标——比如"抓某个需要 JS 渲染的页面"、"登录后下载文件"、"做截图巡检"——告诉我场景,我可以把上面这套封装成一个 `.agents/skills/` 技能,让你以后 `/浏览器` 就能直接调用。 ``` `/tmp/bun-cdp-shot.png`: <img width="756" height="469" alt="Image" src="https://github.com/user-attachments/assets/91947fa6-aa1d-4cf6-b60d-94e696ff829d" /> --- **案例二** 基于纯 skill 实现连接 figma mcp,[commit 56c81b0](https://github.com/BoltDoggy/bun-bot/commit/56c81b0e7a5a06124c49e00125cfc24c85575f06) 1. run_script tool 会基于 Bunjs 启动一个回调服务 2. figma 登录成功后回调本地 Bunjs 服务,鉴权 token 写入本地临时文件 3. run_script tool 调用 figma http mcp 服务携带鉴权信息 无需本地安装配置 mcp
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deepseek-ai/awesome-deepseek-agent#115
No description provided.