把卡片放到安卓系统桌面(Launcher 小组件)。卡片由服务自带定义,AI 通过 service_file_* 工具在服务目录下创建;渲染采用原生 RemoteViews(结构化文本 + 图片,不支持 HTML)。
- 让 AI 给某个服务创建桌面卡片(或手写下方目录文件)
- 长按安卓桌面 → 小组件 → 找到"变形虫"拖上桌面
- 在弹出的选卡页选择要显示的卡片
- 点击桌面卡片 → 打开 App 并跳转
tapPath(若配置)
App 被杀期间卡片显示最后一次推送的内容(原生 SharedPreferences 缓存)。
services/{serviceId}/desktop-widgets/{cardId}/
├── widget.json # 界面 + 行为配置
├── logic.js # 逻辑:隐藏沙箱 iframe 中执行,产出渲染数据
└── assets/ # 图片资源(png/jpg)
widget.json:
{
"id": "todo-card",
"label": "待办速览",
"description": "显示最近 5 条待办",
"layout": "lines",
"size": "medium",
"accentColor": "#5f8f7b",
"maxLines": 5,
"tapPath": "/service/user.note-service",
"updateIntervalMin": 30,
"enabled": true
}| 字段 | 说明 |
|---|---|
label |
选卡页显示名称(必填) |
layout |
lines(文本行列表,默认)/ image(图片为主)/ bigText(大字内容) |
size |
尺寸档位:small(2x2)/ medium(4x2,默认)/ large(4x4)。Android 小组件尺寸只能在 provider meta XML 声明,故三个档位注册为三个 Provider 入口(Launcher 显示「变形虫卡片·小/中/大」),选卡页只列出同尺寸卡片 |
accentColor |
标题颜色,十六进制 |
backgroundColor |
卡片背景色 #RRGGBB / #AARRGGBB(可半透明)。RemoteViews 运行期只能换资源/纯色背景(丢圆角),故原生程序内画 GradientDrawable 圆角位图铺底(192x192 fitXY) |
textColor |
正文文本行颜色(lines/bigText 布局) |
hideTitleBar |
true=隐藏标题栏(icon+标题行),配合 renderHtml 整卡自定义卡面 |
maxLines |
lines 布局最多行数(1-6,默认 6;每行超 60 字截断) |
tapPath |
点击卡片的应用内跳转路径(/... 开头) |
updateIntervalMin |
App 存活期间逻辑重跑间隔(分钟),0 = 仅启动/手动刷新 |
enabled |
首次扫描时的默认启用状态(之后以全局 registry.json 为准) |
logic.js:在隐藏沙箱 iframe 中执行(注入 JSBridge + renderHtml 辅助;脚本自动包在 async 函数中,可直接顶层 await),可用 desktopWidget.publish / desktopWidget.renderHtml + storage(读写本服务数据),其余模块拒绝。完成后必须调用一次:
// 例:读取服务数据并发布
const todos = (await __amiba__.storage.get('todos')) || [];
__amiba__.desktopWidget.publish({
title: '待办清单',
icon: '📝',
lines: todos.slice(0, 5).map(t => '· ' + t.text),
footer: '更新于 ' + new Date().toLocaleTimeString()
});publish 数据字段:title / icon(emoji)/ lines(≤6 条)/ image(相对卡片目录,如 assets/chart.png)/ imageData(PNG dataURL,renderHtml 产物,优先于 image)/ footer。
自定义卡面(renderHtml):沙箱内注入 desktopWidget.renderHtml(html, {width, height, scale})——SVG foreignObject 离屏渲染 HTML 片段(样式须内联/内嵌)或完整 SVG 字符串为 PNG dataURL,沙箱内执行不经桥接。publish 时传 imageData,宿主解码二进制落盘 desktop-widgets/cache/img/{key}.png 并把绝对路径推给原生(layout 用 image,删除卡片时一并清理)。建议渲染宽度:small 480 / medium 720 / large 720(Kotlin 解码上限 720px)。
权限:服务卡片要求 manifest 声明 desktopWidgets 权限,否则不注册;全局卡片无权限要求。
desktop-widgets/
├── registry.json # { enabled: ["serviceId/cardId", "global/cardId"] } 启用状态(唯一来源)
├── cards/{cardId}/ # 全局卡片定义(不依附服务:widget.json + logic.js + assets/,规范同服务卡片)
├── data/{cardId}/ # 全局卡片 logic.js 的 storage 数据(与服务数据隔离)
└── cache/{serviceId}__{cardId}.json # 最近渲染载荷(含图片绝对路径)
全局卡片由 desktop_widget_create 工具创建,key 格式 global/{cardId},logic.js 规范与 publish 数据格式和服务卡片完全一致。
服务 desktop-widgets/ 定义
→ desktop-widget-store.ts 扫描注册(bootstrap 时)→ registry.json
→ desktop-widget-runner.ts 隐藏 iframe 执行 logic.js
(启动时 + updateIntervalMin 周期 + desktop_widget_refresh 手动
+ 服务 storage 数据变更自动——防抖 1s 合并连写,logic.js 自身写入宽限期抑制防回流循环)
→ publish(data) → 合并 widget.json + 图片绝对路径 → cache/*.json
→ invoke('android_widget_update') → Rust widget.rs → JNI
→ Kotlin WidgetHelper.updateCards() → SharedPreferences + 刷新三个 Provider 全部实例
→ AmibaWidgetProvider(中4x2)/ Small(小2x2)/ Large(大4x4)按 appWidgetId→cardKey 绑定渲染 RemoteViews
尺寸档位:Android 小组件尺寸只能在 provider meta XML 声明,故 size(small/medium/large)注册为三个 Provider 入口(Launcher 显示「变形虫卡片·小/中/大」);选卡配置页由 appWidgetId 反查 Provider 类名,只列出同尺寸卡片(载荷 size 缺省按 medium 兼容旧卡片)。
| 工具 | 类别 | 说明 |
|---|---|---|
desktop_widget_create |
manage | 创建全局卡片(不依附服务),含 widget.json 字段 + logicJs 内容 |
desktop_widget_list |
view | 列出全部卡片(key/label/启用状态/最近推送时间) |
desktop_widget_enable |
manage | 启用/停用卡片(key 格式 serviceId/cardId 或 global/{cardId}) |
desktop_widget_refresh |
manage | 立即重跑 logic.js 并推送原生(不传 key 刷全部启用卡片) |
desktop_widget_delete |
manage | 删除卡片(文件 + 启用状态 + 缓存 + 推送原生);桌面已放置实例显示占位文本,需用户手动移除 |
- 仅 Android:桌面/浏览器端 runner 照常执行 logic 写 cache,推送原生一步跳过(no-op)
- 逻辑只在 App 存活时执行:
updatePeriodMillis=0,系统不唤醒 App;被杀期间显示最后缓存 - 数据变更自动刷新:服务经
storage.set/remove写数据后,其名下启用卡片防抖 1s 自动重跑 logic.js(含后台 worker 写入);服务应避免秒级高频写 storage,否则卡片会频繁重跑 + 推原生 - RemoteViews 位图限制:图片按 ≤720px 降采样解码(Binder 传输约 1MB 上限的折中);加载失败隐藏图片区
- 点击跳转:热启动经 WebView JS 事件即时跳转;WebView 未就绪时暂存 SharedPreferences,下次冷启动由 App.vue 消费
- 2026-07-31:
listServiceFiles(serviceId, 'desktop-widgets')返回的是相对于该子目录的路径({cardId}/widget.json),不带desktop-widgets/前缀——扫描正则误加前缀导致服务卡片全部注册失败(选卡页"暂无可用卡片")。教训:调用方必须先确认返回路径的基准目录,此类扫描逻辑要有"扫到 0 个"的告警日志。 - 2026-07-31: 原生选卡页的数据只来自推送的载荷数组,卡片"已启用但 logic.js 未成功运行过"时列表为空,用户无法区分"没创建"和"没跑成"。修复为无缓存时用 def 合成占位载荷(label + "加载中…")一并推送,保证启用即可见。
- 2026-07-31: "新卡片并入启用列表"最初只在 init 执行,会话中途创建的服务卡片要重启才可见。修复为
rescanDesktopWidgets()同样按默认值并入;registry 增加seen集合区分"新发现"与"用户显式停用"(顺带修了停用后重启被重新启用的问题)。 - 2026-07-31: logic.js 以经典
<script>注入时顶层await直接 SyntaxError,卡片永远停在"加载中…"且只有超时日志。修复为 runner 注入时自动包(async function(){ ... })(),顶层 await 与 IIFE 写法均兼容。文档示例此前误用顶层 await,已统一注明自动包裹行为。 - 2026-07-31: runner 组 srcdoc 时直接拼接
BRIDGE_SCRIPT裸 JS 字符串,未包<script>标签——垫片不执行、__amiba__未定义,logic.js 立即 ReferenceError(真实表现为卡片永远"加载中…")。BRIDGE_SCRIPT是纯 JS 不是 HTML 片段,所有消费方(service-container / background-manager / widget-lifecycle)都自行包裹<script>标签,runner 漏了。教训:复用注入脚本时先核对既有消费方的包裹方式。 - 2026-07-31: 服务卡片经
service_file_write/edit创建后无任何触发链路(rescan/enable/refresh 都靠 AI 自觉调工具),漏调就要重启才可见。修复为两个 service_file 工具在写入desktop-widgets/路径后自动rescanDesktopWidgets()+refreshWidgetCard()(afterDesktopWidgetFileChange钩子)。 - 2026-07-31: 卡片尺寸是 AI 的需求(widget.json
size字段),但 Android 小组件尺寸只能在 manifest 注册的 provider meta XML 中声明,运行期无法给单个 Provider 改尺寸——方案是固定档位多 Provider(small/medium/large 三个 receiver + 三份 meta XML),选卡页按入口反查尺寸过滤卡片。教训:AppWidget 的"每卡片自定义尺寸"没有运行时 API,档位化是唯一落地路径;新增档位要同步改四处(meta XML / 子类 / manifest / WidgetHelper 刷新列表)。 - 2026-07-31: 自定义卡面走"HTML/SVG → PNG → ImageView"路线:RemoteViews 无 WebView,但 ImageView 是唯一逃生通道。渲染放在沙箱 iframe 内(SVG
foreignObject+ canvas 导出 dataURL,零依赖),不经桥接;宿主只新增"dataURL → 二进制落盘 cache/img/"一段通道,Kotlin 零改动。教训:foreignObject 方案样式必须内联/内嵌(外链 CSS 与图片会静默渲染空白),文档须明示;老 WebView 画 SVG 可能污染 canvas 导致 toDataURL 抛 SecurityError,renderHtml 要 try/catch 兜底。 - 2026-07-31: 卡片样式字段(backgroundColor/textColor/hideTitleBar)落地时的坑:RemoteViews 运行期改背景只有"资源/纯色"两条路(
setInt("setBackgroundColor")会丢圆角),自定义色保圆角需程序内画 GradientDrawable → Bitmap → 铺底层 ImageView(fitXY,固定 192x192 控制 Binder 体积)。骨架根容器因此从 LinearLayout 重构为 FrameLayout + 背景位图层,三套 XML 必须同步加widget_bg_img/widget_titlebarid——buildViews 共享代码引用的每个 id 都要存在于全部骨架,缺一个即运行时崩溃。 - 2026-08-06: 服务运行时更新 storage 后卡片不更新——数据流是 logic.js 单向拉取,无反向通知链路。修复为
registry.setServiceData/removeServiceData写入成功后发内部变更通知(onServiceDataChanged),runner 订阅后按 serviceId 找启用卡片、1s 防抖重跑。关键难点是 logic.js 自身也写 storage 会回流触发:靠lastRunAt+ 3s 宽限期抑制(running去重挡不住防抖延迟到执行结束后的那波,会形成 ping-pong)。
app/src/main/java/com/amiba/desktop/AmibaWidgetProvider.kt(中尺寸 Provider + WidgetHelper JNI 入口)app/src/main/java/com/amiba/desktop/AmibaWidgetProviderSmall.kt/AmibaWidgetProviderLarge.kt(小/大尺寸档位,纯标记子类)app/src/main/java/com/amiba/desktop/WidgetConfigActivity.kt(选卡配置页,按入口尺寸过滤卡片)app/src/main/res/layout/widget_card.xml/widget_card_image.xml/widget_card_bigtext.xml/widget_config.xmlapp/src/main/res/drawable/widget_bg.xmlapp/src/main/res/values/styles_widget.xmlapp/src/main/res/xml/widget_card_info.xml(中 4x2)/widget_card_info_small.xml(小 2x2)/widget_card_info_large.xml(大 4x4)AndroidManifest.xml(三个 receiver 分别带「变形虫卡片·小/中/大」label + configure activity 注册)AndroidManifest.xml(receiver + configure activity 注册)MainActivity.kt(widget 点击跳转处理段)