中秋「十五的月亮十七圆」?我用码道 Agent 写了个会算月亮的中秋赏月手册
中秋「十五的月亮十七圆」?我用码道 Agent 写了个会算月亮的中秋赏月手册
今天是 2026 年 9 月 25 日,中秋节。
可你抬头看,今晚的月亮其实不是最圆的——真正的满月在明天(9 月 26 日)夜里、北京时间 9 月 27 日凌晨才发生。天文上这叫「十五的月亮十七圆」,是百年里数得着的稀罕年份。
我没有靠查日历 App 知道这个冷知识,而是让华为云码道 CodeArts 的代码智能体,从一条提示词开始,给我完整实现了一个能用天文算法把月亮算出来的小站——「中秋月相历」。它不靠任何接口、不联网、零依赖,双击
index.html就能用,还能把 1900–2100 整整 201 年的中秋是哪一天、当晚月亮圆不圆、几点升起来,全部本地算给你看。
这篇文章记录我怎么用「人定架构与红线、AI 分块实现、本地验收为准」的方法,把这个作品从零做到可交付。仓库全程公开、可运行、带测试。
- 作品:中秋月相历(moonphase-almanac)
- 仓库:https://atomgit.com/sdf56g99988/moonphase-almanac (Public,含 README + MIT LICENSE)
- 运行:克隆后
node --test tests/domain.test.mjs跑 23 个领域测试;双击index.html直接用 - 本期完成人:我与码道 Agent(模型 GLM-5.2)

一、为什么是「中秋月相历」,而不是又一个抽奖/贪吃蛇
挑战赛到第十三篇,我已经做过记账、分发台、ZIP 扫描、风级鸿蒙应用、引力弹弓游戏……坦白说,「做个中秋小游戏」是最省事的,但评审里架构 30% + 代码 30% + 安全 20% 占了大头,UI/UX 只有 15%,博文 5%。也就是说——光好看没用,得「算得准、立得住、防得住」。
中秋这个主题,恰好有一个天然「硬核 + 有话题」的切入点:月相是可以被天体力学精确计算的,而「十五的月亮到底圆不圆」又是每年中秋大家都会争论的话题。把一个能当场算出「今晚月亮照亮 98.3%、其实是盈凸、明晚才满月」的工具做出来,既有真实算法深度,又戳中节日情绪。
所以选题定为:一个纯前端、零依赖、零联网的中秋赏月工具站,核心是真实天文算法而非静态数据堆砌。
二、作品是什么:四个标签页,把月亮算给你看
「中秋月相历」是一个单页应用,四个标签页:
- 今日月相:选任意日期,给出月龄、月相角、照亮比例、月相名(朔/蛾眉/上弦/盈凸/望/亏凸/下弦/残月),并用 Canvas 逐像素渲染当晚的月亮——亮面朝东还是朝西、照到哪条晨昏线,都是按真实黄经差算的,不是贴图。
- 百年中秋:1900–2100 共 201 年,每年中秋对应的公历日期、星期,以及「十五圆 / 十六圆 / 十七圆」标注。
- 赏月时刻:输入观测纬度,估算当晚月升 / 月上中天 / 月落时刻,并给出「最佳赏月时段」及理由(月升后约一小时月亮低、暖、大)。
- 中秋知多少:科普「为什么十五的月亮有时十六圆、十七圆」。
技术红线从一开始就写死:零联网、零存储、零第三方依赖、零 CDN、零外链字体。整个作品就是 index.html + 一份 CSS + 三个 JS(领域层、数据表、主程序)+ 一份测试,拷走双击就能跑。
先看今晚的实拍:2026-09-25 中秋之夜,如实判为「盈凸 · 照亮 98.3% · 月龄 13.65 天 · 月相角 165° · 亮面朝西」,并明确提示满月还未到。

