af —— 交互流(.af)与 action 全表
读者:做工具的用户,和替他干活的 AI。两者读同一份。
它是什么
.af 是用户动了手指之后跑的那条流:点组件、长按编辑、页面上按一个按钮、提交一张表单。它能做副作用 —— 弹 toast、震一下、开页面、写本地数据、把参数写回组件、让组件重新取数。
它和取数流 .df 是同一套 ActionFlow 引擎、同一套写法,分界只有一条:.df 是白名单子集,零 UI、零导航、零 App 注入能力;.af 是全集。取数写 .df,副作用写 .af,后缀就是能力声明,写错后缀App 里会直接回「flow not found」(check G12b)。取数流的写法见 numable docs df。
引擎按载体再切一刀,这刀比后缀更硬:
| 事件流 | 渲染流 | |
|---|---|---|
| 判据 | 用户手指参与 | 取反 |
| 入口 | 仪表盘点整组件 · 点 RCN cell · 桌面小组件点击回放 onClick · 深链 · XPage 节点 events · 表单 onSubmit |
.xwidget 的 canvas.depends · XPage 节点 depends · banner flow |
ui.* / nav.* / widget.* |
可用 | 类型上就不存在,调不到 |
data.* / request |
可用 | 可用 |
| 网络白名单 | 强制 | 强制 |
所以「取数的时候顺手弹个 toast / 顺手刷新一下自己」是做不到的,写了不会报错,只是那一步不存在。
最小可用示例
页面里按「保存」跑的那条流(page/flow/save.af)。写盘 + 让本包的组件重新取数,这是最常见的一种 .af。
{
"version": 1,
"actions": [
{ "action": "ui.haptic", "params": { "type": "tap" } },
{ "op": "set", "props": { "key": "lk", "value": "log_${id}" } },
{ "op": "set", "props": { "key": "sk", "value": "stat_${id}" } },
{ "action": "data.set", "params": { "key": "${lk}", "value": "${log}" } },
{ "action": "data.set", "params": { "key": "${sk}", "value": "${stat}" } },
{ "action": "data.set", "params": { "key": "agg", "value": "${agg}" } },
{ "action": "widget.refresh" }
]
}
逐行:ui.haptic 在 actions[0](按下到画面变化之间没有别的反馈)· op:set 先把动态键名落成变量 · 三个 data.set 写盘 · 最后 widget.refresh 让这个包的组件真取数重渲。所有写盘动作必须排在 widget.refresh 之前,否则刷新读到的是旧盘。
文件格式
{
"version": 1, // 必填,恒 1
"inputs": ["secid", "period"], // 可选,声明期望入参,只做静态校验
"i18n": { "zh-CN": { "k": "文案" }, // 可选,本文件自己的 A 表,用 ${@i18n.k} 取
"en-US": { "k": "text" } },
"actions": [ /* 步骤数组 */ ]
}
_note 字段可写在任何位置,是给人和 AI 的注释,引擎忽略。
步骤的三种形状
| 形状 | 写法 | 说明 |
|---|---|---|
| action 步 | { "id": "resp", "action": "request", "params": { … } } |
调一个 action;id = 结果键 |
| operation 步 | { "op": "set", "props": { "key": "x", "value": "…" } } |
控制流与赋值,子节点放 items |
| 复合步 | { "action": "concurrent", "items": [ … ] } |
sequential / concurrent 写在 action 位,不是 op 位 |
复合步写成 {"op":"concurrent"} 会被整块静默跳过 —— 流照样 success,里面一个 action 都没跑。
policy(可选,写在步骤上):wait(默认,等它完成)/ detach(不等)/ skip(跳过)。
params 递归求值:字符串走表达式引擎,对象的键也求值,数组逐元素求值;@[…] 整值引用求不出来时按字面量透传。表达式与方法见 numable docs methods。
id = 结果键
带 id 的 action 步,结果以 id 为键写进作用域,后面用 ${id.xxx} 取。两种情况结果被丢弃:没写 id;结果是 null 或 undefined。
后者是最常见的静默失效来源:data.get 取不到值又没写 default,那个键根本不出现在作用域里,下游 ${k} 恒空且不报错。data.get 一律写 default。
除 concurrent 分支外,写入后下一步立刻可见。
内联 flow 绑定的四种形态
节点的 depends、events 的值、onSubmit 的绑定,都用这四种形态之一:
"depends": [
{ "flow": "@[file://page/flow/quote.df]", "params": { "secid": "${secid}" } }, // ① 文件引用 + 入参(推荐)
{ "flow": { "version": 1, "actions": [ … ] }, "params": { "x": "${y}" } }, // ② 内联对象
"@[file://page/flow/noop.df]", // ③ 裸引用,入参 = {}
{ "op": "set", "props": { "key": "cards", "value": [] } } // ④ 裸内联单步
]
flow 写成数组无效。裸引用形态传空入参 —— 需要入参却写成③,流仍 success、组件渲一片 --(check G12c)。不参与取数但要显示的键(比如城市名)也必须列进 params。
@[file://…] 的路径基准跟载体走
| 载体 | 基准 | 写法 |
|---|---|---|
.xwidget 的 events · RCN cell 事件 |
xWidget/ |
@[file://flow/mark-today.af] |
页面(router.json 指向的页、页内节点、onSubmit) |
包根 | @[file://page/flow/save.af] |
基准写错的表现是「点了没反应」,App 里读不到 actions 就静默结束(check G12d)。
action 全表
系统内置(.df 一列 = 渲染流 / 取数流里也能用)
| action | 入参 | 出参 | .df |
|---|---|---|---|
cancel |
— | — | ✅ |
error |
errorMsg |
— | ✅ |
finish |
— | — | ✅ |
sleep |
timestamp(毫秒,写成字符串 "1500";参数名就叫 timestamp) |
— | ✅ |
log |
log |
— | ✅ |
resultFilter / resultfilter |
keys[] |
过滤后的结果集 | ✅ |
request |
url(必填)· method(默认 GET)· queryParams(对象)· header(对象)· formData(multipart 数组)· body(字符串)· formatType(string 默认 / json / xml / base64 / tsv / csv)· columns(字符串数组,只对 tsv / csv 生效)· credential(凭证声明 id,字面量)· timeout(毫秒) |
响应 | ✅ |
clearCookie |
pattern(正则) |
— | ✅ |
htmlParse |
content · rules(规则数组,写法见 numable docs df) |
结构化结果 | ✅ |
xmlParse |
content · encoding · rules |
结构化结果 | ✅ |
showLoading |
progress |
— | ❌ |
hideLoading |
status · message · delay |
— | ❌ |
toast |
message · style = success / warning / error |
— | ❌ |
data.get |
key · default |
值 / default / null |
✅ |
data.set |
key · value |
写入的值 | ✅ |
data.remove |
key |
null | ✅ |
data.has |
key |
布尔 | ✅ |
data.merge |
key · value(必须是对象) |
null | ✅ |
data.keys |
— | 键数组 | ✅ |
data.getAll |
— | 整个对象 | ✅ |
data.clear |
— | null | ✅ |
data.* 没有 scope 参数,写了静默忽略;数据域按包隔离,由宿主注入身份。
页面里要 loading / toast,用下面 App 注入的 ui.showLoading / ui.toast(能拿到文案与类型),别用内置的 showLoading / toast。
⚠️ 这两个 toast 参数名和取值都不一样,写混了不报错、只是没样式:
| 参数名 | 取值 | |
|---|---|---|
内置 toast |
style |
success / warning / error |
注入 ui.toast |
type |
info(默认)/ success / error |
也就是说 ui.toast 没有 warning 这一档,而内置 toast 没有 info;把 style 写到 ui.toast 上,或者把 type 写到内置 toast 上,都会被当成没写、落回默认样式。事件流里一律用 ui.toast。
App 注入(只在事件流里存在)
| action | 入参 | 出参 | xpage | .xwidget 事件流 | 渲染流 |
|---|---|---|---|---|---|
xpage.const |
任意对象,逐键写进作用域 | — | ✅ | ✅ | ❌ |
xpage.setState |
对象,整体替换页面状态 | — | ✅ | 空转 | ❌ |
xpage.patchState |
对象,增量合并 | — | ✅ | 空转 | ❌ |
xpage.redraw |
id |
— | ✅ | 空转 | ❌ |
xpage.redrawPage |
— | — | ✅ | 空转 | ❌ |
xpage.reloadPage |
— | — | ✅ | 空转 | ❌ |
xpage.reenterPage |
— | — | ✅ | 空转 | ❌ |
page.setResult |
任意对象 = 返回值 | — | ✅(含 html 页) | 空转 | ❌ |
page.close |
— | — | ✅ | 空转 | ❌ |
ui.showLoading |
text · progress(0–1 的小数;不写就是转圈的不定进度) |
— | ✅ | ✅ | ❌ |
ui.hideLoading |
— | — | ✅ | ✅ | ❌ |
ui.toast |
message(可写 ${@i18n.k})· type = info(默认)/ success / error · duration(毫秒,不写用平台默认) |
— | ✅ | ✅ | ❌ |
ui.haptic |
type = tap(默认)/ impact / success / warning / error / selection |
— | ✅ | ✅ | ❌ |
ui.alert |
title · message |
— | ✅ | ✅ | ❌ |
ui.confirm |
title · message · okText · cancelText · destructive |
布尔 | ✅ | ✅ | ❌ |
ui.presentSheet |
source · params |
子页 page.setResult 的值 / null |
✅ | ✅ | ❌ |
ui.dismissKeyboard |
— | — | ✅ | 空转 | ❌ |
input.focus / input.blur / input.clear / input.selectAll |
id |
— | ✅ | 空转 | ❌ |
input.setValue |
id · value |
— | ✅ | 空转 | ❌ |
nav.open |
url(必填)· container(page 默认 / sheet / dialog)· params · fallback |
见下文五枚 | ✅ | ✅ | ❌ |
nav.openForResult |
url · params |
子页结果 / null | ✅ | ✅ | ❌ |
startPageForResult |
page(必填)· container(缺省 sheet)· params |
{ value, cancelled } |
✅ | ✅ | ❌ |
singleValue |
整个 params 就是单值配置,必须含 container |
{ value: { value }, cancelled } |
✅ | ✅ | ❌ |
widget.updateParams |
顶层对象就是要合并的 params | { params } |
✅ | ✅(仅 onEdit) |
❌ 硬拒 |
widget.refresh |
scope(bundle 默认 / widget / self)· widgetId · desktop |
{ refreshed } |
✅ | ✅ | ❌ 硬拒 |
widget.pick |
items?: [{ id, params? }] |
{ listed } |
✅ | ✅ | ❌ |
installBundle |
id(必填)· version? · ref? |
null | ✅ | ✅ | ❌ |
alert.add |
id(必填,本包 xJob/<id>.xjob)· params?(预填值,用户可改) |
裸串 ok / cancel / quota;id 在本包找不到(或那条不是提醒)→ 流失败,错误 rule_not_found,不开面板 |
✅ | ✅ | ❌ |
alert.skip |
id(必填)· params?(省略 = 这条规则的全部实例)· until = today(默认)或一个 ISO 时刻 |
{ skipped },跳过了至少一条时另带 { until }(生效截止,毫秒) |
✅ | ✅ | ❌ |
alert.remove |
id(必填)· params?(子集匹配:给出的键相等就删;省略 = 这条规则的全部实例) |
{ removed };没有匹配 / 规则不存在 = { removed: 0 },不算失败。只删提醒不动后台任务,不弹面板;需要 minEngine ≥ 3 |
✅ | ✅ | ❌ |
「空转」= 调得到但什么都不做(组件宿主上没有页面可改)。
别再写的名字
| 见到 | 改写成 |
|---|---|
inputValue |
singleValue(没有别名,写老名字直接找不到 action) |
inputForm |
用 startPageForResult 打开一张 form 页 |
xpage.setResult |
page.setResult |
nav.back |
没有这个 action;返回用 page.close,或让宿主的返回手势处理 |
operation 节点
| op | props | items | 说明 |
|---|---|---|---|
if |
val |
条件为真时执行 | 条件键是 val,不是 cond |
for |
count · index |
循环体 | 定次循环,index 是计数变量名 |
forEach |
items · key · index |
循环体 | 遍历数组;注意 items 在 props 里是被遍历的数据 |
set |
key · value |
— | 求值后写进作用域 |
remove |
key |
— | 删一个键 |
include |
dsl(另一条 flow) |
— | ⚠️ 别用:在 .af / .df 里恒展开成空 |
op:include 是这张表里唯一一个「写了也不会跑」的:流引擎不实现它的展开,那一步恒展开成零个子步骤,而整条流照报 success。想复用公共步骤,只有两条路:把那几步复制过去,或者拆成独立的 .df,在 depends 里多绑一条(见 numable docs xwidget 的多条 depends)。
两条必须记住的:
op:if的条件键是props.val。 写成cond的后果是条件恒假、整个分支体静默不执行、流仍报 success、分支外的动作全部正常 —— 只有分支里的写盘什么都没发生(check G30)。op:if分支里op:set出来的键,出了分支取不到。 分支退出会恢复外层绑定。所以:算值用表达式嵌套if::,op:if只用来分派动作。
events:哪里能挂 .af
.xwidget 的 onClick / onEdit
events 只有这两个键,写在 .xwidget 的外壳上(不在 canvas 里)。字段细节见 numable docs xwidget。
"events": {
"onClick": "/detail?secid=${secid}",
"onEdit": "/pick?ref=board&base=${base}"
}
值解析后是字符串 → 当导航串走;是结构体(@[file://x.af] 或内联对象)→ 当 ActionFlow 跑。不许写裸的 flow 文件名。
四种可用写法:
| 写法 | 例子 |
|---|---|
| 相对路由 path | "/detail?secid=${secid}" |
| 开本包首页 | "numable://self" |
| 开本包某页 | "numable://self/page/item?id=${itemId}" |
| af 引用 | "@[file://flow/mark-today.af]" |
onEdit走导航时必须是裸路由 path(/pick),不能写numable://形态 —— App 里拿这个串逐字匹配router.json,匹配不上就开出空白页且不报错(check G12d)。onClick两种形态都行。onEdit的目标页型 html / xpage / form 都可以,但必须证明得出写回(check G12e)。- af 形态的路径基准是
xWidget/。 - 导航串里的
${…}只取顶层标量;作用域从低到高:外壳 params → 实例 params → 本地数据 →depends结果集。
整组件 onClick / onEdit 跑 af 时的入参 = 这个组件的实例 params + @i18n(只有这个 af 文件自己的表)+ @app,没有 @event。 组件整体被点,没有「哪个元素」可言。
@event.*(XPage 节点 / RCN cell / 表单)
| 触点 | 能读到 |
|---|---|
onClick / longClick / 菜单项 |
@event.id |
input 的 onChange / onBlur / onSubmit |
@event.value |
onPageChange |
@event.page |
.xform 的 onSubmit |
@event.value = { 字段名: 值 } |
searchSelect 的 dataSource |
@event.keyword |
dynamicCascader 的 dataSource |
@event.path / @event.level |
onLoad / onReachEnd |
无 payload |
@i18n 的取法见 numable docs i18n。
常用原语
widget.refresh —— 让组件真取数
{ "action": "widget.refresh", "params": { "scope": "bundle", "widgetId": "streak", "desktop": true } }
| scope | 作用范围 | 备注 |
|---|---|---|
self |
触发这条流的那个组件 | 需要宿主给出组件实例上下文 |
widget |
本包里某个组件的所有实例 | widgetId 必填 |
bundle |
本包全部组件(缺省) | 最常用 |
- 语义 = 清取数计时 + 真取数 + 重渲,不是 reload。reload 会命中渲染图缓存,等于什么都没刷。
- 返回
{ refreshed: <张数> }。匹配到 0 张是成功,不是错误。 - 5000 毫秒节流窗内再调,直接返回
{ refreshed: 0 }。 - 跨包无效;渲染流里硬拒;
.df里不可用。 - 错误(
no_brick_context/no_bundle_context/unknown_scope/widget_not_in_bundle)会让整条流失败。 - 写盘之后必须调它,且所有
data.set排在它前面(人审;run层看不出来,现象是「设置改了组件不变」)。
widget.updateParams —— 把值写回组件实例
{ "action": "widget.updateParams", "params": { "city": "${r.value.value}", "sub": null } }
- 顶层对象就是要合并的 params,不套一层。逐键 merge:给值 = 覆盖,给
null= 删键、回落.xwidget里的默认值。 - 值必须是标量,并且会被规范成标准形:
1.0→"1"、true→"true"。传对象或数组直接报错(non_scalar_value)。 - 返回
{ params };失败(no_brick_context/non_scalar_value/brick_not_found)让流失败。 - 只有
onEdit的 af 里调得到。 整组件onClick的 flow 拿不到组件实例上下文,调它必失败。html 参数页用 JS 桥的xbridge.updateParams(见numable docs bridge),其余页型用这个 action。 - 渲染流硬拒。
典型串法:onEdit: "@[file://flow/edit.af]" → singleValue 或 startPageForResult 收一个值 → widget.updateParams 写回。整个过程不需要做一张页。
startPageForResult —— 开一张页,等它回值
{
"id": "picked",
"action": "startPageForResult",
"params": { "page": "/for-result-pick", "container": "sheet", "params": { "secid": "${secid}" } }
}
输出恒为 { value, cancelled },读 ${picked.value.xxx} 与 ${picked.cancelled}。
| container | 语义 | 高度 |
|---|---|---|
page |
有栈就压栈;没有栈就先立一个容器再压栈 | 铺满 |
sheet(缺省) |
贴容器底的浮层 | 容器高的 0.8,固定 |
dialog |
容器内居中浮层 | 容器高的 0.6,固定 |
xpage / html / form 三种页型通吃。子页回值的写法:
| 子页里做了什么 | 结果 |
|---|---|
page.setResult({...}) |
关闭并回传;这张页若是被常规路由打开的,则纯空转 |
page.close() |
关闭,cancelled: true;常规路由页则真的关页 |
| 什么都没做 / 流失败 | 页面保持挂载,值还在,可以再提交 |
用户按 ✕、‹、点遮罩、系统返回手势 = 取消,不经过 onSubmit,宿主直接兑现 { value: null, cancelled: true }。取消是正常路径,一定要判 cancelled 再用 value。
ui.confirm —— 二次确认,拿一个布尔
{ "id": "ok", "action": "ui.confirm",
"params": { "title": "清空全部记录?", "message": "这一步不可撤销", "okText": "清空", "destructive": true } }
后面直接拿 ${ok} 进 op:if:
{ "op": "if", "props": { "val": "${ok}" },
"items": [ { "action": "data.clear" }, { "action": "widget.refresh" } ] }
ok是真布尔,直接写进props.val就行。- ⚠️ 别把它先转成数字再判。
props.val的真值口径很窄:布尔取自身,字符串只认1/true/on/yes/y(不分大小写),数字只有恰好等于 1 才算真(2、-1都是假)。算出个2就以为「有货」的写法会静默走进假分支,而流照报成功。 destructive: true把确认钮标红,用在删除这类做完回不去的操作上。- 它等用户操作,期间不计入事件流的 15 秒超时。
ui.alert —— 单钮提示,等用户点掉
{ "action": "ui.alert", "params": { "title": "今天已经打过组件了", "message": "明天再来" } }
没有返回值。要「告诉一句就走」用 ui.toast(不打断);要用户明确点头才继续,才用 ui.alert。
ui.presentSheet —— 贴底弹一张子页
{ "id": "r", "action": "ui.presentSheet",
"params": { "source": "/detail", "params": { "id": "${id}" } } }
返回的是子页 page.setResult 传回来的那个对象本身,子页被关掉则是 null。它是 startPageForResult 的底层原语 —— 一般写 startPageForResult:那个的返回值统一成 { value, cancelled },不用自己判 null 到底是「取消了」还是「回了个空」。
nav.openForResult —— 压栈打开一张页,等它回值
{ "id": "picked", "action": "nav.openForResult",
"params": { "url": "/city-pick", "params": { "cur": "${city}" } } }
同样是 startPageForResult 的底层原语,只是恒压栈(相当于 container: "page"),返回值也是裸的结果 / null。新写的流一律用 startPageForResult,这两个原语留着是给已有的包。
xpage.setState 与 patchState —— 改页面数据
两个的入参都是「整个 params 就是那张表」,不套 value:
{ "action": "xpage.patchState", "params": { "tab": "week", "loading": "0" } }
| 语义 | 用在哪 | |
|---|---|---|
xpage.patchState |
浅合并:只覆盖写出来的键,其余原样保留 | 日常都用这个 |
xpage.setState |
整份替换:没写进来的键会被清掉 | 只在「整页重置」时用 |
最常撞的坑是拿 setState 当 patchState 用:改一个 tab 值,把页面上其它键一起清空,现象是别处的内容突然全空了。
改完 state 再决定重画多少:
| action | 重画范围 | 会不会重跑 depends |
|---|---|---|
xpage.redraw |
只有 params.id 那一个节点 |
不会 |
xpage.redrawPage |
整页 | 不会 |
xpage.reloadPage |
整页,页面 state 与滚动位置延续 | 会,onLoad 不重跑 |
xpage.reenterPage |
等同关掉重进:清 state、首帧走骨架 | 会,onLoad 也重跑 |
{ "action": "xpage.patchState", "params": { "tab": "week" } }
{ "action": "xpage.redraw", "params": { "id": "chart" } }
还有一个 xpage.const:它写的是这条流自己的作用域顶层变量(后面的步骤用 ${名} 读),不写进页面 state,页面上的节点看不见。要页面看得见就用 patchState。
input.* —— 命令式操作输入框
input 节点的值平时是自己静默写进页面 state 的(键名 = props.bindKey,不写就是节点 id)。下面五个是「用流去动它」:
{ "action": "input.focus", "params": { "id": "kw" } }
{ "action": "input.setValue", "params": { "id": "kw", "value": "${@i18n.preset}" } }
{ "action": "input.clear", "params": { "id": "kw" } }
{ "action": "input.selectAll", "params": { "id": "kw" } }
{ "action": "input.blur", "params": { "id": "kw" } }
id是节点 id,不是bindKey。input.setValue同时更新bindKey指向的 state,效果和用户自己敲进去一样。- 没有输入宿主的地方(组件事件流)这五个都是空转,不报错。
- 收键盘另有一个
ui.dismissKeyboard(无参),它不改值、只收键盘:{ "action": "ui.dismissKeyboard" }。
widget.pick —— 打开「加到仪表盘」面板
{ "action": "widget.pick",
"params": { "items": [ { "id": "today", "params": { "habit": "water" } }, { "id": "streak" } ] } }
items省略 = 列本包全部组件;写了就只列这几个,各带各的实例参数。id是xWidget/<名>.xwidget的文件名(不含后缀)。- 一个 id 都对不上时这一步会失败(
no_matching_widget),不会回落成「列全部」。所以改了组件文件名记得同步改这里。 - 返回
{ listed: <列出几张> }。 - ⚠️ 落盘发生在面板里的按钮上,不在这一步。调用之后不要提示「已添加」—— 用户完全可能翻两下就关掉(
check G24)。
installBundle —— 提议安装另一个包
{ "action": "installBundle", "params": { "id": "01M050AARQ0R08T9EHGDSGZBHJ" } }
id 必填(目标包的 ULID),version / ref 可选。它只是弹出安装面板,装不装由用户在面板里决定;流拿不到「装没装成」的结果(返回恒 null)。同 widget.pick:调用之后不要提示「已安装」。
page.close —— 关掉本页
{ "action": "page.close" }
无参。常规路由打开的页 = 真的关掉这一页;被 startPageForResult 打开的子页 = 收起浮层并兑现 { value: null, cancelled: true }。要带值关闭用 page.setResult,那个会自己关页,不用再补一个 page.close。
表单输入
单个值用 singleValue,一次收多个字段做一张 .xform 页。
{
"id": "r",
"action": "singleValue",
"params": {
"container": "page",
"title": "列表选择",
"desc": "副标题",
"confirmTxt": "确定",
"component": {
"type": "select",
"value": "b",
"props": { "items": [ { "label": "选项 A", "value": "a" }, { "label": "选项 B", "value": "b" } ] }
}
}
}
container 必填(page / sheet / dialog)。返回 { value: { value: <字符串 | 数组 | null> }, cancelled },值在 ${r.value.value} —— 多套了一层。
.xform 是页型 form 的页,用 startPageForResult 打开,自己不写 container(三态由调用方裁决);form 的键就是结果键,声明顺序就是渲染顺序;onSubmit 绑定的 flow 只看得见 @event.value。字段类型全表、props、值形状见 numable docs params。
nav.open 与跳外部 App
{
"id": "r",
"action": "nav.open",
"params": { "url": "futunn://quote/00700", "fallback": "https://www.futunn.com/stock/00700-HK" }
}
判定次序:危险 scheme 黑名单(javascript / file / data / intent / about / blob,check G16)→ 手势门(不是用户手势引发的 10 秒窗外静默拒绝,连弹窗都不给)→ 该包对该 scheme 的授权记忆 → 首跳确认弹窗。授权按(包, scheme)记,不进备份,卸载即清。
返回值五枚:opened · no-handler · denied · fallback · presented(container 为 sheet / dialog 时不等待,立即返回这个)。
fallback可选、只认 http(s)(check G16),只在外部 App 且no-handler时触发;用户拒绝(denied)恒不回落。nav.open会等用户,标了 interactive;不标 interactive 的等待型 action 会撞上事件流 15 秒硬超时。- URL 里 scheme 之后拼进去的
${…}要用$[urlEncode::(…)]包一层(check G16,警告级)。 numable://self在nav.open里不成立 —— 它走系统 scheme 回环,收端没有「当前是哪个包」的上下文,会弹「工具『self』未安装」。self只在页内路由里有效;要从 flow 里开自己的包,写本包 ULID 字面量:{"action":"nav.open","params":{"url":"numable://01M050AARQ0R08T9EHGDSGZBHJ"}}。
规则(违反 = 返工)
| 规则 | 检查方式 | 违反时的现象 | 修法 |
|---|---|---|---|
op:if 的条件键必须是 props.val |
check G30 |
条件恒假、分支静默不执行、流仍 success | 把 cond 改成 val |
concurrent 之后紧邻的节点不得引用并发分支的 id |
check G4b |
并发结果晚一拍,那几个字段静默取空,组件上全是 -- |
中间插一个屏障:{"op":"set","props":{"key":"_b","value":"1"}} |
parseDate 的 pattern 去掉 token 后不许再剩字母 |
check G4c |
手机上整条解析失败返 null,算出荒唐的天数 | 先 subString:: 切出日期段再 parseDate:: |
onEdit 的 af 只认包内 .af、禁 ..、文件必须存在(基准 xWidget/);走路由的必须是裸 path 且在 router.json 里 |
check G12d |
「点了没反应」/ 打开一张空白页且不报错 | 补文件,或把 numable://… 改成 /pick |
onEdit 的 af 必须含 widget.updateParams;目标页必须有写回能力 |
check G12e |
打得开、按了不会有任何变化 | af 里补写回;form 页在 onSubmit 里写回,xpage 在节点 events 里写回 |
危险 scheme 禁用;fallback 只认 http(s);第三方 scheme URL 里的 ${} 要 urlEncode |
check G16 |
App 里直接拒绝跳转 / 参数带空格特殊字符时跳错地方 | 见上一节 |
cell 事件绑的 .af,actions[0] 应是 ui.haptic |
check G22(警告) |
按下到画面变化约 250 毫秒无反馈,用户会再点一次 | 把 ui.haptic 挪到第一位 |
写盘(data.set / data.merge / data.remove)的 .af 里必须有 widget.refresh,且所有写盘排在它前面 |
check G37(警告) |
存了,但组件上还是旧值,要等下一次自然刷新 | 流尾补 widget.refresh;页面自己的刷新链写了 xpage.reloadPage 或 widget.updateParams 的不算违反 |
op:include 不要用 |
人审 | 那一步恒展开成空,流仍 success | 把步骤复制过去,或拆成独立 .df 多绑一条 depends |
sleep 的参数名是 timestamp,值写成字符串 |
人审 · check G52(写成数字) |
不等待,直接往下跑;写成数字时鸿蒙上这一步不执行 | 改参数名;数字加引号 |
数组要用真的数组字面量,先 op:set 落地再引用 |
人审 | 方法参数里读不到,取空 | 先 {"op":"set","props":{"key":"list","value":[…]}} |
op:if 分支里 op:set 的键出了分支不可见 |
人审 | 分支外读那个键恒空 | 算值用表达式嵌套 if::,op:if 只分派动作 |
| 等用户的 action 之外,事件流有 15 秒硬超时 | run 层 | 流跑到一半被掐,后面的写盘没发生 | 拆流,或把长耗时放进 .df |
复合步写在 action 位 |
run 层 | 写成 op 会被整块静默跳过 |
{"action":"concurrent","items":[…]} |
data.get 必须写 default |
run 层 | 键不进作用域,下游 ${k} 恒空不报错 |
补 "default": "" |
出错怎么办
| 现象 | 最可能的原因 | 先做什么 |
|---|---|---|
| 点组件/按钮没任何反应 | af 文件路径基准写错(.xwidget 事件的基准是 xWidget/ 不是包根),或文件名后缀不对 |
跑 numable check,看 G12d;确认文件真在 xWidget/flow/ 下 |
| 参数编辑页打得开,填完保存组件一点没变 | 那条路径上没有 widget.updateParams,或它在整组件 onClick 里(拿不到组件实例) |
跑 numable check 看 G12e;把写回放进 onEdit 的 af 或参数页 |
| 数据存了,组件还是旧的 | 流尾少了 widget.refresh,或它排在 data.set 前面 |
把 widget.refresh 挪到所有写盘之后 |
| 弹窗/表单出来了,但流在那之后就断了 | 事件流 15 秒硬超时 —— 等用户的那一步前后堆了长耗时动作 | 把取数挪进 .df,事件流只留交互与写盘 |
| 某个分支怎么都不执行,日志还干净 | op:if 写成了 cond |
改成 props.val,跑 numable check 看 G30 |
| 并发之后的字段全空 | concurrent 结果晚一拍 |
插屏障 op:set,或退回串行 |
组件上部分字段恒 -- |
depends 写成裸 @[file://…],入参被吞成空 |
改成 {"flow":…,"params":{…}} |
更多静默失效见 numable docs pitfalls。
相关
numable docs df—— 取数流的白名单与写法numable docs xwidget——events挂在哪、实例 params 怎么来numable docs params—— 表单字段类型全表与.xform