Build Pigway Frame Flow with bilingual UI, complete KoKonna driver and safe automation

This commit is contained in:
Pigway
2026-10-02 00:03:48 +08:00
commit 6652590b84
79 changed files with 13773 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
# Synology NAS deployment / 群晖部署
Target: DS923+, DSM 7.3.1, AMD64, Container Manager.
Directory: /volume1/docker/pigway-frame-flow. Port: 18787.
## 1. Package and upload / 打包上传
From the repository:
```sh
python3 scripts/package-nas.py
scp -O dist/pigway-frame-flow-nas.tar.gz nas-example:/tmp/pigway-frame-flow-nas.tar.gz
ssh nas-example
```
The package contains tracked source/config examples/scripts only, never .env,
actual keys, data or .git. It is a deployment artifact, not a Git backup.
Use `git bundle create frame-flow.bundle --all` separately for history backups.
## 2. Prepare project directory / 准备目录
Verify target is empty or explicitly backed up before extracting. Do not extract
onto another existing installation or other services. If the directory exists,
inspect it first; preserve its .env and data.
```sh
mkdir -p /volume1/docker/pigway-frame-flow
# Only for an empty/new target:
tar -xzf /tmp/pigway-frame-flow-nas.tar.gz -C /volume1/docker
cd /volume1/docker/pigway-frame-flow
mkdir -p data/images data/generated data/cache config
sudo chown -R 10333:10333 data config
cp .env.example .env
chmod 600 .env
```
Edit `.env` on the NAS. Set FRAME_FLOW_TOKEN to a strong random token. API key
stays empty. KOKONNA_MOCK=true and KOKONNA_WRITE_ENABLED=false.
Set WEB_BIND to the actual NAS LAN address (recommended) or explicitly 0.0.0.0;
WEB_PORT=18787. The latter binds every NAS interface, so use the LAN IP when
strict LAN-only binding is required. No firewall/router/proxy edits are performed.
Copy deploy/devices.yml.example to config/devices.yml if multiple devices are desired.
## 3. Validate, build, start / 验证构建启动
Synology Docker is /usr/local/bin/docker. The SSH user may require sudo. Do not
change socket ownership, groups, Docker network settings or other containers.
```sh
sudo /usr/local/bin/docker compose config --quiet
sudo /usr/local/bin/docker compose build --pull
sudo /usr/local/bin/docker compose up -d
sudo /usr/local/bin/docker compose ps
sudo /usr/local/bin/docker compose logs --tail 100
sudo /usr/local/bin/docker inspect pigway-frame-flow \
--format 'id={{.Id}} started={{.State.StartedAt}} health={{.State.Health.Status}} restart={{.HostConfig.RestartPolicy.Name}} ports={{json .NetworkSettings.Ports}} mounts={{json .Mounts}} network={{json .NetworkSettings.Networks}}'
sudo /usr/local/bin/docker image inspect pigway-frame-flow:latest \
--format 'id={{.Id}} size={{.Size}} architecture={{.Architecture}}'
```
Do not print unfiltered `docker inspect`: its environment can contain keys/tokens.
Likewise do not print `docker compose config` without --quiet on a live installation.
Run the read-only smoke checker from a machine with Python and set FRAME_FLOW_URL
to http://NAS_LAN_IP:18787; FRAME_FLOW_TOKEN stays in private env/file.
Health: GET /health → 200. Readiness: GET /ready → 200 when SQLite, driver config,
auth and Scheduler are ready. Missing credentials in live mode → 503. The cloud
is not probed by readiness. Cloud loss does not cause healthcheck restart loops.
## 4. Verify persistence / 持久化
Create a disabled test schedule in Web UI and locally stage one image (Mock only).
Record the job and asset IDs, then:
```sh
sudo /usr/local/bin/docker compose restart pigway-frame-flow
```
Wait for healthy state, check /health and /ready, and verify the same job/asset
IDs and history remain. Database is data/frameflow.db with WAL/SHM alongside it;
back up while stopped or use SQLite backup API, not a lone live DB file copy.
Restart policy must read unless-stopped. Do not reboot the NAS for acceptance.
## 5. Live read-only verification / 实机只读
After Mock acceptance, fill KOKONNA_API_KEY; set mock=false, keep writes=false.
Recreate only this service to apply env. Query device and images metadata only.
Verify known model/geometry/timezone/name against real data; missing vendor model
fields are not claimed verified. No original downloads are necessary. Stop on
mismatch; never try to fix cloud state automatically. Obtain explicit owner
permission before any upload/switch/delete. See REAL_DEVICE_TEST.md.
## Layout
```
/volume1/docker/pigway-frame-flow/
docker-compose.yml
Dockerfile
.env
requirements.lock
pigway_frame_flow/
vendor/
scripts/
config/
data/
frameflow.db
images/
generated/
cache/
```
Container runs UID/GID 10333:10333; only data/config need ownership. Avoid recursive
ownership changes outside this project's paths. Standard Compose bridge is private
and isolated by project name. Logs use stdout/stderr with Docker's capped json-file.
No logs directory or Docker socket mount.
+27
View File
@@ -0,0 +1,27 @@
# Real device test plan / 单次实机测试计划
**Not executed. Explicit owner authorization is required before any write.**
只读核对完成后询问:“真实设备只读验收完成,是否允许进行一次真实图片上传与显示测试?”
1. GET Frame Flow device (driver POST /device); record current imageId AND coverId,
synced, geometry, model, rotation, timezone, firmware, heartbeat and battery.
2. POST /listImages for metadata only. Record count, current image, sorted IDs,
duplicates, name/date/dimensions anomalies. No download-all action.
3. Stop on discrepancy. Missing model fields must be reported as unavailable.
4. Once explicitly authorized, locally render **one** 1200×1600 test image containing
PIGWAY FRAME FLOW TEST and Shanghai timestamp, no private content.
5. Record an operation ID and previous image; one upload only, no write retries.
6. Poll device read-only no more often than 60 seconds, respecting the 20/min budget.
Confirm imageId and synced. If sent but response lost, mark UNKNOWN, block writes,
reconcile gallery/device; never upload again to guess the outcome.
7. Keep the test image visible at most 1–3 minutes, then restore the recorded artwork
with a separate tracked switch operation. Query once more to confirm restored
imageId and synced. Record coverId after restoration too.
8. Delete only that known test image after restoration is positively confirmed AND
it is not a base/previous/override/current reference. If ambiguous, leave it and
report. Never delete art or retry a timed-out delete.
9. Disable real writes again. Report each operation ID/result and remaining test image.
The API cannot set coverId. Restoration guarantees a requested image switch when
confirmed, not arbitrary cover metadata restoration. Physical screen refresh has
no documented pixel telemetry; visual owner observation supplements cloud synced.
+55
View File
@@ -0,0 +1,55 @@
# KoKonna API research / 接口调查
Checked 2026-10-01 against https://kokonna.art/pages/openapi.
Base: https://api.kokonna.art/openapi. No fallback to legacy galaxyguide.cn.
| Official endpoint | Frame Flow capability |
|---|---|
| POST /device | Device panel, standardized device model, charging, firmware, sync, cover, storage, native slideshow mode/interval |
| POST /upload | Rendered JPEG upload and immediate activation, asynchronous operation |
| POST /listImages | On-demand cloud metadata gallery, current marker, name/date/geometry/size |
| POST /deleteImage | Confirmed deletion with restore-reference protection |
| GET /image/:apikey/:imageId | Authenticated server proxy, original bytes, lazy cached thumbnails; key never sent to browser |
| POST /displayImageById | Switch through unified DisplayService |
| POST /displayImageByName | Explicit filename switch; ambiguous names rejected |
## Native slideshow / 原生轮播
`switchType` (queue/random) and `switchMinute` are readable through device.
There is no documented playlist-membership API and no slideshow-setting API.
`listImages` returns the entire cloud gallery sorted by descending ID; it does
not establish playlist membership. No official pagination parameter is documented.
Frame Flow labels membership as unavailable, native editing unsupported, and
provides its own SQLite-backed playlists as an independent capability.
## Cloud constraints / 云端边界
Official limit: 20 requests per minute per device key; local transport reserves
headroom at 18. Device state has a 60-second cache, gallery is loaded on demand.
Uploads automatically activate a new image; there is no upload-without-activation
flag. Gallery-only uploads are therefore local staging, not a fictitious cloud
operation. Maximum upstream request size is 50 MB; this service accepts 20 MiB
image files with bounded decoded pixels. No non-idempotent cloud write is retried.
Cloud acknowledgement is not physical refresh confirmation; /device synced and
imageId drive the operation state. Unconfirmed or interrupted writes become UNKNOWN
and block later device writes until read-only reconciliation/explicit acknowledgement.
## Reuse / 复用
ClawHub API: https://clawhub.ai/api/v1/skills/kokonna-frame
Versioned ZIP: https://clawhub.ai/api/v1/download?slug=kokonna-frame&version=1.2.0
Community package owner: guooscar; metadata license MIT-0. Reused protocol payload
and device request, with an injected safe transport. It is a skill/CLI, not a
proven NAS daemon, and its default host and screen field assumptions are dated.
We implement scheduling, storage, driver normalization, renderer, REST, auth,
operations, idempotency, recovery, bilingual UI and deployment independently.
Other inspected repositories:
- https://github.com/patrickchen914/kokonna : Home Assistant integration, MIT;
depends on Home Assistant and had legacy host/key-in-image-URL behavior.
- https://github.com/nixihz/kokonna-skill : broader CLI/Hermes endpoint coverage,
README MIT, legacy host and raw upstream error messages. Not incorporated.
- https://github.com/zhengweiran66-eng/kokonna-product.html : product page.
No real device key was used during development. Cloud host root returned 530 in
an unauthenticated research request; no claim of current real-device connectivity.
+101
View File
@@ -0,0 +1,101 @@
# Optional external integration contract
Ticktar integration is optional. Frame Flow has no runtime dependency on Ticktar.
No external client receives a KoKonna API key, accesses the SQLite database, or
imports a vendor SDK. Web UI and external clients use the same application services.
## Architecture
External client → /api/v1 → Application / DisplayService → FrameDeviceDriver → KoKonna.
Configure the caller with FRAME_FLOW_URL and FRAME_FLOW_TOKEN only. No caller-specific
branch exists in core. Frame Flow starts and works with no integration configuration.
## Authentication and permissions
Authorization: Bearer FRAME_FLOW_TOKEN. The bootstrap token is provided through
.env or a secret file; database stores SHA-256 hash only. The client model supports
client_id, name, permissions, enabled, created_at, last_used_at. Permissions:
devices:read, images:read, images:delete, display:write, templates:read, render:write,
jobs:read, jobs:write, history:read, settings:write, admin.
0.1.0 seeds one admin token; provisioning scoped client rows is supported by middleware
but multi-token management UI is not implemented. Use a trusted server-to-server
connection; CORS is opt-in by exact origins, never wildcard by default.
## Discovery and capabilities
GET /api/v1/devices; GET /api/v1/devices/{id}; GET /api/v1/templates.
Device provides id/name/status, display geometry, power, current_content, firmware,
model/model_code, timezone, last_seen_at, capabilities. Model fallback is flagged
model_verified=false when the cloud omits model fields. Do not branch on driver:
check capabilities upload/gallery/delete/switch/battery/color/remote_refresh.
Native slideshow members/editing are unavailable in KoKonna's documented API.
## Display
POST /api/v1/display, Idempotency-Key: a-unique-client-request-key
```json
{
"device_id": "living-room",
"content": {
"type": "template",
"template": "daily-dashboard",
"data": {"title": "Good Morning", "events": []},
"profile": "color"
},
"options": {"duration": 1800, "restore_previous": true, "priority": 50},
"source": {"client": "external-app", "request_id": "caller-trace"}
}
```
Content supports image (exactly one image_id/image_name/asset_id/base64), image_url
(HTTPS/public DNS/exact allowlist), template. html/markdown explicitly return
UNSUPPORTED_CAPABILITY. For upload-only use device /upload with display_now=false;
this is safe local staging because KoKonna cloud upload always activates an image.
Returns HTTP 202: {operation_id, job_id, status:"accepted"}. job_id is a legacy
acceptance alias for operation_id, not a scheduler job ID.
## Render and operations
POST /api/v1/render with template content returns 202 and a local rendered asset
when completed; no device write. GET /api/v1/operations/{operation_id}:
QUEUED → RENDERING → UPLOADING → SYNCING → COMPLETED / FAILED / UNKNOWN.
Result includes image_id or asset_id/url. SYNCING confirms imageId plus synced
through read-only device status; there is no documented physical-screen telemetry.
Token must own the operation or be admin. Never interpret HTTP 202 as refreshed.
## Idempotency
Key is scoped to authenticated client, retained seven days. Same key and same
normalized action/body returns the same operation, even across restart. A different
body with the same key returns CONFLICT (409). Do not retry UNKNOWN with a new key.
Cloud writes have no automatic retries. UNKNOWN blocks further device writes;
admin /operations/{id}/reconcile reads device/gallery and may acknowledge a manually
verified outcome without reissuing a write.
## Errors and tracing
All API errors: {error:{code,message,request_id,details:{}}}. Stable codes include
INVALID_REQUEST, UNAUTHORIZED, FORBIDDEN, DEVICE_NOT_FOUND, DEVICE_OFFLINE,
UNSUPPORTED_CAPABILITY, RENDER_FAILED, UPLOAD_FAILED, WRITE_RESULT_UNKNOWN,
RATE_LIMITED, INTERNAL_ERROR, CONFIG_REQUIRED, WRITE_DISABLED, CONFLICT, NOT_FOUND,
CLOUD_UNAVAILABLE. Use code, not message text. Validation does not echo inputs.
X-Request-ID accepts a bounded safe identifier; it is returned on the response.
The authenticated client is authoritative; source.client is optional metadata.
History stores client_id/request_id/operation_id/device_id/action/result/timestamp.
No internal endpoints, headers, stacks, paths or secrets are returned.
## Future webhook
SQLite events and webhook_subscriptions reserve operation.completed/failed,
display.changed, device.online/offline/low_battery events. 0.1.0 emits operation/display
events only; no callback delivery, retry worker or registration API is implemented.
Future deliveries should use signed payloads and bounded retries.
OAuth, tenancy, RBAC management and generated SDKs are future work.
## Compatibility
/openapi.json and /docs are the contract. Snapshot tests cover paths, response
codes, standard device fields, security and operation enums. Static snapshot is
also supplied in docs/openapi.json. Breaking changes require /api/v2.
+6766
View File
File diff suppressed because it is too large Load Diff