三、需求澄清:先把「算得准」的尺子立起来
我没有一上来就让 Agent「写个中秋网站」,而是先把验收标准立清楚,尤其针对最容易翻车的「算不准」。关键约定:
- 月相一律以真实「日—月黄经差」为准,采用 Meeus《Astronomical Algorithms》的日月黄经解析级数(含主要摄动项),精度约 0.1°–0.6° 级。线性平均模型(月龄 = 天数 mod 朔望月)在数十年跨度会漂移约 ±1 天,只能用于教学演示,不能用来判望。
- 中秋历表用权威农历推排数据生成 1900–2100 共 201 条,并以史实交叉验证(如 2020-10-01 中秋国庆同日)。
- 所有读数统一锚定在「当日 12:00 UTC」,避免本地时区跨日误差。
- 月升时刻明确标注为「教学化简化估算」,仅供科普。
这一步的产出,后来几乎原样成了 js/domain.js 顶部的算法注释。
四、架构:领域层纯函数 + 数据表 + 渲染层,三层彻底分离
这是本篇最想强调的架构决策。我把代码切成三层,并让领域层完全不碰 DOM / Canvas:
js/domain.js 领域层:儒略日、Meeus 日月黄经、月相、月升估算、安全层(纯函数,可跑在 Node)
js/data-midautumn.js 数据层:1900–2100 中秋历表(201 条纯数据)
js/main.js 表现层:Tab 切换、主题、Canvas 月相盘像素渲染、输入绑定
tests/domain.test.mjs 测试:node:test 跑领域层,零依赖
好处立竿见影:
- 可测试:领域层是纯函数 + UMD 封装(浏览器挂
window.MoonAlmanac,Node 下require),测试直接用 Node 内置node:test,零测试框架依赖。 - 可验收:核心正确性用「天文硬尺子」在命令行就能验,不必开浏览器。
- 防退化:渲染层随便改样式,只要不碰领域层,测试结果不变。
js/domain.js 的 UMD 头(浏览器 / Node 双端通用):
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(require('./data-midautumn.js'));
} else {
root.MoonAlmanac = factory(root.MID_AUTUMN);
}
})(typeof globalThis !== 'undefined' ? globalThis : this, function (MID_AUTUMN) {
'use strict';
// …… 全部纯函数,最后用 deepFreeze 冻结导出
});
导出对象用 deepFreeze 递归冻结,常量(朔望月长度、黄赤交角、八个月相区间、参考新月锚点)全部不可变——这是安全与确定性的第一道闸。
五、核心算法:用 Meeus 级数把月亮「算」出来
整个作品的灵魂是日—月黄经差。月相角 = 月球视黄经 − 太阳视黄经,0° 是朔、180° 是望。solarLongitude / lunarLongitude 两个函数实现了 Meeus 的解析级数(含主要摄动项):
/** 太阳视黄经(度,含光行差与主章动修正) */
function solarLongitude(T) {
var L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
var M = 357.52911 + 35999.05029 * T - 0.0001537 * T * T;
var C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * sin(M)
+ (0.019993 - 0.000101 * T) * sin(2 * M)
+ 0.000289 * sin(3 * M);
var omega = 125.04 - 1934.136 * T;
return normalize(L0 + C - 0.00569 - 0.00478 * sin(omega));
}
/** 月球视黄经(度,含主摄动项),精度约 0.1°–0.6° 级 */
function lunarLongitude(T) {
var Ld = 218.3164477 + 481267.88123421 * T - 0.0015786 * T * T;
// …… D / Ms / M / F / E 五个基本引数
var lam = Ld
+ 6.288774 * sin(M) // 主摄动项
+ 1.274027 * sin(2 * D - M)
+ 0.658314 * sin(2 * D)
+ 0.185116 * E * sin(Ms)
// …… 共 16 项
return normalize(lam - 0.00478 * sin(omega));
}
/** 日月黄经差(0–360°,0°=朔,180°=望) */
function phaseAngle(date) {
var T = julianCenturies(date);
return normalize(lunarLongitude(T) - solarLongitude(T));
}
/** 照亮比例 = (1 − cos(月相角)) / 2 */
function illuminatedFraction(date) {
var pa = phaseAngle(date) * D2R;
return (1 - Math.cos(pa)) / 2;
}
有了真实黄经差,「距满月还有几天」「是盈凸还是望」全部迎刃而解。比如判定「十五的月亮几圆」,是把中秋日 12:00 UTC 的月相角换算成满月时刻,再折算到北京时区(UTC+8)的日界:
var phase = phaseAngle(noon); // 中秋日 12:00 UTC 的月相角
var offDays = (180 - phase) * SYNODIC_MONTH / 360; // 距满月还需几天
var bjClock = 20 + offDays * 24; // 中秋 12:00 UTC = 北京 20:00
var dayShift = Math.floor(bjClock / 24); // 0=十五 1=十六 2=十七
正是这段代码,把 2026 年如实判为「十七圆」——这也是文章开头那个冷知识的来源。
六、最爽的部分:Canvas 逐像素把月亮画出来
月相盘不是贴图,是逐像素按光照模型渲染的。核心思路:把月面当成单位球,对圆盘内每个像素求「太阳方向」与该点法向的点积,得到明暗;边缘做柔和过渡:
function drawMoonLayer(phaseDeg) {
var phi = Math.PI - 2 * Math.PI * (phaseDeg / 360);
var sp = Math.sin(phi), cp = Math.cos(phi);
for (var y = 0; y < MOON_SIZE; y++) {
for (var x = 0; x < MOON_SIZE; x++) {
var dx = (x - half) * inv, dy = (y - half) * inv;
var r2 = dx * dx + dy * dy;
if (r2 >= 1) continue;
var z = Math.sqrt(1 - r2);
var lit = sp * dx + cp * z; // 半球面点积(太阳方向)→ 明暗
var br = 0.5 + 0.5 * lit;
var v = br * (1 - Math.pow(r2, 3)); // 边缘微暗,柔和过渡
// 写入 RGBA:暗面深蓝(8,10,30) → 亮面暖白(250,222,158)
}
}
}
再叠加两层径向渐变光晕、固定位置的环形山暗斑、暖色光泽,月亮就有了立体感。因为明暗直接由 phaseDeg 驱动,所以亮面朝东朝西、晨昏线弧度,全都跟真实天文一致——今晚 165° 的盈凸,亮面就是朝西(向右张开)。
界面还内置了深色「夜」与浅色「笺」双主题,一键切换。浅色下同一个月相盘依旧层次分明:

