Commit 01ac8ea
authored
feat(libtv-video): direct Feishu delivery via detached background waiter (#973)
* feat: harden libtv delivery notifications
* docs: add libtv direct-key routing design
* refactor(libtv-video): deliver directly to Feishu via detached waiter
Replace the sessions_spawn + subagent delivery attempt (and the earlier
HTTP-notify callback path) with a simple, stable design: create-session
forks a detached wait-and-deliver process, and that process calls the
Feishu file/message APIs directly on terminal success.
Skill changes (apps/desktop/static/bundled-skills/libtv-video/):
- scripts/libtv_video.py:
- create-session takes --channel and --chat-id CLI args. The model
extracts sender_id from the inbound Feishu metadata block and
passes them explicitly, the same way deploy-skill's submit command
does.
- Persist only {channel, chat_id} in libtv-sessions.json. No more
account_id / session_key / thread_id — those were the root cause
of the earlier silent-delivery bug where stale account_id values
made the controller gateway unable to route.
- _spawn_background_waiter uses subprocess.Popen(start_new_session=
True, stdin=DEVNULL, close_fds=True) to fork a detached worker
that survives the parent's exit. Parent closes its copy of the log
file descriptor in a try/finally to avoid fd leaks.
- cmd_wait_and_deliver polls libtv until terminal, then shells out
to feishu_send_video.py for each result URL. Persists a
delivered_at idempotency marker so re-invocations on a delivered
session are a safe no-op.
- Transient network errors (RemoteDisconnected, ConnectionError,
URLError, TimeoutError) in the poll loop are caught and retried
on the next tick instead of crashing the waiter.
- scripts/feishu_send_video.py (new file):
- Copied from medeo-video's proven helper as the starting point.
- Extended with _extract_permission_grant_url +
_handle_upload_permission_error: if upload fails with Feishu's
im:resource:upload scope error (code 99991672), fall back to a
plain text message containing the video URL AND the grant URL
so the user can enable native-media delivery for future runs.
Uses the already-granted im:message scope.
- On permission error with no chat_id available, emit an explicit
diagnostic instead of silently falling through.
- SKILL.md:
- New delivery contract section explaining the background waiter
pattern.
- CRITICAL instruction that the model must extract sender_id from
the inbound Feishu metadata block and pass it as --chat-id on
every create-session invocation. Includes a concrete example.
- Updated guard checklist to match the new contract.
Controller cleanup (apps/controller/):
- Delete POST /api/internal/libtv-notify, its request/response
schemas, and the getBotIdFromSessionKey helper from
src/routes/desktop-routes.ts.
- Delete the whole apps/controller/tests/desktop-routes.test.ts file —
it only contained tests for the removed route.
- Delete tests/controller/desktop-routes.test.ts (re-export of above).
- Regenerate openapi.json, sdk.gen.ts, types.gen.ts via
pnpm generate-types.
Tests (tests/desktop/libtv-video-skill.test.ts):
- Full rewrite against the new contract. 6 tests cover: Seedance URL
constant, submit-confirmation JSON shape, persisted delivery block,
empty-delivery fallback, explicit video ratio override, sk-libtv-
direct API routing, malformed submit rejection. All green.
- Use LIBTV_SKIP_BACKGROUND_WAITER=1 to prevent tests from leaking
detached processes into CI.
E2E validation:
- CLI smoke-tested against real libtv direct API with sk-libtv- key —
real video generated and locally downloaded.
- Live Feishu DM test with hot-swapped state-dir skill: the model
read the updated SKILL.md, ran create-session with --channel feishu
--chat-id ou_333..., the detached waiter polled libtv, invoked
feishu_send_video.py, and delivered a native media video message
to the user's Feishu chat. End-to-end round trip: ~3 seconds after
create-session.
Follow-ups (not in this commit):
- Support additional channels by dropping a <channel>_send_video.py
helper alongside feishu_send_video.py and adding a branch to
_deliver_results.
- Add a scripts/probe/feishu-send-probe.mjs as a reusable smoke probe
for bisecting "skill vs channel" delivery failures.
* chore(libtv-video): rename display name and surface image generation
Rename the bundled skill's display name from "LibTV Video" to
"LibTV - Image&Video(Seedance 2.0)" and update the description to
make it clear the skill supports BOTH image and video generation,
powered by Seedance 2.0.
The skill directory (libtv-video) and ledger slug stay the same so
persisted state, the compiled openclaw config, and the e2e delivery
path from the previous commit remain valid.
Updated surfaces:
- SKILL.md frontmatter name + description
- SKILL.md H1 heading
- libtv_video.py module docstring
- libtv_video.py ArgumentParser description
* chore(libtv-video): update description and drop Seedance 2.0 prompt pin
- Rewrite SKILL.md frontmatter description to match the approved copy:
"Seedance 2.0 video & image generation via LibTV Gateway - AI
text-to-video, image-to-video, video continuation, style transfer,
and text-to-image using Seedance 2.0 model. Also supports Kling 3.0,
Wan 2.6, Midjourney, Seedream 5.0. Trigger phrases: ..."
This expands trigger coverage to image-generation verbs (draw,
generate image, make an image) and enumerates the supported
alternative models (Kling 3.0, Wan 2.6, Midjourney, Seedream 5.0).
- Drop the hardcoded MODEL_HINT constant and _append_model_hint helper
from libtv_video.py. The skill used to silently append
", please use Seedance 2.0" to every prompt that did not mention a
model, which forced Seedance 2.0 even when the user was happy with
the LibTV backend default or wanted another supported model.
_build_session_message now only appends the video ratio hint.
* feat(skillhub): always install latest libtv-video from bundle on boot
Add replaceLibtvVideoFromBundle and call it from
skillhub-service.initialize() after copyStaticSkills. Unconditionally
wipes and re-copies the bundled libtv-video into the state dir on
every controller startup, then upserts the managed ledger record to
status: installed. Ships bundled libtv-video updates (including the
detached-waiter + direct-Feishu-delivery refactor) to existing users
on their next app boot.
Bypasses copyStaticSkills on purpose — its knownSlugs check skips on
any ledger source, which would silently fail if the user has a
workspace or user-level libtv-video entry. The dedicated function
only touches the state-dir copy under <openclawSkillsDir>/libtv-video/
and only the managed ledger record. User-scoped copies in
~/.agents/skills/ and per-agent copies under agents/<id>/skills/
are left untouched. Does not respect the managed record's
uninstalled status — libtv-video is treated as a core bundled
capability that always tracks the shipped version.
New tests in apps/controller/tests/replace-libtv-video-from-bundle.
test.ts (5 cases, real SkillDb on a temp ledger + real filesystem):
- fresh-install
- replaces stale content while keeping managed record installed
- resurrects over an uninstalled managed record instead of honoring it
- leaves workspace records for the same slug byte-identical
- returns bundle-missing when bundled source dir is absent
Updated the existing skillhub-service test mock to expose the new
export so the 18 mocked-SkillDb cases still pass.
* chore: gitignore docs/superpowers and remove stale design doc
docs/superpowers/ is a working directory for agent-generated plans and
design notes that should not be tracked in the repo. Add it to
.gitignore. Remove the stale direct-key routing design doc committed
in adf2ece — it describes an interim approach that has since been
superseded by the detached background waiter + direct Feishu delivery
design landed in 5fca9ad.
* fix(libtv-video): exit non-zero when feishu send_video_message fails
main() was ignoring the False return from send_video_message and
exiting 0, which caused _deliver_feishu_video in libtv_video.py to
treat the failed send as successful and set delivered_at — the user
would never receive the video but the skill would record delivery as
complete. Now sys.exit(1) on False so the caller correctly detects the
failure and skips setting delivered_at.1 parent a655585 commit 01ac8ea
10 files changed
Lines changed: 1951 additions & 135 deletions
File tree
- apps
- controller
- src/services
- skillhub
- tests
- desktop/static/bundled-skills/libtv-video
- scripts
- tests
- controller
- desktop
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
5 | 8 | | |
6 | 9 | | |
7 | 10 | | |
| |||
163 | 166 | | |
164 | 167 | | |
165 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
166 | 180 | | |
167 | 181 | | |
168 | 182 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
5 | 7 | | |
6 | 8 | | |
7 | 9 | | |
| |||
99 | 101 | | |
100 | 102 | | |
101 | 103 | | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
102 | 165 | | |
103 | 166 | | |
104 | 167 | | |
| |||
Lines changed: 240 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
134 | 139 | | |
135 | 140 | | |
136 | 141 | | |
| |||
140 | 145 | | |
141 | 146 | | |
142 | 147 | | |
| 148 | + | |
143 | 149 | | |
144 | 150 | | |
145 | 151 | | |
| |||
163 | 169 | | |
164 | 170 | | |
165 | 171 | | |
| 172 | + | |
166 | 173 | | |
167 | 174 | | |
168 | 175 | | |
| |||
0 commit comments