alerts —— 提醒与后台任务
读者:做工具的用户,和替他干活的 AI。两者读同一份。
目标
给包加一条提醒(到点或条件成立时给用户发一条通知)或一条后台任务(不发通知,定期跑一趟把结果写进本包的 data.*,由组件和页面下次渲染时读)。
两者是同一种文件:xJob/<id>.xjob。一个 Job 就是一个没有画面的组件 —— 外壳(title / sub / i18n / params / form / events)照 .xwidget 来,里面装两块配方:task(跑什么)和 alert(跑完发什么)。
前置
- 包已经能跑(
numable check在personal档零 error)。 - 提醒要判条件的话,包里已经有一条取数流,而且它
resultFilter.keys里真的透出了你要判的那个键 —— 提醒直接复用它,不另写一份。取数流怎么写见numable docs df。
步骤 1 · 先认三种形状
没有 type 字段。 型是组合出来的,check 和运行时都按同一条判据认:
有 alert 吗 |
task 里有 depends 吗 |
这是什么 | 它怎么知道该响 |
|---|---|---|---|
| 有 | 没有 | 静态提醒 | 用户创建那一刻,发什么、什么时候发就全定死了,交给系统定时 |
| 有 | 有 | 动态提醒 | 到点先取数,再判条件,判「真」才发 |
| 没有 | 有 | 后台任务 | 到点取数,把结果写进 data.*,不发任何通知 |
判据两句话:要不要通知? 不要 → 只写 task。要 → 写 alert;发不发、发什么,在创建那一刻能不能完全确定? 能 → task 里只写 refresh;不能 → task 里写 depends。
task 与组件的 canvas 同形:canvas 是 {source, depends, refresh},task 是 {depends, then, refresh} —— 去掉画法、加上一段跑完之后的写入。会写组件就会写 Job,depends 的契约逐字相同(各槽独立、按声明顺序合并、入参只从调用点取、看不见别的槽的输出)。
最小写法
// 静态提醒:每天 8 点(3 个键。task 里只有 refresh = 没有数据可判,到点就发)
{
"title": "喝水",
"task": { "refresh": { "at": ["08:00"] } },
"alert": { "message": { "title": "该喝水了", "body": "一杯 250ml" } }
}
// 动态提醒:有没处理完的事就提醒(5 个键。不新写取数流 —— depends 直接引组件已有的那条)
{
"title": "待处理",
"task": { "depends": [{ "flow": "@[file://xWidget/flow/todo.df]" }],
"refresh": { "interval": ["1800"] } },
"alert": { "activeCondition": "$[gt::(${n},0)]",
"message": { "title": "有 ${n} 件事等你", "body": "点开处理" } }
}
// 后台任务:每天记一个值,不通知(4 个键。没有 alert 块 = 不响)
{
"title": "记录每日金价",
"task": { "depends": [{ "flow": "@[file://xWidget/flow/range.df]" }],
"then": { "flow": "@[file://xJob/flow/record.df]", "params": { "cur": "${cur}" } },
"refresh": { "at": ["23:55"] } }
}
住 xJob/<id>.xjob,与 xWidget/ 平级;manifest 不用登记,放进目录就算数。depends 引组件的取数流(只读),then 那一段的取数流住 xJob/flow/。.xjob 里 @[file://…] 的路径从包根算起,和 .xwidget 里从 xWidget/ 算起不一样 —— 写错的现象是解析成空,取数根本不发,而且不报错。
步骤 2 · 用 init 生成骨架
命令(在包目录里跑):
numable init --job price --kind cross
--kind 是模板名,生成的文件里没有它。六种:
--kind |
生成 | 什么时候用 |
|---|---|---|
static |
xJob/<id>.xjob |
到点就发,不取数 |
once |
xJob/<id>.xjob |
选一个日期和时间,只响一次(见下文「只响一次的提醒」) |
cross |
xJob/<id>.xjob |
越过你设的那条线时发一次(判定用内置配方,见步骤 4) |
level |
xJob/<id>.xjob |
条件持续为真时每隔一段时间发一次 |
changed |
xJob/<id>.xjob |
值和上次不一样就发(判定用内置配方,见步骤 4) |
task |
.xjob + xJob/flow/<id>.df |
不发通知,产出写进 data.* |
看到什么算对:列出写了哪几个文件,加三条待办。文件已经存在时它什么都不写、直接报错 —— 不会覆盖你改过的东西。裸字段用的语言跟包的 lang 走,--lang 可以现场指定。
生成之后必做一件事:把 task.depends 的 flow 换成本包真有的那条取数流。manifest.minEngine 由 init --job 自动抬到这类提醒要求的引擎版本(越线 / 数值变化 4,一次性提醒 3,其余 2;已经够高就不动),输出里会写出改了什么。以后要是手动改低了,check 会报 G45 —— 版本低了的现象是:装在旧版应用上,这条提醒只是不响,一条错都不报。
也可以在桌面 App 的工作台里做(Mac / Windows):打开这个包,左侧树里「提醒与任务」一组就是 xJob/ 下的文件,「新建提醒 / 任务…」生成的骨架与 init --job 是同一份模板,也会自动抬 minEngine。编辑器把 .xjob 拆成表单逐段填(判定方式可直接选「越线」「超出阈值」「数值变化」配方),旁边实时预览用户会看到的同意面板、通知和接下来的触发时间;点「试运行」真跑一次取数与判定,结果代入预览。改的是同一个 .xjob 文件,CLI 与编辑器可以交替用。
步骤 3 · 逐字段写
外壳
| 字段 | 静态 | 动态 | 任务 | 类型 / 一句注意 |
|---|---|---|---|---|
version |
否 | 否 | 否 | int,缺省 1 |
id |
否 | 否 | 否 | [a-z0-9-],缺省 = 文件名。别改名:用户建的每一份都按「包 + 规则 id + 参数」记账,换个拼法就是另一条规则,老的那些连同用户填过的东西一起成了孤儿 |
title |
是 | 是 | 是 | 创建面板、提醒管理页、长按组件的菜单都拿它当这条规则的名字 |
sub |
否 | 否 | 否 | 一行副标题 |
i18n |
建议 | 建议 | 建议 | 一层覆盖表,只认四个槽,见步骤 5 |
params |
否 | 否 | 禁 | 默认值表,值恒字符串(数字也写成串);"" = 必填,用户不填不能确认 |
form |
否 | 否 | 禁 | 创建面板字段区的描述,键必须 ⊆ params;字段类型与属性见 numable docs params。不写就一律渲成文本框 |
events.onClick |
否 | 否 | 禁 | 点通知打开什么(包内路由或 numable:// 串),缺省 = 打开包首页。只走导航,不派发交互流 —— 点通知是冷启动,没有流的上下文 |
「禁」= 后台任务里写了就是 error(G45):任务是包级的,一个包里只有一份,没有用户输入、没有点击。
阈值类参数默认给空,别给一个「合理数字」:你猜的 5000 对盯着 4500 的人是误导。默认值也不本地化。
task:跑什么
| 字段 | 静态 | 动态 | 任务 | 一句注意 |
|---|---|---|---|---|
depends |
禁 | 是 | 是 | 取数槽数组,与组件 canvas.depends 逐字同契约,只接 .df。写了就不再是静态提醒 |
then |
禁 | 否 | 是 | 二选一:内置判定配方 {recipe, …}(只用于提醒,见步骤 4),或一个 {flow, params} 绑定(depends 合并完之后跑,写状态只能在这一段) |
refresh |
是 | 是 | 是 | 见下。缺了它这条规则永远不会被评估(G45) |
alert:跑完发什么
| 字段 | 静态 | 动态 | 任务 | 一句注意 |
|---|---|---|---|---|
message |
是 | 是 | — | {title, body?}。静态提醒的 message 只能引用 params 与 @app —— 创建那一刻别的键根本不存在,引了渲出来是空(G45) |
activeCondition |
禁 | 否 | — | 表达式,结果字面为 1 / true 才算成立。省略 = $[eq::(${hit},1)],读 then 输出的 hit |
level |
否 | 否 | — | quiet / normal / urgent,缺省 normal。作者给默认档,用户可以按自己那份改,但只能压低不能抬高 |
alert 只认这三个键,多写一个就是 error。别把一个包里的提醒全标 urgent:分档是留给真的不能等的那一条的,一包全 urgent 等于没有分档,用户多半把这个包的通知整体关掉,那时真急的那条也一起哑了(发布档会警告)。
条件写不出来时不要硬拗表达式:把判断挪进 then 那条取数流,让它输出一个 hit。
refresh:节律与冷却
interval / at / tz 的写法与组件完全一样(见 numable docs xwidget),外加两个只有 Job 才有的键:
| 键 | 值 | 语义 |
|---|---|---|
cooldown |
秒 | 命中之后冷却:这一次判「真」(任务是成功写入)之后,cooldown 秒内整条规则不再评估 —— 连 then 都不跑。缺省 0 |
days |
["mon"…"sun"] |
按星期几过滤 at,缺省每天。只有静态提醒能用 |
只有静态提醒的 at 里能写 ${参数}(「用户设几点就几点」)。动态的节律归平台管,写了参数就是 error(G45)。
冷却只有这一种语义。要「照常评估但先别响」,在 then 里用 data.* 记下上次响的时刻自己节流。
只响一次的提醒:日期式 at
「10 月 1 日 9 点提醒我交房租」不用改成动态提醒去轮询 —— 把静态提醒的 at 写成日期式 YYYY-MM-DD HH:MM,它就只响一次:
{
"title": "到时提醒我",
"params": { "what": "", "date": "", "time": "09:00" },
"form": {
"what": { "title": "提醒我做什么", "component": { "type": "textInput" } },
"date": { "title": "日期", "component": { "type": "datePicker", "props": { "format": "YYYY-MM-DD" } } },
"time": { "title": "时间", "component": { "type": "timePicker" } }
},
"task": { "refresh": { "at": ["${date} ${time}"] } },
"alert": { "message": { "title": "${what}", "body": "你定在 ${date} ${time}" } }
}
numable init --job <id> --kind once 生成的就是这个骨架。规则:
- 怎么认日期式:
at的一项首尾去掉空白后中间还有空白,就是日期式("${date} ${time}"、"2026-10-01 09:00");每日式HH:MM里永远没有空白。只写一个"${when}"的,按用户填进去的值再判。 - 格式要严:一个半角空格、24 小时制、全部补零,日期得真实存在。
2026-10-1 9:00、2026-02-30 09:00都排不出来。 - 日期没选全就不响,绝不退化成每天响。
date为空时"${date} ${time}"代出来是" 09:00",这一项直接作废;创建面板的主按钮会置灰。选了一个已经过去的时刻也一样,面板下方会提示用户换一个。 - 时区只看
tz:不写 = 跟着手机走(出差换了时区还是当地 9 点,适合待办);写了 = 钉死一个绝对时刻(「纽约时间 8:30 公布」写"tz": "America/New_York")。串里不许带时区偏移。 - 多远都现在就排上:一个月后的也在用户创建那一刻交给系统定时,不受别的节律的时间窗管。
- 只能用在没有
depends的静态提醒里;同一条at不能与每日式混写,也不能再写interval/days—— 这几种写法运行时都不报错,但你得到的一定不是想要的(G45)。 - 响过之后,提醒管理页里这一行写「已结束」、没有开关,过 7 天自动清掉;已结束的不占每个包 20 份的上限。用户改个日期就重新排上。
- 需要
manifest.minEngine至少 3:旧版应用读不懂日期式,那一项会被静默丢掉 —— 装得上、就是不响(G45)。
事情提前办完了(待办勾掉了、改了时间),用下面的 alert.remove 把它撤掉。
步骤 4 · 判定段 then:状态与边沿
先用内置判定配方
「跌破那一下才响」「和上次不一样才响」这两类最常见的判定,不用写判定流程,在 then 里写一个内置配方,由应用执行:
"then": { "recipe": "cross", "value": "${px}", "line": "${price}", "dir": "${dir}" }
"then": { "recipe": "changed", "keys": ["${ver}", "${state}"] }
| 配方 | 字段 | 什么时候触发 |
|---|---|---|
cross 越线 |
value 数值 · line 线 · dir 方向(below 缺省 / above) |
上一次在线的一侧、这一次到了另一侧(below:上次 ≥ 线且这次 < 线) |
changed 数值变化 |
keys:1~4 个值 |
任一值与上一次不同 |
- 每个字段要么整串
${键}(取实例参数与取数输出里的这个键),要么写字面值。不支持表达式。 - 上一次的值由应用按每条提醒分别记录 —— 盯 5000 和盯 4500 的两份提醒各记各的,不用拼状态键。
- 首次运行只记录基准、不触发;这一次取不到值(断网、字段为空)就不比较、也不记录,基准不会丢。
- 两个可选项:
"oncePerDay": true同一天最多触发一次(按refresh.tz,缺省设备时区);"fireOnFirst": true首次运行时已经满足就触发。 - 配方会往作用域里放
hit(0 / 1)和prev(上一次的值),通知里可以写已从 ${prev} 变为 ${px}。另写了activeCondition时,配方与条件都成立才触发。 - 需要
manifest.minEngine ≥ 4(init --job与编辑器会自动抬;旧版应用不认识配方,装得上但永远不响)。配方只用于提醒 —— 后台任务要把结果写进data.*,仍用下面的判定流程。 - 「条件成立就提醒、隔一段时间再提醒」不用配方:直接写
alert.activeCondition(如$[ge::(${chg},${pct})])加refresh.cooldown。
判定更复杂时(要看多个字段的组合、要数新增了几条、要恢复时再报一次),才自己写判定流程,见下文。
自己写判定流程
「跌破那一下才响」这类规则要记住上次的值。记在哪里是这一章最容易出事的地方:
取数那一段不能记。 depends 引的是组件也在用的那条取数流,组件每渲一次就把上次的值顶掉,提醒于是永远看不到跨越。所以分两段:depends 只取数(它自己原有的 data.* 缓存照常,不受限制),then 才写状态。
{
"id": "prevRaw",
"action": "data.get",
"params": { "key": "price.last.${dir}.${price}", "default": "" }
}
状态键必须拼上参数
上面那个键里的 ${dir}.${price} 不是命名习惯,是正确性:
同一条规则可以有好几份(盯 5000 的和盯 4500 的是两份,不同股票又是几份),它们在同一个机会里挨个跑 then。共用一个键的话,第一份写完,第二份读到的「上次值」已经等于这一次的值,谁也看不到跨越;不同对象共用更是直接拿别的东西的价当基线。把这条规则的全部 params 都拼进键,data.get 和 data.set 两处都要改。
内置配方不需要这一步(应用按每条提醒分别记)。自己写判定流程时这条不设 lint —— 静态判不准,只能靠你自己守。
空值守卫
取数失败那一次,值是空的。先立一位「这次到底有没有值」的旗标:
{
"op": "set",
"props": { "key": "hasCur", "value": "$[if::(eq::(findNotEmpty::(${cur},__none__),__none__),0,1)]" }
}
判空别用 length::。 $[if::(gt::(length::(${cur}),0),1,0)] 是手最顺的写法,也是这一章最值得先记住的一个陷阱:length:: 只对字符串、数组、对象有意义,喂给数字一律返回 0。于是值是数字的那一档,旗标恒为 0、守卫恒不通过、data.set 一次都不执行 —— 而取数流照样报成功、日志干净、界面上什么也看不出来。上面那个哨兵写法对字符串、数字、空串、缺键四种都成立;数字 0 在它这里判成「有值」,这是对的。
同一条流里的上次值也一样:它读回来的类型跟着上一次写进去的那个值走,所以 hasPrev 同样不能用 length:: 判。值是不是数字,取决于取数流那一步是怎么算出来的 —— length::(…)、calc::(…) 出来的都是数字,JSON 里的数字字段也是;拿不准就假定它是数字,哨兵写法两种都兜得住。
立好旗标之后,只在真取到值时才写:
{
"op": "if",
"props": { "val": "$[eq::(${hasCur},1)]" },
"items": [{ "action": "data.set", "params": { "key": "price.last.${dir}.${price}", "value": "${cur}" } }]
}
不守的现象是断网那一刻弹一条「金价跌破」——这是最伤信任的一种错。判断也要一起守:上次值为空时 hit 恒 0,于是新建、换设备、重装之后只建基线、不会把当下已经满足条件的提醒一次全响出来。
平台自己还兜一层:取数流失败、或者 activeCondition 引用的键这一次缺失或是空串,判定结果算「不知道」——不响、也不动任何计时。但它兜不住你在 then 里已经写下去的那一笔。
步骤 5 · 双语
.xjob 只有一层 i18n,而且是元数据表(平台读来直接显示的那类):裸字段是基准语言,i18n[locale] 是对可译字段的同形覆盖,只认四个槽 —— title / sub / message / form。别的键写了不读(G47 会警告)。
form 槽里每个字段能覆盖三样东西,位置都和基准语言里的写法一样:
| 覆盖什么 | 写在哪 |
|---|---|
| 字段名 | form.<键>.title |
| 输入框里的提示文字 | form.<键>.component.props.placeholder(或 form.<键>.props.placeholder) |
| 选择类字段的选项文字 | form.<键>.component.props.items,按 value 对齐,只换 label |
{
"title": "金价提醒",
"i18n": {
"en-US": {
"title": "Gold price alert",
"message": { "title": "Gold ${cur}", "body": "Crossed your ${price}" }
}
},
"alert": { "message": { "title": "金价 ${cur}", "body": "已越过你设的 ${price}" } }
}
两件事和内容文案表(.rcn 里那种)不一样,见 numable docs i18n:
- 译文里直接写
${},变量摆在哪儿每种语言自己定,不用拆成一堆 key。平台先按当前语言挑好整份message,再代入这一次的数据。 - 因此译文的
${}引用集必须和基准语言逐字相同:翻译时把${cur}写丢或写错,那一处渲出来是空,而通知已经发出去了,事后无从复现。这条是 error(G47)。
选项译文只换文字、不换选项:译文里多写的 value 不会变成新选项,少写的那项回落基准语言的 label。选项集合是参数契约(用户建的每一份按参数记账),不该随语言变。
{
"params": { "dir": "down" },
"form": { "dir": { "title": "方向", "component": { "type": "radio", "props": {
"items": [{ "value": "down", "label": "跌破" }, { "value": "up", "label": "涨破" }] } } } },
"i18n": { "en-US": { "form": { "dir": { "title": "Direction", "component": { "props": {
"items": [{ "value": "down", "label": "Falls below" }, { "value": "up", "label": "Rises above" }] } } } } } }
}
输入框的提示文字写了就显示作者的(比如「你认得的叫法即可」),没写才回落 App 自带的「必填」/「选填」—— 值得写。
步骤 6 · 静态提醒看本地状态:alert.skip
「8 点提醒吃药,今天已经记过就别响」——它有本地状态,但不要因此改成动态提醒:动态在手机上只能尽力而为,而漏提醒正是这类场景最糟的方向。
正解是提醒保持静态,在记录「已吃」的那条交互流里顺手撤掉今天那一发:
{ "action": "alert.skip", "params": { "id": "dose", "until": "today" } }
- 只能操作本包的提醒;
params省略 = 这条规则的全部实例(交互流并不知道用户设了几点、叫什么)。 - 语义 = 撤掉
until之前已经排好的那几发,之后照旧;重复调用无副作用。until: "today"= 下一个零点,也可以给一个带时区的时刻串。 - 它有副作用,所以只能写在
.af里,取数流的白名单里没有它。 - 记录「已吃」的路径有几条就要挂几条(点整个组件、页内切换、补录……),漏一条 = 从那条路记的「已服」不销当天的提醒。
alert.skip 只跳过一段时间,规则和用户建的那份都还在。要整条撤掉,用下一步的 alert.remove。
步骤 7 · 撤销提醒:alert.remove
一次性提醒最常见的收尾:待办完成了、删了、改了时间,旧提醒不该再响。在对应的交互流里写:
{ "id": "rm", "action": "alert.remove", "params": { "id": "once", "params": { "tid": "${tid}" } } }
- 返回
{ removed },是这一次删掉了几份。没有匹配、规则 id 不存在都是{ removed: 0 },不报错 —— 「完成」那条流不用先查用户有没有设过提醒,直接调就行,重复调用也无副作用。 params按子集匹配:你给出的每个键,那份提醒里都有、而且值相等,就删;那份提醒多出来的键不管。省略或写{}= 这条规则的全部实例。这和alert.skip的params不一样 —— 撤销的一方通常只知道「哪一件事」(一个业务 id),不知道当初建提醒时标题、时刻都填了什么。所以建提醒时带一个不会变的业务键(比如待办的tid,在params里给默认值、form里不列出来,用户看不见也改不了),撤销时只按它删。- 只能删本包的提醒;只删提醒,不动后台任务;已经关掉的、已经结束的一样删。
- 它和用户在管理页点「删除此提醒」是同一件事:已经排好的那几发一并撤掉。不弹面板、不出提示,提醒历史里也不记一笔。
- 只能写在交互流(
.af)里,取数流调不到。H5 页面里对应的是xbridge.alertRemove({ id, params })(见numable docs bridge)。 - 需要
manifest.minEngine至少 3,否则旧版应用上这是一个不认识的动作,整条流报错(G45)。
改时间 = 先 alert.remove 再 alert.add 预填新时刻,让用户在面板上确认。
步骤 8 · 在页面上放一个「加提醒」按钮:alert.add
用户除了长按组件(见文末 jobs)和去「我的 › 提醒」,还可以在你的页面里直接加。按钮的交互流里写:
{ "id": "r", "action": "alert.add", "params": { "id": "price", "params": { "code": "${code}", "price": "" } } }
- 它只打开 App 自带的「添加提醒」面板,
params只是预填、用户可改;用户按下面板上的主按钮才算建好。 - 返回裸串,三种要分开处理:
ok= 建好了;cancel= 用户不想加,什么都别做;quota= 本包提醒已满(面板照开、按钮置灰),可以提示用户去「我的 › 提醒」清理 —— 别把它也当成「下次再说」。 id只能是本包xJob/里带alert的那条;找不到或者那条是后台任务,整条流失败(rule_not_found),不开面板。id 写错(包里没有这个文件)check会用 G46 拦下。- H5 页面里对应的是
xbridge.alertAdd({ id, params }),返回值相同(见numable docs bridge)。 - 它会等用户操作,所以只能写在交互流(
.af)里。
什么时候会真的响
如实写进你的文案,别承诺「准时」:
- 静态提醒交给系统定时,应用没开也会响;在 HarmonyOS 上需要平台先放行,没放行就退回下面那条路;在 Windows 上需要应用常驻。
- 动态提醒与后台任务到点得真取一次数才知道发不发,所以它跟着系统给的机会走:打开应用一定检查一次;桌面上放着小组件会明显提高频率;Android 上按设定的频率检查,省电模式会推迟,用户在「设备状态」里打开「实时监测」(常驻一条通知)后才能做到到点即查;Mac / Windows 上应用开着时按设定频率检查。
- 能诚实承诺的一句话是「打开应用、或者桌面上放着小组件时会检查」。「每 5 分钟一定」「每天准时」这类话一句都别写。
- 免费版用户的最小间隔会被钳到半小时,Pro 用户按你写的节律检查;那是会员档位不是能力,文案别承诺比半小时更勤的检查。
- 上限由平台强制、包改不了:每个包最多 20 份提醒,每天最多发 30 条,其中
urgent每天最多 5 条。超出的会被丢弃并在管理页标出来。
组件那边可以声明 jobs,让用户长按组件就能加一条本包的提醒(参数从组件的参数预填,可改)——那一条引用的就是这里的 xJob/<id>,id 写错是 error(G46)。
装包时用户会看到什么
安装 / 更新面板会如实写出这个包带了什么,这是写给用户看的,所以你的 title 要写成用户看得懂的话:
- 包里有提醒 → 一行「可以发送提醒」,并说明要用户逐条添加才生效。不列规则名(装上并不会响)。
- 包里有后台任务 → 一行「这个工具会在后台定期更新数据」,后面逐条列出任务的
title和节律,比如「记录每日金价(每天 23:55)」。后台任务没有同意面板,装即同意,所以用户能在「我的 › 后台任务」按包关掉。
关掉之后那条任务不再跑,then 写的 data.* 就停在关掉那一刻。读这些数据的组件和页面要能兜住「一直没更新」:显示记录的时间,别把老数据当成今天的。
常见错
| 现象 | 最可能的原因 | 先做什么 |
|---|---|---|
check 说「这个包用了 xJob/,但 minEngine 低于这组能力要求的引擎轴」 |
提醒与后台任务需要引擎轴 2 以上的应用(G45) | 把 manifest.minEngine 抬到它报的那个版本(2) |
check 说「这个包用了 task.refresh.at="YYYY-MM-DD HH:MM" / alert.remove …低于这组能力要求的引擎轴 3」 |
一次性提醒与撤销提醒需要引擎轴 3 以上的应用(G45) | 把 manifest.minEngine 抬到 3 |
check 说「这个包用了 …(运行时判定配方),但 manifest.minEngine=… 低于这组能力要求的引擎轴 4」 |
判定配方 task.then.recipe 需要引擎轴 4 以上的应用(G45);写低了在旧版应用上装得上、永远不响 |
把 manifest.minEngine 抬到 4 |
| 只响一次的提醒一直不响 | 日期不合法(没补零、2 月 30 日),或 date 没填,那一项作废了 |
numable check 看 G45;面板上日期必选 |
| 待办早就完成了,提醒还是响 | 建提醒时没带业务键,撤销时匹配不上;或者「完成」那条路没挂 alert.remove |
建提醒时在 params 里放一个隐藏的业务 id,每条收尾的路都调 alert.remove |
| 提醒一次都没响过 | 判定拿不到值:depends 那条取数流的 resultFilter.keys 里没有你要判的键 |
numable run <包> 看那条流真正透出了哪些键,缺的补进 keys |
| 提醒一次都没响过,且一条错都不报 | 省略了 activeCondition,而 then 又没输出 hit |
在 then 的最后 resultFilter 里留下 hit,或者显式写 activeCondition |
| 断网那一刻弹了一条假提醒 | then 里没守空值,空值参与了比较 |
加一位 hasCur 旗标,判定和写回都先过它 |
| 同一条规则用户设了两份,只有一份响 | 状态键没拼参数,两份互相覆盖上次值 | 把全部 params 拼进 data.get / data.set 的键 |
| 换了英文,通知里某个数字位置是空的 | 译文的 ${} 写丢或写错(G47) |
让译文的引用集和基准语言逐字相同 |
| 创建面板上少一个字段 | form 的键不在 params 里(G45) |
两边对齐;params 是那份名单 |
| 条件明明成立,却隔很久才响一次 | refresh.cooldown 判真之后把整条规则冷却住了 |
调小或删掉 cooldown |
| 后台任务记下的某天是空的 | 那天取数失败,却照样写了进去 | 写入放进空值守卫的分支里 |
| 提醒从来不响,后台任务也什么都没记下,却一条错都不报 | 空值旗标用 length:: 判的,而那个值是数字 —— 旗标恒为 0,写回和判定整条都被守卫挡住了 |
把旗标改成 findNotEmpty 哨兵写法 |
下一步
- 取数流怎么写、怎么验:
numable docs df - 组件声明与
refresh的完整写法:numable docs xwidget - 创建面板的字段类型与属性:
numable docs params - 错误码逐条对照:
numable docs lint-codes