让 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构造数据。
如果希望 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/。因此,代码应根据解压后的真实文件结构定位运行时,不要从产品版本号推测内部目录名。
一个最小离线页面
下面的页面绘制
为了保持示例紧凑,代码只展示应用初始化、数学检查和导出部分:
<!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>
这里有几个容易被忽略的细节:
evalCommand()应使用英文命令名,多条命令可以用换行分隔。If(x>0,...)显式表达实函数定义域,避免表达式被按其他语义处理。appletOnLoad(api)是取得当前 applet API 的明确入口,多 applet 页面不应依赖“最后激活对象”的偶然状态。exportSVG(callback)使用异步回调,回调结束前不能把任务判为完成。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:
- 发现并验证 Math Apps Bundle。
- 启动仅监听
127.0.0.1的临时 HTTP 服务。 - 加载
/plot.html。 - 等待
window.__ggbResult.status离开loading。 - 读取 API 结果和 SVG、PNG 数据。
- 保存导出物与 JSON 证据。
- 在
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 能够渲染表达式,并不自动证明输入表达式、定义域和视窗符合题意。以本例为例:
因此唯一驻点为
函数在该点取得最小值
同时有
这次实际执行得到的证据是:
| 检查项 | 实际结果 | 判断 |
|---|---|---|
| GeoGebra 版本 | 5.4.927.1 | 已记录 |
evalCommand() | true | 命令成功 |
f(1) | 1 | 与解析结果一致 |
| 极小值点横坐标 | 0.3678794412 | 与 一致 |
| 极小值点纵坐标 | 0.6922006276 | 与 一致 |
| 外部请求 | 0 | 严格离线 |
| 失败请求 / 控制台错误 | 0 / 0 | 资源完整 |
| SVG / PNG | 均有效 | 导出完成 |
图:本站在本地使用 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 映射 |
| 大量资源 404 | Bundle 不完整或目录多套一层 | 以 deployggb.js 所在目录为根 |
evalCommand() 返回 false | 本地化命令名或语法错误 | 改用英文命令并逐条缩小范围 |
| 直接打开 HTML 失败 | file:// 资源限制 | 使用回环地址上的本地 HTTP 服务 |
| SVG 没有生成 | applet 尚未就绪或活动视图为 3D | 等待 API 回调并检查导出结果 |
| 曲线形状看似正确,定义域却错了 | 表达式语义不明确 | 用 If(...) 显式限制定义域 |
| 导图偶尔不完整 | 只等待 DOM,没有等待任务状态 | 等待自定义完成标记 |
| 离线检查出现外部请求 | 仍引用 CDN、材料或远程资源 | 本地化依赖并保留请求日志 |
| 升级后无法启动 | 目录变化或资源不完整 | 重新发现版本并检查必需文件 |
结语
将 GeoGebra 接进 Codex 后,自动化的价值不只在于少点几次鼠标。更重要的是,函数定义、视图、导出和验证都进入了同一条可重复执行的链路:数学关系可以复算,网络行为可以审计,SVG 与 PNG 可以重新生成,最终图片也仍然接受人的视觉判断。
当这些要求被写进 Skill 和脚本后,下一次绘图任务不必重新讨论“怎样才算完成”。Codex 只需要针对新的数学问题替换表达式、关键值和视窗,其余步骤沿用同一套验证方法。
评论
由 GitHub Discussions 提供支持。