${esc(x.tags.join(' · '))}
${esc(x.arg_keys.join(' · '))}diff --git a/docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md b/docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md
new file mode 100644
index 0000000..72da400
--- /dev/null
+++ b/docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md
@@ -0,0 +1,60 @@
+# KoKonna official account capabilities
+
+Reviewed 2026-10-03. Source: official Android nightly 1.6.5 client, downloaded
+from https://kokonna.art/pages/download-app. RPC method names and usage were
+inspected without running or modifying the app. No vendor source is distributed.
+
+## Two independent access paths
+
+Frame Flow retains its existing device API Key integration. The documented
+OpenAPI provides device status, upload, image listing/download/delete and display
+selection by ID/name. It exposes rotation, timezone and automatic image-change
+status as read fields, but does not document setters for them.
+
+Account login accesses the official RPC service. The current device key was
+explicitly tested against device discovery and widget discovery: both rejected
+it with `请重新登陆`. Account login, device discovery/detail and widget discovery
+have been verified successfully with HTTP 200. Other methods below are found in
+client code only: their availability, permissions and side effects have not been
+live-tested. Missing OpenAPI documentation is not proof that no undocumented
+API Key endpoint exists.
+
+## Additional frame-related capabilities found
+
+| Capability | RPC methods | Evidence and boundary |
+| --- | --- | --- |
+| Official widgets and parameters | getFrameCovers / setFrameCover | Catalog read verified (75 items); current selection can be empty. Selection/cancellation discovered, not tested. |
+| Rotation configuration | setRotate | Discovered setter; OpenAPI only reads rotation. |
+| Device name and timezone | changeName / app.setTimezone | Discovered setters; OpenAPI only reads these fields. |
+| Slideshow mode and period | setFrameImageChangeType / setFrameImageChangeMinute | Discovered setters; actual rotation sequence must be derived from mode/filter/order, not invented. |
+| Slideshow tag filters | setFrameImageChangeTags | Discovered filter configuration. |
+| Tagged, paginated image library | getImages / addTag / removeTag / setImageTags | Discovered; original client passes `page`, `paegSize` (spelling as observed) and `tags`. Requires separate validation. |
+| Image renaming | changeFrameImageName | Discovered; distinct from rename-device. |
+| Official gallery collections | getFrameGallerys / getFrameGalleryImages / addFrameGalleryImage | Discovered browse/add flow; may depend on entitlement. |
+| Artist content | getFrameArtists / addFrameArtist | Discovered; subscriptions/selection may consume points or alter display. |
+| Startup/splash image | getFrameSplashs / setSplash | Discovered browse/set flow. |
+| Remote refresh | refreshImage | Discovered write; do not infer display success from RPC acceptance. |
+| Image sharing/import | getShareImages / shareImage / importImagesByTransferToken | Discovered; do not expose transfer tokens or share credentials. |
+| Device sharing and pairing | enablePair / enableShareCode / refreshShareUUID; app.addShareRobot / removeShare | Discovered sensitive configuration; excluded from automatic tests. |
+| Firmware information/upgrade | app.getFirmwares / setUpgradeFirmware | Discovered; not enabled or tested. |
+| Wake/sleep interval | setFrameImageChangeInterval | Client updates `defaultSleepTime`; do not mistake this for slideshow minutes. |
+| AI/voice content | chat / recognizeVoice / text2speech | Discovered; support may vary with hardware, service and account credits. |
+| Feedback and likes | reportFrameImage / likeImage | Discovered; modifies remote account data. |
+
+RPC methods in this table use the `appFrame.` prefix unless explicitly `app.`.
+Generic account methods also cover profile, password, device binding/unlinking,
+credits/orders and custom assistant roles. Their presence in the shared client
+is not evidence that all are supported by the 13.3-inch B101 frame. No binding,
+reset, firmware, payment or subscription mutation was executed during research.
+
+## Frame Flow implementation boundary
+
+- API Key device/gallery/upload/display support remains unchanged.
+- Official widget panel separately explains account authorization requirements.
+- The existing WidgetReader uses a server-side official session secret; direct
+ account/password connection UI and automatic session renewal are not implemented.
+- Live `NO_WIDGET` requires explicit null current-widget association and null
+ robot association ID; missing fields remain `CURRENT_UNVERIFIED`.
+- Unconfigured access returns `AUTH_REQUIRED` and an explicitly dated snapshot.
+- Widget selection and the other RPC writes above are not implemented. Account
+ login capability discovery is not permission to exercise those writes.
diff --git a/pigway_frame_flow/drivers/kokonna/widgets.py b/pigway_frame_flow/drivers/kokonna/widgets.py
index ecd41ff..96a1bd1 100644
--- a/pigway_frame_flow/drivers/kokonna/widgets.py
+++ b/pigway_frame_flow/drivers/kokonna/widgets.py
@@ -83,13 +83,24 @@ class WidgetReader:
cover = selected.get("frameCover") or {}
current_id = cover.get("id", selected.get("coverId"))
current = next((row for row in catalog if row.id == current_id), None)
+ robot = data.get("robot") or {}
+ no_widget = (
+ "frameCoverRobot" in data
+ and data["frameCoverRobot"] is None
+ and "frameCoverRobotId" in robot
+ and robot["frameCoverRobotId"] is None
+ )
result = WidgetState(
source="live",
configured=True,
catalog=catalog,
current=current,
- current_verified=current is not None,
- status="OK" if current else "CURRENT_UNVERIFIED",
+ current_verified=current is not None or no_widget,
+ status="OK"
+ if current
+ else "NO_WIDGET"
+ if no_widget
+ else "CURRENT_UNVERIFIED",
)
except FlowError:
raise
diff --git a/pigway_frame_flow/web/features.js b/pigway_frame_flow/web/features.js
index b838cad..9791a92 100644
--- a/pigway_frame_flow/web/features.js
+++ b/pigway_frame_flow/web/features.js
@@ -81,7 +81,8 @@ async function officialContentInfo(){
try {
const data=await api('/api/v1/devices/'+state.device+'/widgets');
const label=data.source==='live'?(zh?'实时目录':'Live catalog'):data.source==='mock'?'Mock':(zh?'历史目录 · '+data.observed_at:'Catalog snapshot · '+data.observed_at);
- info.innerHTML=`
${esc(label)}
${data.current_verified?esc((zh?'当前小组件:':'Current widget: ')+data.current.name):(zh?'当前小组件尚未核实。官方微信会话与设备 API Key 是不同凭据。':'Current widget is unverified. The official session differs from the device API Key.')}
${zh?'此入口只读,不会切换或取消官方订阅。':'Read only. This does not change official subscriptions.'}
`; + const currentLabel=!data.configured?(zh?'尚未授权官方账号,当前状态不可读取。':'Official account not authorized; current state is unavailable.'):data.status==='NO_WIDGET'?(zh?'未使用官方小组件':'No official widget selected'):data.current_verified&&data.current?((zh?'当前小组件:':'Current widget: ')+data.current.name):(zh?'当前小组件尚未核实':'Current widget is unverified'); + info.innerHTML=`${esc(label)}
${esc(currentLabel)}
${zh?'此入口只读,不会切换或取消官方订阅。':'Read only. This does not change official subscriptions.'}
`; const render=()=>{const query=info.querySelector('input').value.toLowerCase();info.querySelector('.widget-list').innerHTML=data.catalog.filter(x=>(x.name+' '+x.tags.join(' ')).toLowerCase().includes(query)).map(x=>`${esc(x.tags.join(' · '))}
${esc(x.arg_keys.join(' · '))}