feat(libtv-video): direct Feishu delivery via detached background waiter#973
Merged
alchemistklk merged 8 commits intomainfrom Apr 10, 2026
Merged
feat(libtv-video): direct Feishu delivery via detached background waiter#973alchemistklk merged 8 commits intomainfrom
alchemistklk merged 8 commits intomainfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Deploying nexu-docs with
|
| Latest commit: |
7ed36f2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c2229b49.nexu-docs.pages.dev |
| Branch Preview URL: | https://feat-libtv-skill-requirement.nexu-docs.pages.dev |
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
- 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.
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.
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.
Contributor
Author
|
/cr |
|
✅ CR topic created in Feishu topic group Refly CR. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de86300613
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
lefarcen
approved these changes
Apr 9, 2026
lefarcen
approved these changes
Apr 9, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Rework the
libtv-videobundled skill so video results are delivered directly to the originating Feishu chat via a detached background waiter that calls the Feishu file/message APIs. Removes the earlier HTTP-notify controller route and the interim sessions_spawn/subagent attempt, keeping only the minimum stable routing state ({channel, chat_id}).Why
Two prior attempts at libtv delivery were silently broken:
POST /api/internal/libtv-notify(shipped ine7a18d86) routed throughcontainer.gatewayService.sendChannelMessageusing persisted{channel, to, sessionKey, accountId, threadId, idempotencyKey}.account_idwent stale across session migrations — the gateway couldn't resolve the account, and the user never received anything.sessions_spawn+ subagent speech (interim refactor) relied on OpenClaw spawning a child session that inherited the parent's Feishu binding and then having the child's model speak the final message verbatim. Verified at the OpenClaw source level that the mechanism exists — but live testing showed two additional failure modes: the model can pick the wrong skill for video generation (triedliblib-ai-geninstead), and even when the right skill runs, there's no guarantee the child's model speaks the structured output verbatim.What actually works is what Colin's manual Feishu probe proved: call
/im/v1/messageswith{app_id, app_secret} → tenant_access_token → open_id, usingopen_idfrom the inbound message metadata (stable per-user per-bot — never goes stale). This PR bakes that into the skill, with zero controller involvement.How
Delivery chain:
Key design decisions:
sessions_spawnworks but has too many moving parts (skill selection, model speech, OpenClaw hook inheritance). A plainPopen(..., start_new_session=True)is simpler and doesn't depend on the parent session staying alive or on any LLM speaking the result.OPENCLAW_CHAT_IDconvention that earlier libtv code assumed, OpenClaw does not inject inbound channel metadata as env vars into skill subprocesses. It passes inbound metadata to the model in the user message text. The model must extract it and pass it as--channel feishu --chat-id <ou_...>on everycreate-sessioninvocation. SKILL.md documents this as aCRITICALstep with a concrete example. Same convention deploy-skill uses for itssubmitcommand.feishu_send_video.pylives next tolibtv_video.pyand is invoked via subprocess. Adding Slack/Discord/WeChat is just dropping a new<channel>_send_video.pyhelper and adding one branch in_deliver_results. No controller changes needed.code: 99991672(missingim:resource:upload), the helper parses the grant URL out of the error body and sends a plain text message containing the video URL + grant URL, so the user is still notified AND knows how to enable native-media delivery.delivered_at. Re-invokingwait-and-deliveron an already-delivered session is a safe no-op.Controller cleanup:
POST /api/internal/libtv-notifyroute +libtvNotifyRequestSchema+libtvNotifyResponseSchema+getBotIdFromSessionKeyhelper — deleted fromapps/controller/src/routes/desktop-routes.ts.apps/controller/tests/desktop-routes.test.ts— deleted (only containedlibtv-notifytests).tests/controller/desktop-routes.test.ts— deleted (re-export of above).apps/controller/openapi.json,apps/web/lib/api/sdk.gen.ts,apps/web/lib/api/types.gen.ts— regenerated viapnpm generate-types.Pre-merge code review (superpowers:code-reviewer) flagged two high-confidence issues before commit, both fixed:
_spawn_background_waiter— parent held the log file descriptor afterPopenreturned. Fixed by wrapping intry/finallyand closing afterPopendup()s the fd into the child.upload_to_feishuwhengrant_urlparsed butchat_idempty — now emits an explicit diagnostic referencing the missing context.Affected areas
apps/desktop/static/bundled-skills/libtv-video/libtv-notifyroute + schemas + testslibtv-videorewrite + newfeishu_send_video.pyhelperChecklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (704 passed, 38 skipped — 6 are the rewritten libtv-video tests)pnpm generate-typesrun (libtv-notify route removed from openapi.json + sdk.gen.ts + types.gen.ts)app_secretandtenant_access_tokenare never printed to stderr or the waiter log)anytypes introducedNotes for reviewers
End-to-end validation (real network calls):
sk-libtv-...key):create-session→ persisted → forked waiter → polled → terminal with a real.mp4URL onlibtv-res.liblib.art, auto-downloaded locally to~/Downloads/libtv_results/.--helpfirst to discover the new--channel/--chat-idargs (proves the updated SKILL.md was loaded), then invokedcreate-session "..." --channel feishu --chat-id ou_33314772052f837a3cb2f919aa4605de.libtv-sessions.jsonhaddelivery: {channel: "feishu", chat_id: "ou_333..."},status: "completed",delivered_at: "2026-04-09T20:13:29.692667".~/.nexu/libtv-waiter-<session-id>.log) captured the full chain: spawn → poll → terminal → download (3090KB) → upload to Feishu (file_key: file_v3_0010j_63f039c9-...) → send media message →✅ Delivered to originating channel.Review focus areas:
_spawn_background_waiterfd lifecycle (apps/desktop/static/bundled-skills/libtv-video/scripts/libtv_video.py) — reviewer flagged, fixed with try/finally._extract_permission_grant_urlregex and the permission-fallback path (feishu_send_video.py) — reviewer flagged the empty-chat_id branch, fixed with explicit diagnostic.SKILL.md"CRITICAL: always pass--channeland--chat-id" — this is the instruction the model has to obey every turn.Out of scope / follow-ups:
<channel>_send_video.pyhelpers and branch in_deliver_results. The architecture already accommodates this.scripts/probe/feishu-send-probe.mjsas a reusable "skill vs channel" bisection tool.open_id/chat_idprefix detection from medeo-video; group chats with thread-required replies would need additional handling.