让 Codex 画一条函数曲线,可以有很多办法:调用 Python 绘图库、打开在线计算器,或者直接生成一张看起来像函数图的图片。对数学内容来说,真正困难的部分却发生在“图已经出来”之后:定义域是否正确,极值点是否准确,导出是否完整,以及所谓的离线流程有没有悄悄访问外网。

GeoGebra 提供了另一条很适合自动化的路线。官方 Math Apps Bundle 可以在本地托管完整的网页运行时,页面再通过 JavaScript API 创建对象、读取数值并导出图片。只要在外层补上短生命周期 HTTP 服务和无头浏览器,Codex 就能把一条绘图需求转成可检查的 SVG、PNG 与验证数据。

一张数学图的完成条件,不应只有“文件已经生成”,还应包括命令成功、关键数值正确、资源完整、没有意外联网,以及人工检查通过

本文记录的实现已在 Windows 11 与 GeoGebra 5.4.927.1 上完成离线验证。版本与目录结构以后可能变化,调用方法以 GeoGebra 当前官方文档和实际解压目录为准。

先看完整调用链

这套工作流由七个环节组成:

%%{init: {"flowchart": {"nodeSpacing": 18, "rankSpacing": 24, "curve": "linear", "wrappingWidth": 150}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
  A["Codex 接收绘图需求"] --> B["分析定义域与关键值"]
  B --> C["生成任务 HTML"]
  C --> D["本地 HTTP 服务"]
  D --> E["Math Apps Bundle"]
  D --> F["无头浏览器"]
  E --> G["GeoGebra Apps API"]
  F --> G
  G --> H["SVG / PNG"]
  G --> I["数值与网络证据"]
  H --> J["人工视觉检查"]
  I --> J

这条链路里,GeoGebra 负责数学对象、视图和导出;本地服务负责资源映射;浏览器负责运行网页;Codex 负责理解需求、生成任务文件、组织验证并检查最终结果。

层次主要职责完成证据
数学分析定义域、极值、渐近线、端点和视窗可复算的关键值与约束
GeoGebra Bundle提供可自托管的网页运行时必需文件存在且能完整加载
Apps API创建对象、查询状态、导出图像命令返回成功,关键对象可读
本地执行器临时 HTTP 服务和浏览器生命周期任务结束后进程与端口关闭
离线验证阻断并记录非回环请求外部请求、失败请求、控制台错误均为空
视觉检查检查坐标轴、标签、裁切和遮挡SVG 与 PNG 实际可读

网页 API 的准确位置

GeoGebra Apps API 是嵌入页面中的 JavaScript 对象。页面通过 GGBApplet 创建应用,并在 appletOnLoad(api) 中取得 API 引用。随后可以调用:

  • evalCommand() 创建函数、点、曲线和几何对象;
  • exists()getValue()getXcoord() 查询构造状态;
  • setCoordSystem()setAxesVisible() 控制视图;
  • exportSVG()getPNGBase64() 导出图像;
  • getBase64() 保存 .ggb 构造数据。
这里的网页 API 运行在浏览器 JavaScript 上下文中,本身不提供可直接调用的 HTTP JSON 接口。

如果希望 Codex 或另一个进程提交函数并取得图片,需要在 API 外面自行增加执行层。本文采用的执行层很薄:启动本地网页、等待 API 完成,然后把导出内容和检查结果取回。它不需要长期驻留,也不把 GeoGebra 变成对外开放的网络服务。

本地 Bundle 怎样接进页面

GeoGebra 官方的自托管说明包含两个关键变化:把 deployggb.js 指向本地文件,并在 inject() 前用 setHTML5Codebase() 指向 Bundle 内的 HTML5 运行目录。

解压后的目录至少应包含:

<Bundle目录>/
└─ GeoGebra/
   ├─ deployggb.js
   └─ HTML5/
      └─ 5.0/
         └─ web3d/
            ├─ web3d.nocache.js
            └─ ...

自动发现版本时,不要只相信文件夹名称。候选目录至少要同时通过下面两项检查:

<GeoGebra根目录>/deployggb.js
<GeoGebra根目录>/HTML5/5.0/web3d/web3d.nocache.js

官方文档还提醒:CDN 在 911 之后的固定版本 URL 中使用 5.4,但下载 Bundle 内部目录仍可能保留 HTML5/5.0/。因此,代码应根据解压后的真实文件结构定位运行时,不要从产品版本号推测内部目录名

一个最小离线页面

下面的页面绘制

f(x)=exlnx=xx,x>0.f(x)=e^{x\ln x}=x^x,\qquad x>0.

为了保持示例紧凑,代码只展示应用初始化、数学检查和导出部分:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <script src="/GeoGebra/deployggb.js"></script>
</head>
<body>
  <div id="ggb-element"></div>
  <script>
    window.__ggbResult = { status: "loading" };
    window.__ggbExport = {};

    const params = {
      appName: "graphing",
      width: 900,
      height: 600,
      showToolBar: false,
      showMenuBar: false,
      showAlgebraInput: false,

      appletOnLoad(api) {
        const commandOk = api.evalCommand([
          "f(x)=If(x>0,exp(x*ln(x)))",
          "v=f(1)",
          "M=(1/e,exp(-1/e))"
        ].join("\n"));

        api.setCoordSystem(-0.25, 2, -0.3, 4.2);
        api.setAxesVisible(true, true);
        api.setAxisLabels(1, "x", "y", "");
        api.setGridVisible(true);

        const result = {
          status: "ready",
          commandOk,
          functionExists: api.exists("f"),
          valueAtOne: api.getValue("v"),
          minimumX: api.getXcoord("M"),
          minimumY: api.getYcoord("M")
        };

        api.exportSVG((svg) => {
          window.__ggbExport.svg = svg;
          window.__ggbExport.pngBase64 =
            api.getPNGBase64(2, false, 144);
          result.svgLooksValid = svg?.includes("<svg");
          result.pngLooksValid =
            window.__ggbExport.pngBase64?.length > 100;
          window.__ggbResult = result;
        });
      }
    };

    const applet = new GGBApplet(params, true);
    applet.setHTML5Codebase("/GeoGebra/HTML5/5.0/web3d/");
    window.addEventListener(
      "load",
      () => applet.inject("ggb-element")
    );
  </script>
</body>
</html>

这里有几个容易被忽略的细节:

  1. evalCommand() 应使用英文命令名,多条命令可以用换行分隔。
  2. If(x>0,...) 显式表达实函数定义域,避免表达式被按其他语义处理。
  3. appletOnLoad(api) 是取得当前 applet API 的明确入口,多 applet 页面不应依赖“最后激活对象”的偶然状态。
  4. exportSVG(callback) 使用异步回调,回调结束前不能把任务判为完成。
  5. getPNGBase64() 返回 Base64 数据,需要由外层脚本解码落盘。

为什么还需要本地 HTTP 服务

直接双击 HTML 会得到 file:// 页面。GeoGebra 的 HTML5 运行时还要继续加载拆分后的 JavaScript、字体和其他资源,不同浏览器对本地文件来源的限制也并不一致。通过 http://127.0.0.1:<随机端口>/ 提供资源,路径和同源行为会稳定得多。

执行器只需要映射两类 URL:

/plot.html          -> 当前任务 HTML
/GeoGebra/<path>    -> 已安装 Bundle 中的静态文件

服务应绑定回环地址并让系统分配空闲端口。请求 /GeoGebra/ 下的文件时,还要解析最终路径并确认它仍位于 Bundle 根目录内,避免 ../ 产生目录穿越。

const resolved = path.resolve(bundleRoot, relativePath);
if (!resolved.startsWith(bundleRoot + path.sep)) {
  return respondNotFound();
}

任务完成后关闭浏览器和 HTTP 服务。这样每次绘图都拥有独立生命周期,不会在后台留下难以追踪的端口和进程。

用无头浏览器取得导出与证据

外层执行器可以使用 Playwright 启动无头 Edge 或 Chromium:

  1. 发现并验证 Math Apps Bundle。
  2. 启动仅监听 127.0.0.1 的临时 HTTP 服务。
  3. 加载 /plot.html
  4. 等待 window.__ggbResult.status 离开 loading
  5. 读取 API 结果和 SVG、PNG 数据。
  6. 保存导出物与 JSON 证据。
  7. finally 中关闭浏览器和服务。

等待条件应来自页面明确设置的状态,不能只等待 DOM 出现。GeoGebra applet 的容器已经存在,并不代表命令、数值检查和异步 SVG 导出都已经完成。

自动化等待的对象应是任务完成状态,而不是某个看起来已经出现的界面元素。

怎样证明它确实离线

页面断网后还能打开,只能说明主流程可能不依赖外网。更严格的做法是在浏览器层阻断所有非回环请求,并把尝试访问的地址记入证据:

await page.route("**/*", async (route) => {
  const target = new URL(route.request().url());
  if (target.origin === localOrigin) {
    await route.continue();
  } else {
    externalRequests.push(target.href);
    await route.abort("blockedbyclient");
  }
});

一次严格通过至少应满足:

apiResult.status == "ready"
apiResult.commandOk == true
externalRequests.length == 0
failedRequests.length == 0
consoleErrors.length == 0

如果页面仍使用 CDN 版 deployggb.js、远程字体、外部图片或在线 material_id,这些请求会立即暴露出来。

图画出来以后,还要验证数学

GeoGebra 能够渲染表达式,并不自动证明输入表达式、定义域和视窗符合题意。以本例为例:

f(x)=xx(lnx+1),f'(x)=x^x(\ln x+1),

因此唯一驻点为

x=1e,x=\frac{1}{e},

函数在该点取得最小值

f(1e)=e1/e.f\left(\frac{1}{e}\right)=e^{-1/e}.

同时有

f(1)=1,limx0+xx=1.f(1)=1,\qquad \lim_{x\to0^+}x^x=1.

这次实际执行得到的证据是:

检查项实际结果判断
GeoGebra 版本5.4.927.1已记录
evalCommand()true命令成功
f(1)1与解析结果一致
极小值点横坐标0.36787944121/e1/e 一致
极小值点纵坐标0.6922006276e1/ee^{-1/e} 一致
外部请求0严格离线
失败请求 / 控制台错误0 / 0资源完整
SVG / PNG均有效导出完成
由本地 GeoGebra Math Apps Bundle 生成并经过数值与离线检查的 x 的 x 次方函数图

图:本站在本地使用 GeoGebra 5.4.927.1 生成。Made with GeoGebra®。另提供 SVG 版本 便于下载与后续编辑。

图像本身还需要人工检查:坐标轴是否完整,原点和少量负半轴是否保留,定义域端点是否使用空心样式,极值标签有没有遮挡曲线,以及快速增长区间是否被裁掉。若完整坐标系让局部特征太小,可以另外生成局部放大图,不应通过裁掉关键坐标轴来制造“更清楚”的假象。

数值断言负责证明关键数学关系,人工视觉检查负责发现标签遮挡、裁切、比例和端点样式问题;两类检查不能互相替代

把流程沉淀成 Skill

当同类图反复出现时,可以把方法整理成一个 Skill:

plot-with-local-geogebra/
├─ SKILL.md
├─ assets/
│  └─ offline-applet-template.html
└─ scripts/
   └─ render-offline-geogebra.cjs

SKILL.md 约定数学分析、定义域处理、坐标轴和验证要求;HTML 模板保存稳定的 applet 初始化方式;脚本负责发现 Bundle、启动服务、阻断外网、导出文件和输出证据。每次任务只需要替换命令、视窗与断言。

这与 AI Agent 的 Skill 是什么 中的分工一致:Skill 保存方法和验收标准,脚本承担机械而确定的执行步骤。若要继续比较不同图表工具的资产生命周期,可以阅读 让 Codex 生成可维护的 Draw.io 图表;Draw.io 关注可编辑结构图,GeoGebra 关注可计算、可验证的数学构造。

许可和分发边界

GeoGebra 当前许可页说明:完整软件与相关材料可以在符合条款的非商业场景中使用;商业用途需要单独许可。其源代码、安装包、语言文件和界面资源又分别涉及 EUPL、GeoGebra Non-Commercial License 与 CC BY-NC-SA 等不同条款,不能简单概括为“整个产品采用一种开源许可证”。

本文采用较保守的发布方式:

  • 只介绍安装后的调用流程和本站自己的执行器设计;
  • 不把 Math Apps Bundle 复制进博客仓库或提供镜像下载;
  • 示例图标注 Made with GeoGebra® 并链接官方资料;
  • 引导读者从 GeoGebra 官方渠道取得 Bundle;
  • 收费课程、出版物、带广告或其他营收用途应重新核对官方许可,并在需要时联系 GeoGebra。

许可判断取决于具体用途。本文记录的是个人非商业技术实践,不构成法律意见。

常见失败模式

现象常见原因处理方向
页面空白deployggb.js 或 codebase 路径错误检查必需文件与 URL 映射
大量资源 404Bundle 不完整或目录多套一层deployggb.js 所在目录为根
evalCommand() 返回 false本地化命令名或语法错误改用英文命令并逐条缩小范围
直接打开 HTML 失败file:// 资源限制使用回环地址上的本地 HTTP 服务
SVG 没有生成applet 尚未就绪或活动视图为 3D等待 API 回调并检查导出结果
曲线形状看似正确,定义域却错了表达式语义不明确If(...) 显式限制定义域
导图偶尔不完整只等待 DOM,没有等待任务状态等待自定义完成标记
离线检查出现外部请求仍引用 CDN、材料或远程资源本地化依赖并保留请求日志
升级后无法启动目录变化或资源不完整重新发现版本并检查必需文件

结语

将 GeoGebra 接进 Codex 后,自动化的价值不只在于少点几次鼠标。更重要的是,函数定义、视图、导出和验证都进入了同一条可重复执行的链路:数学关系可以复算,网络行为可以审计,SVG 与 PNG 可以重新生成,最终图片也仍然接受人的视觉判断。

当这些要求被写进 Skill 和脚本后,下一次绘图任务不必重新讨论“怎样才算完成”。Codex 只需要针对新的数学问题替换表达式、关键值和视窗,其余步骤沿用同一套验证方法。

参考资料