鸿蒙系列开发之看不见流星雨几颗?我用华为云码道把猎户座流星雨观测台做成了鸿蒙应用
鸿蒙系列开发之看不见流星雨几颗我用华为云码道把猎户座流星雨观测台做成了鸿蒙应用10 月 21 日深夜猎户座流星雨迎来极大。它的母体是哈雷彗星留下的尘埃带辐射点落在猎户座赤经 95°、赤纬 16°极大夜的天顶每小时流量大约二十来颗——不是英仙座那种「满天都是」的场面更像一场要耐心守的小雨。天气预报只会说晴或多云不会回答「你今晚能看到几颗」。那个数取决于四件事你站在哪一纬度、辐射点升到多高、月亮亮到什么程度、头顶的天空黑不黑。我把这道估算题做成一个鸿蒙应用猎户座流星雨观测台 orion-lab。它不联网、不读定位、不拉任何预报接口打开就是一块能拖的观测台纬度、经度、时刻、云量、光污染五样输入出口是「每小时预计可见流星数」和一句观测建议。开发全程交给华为云码道 CodeArts模型用的是GLM-5.2范式是 ArkTS ArkUI 声明式。我只负责把辐射点坐标、ZHR、月相公式和禁令写死把仓库拉回本地点一遍。仓库公开https://atomgit.com/2601_95131637/orion-lab协议MIT运行DevEco Studio 5.0 打开工程自动签名后运行到模拟器或真机测试本地复跑22/22 通过HEADbbde236权限requestPermissions: []一个都不申请网络零联网无远程资源作品介绍把「今晚能看几颗」算清楚打开应用就能做这几件事。每一项都对应公式链上的一个输入没有装饰性控件观测条件输入。纬度滑杆覆盖北纬 0–60°经度滑杆覆盖东经 73–135°正好框住中国大陆的教学域不必处理南半球和西经的符号约定。观测时刻从 10 月 21 日 18:00 拖到次日 06:00步进 30 分钟——流星雨是整夜的事半小时一档足够看出辐射点从地平线下爬上来的过程又不会让滑杆变成无意义的连续条。云量四档点选。晴、少云、多云、阴对应折减系数 0、0.25、0.6、1.0。云不是连续变量人眼也分不出「0.37 的云」所以做成离散档而不是再加一根滑杆。波特尔光污染九级点选。从 1 级的乡村暗夜极限星等 6.8到 9 级的市中心3.7 等每一级写死一张映射表。城市里站在路灯下和郊区山脊上看到的不是同一场雨。弧形仰角面板。Canvas 自绘一条地平线和辐射点走过的弧。辐射点还在地平线下时不画星点只写「未升起」——看不见就是看不见不拿一条贴着地平线的弧假装它已经出来了。结果卡。大字是每小时预计可见流星数下面并列辐射点仰角、月亮照亮比例、月相名称再跟一句观测建议。建议只有四档辐射点未升起、月光过强、光污染过重、条件良好优先级按这个顺序先说最致命的那一条。零权限启动。打开应用不会弹任何授权框因为module.json5的requestPermissions是空数组。估算要用的数全是用户自己拨出来的。整个工程的分层一眼能看完entry/src/main/ets/ ├── domain/ # orion.ets常量表 恒星时 仰角 月相 流量估算纯函数 ├── security/ # sanitize.ets数值夹取 档位分流 非有限数回退纯函数 ├── ui/ # SkyPanel / ConditionList / ResultCard 三个组件 ├── pages/ # Index.ets状态持有与计算链装配 └── entryability/ # EntryAbility默认入口domain和security是纯函数不碰任何ohos能力ui和pages只负责把算好的数摆上屏幕。规则集中在纯函数里评审想核对「阴天是不是一定归零」读测试就够不必先把模拟器拉起来。页脚和 README 同一句免责教学近似模型ZHR、辐射点与月相均取教学近似值不替代 IMO 官方预报夜间户外观测注意安全、保暖、结伴远离公路与水域。这句话不是装饰。流星计数本身就是概率模型给出的是「值不值得出门」的量级不是一张可以拿去和目视记录逐颗对账的预报单。应用在 DevEco Studio 里自动签名后一键运行到模拟器首屏实拍如下首屏即工作台弧形面板里能看到辐射点相对地平线的位置三根滑杆分别控制纬度、经度、观测时刻云量与波特尔档位点选——它们全部是领域层公式的输入。后面的实拍会跟着代码走公式链的出口看结果卡档位的联动看安全净化层建仓与开发对话看开发过程一节。领域模型一条五步计算链这个应用的本质是一条公式链。链上每一个天文参数都写成常量不留「运行时再查一张表」的口子——查来的数进不了测试写死的数可以。exportconstRADIANT_RA_DEG:number95.0// 辐射点赤经exportconstRADIANT_DEC_DEG:number16.0// 辐射点赤纬exportconstZHR_MAX:number25// 天顶每小时流量exportconstPOPULATION_INDEX_R:number2.5// 星群指数exportconstREFERENCE_NEW_MOON_JULIAN_DAY:number2451550.1// 参考新月exportconstSYNODIC_MONTH_DAYS:number29.530588853// 朔望月exportconstJ2000_JULIAN_DAY:number2451545.0// J2000 历元exportconstMAX_HOURLY_RATE:number500// 输出上限这八个数各管一件事。辐射点赤经赤纬钉死猎户座ZHR 取教学值 25表示理想暗夜、辐射点在天顶时的每小时流量星群指数 2.5 决定天空每亮一等暗流星掉得有多快参考新月儒略日和朔望月长度用来推月相J2000 是恒星时公式的零点500 是输出上限防止极端组合算出一个没人信的天文数字。从观测时刻到流星数五步走。每一步都是纯函数输入输出写在签名上中间不藏状态。第一步把滑杆上的本地时刻换成自 J2000 起的天数d。观测夜固定在 2026-10-21 这一晚东八区 18:00 到次日 06:00。滑杆用「18 到 30」这一个数表达跨夜21 是当晚九点30 是次日六点。函数内部先减 8 小时得到世界时再接到极大夜 18:00 对应的儒略日上。不让用户选日期是因为这篇作品只回答「极大夜这一晚」不是做一本万年历。/* 本地东八区观测时刻 - 儒略日。 * localTotalHours: 当晚 18:00值 18到次日 06:00值 30。 */exportfunctionjulianDayFromLocalHours(localTotalHours:number):number{consthfiniteOr(localTotalHours,(LOCAL_HOUR_MINLOCAL_HOUR_MAX)/2)constutHoursh-OBSERVATION_UTC_OFFSET_HOURSreturnOBSERVATION_BASE_JD_LOCAL18(utHours-10)/24}exportfunctiondaysFromJ2000(julianDay:number):number{returnfiniteOr(julianDay,J2000_JULIAN_DAY)-J2000_JULIAN_DAY}滑杆拖过午夜时界面上的「当晚」会换成「次日」儒略日连续增加不会在 24 点处跳回前一天。非有限输入回退到区间中点不把 NaN 放进后面的三角公式。第二步平恒星时。恒星时回答的是「此刻哪一片天正对着你」。它是一个只含乘加和取模的线性式系数 360.98564736629 是地球相对恒星背景每天多转出来的那一度多一点经度直接加进去东经为正所以同一时刻越往东猎户座升得越早/* 平恒星时度。LST (280.46061837 360.98564736629 * d 经度) mod 360 */exportfunctionlocalSiderealTime(d:number,longitudeDeg:number):number{constddfiniteOr(d,0)constlngfiniteOr(longitudeDeg,0)returnmod360(280.46061837360.98564736629*ddlng)}取模收进 0–360避免天数一大角度就漂出圆周。d 0、经度 0 时这个函数必须给出 280.46061837——这条写进了测试系数抄错一位断言立刻红。第三步辐射点仰角的正弦。球面三角压成一行sin(alt) sin(lat)·sin(dec) cos(lat)·cos(dec)·cos(LST − RA)。正弦而不是角度本身是因为下一步的流量公式吃的就是sin(alt)辐射点在天顶时因子为 1贴着地平线时接近 0落到地平线下则为负。输出用clampUnit钉在 [-1, 1]浮点噪声不会把 1.0000002 送进后面的乘法exportfunctionsinRadiantAltitude(d:number,latitudeDeg:number,longitudeDeg:number):number{constddfiniteOr(d,0)constlatDegfiniteOr(latitudeDeg,30)constlngDegfiniteOr(longitudeDeg,0)constlstRadlocalSiderealTime(dd,lngDeg)*DEG_TO_RADconstlatRadlatDeg*DEG_TO_RADconstdecRadRADIANT_DEC_DEG*DEG_TO_RADconstraRadRADIANT_RA_DEG*DEG_TO_RADconstsMath.sin(latRad)*Math.sin(decRad)Math.cos(latRad)*Math.cos(decRad)*Math.cos(lstRad-raRad)returnclampUnit(s,0)}纬度默认回退 30°、经度回退 0°都是有限数兜底不是业务默认值——业务默认值在页面状态里北纬 30°、东经 105°净化和兜底是两层事。第四步月相。以 2000 年 1 月 6 日那次新月为参考历元相位是一个 0 到 1 的小数0 附近新月0.5 满月再绕回 0。负余数要加 1 拉回正区间否则月相会在跨周期时跳出 [0, 1]。照亮比例用余弦(1 − cos(2π·phase)) / 2新月为 0满月为 1形状对称不需要再维护一张月相名称表/* 月相 phase ((d − 4.9) / 29.530588853) mod 1取正余数 */exportfunctionmoonPhase(d:number):number{constddfiniteOr(d,MOON_PHASE_OFFSET_DAYS)constv((dd-MOON_PHASE_OFFSET_DAYS)/SYNODIC_MONTH_DAYS)%1if(v0){returnv1}returnv}/* 照亮比例 illum (1 − cos(2π·phase)) / 2钳制在 [0, 1]。 */exportfunctionmoonIllumination(phase:number):number{constpfiniteOr(phase,0)constwrappedp-Math.floor(p)constillum(1-Math.cos(2*Math.PI*wrapped))/2returnclampFraction(illum)}月相名称是照亮比例的旁路不参与乘法接近 0 或 1 的两端显示「新月」或「满月」中间一律「峨眉/盈凸」。教学模型不区分上弦下弦因为流量公式只关心有多亮不关心亮的是左边还是右边。第五步全部乘起来。这是整条链的出口也是界面上那个大数字的唯一来源/* HR ZHR × max(0, sin(alt)) × r^(6.5 − LM) × (1 − 0.9×illum²) × (1 − cloud) * alt ≤ 0 → HR 0结果钳制 [0, 500]非有限输入各自回退默认值。 */exportfunctionhourlyRate(altSin:number,lmLimit:number,illum:number,cloud:number):number{constaltfiniteOr(altSin,0)if(alt0){return0}constlmfiniteOr(lmLimit,6.5)consticlampFraction(illum)constcclampFraction(cloud)constaltFactorclampNumber(alt,0,1,0)constlightFactorMath.pow(POPULATION_INDEX_R,6.5-lm)constmoonFactor1-0.9*i*iconstcloudFactor1-cconsthrZHR_MAX*altFactor*lightFactor*moonFactor*cloudFactorreturnclampNumber(hr,0,MAX_HOURLY_RATE,0)}这个公式里有三个因子值得单独说外加一条提前返回。仰角因子就是sin(alt)而且先看符号小于等于 0 直接返回 0。地平线下的流星对观测者来说不存在不是负数也不是一个「待会就有」的预告。光污染因子r^(6.5 − LM)以 6.5 等为基准LM 6.5 时因子恰为 1。指数是6.5 − LM极限星等每低 1 等因子就乘上一个 2.5——LM 5.5 得到 2.5LM 4.5 得到 6.25。这两条不是口头估计是手算尺子和Math.pow(2.5, 6.5 - lm)逐字对应。月光因子1 − 0.9×illum²用平方而不是线性因为月光的干扰在接近满月时才陡增新月零损耗半月只削去两成出头满月则只留下一成。云量因子1 − cloud最干脆阴天系数是 1整项变 0后面乘什么都没用。四个因子相乘之后钳进 [0, 500]。上限不是物理定律是展示纪律教学组合就算漂了界面上也不出现三位数以上的「每小时可见」。建议不走这条乘法单独由adviceLevel给出四档、三条阈值、固定优先级仰角正弦 ≤ 0 → 「辐射点未升起」。流星还没进视野谈月光和光污染没有意义。否则月照亮比例 0.4 → 「月光过强」。实拍里 75% 就落在这一档文案是「会压住暗流星建议选择下半夜或月落后的窗口」。否则极限星等 5.0 → 「光污染过重」建议换更暗的郊区或山野。三条都没触发 → 「条件良好」。优先级写死为「未升起 月光 光污染」。一条建议只说当前最致命的那件事不把四句话叠在一起。这条公式链的实拍出口就是结果卡。纬度 55°N、东经 93°、次日 04:30、少云、波特尔 5 级辐射点已升到 46.4°月照亮 75%峨眉/盈凸每小时预计可见 20 颗。20 不是把 ZHR 原样贴上去而是 25 与仰角正弦、波特尔 5 级对应的 5.3 等、75% 月光、0.25 云量五项乘完再取整的结果。观测建议落在月光档——大数字和下面那句话来自同一组输入不是两套各说各话的文案。手算尺子九条断言钉死公式公式写完不算完。模型最容易做的事是写出一个「看起来像天文学」的函数再配几条四舍五入也能过的断言。所以尺子必须先于实现写死每条都是一个可以手算的数容差只留给周期回绕那种必然有浮点尾数的地方其余用严格相等。辐射点仰角alt ≤ 0→ HR 0。地平线下就是看不见不是负数。sin(alt) 1、LM 6.5、illum 0、cloud 0→ HR 25ZHR 全额。LM 5.5→ 光污染因子 2.5LM 4.5→ 6.25。满月illum 1→ 月光因子 0.1。cloud 1阴天→ HR 0。illum 0新月→ 月光因子 1phase 0.5→ illum 1。恒星时尺子d 0、经度 0 → LST 280.46061837 mod 360。月相周期phase(d 29.530588853) ≈ phase(d)±1e-6。对任意输入随机扫描HR ∈ [0, 500]、illum ∈ [0, 1]、sin(alt) ∈ [−1, 1]输出有限不抛错。前六条钉的是出口后三条钉的是中间齿轮。恒星时那条尤其较真常数 280.46061837 必须原样出现少抄一位小数整晚的辐射点都会偏出几度而界面上的仰角看起来「还是那么回事」。月相周期则防止有人用 29.5 这种两位小数代替朔望月——差一晚极大夜的月相就能从峨眉滑到另一个名字。随机扫描不验证对不对只验证「乱输入也不会炸」NaN、无穷、负数纬度进得去出得来的仍是有限数。安全净化层档位分流非有限数回退和上一篇一样「人拨出来的数」和「领域层吃进去的数」之间隔着一道闸机。滑杆和点选在界面上看起来总是合法的但领域函数会被测试拿去喂 NaN、无穷和越界索引。闸机的职责就是有限数夹进教学域非有限数退回一个事先写明的默认值离散档位则走最近邻而不是四舍五入到不存在的第 5 档。这次比风速多出来的是档位分流。云量和波特尔都是离散的选档映射必须双向一致——从系数找回档位再从档位取出系数必须回到原点/* 云量档位0晴 1少云 2多云 3阴 */exportfunctioncloudIndexOf(cloudFactor:number):number{constffiniteOr(cloudFactor,CLOUD_LEVELS[0])letnearest0letbestDistanceNumber.MAX_VALUEfor(leti0;iCLOUD_LEVELS.length;i){constdistanceMath.abs(CLOUD_LEVELS[i]-f)if(distancebestDistance){bestDistancedistance nearesti}}returnnearest}exportfunctioncloudFactorAt(index:number):number{constiMath.round(finiteOr(index,0))if(i0){returnCLOUD_LEVELS[0]}if(iCLOUD_LEVELS.length){returnCLOUD_LEVELS[CLOUD_LEVELS.length-1]}returnCLOUD_LEVELS[i]}cloudIndexOf不设阈值表而是逐档算绝对距离、取最近的一档。0.1 仍是晴0.4 更靠近少云的 0.25 还是多云的 0.6由距离说了算不靠注释里的「大约」。cloudFactorAt按索引取值负数退回晴超出末档退回阴。两个函数互为逆运算表里每一个系数进来出去还是它自己。这条性质写进测试之后有人把 0.6 改成 0.55往返断言会立刻红。纬度、经度、时刻、云量、照亮的夹取全部收敛到一个clampNumber(value, lo, hi, fallback)。有限数夹取非有限数不夹取、直接回退。于是领域层对任意输入都输出有限数绝不抛错——抛错会变成界面上的白屏而白屏不是一种观测建议。档位语义在模拟器上按一下就能复现云量切到「阴」cloud 1.0的瞬间cloudFactor 0任意仰角与星等下每小时预计流星数直接归零——阴天就是看不见公式直接说话。这正是手算尺子里cloud 1 → HR 0那条断言的实机版本。波特尔等级到极限星等也是一张写死的表不接外部星空亮度接口。1 级 6.8 等到 9 级 3.7 等中间按肉眼可感的台阶下降不是等差数列——从 3 级到 4 级掉 0.5 等从 1 级到 2 级只掉 0.2 等因为暗夜那一端人眼已经接近极限城市这一端则掉得更快。越界不外推回退到 5 级的 5.3 等也就是「城乡结合部」这一档既不明亮得离谱也不假装用户站在荒漠里constBORTLE_LM_TABLE:ArrayArraynumber[[1,6.8],[2,6.6],[3,6.3],[4,5.8],[5,5.3],[6,5.0],[7,4.6],[8,4.2],[9,3.7]]主页面五个状态一条派生链pages/Index.ets是装配层不是第二套天文引擎。它只持有五个State纬度、经度、时刻、云量档、波特尔档。仰角、月相、极限星等、每小时流星数全部是派生方法输入一变就整链重算没有任何一份「算好了再缓存」的副本。缓存一旦存在滑杆和结果卡就会各说各话。EntryComponentstruct Index{Statelatitude:number30Statelongitude:number105StatelocalHour:number21StatecloudIndex:number0StatebortleIndex:number4privatecurrentAltSin():number{constlatclampLatitude(this.latitude)constlngclampLongitude(this.longitude)returnsinRadiantAltitude(this.currentDays(),lat,lng)}privatecurrentIllum():number{returnmoonIllumination(moonPhase(this.currentDays()))}privatecurrentLm():number{returnbortleToLm(bortleLevelAt(this.bortleIndex))}privatecurrentHourlyRate():number{constcloudcloudFactorAt(this.cloudIndex)returnhourlyRate(this.currentAltSin(),this.currentLm(),this.currentIllum(),cloud)}// build() 里滑杆变 → 派生方法重算 → 面板与结果卡自动跟上}默认值选在北纬 30°、东经 105°、当晚 21:00、晴、波特尔第 5 档。这是中国腹地、天刚黑、还没被云和灯光为难的一个起点打开应用不用先拨半天才能看到一张有内容的结果卡。时刻越过 24 之后页眉从「当晚」换成「次日」时钟用hour % 24显示内部仍保留 24 以上的连续值儒略日不会在午夜被重置。任何输入变化都会触发整条链重算。链上每一步都是纯函数一次拖动的成本可以忽略因此没有节流、没有「松手再算」。这里有一条命名纪律写在提示词里也写在组件上。自定义组件的Prop不要叫scale、opacity这类名字——它们与 ArkUI 内置通用属性方法重名编译器会按通用属性的类型签名去校验报错还不太指向根因。仰角面板的属性因此叫radiantAltSin。同理Canvas 的对齐枚举是类型不是值ctx.textAlign只能赋字符串字面量center不能写CanvasTextAlign.CENTER。这两条提前写明比等编译器指出来再改一轮便宜。开发过程两条对话从空仓到 HEAD码道上的开发和上一篇一样故意拆成两条对话。建仓那一轮会把认证、克隆、推送的上下文吃掉一块如果接着在同一条对话里写领域层后面的公式和尺子就挤在一条越来越长的历史里。第一条对话因此只做一件事——建空仓不写业务代码仓库名orion-lab可见性 Public初始化 README、MIT、.gitignore本地路径/workspace/orion-lab建好后把仓库 URL 发回来码道先查ag auth status确认认证是2601_95131637再ag repo create建仓、克隆到本地、补齐三个初始化文件、git push -u origin main全程 1 分 22 秒仓库地址https://atomgit.com/2601_95131637/orion-lab空仓的意义是给第二条对话一个干净的main。README、协议和忽略规则已经在远端开发对话不用再花回合解释「仓库长什么样」。第二条对话新建绑定2601_95131637/orion-lab的main模型选GLM-5.2。提示词一次写死五块主题与边界只做极大夜、不联网、不申请权限、领域常量与五步函数签名、九条手算尺子、分层目录和 ArkTS 语法红线、测试命令与提交要求。能手算的数不留给模型「查一下」能编译期拦住的写法提前列成禁令。模型照单执行自由发挥的空间只剩变量名和注释措辞权限与资源零权限的合规起点module.json5里最值钱的一行是空数组{ module: { name: entry, type: entry, deviceTypes: [phone, tablet], requestPermissions: [], abilities: [ /* EntryAbilityentity.system.home */ ] } }鸿蒙的权限模型是「写进清单就会在首次使用时面对用户」。定位、网络、存储这个应用一个都用不上纬度经度是滑杆时刻是滑杆云量和星等是点选。真去申请LOCATION用户看到的第一屏会是授权框而不是辐射点。评审打开module.json5看见[]比文章里写十句「注重隐私」都直接。中文文案全部收在resources/base/element/string.json。四档建议、月相三个名字、「辐射点已升起 / 未升起」、「颗/小时」组件里只有$r(app.string.xxx)没有一处拼接用户输入。色板收在color.json深空蓝黑是默认主题深色模式靠resources/dark/element/下同名文件换装键名不动组件里没有if (isDark)。想加一版英文界面只需要补en_US目录计算链一行都不用改。测试还是那条「测试对象发布对象」路线领域层是.ets文件node 不认识这个扩展名但文件内容是 TypeScript 的一个子集没有装饰器没有 ArkUI只有函数和常量。与其让模型再抄一份「测试专用」的逻辑不如把发布文件换个扩展名直接跑。转译脚本因此只做两件事——复制以及给相对 import 补上.tsconstsources[{src:path.join(root,entry,src,main,ets,domain,orion.ets),out:path.join(outDir,domain,orion.ts)},{src:path.join(root,entry,src,main,ets,security,sanitize.ets),out:path.join(outDir,security,sanitize.ts)}];functionrewriteImports(code){returncode.replace(/(\b(?:from|import)\s(?:type\s)?[])(\.[^]?)([])/g,(match,head,target,tail){if(target.endsWith(.ts)||target.endsWith(.ets)||target.endsWith(.json)){returnheadtargettail;}returnheadtarget.tstail;});}正则只动from/import后面的相对路径其余字符原样落地。tests/generated/进了.gitignore生成物不进版本库避免「仓库里有两份领域层、不知该信哪份」。22 个用例覆盖四类手算尺子的全部条目、波特尔九级逐级核对、月相走过一个朔望月后回到同一相位、随机样本的有限性扫描。测试命令零第三方依赖DevEco 自带的 Node 就够nodetests/prepare.mjsnode--test--experimental-strip-types tests/cases/码道汇报 22 通过、提交7787a12。开发对话里领域层、组件、资源、测试逐项落地任务列表从多项待处理一路清零我把仓库克隆回本地DevEco 自带 Node v24复跑结果ℹ tests 22 ℹ pass 22 ℹ fail 0与汇报一致。随后本地用 DevEco 的 hvigor 命令行执行assembleHapArkTS 严格模式编译通过应用打包成功。收尾时 GLM-5.2 还做了一轮核验工作区与远程 HEAD 对齐、as string之类类型断言残留逐项 grep 清零确认没有暗雷才交付写提示词时真正有用的几条先建空仓再开新对话写代码。建仓那条上下文会消耗掉一部分开发需求绑到仓库的main不要在同一条对话里接着写。天文常数全部写死。辐射点坐标、ZHR、星群指数、参考新月历元、朔望月长度一个都不让模型自己查——它查来的数没法验写死的数可以进测试。公式链条拆成纯函数列出来。恒星时、仰角、月相、流量各一步每步的输入输出写进提示词模型只会照着搭不会自作主张合并。尺子写进提示词也写进测试。「地平线下 HR0」如果只是一句文案模型可能输出负数。hourlyRate(-0.5, 6.5, 0, 0) 0必须出现在断言里。档位映射要求双向一致。云量「选档→系数」「系数→选档」互逆这条性质直接变成测试档位改了测试立刻红。ArkTS 语法红线提前列。不用 any/unknown、不用展开运算符进对象字面量、组件属性避开内置通用属性名、Canvas 枚举是类型不能当值——提前写明编译期零意外。文案和色值全部资源化。提示词里写明「中文文案进string.json、颜色进color.json、深浅色两套」深色模式和未来国际化都省掉大半工作量。让测试对象等于发布对象。最小转译脚本把.ets换名喂给 node这条路线连续两篇验证有效零第三方依赖。边界流星雨的流量预报是概率不是点名。ZHR 定义在理想暗夜、辐射点位于天顶、观测者持续注视的条件下真到了野外一阵云、一次低头看手机、一颗被树梢挡住的亮流星都会让「数出来的颗数」和「公式给的颗数」对不上。这个应用把 ZHR 取成常数 25月相取平均朔望月月光和云量用教学形式的乘子经纬度也框在北纬 0–60°、东经 73–135°。它回答的是极大夜这一晚、这一块教学域里的量级不替代国际流星组织的实时预报也不外推到南半球或西经。和目视记录对不上不是实现失误是模型到此为止。页脚那句「夜间户外观测请注意安全、保暖并结伴出行远离公路与水域」写的是另一件更具体的事算完值得出门路还是要自己看。流星雨的浪漫在于它不好提前数清。这个应用只是把「值不值得出门」拆成纬度、时刻、云量和月光四道可以拨动的题。剩下的抬头。总结这次参赛延续并强化了前几篇沉淀的 AI 开发工作流。码道负责写代码我负责把规则写死天文常数、公式链、手算尺子、ArkTS 语法红线在提示词里一次钉死GLM-5.2 照单执行建仓 1 分 22 秒开发对话一次跑通测试与提交。领域层 196 行纯函数承载全部天文计算安全净化层负责档位分流与非有限数回退UI 层五个状态撑起整条派生链——测试不启动模拟器22 个用例与发布代码同源。本地复跑 22/22、hvigor 命令行编译通过全链路可验证。