七、测试:用「天文硬尺子」把正确性钉死
这是我坚持的第二条红线:Agent 说「全绿」不算数,本地能复现的测试才算数。tests/domain.test.mjs 共 23 个用例,挑几个最能打的:
儒略日逐位对照(精度 1e-9):
test('julianDay:2000-01-01 12:00 UTC = JD 2451545.0', () => {
const jd = julianDay(new Date(UTC(2000, 0, 1, 12, 0, 0)));
assert.ok(Math.abs(jd - 2451545.0) < 1e-9);
});
中秋判望硬尺子(2024、2025 中秋当晚应为「望」):
test('关键硬尺子:2025-10-06(中秋)判望', () => {
const dt = noonUTC(2025, 10, 6);
assert.ok(phaseName(dt).name.includes('望'));
assert.ok(illuminatedFraction(dt) > 0.98);
});
最狠的一条——2026 的特殊性:规格初稿假设「中秋=望」,但权威数据显示 2026 中秋夜其实是盈凸。测试如实校准,绝不放宽容差、不硬编码:
test('2026 真满月日 2026-09-26 判望,中秋 09-25 之夜实为盈凸', () => {
const full = noonUTC(2026, 9, 26);
assert.ok(phaseName(full).name.includes('望')); // 真满月日判望
const mid = noonUTC(2026, 9, 25);
assert.ok(phaseName(mid).name === '盈凸'); // 中秋夜如实判盈凸
assert.ok(illuminatedFraction(mid) > 0.97);
});
1 万次随机输入不崩(安全/健壮性兜底):
test('随机 10000 次混合输入:全部领域函数不抛错、输出有限', () => {
// phase∈[0,360)、fraction∈[0,1],NaN/Infinity/极区纬度都不抛错
});
本地实测结果:
ℹ tests 23
ℹ pass 23
ℹ fail 0
23/23 全绿,且全部用 Node 内置 node:test 跑,没有装任何一个第三方包。
八、安全红线:零联网、零存储、零依赖
占评审 20% 的安全,这个作品是「天然免疫」型——因为它根本没有攻击面:
- 零联网:无任何
fetch/XMLHttpRequest,所有数据本地计算。 - 零存储:不用
localStorage/cookie/indexedDB,刷新即重置。 - 零第三方依赖:
package.json里dependencies为空,连测试都用 Node 内置 runner。 - 零外链:无 CDN、无外链字体、无外链图片,断网可用。
- 输入防护:所有用户输入(年份、纬度)必经
sanitizeNumber——NaN/Infinity/空值回落、越界 clamp;midAutumnDate对非整数或超界年份直接throw RangeError。
function sanitizeNumber(x, min, max, fallback) {
if (x === '' || x === null || x === undefined) return fallback;
var n = (typeof x === 'number') ? x : Number(x);
if (!Number.isFinite(n)) return fallback;
return n < min ? min : (n > max ? max : n);
}
极端情形也处理了:纬度 clamp 到 ±89.9 防 tan(90°) 除零;极区「月亮不升不落」时 moonriseApprox 返回 null 而非抛错,界面友好提示。
九、运行效果
「今日月相」与「双主题」的效果前面已经看过,这里补另外三个标签页。
百年中秋:1900–2100 共 201 年历表,每年标注公历日期、星期与「十五/十六/十七圆」。

赏月时刻:默认北京纬度 39.9°,给出月升、月上中天、月落时刻与最佳赏月时段。

中秋知多少:科普「为什么十五的月亮有时十六圆、十七圆」。

十、踩坑与收获
-
最大的坑是「想当然」:规格初稿写「中秋当晚判望」,Agent 在自验时发现 2026 年根本不成立——满月在北京时间八月十七凌晨。如果当时偷懒用线性平均模型,就会把今晚误判成「满月」,闹笑话。用真实天文数据当尺子,是这类作品的生命线。
-
Agent 全绿 ≠ 本地验收:Agent 报告「23/23、E2E 19/19」,我仍在本地重新
node --test跑了一遍确认,并用浏览器逐页核对渲染数据(月龄 13.65、照亮 98.3% 与算法自洽)才算交卷。
-
图片分析工具偶发失败:Agent 想做最终视觉复核时
AnalyzeImage工具超时失败了一次,导致界面一度「看似卡住」。此时不能干等,要用git ls-remote看远端提交、用页面快照看工具命令流判断真实进度——本次确认 commit 已推进后才继续。 -
模型要选对人:本轮我特意把码道对话模型切到 GLM-5.2,长链路的算法校准 + 编码 + 测试一气呵成,没有来回返工。
十一、建仓与复现
整个开发过程在码道工作台完成:建仓对话确立仓库归属与可见性,开发对话绑定仓库后由 Agent(GLM-5.2)分块实现,最终本地 HEAD 与远端 main 完全同步。

仓库全程公开,欢迎复现:
# 克隆
git clone https://atomgit.com/sdf56g99988/moonphase-almanac.git
cd moonphase-almanac
# 跑领域层测试(零依赖,Node ≥ 18 即可)
node --test tests/domain.test.mjs
# 直接用浏览器打开(无需任何构建/服务)
# 双击 index.html,或:
python -m http.server 8000 # 然后访问 http://localhost:8000
提交历史干净(feat / test / docs 分离):
15b61f6 chore: init repository (README, MIT license, gitignore)
e4a01eb feat: 领域层算法(Meeus 摄动级数)+ 中秋历表 201 条
bc95ce1 test: 领域层 23 个测试(node:test,零依赖)
41bf396 feat: 四标签页界面 + Canvas 月相盘像素渲染
53c29e2 docs: README(功能/天文公式表/数据来源/运行测试)

十二、五维自检表
| 维度 | 权重 | 本作品对应 |
|---|---|---|
| 架构 | 30% | 领域层纯函数 / 数据层 / 表现层三层分离,UMD 双端通用,deepFreeze 不可变导出 |
| 代码 | 30% | Meeus 级数真实算法 + 逐像素 Canvas 光照渲染,注释即文档,feat/test/docs 提交分离 |
| 安全 | 20% | 零联网/零存储/零依赖/零外链,sanitizeNumber 统一入口防护,极区/非法输入不抛错 |
| UI/UX | 15% | 四标签页 + 夜/笺双主题 + Canvas 月相盘 + 响应式,赏月时段给出人话理由 |
| 博文 | 5% | 本篇:提示词→架构→算法→代码→测试→踩坑→复现全链路 |
结语
中秋节最浪漫的事,大概是「知道月亮什么时候最圆」。这个作品没有用一张月亮贴图,而是把 400 多年前的开普勒式思考、浓缩进 Meeus 的级数里,让浏览器现场把月亮算出来、画出来。
而整个从提示词到交付的过程,代码智能体(GLM-5.2)承担了繁重的实现与自验,我只需要守住三件事:定架构、立红线、做验收。这大概就是「AI 编程」最舒服的姿势——人是导演,AI 是执行力最强的团队。
今晚月亮 98.3% 圆,明晚才是 100%。但没关系——十五的月亮十七圆,也算是一种圆满。
中秋快乐。
更多推荐




所有评论(0)