Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ apps/mobile/test/message-selection.test.ts, packages/client/test/message-selecti

## Gotchas

- The #1023 Lark parent-video candidate needs a native `media` parent quoted by an actual Bot-mentioned Human request in a synced conversation. Open that Channel and confirm there is no video/cover request before **Prepare video**. Prepare, then manually play, pause and seek; verify advancing decoded frames rather than only a successful HTTP response. Original download is independent, and a missing/failed native cover must not block playback. Use a fresh native resource to verify stop-before-acquisition; reusing an acquired parent tests cached access instead. Native and installed-product qualification remain pending in `docs/dev/verification/lark-1023-channel-video.md`; do not fabricate a Source Event or rename an ordinary file to qualify video.
- Open the same event's Bot Inbox source inspector before preparing the Channel video. It must acquire neither video nor cover automatically; an explicit original download should still work. An automatic request here defeats the stop-before-acquisition precondition even when the Channel player itself is lazy.
- **Expo Native Bot mentions (#2160).** Open a group with canonical `mentions` ranges (a Bot reply starting with `@nickname`, and a Human inline mention). The current avatar and primary-colored `@name` share the body text line. A leading mention joins the first supported Markdown paragraph; subsequent blocks keep the existing renderer. Tap the inline name and verify `/dm/<botSlug>` against the canonical ID, including after a rename. Missing IDs retain a readable stored label without navigation; unstructured `@name` stays plain text. Verify cached history, live and optimistic data through `apps/mobile/test/conversation.test.ts`; `apps/mobile/test/message-mentions.test.ts` covers ranges, identities, CRLF, references and streaming first paragraphs. Native checks: default/intermediate/maximum text × light/dark, inline wrapping, per-Bot name colors, long-press copy/reactions and keyboard draft retention. Follow the Human-confirmed inline-size exception in mobile DESIGN; do not restore a 44/48-point block height. The Human-selected presentation has no background: avatar + @ + primary-colored name, inline with body text. Record platforms separately; iOS remains unverified without its native lane. Bot mentions embedded later in Markdown retain Web's plain-Markdown behavior.

- Read positions only move forward. To see the divider again, post a new message so the Bot replies after the anchor.
Expand Down
40 changes: 21 additions & 19 deletions apps/mobile/src/host/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,25 +394,27 @@ export function attachmentsOf(
};
});
const sourceEventId = message.bridgeOrigin?.sourceEventId;
const bridged = (message.bridgeMedia?.items ?? []).map((item): MessageAttachment => ({
key: `${message.id}:bridge:${item.id}`,
identity: `${sourceEventId ?? message.id}:bridge:${item.id}`,
kind: item.kind,
name: item.name,
...(item.sizeBytes === undefined ? {} : { size: item.sizeBytes }),
...(sourceEventId === undefined
? {}
: {
source: resolve(
attachmentPath({
channelId: entry.channelId,
sourceEventId,
attachmentId: item.id,
}),
),
}),
local: false,
}));
const bridged = (message.bridgeMedia?.items ?? [])
.filter((item) => item.id !== message.bridgeMedia?.video?.posterAttachmentId)
.map((item): MessageAttachment => ({
key: `${message.id}:bridge:${item.id}`,
identity: `${sourceEventId ?? message.id}:bridge:${item.id}`,
kind: item.kind === 'video' ? 'file' : item.kind,
name: item.name,
...(item.sizeBytes === undefined ? {} : { size: item.sizeBytes }),
...(sourceEventId === undefined
? {}
: {
source: resolve(
attachmentPath({
channelId: entry.channelId,
sourceEventId,
attachmentId: item.id,
}),
),
}),
local: false,
}));
const cleared = (message.clearedAttachments ?? []).map((item, index): MessageAttachment => ({
key: `${message.id}:cleared:${index}`,
kind: 'file',
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/host/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export interface ChannelWireMessage extends WireMessage {
size: number;
clearedAt: string;
}[];
bridgeMedia?: { items: readonly BridgeMediaItem[] };
bridgeMedia?: {
items: readonly BridgeMediaItem[];
video?: { attachmentId: string; durationMs?: number; posterAttachmentId?: string };
};
bridgeOrigin?: { sourceEventId: string };
toolApprovalRequest?: unknown;
userQuestionRequest?: unknown;
Expand All @@ -99,7 +102,7 @@ export interface ChannelWireMessage extends WireMessage {

export interface BridgeMediaItem {
id: string;
kind: 'image' | 'file' | 'audio';
kind: 'image' | 'file' | 'audio' | 'video';
name: string;
sizeBytes?: number;
}
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/test/conversation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,36 @@ describe('DM conversation', () => {
]);
});

it('keeps bridged video as an explicit original-file action and does not expose its cover as an eager image', async () => {
const host = new FakeHost();
const channel = host.channel(DM);
channel.append(
{
id: 'native-video',
author: { kind: 'bridged', source: 'lark' },
body: '',
bridgeMedia: {
video: { attachmentId: 'clip', posterAttachmentId: 'cover' },
items: [
{ id: 'clip', kind: 'video', name: 'clip.mp4' },
{ id: 'cover', kind: 'image', name: 'cover' },
],
},
bridgeOrigin: { sourceEventId: 'video-event' } as never,
},
false,
);
const target = open(host);
const live = await until(target, (s) => s.messages.length === 1);
expect(live.messages[0]?.attachments).toHaveLength(1);
expect(live.messages[0]?.attachments[0]).toMatchObject({
kind: 'file',
name: 'clip.mp4',
local: false,
source: { uri: expect.stringContaining('attachmentId=clip') },
});
});

it('shows cleared media as a cleared file card without a download source', async () => {
const host = new FakeHost();
host.channel(DM).append(
Expand Down
11 changes: 11 additions & 0 deletions changes/1023-lark-channel-video.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Added
---

## en

- Prepare a checked Lark parent video in its Channel request bubble and play it manually, with platform duration and a native cover when supplied, separate original download and bounded source authorization; audio and direct media messages remain separate qualification scopes ([#1023](https://github.com/BotHarness/DeepSeekBot/issues/1023)).

## zh

- 在 Channel 请求气泡内明确准备并手动播放经核验的 Lark 引用视频,显示平台已知时长及原生封面,支持单独下载原件并沿用有界来源授权;音频和直接媒体消息仍需独立验收([#1023](https://github.com/BotHarness/DeepSeekBot/issues/1023))。
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ No schema migration, parallel media catalog, remote original editing, autoplay,

## Qualification

The #1023 first video tracer extends this application-defined contract to a checked native Lark `media` parent of an App-mentioned text request. Provider `sourceVideos` opt-in retains the exact video descriptor and optional platform duration in milliseconds; `sourceImages` additionally retains a cover only when the same native parent supplies its image key. Generic file names never establish video kind. Canonical Source Event evidence owns this association; Channel projection exposes opaque video/cover IDs and known duration without resource keys. Intake acquires no media bytes.

Human preparation reuses the existing authenticated attachment route, source authority and bounded acquisition owner. `representation=playback` serves the unchanged original only when byte sniffing identifies MP4; the browser independently verifies decoding. No transcoder, generated poster or model transcript is introduced. The original remains independently downloadable even when its playback format is refused. Covers are acquired only after preparation, through their own exact source descriptor; missing or failed covers do not block video. Browser resources cancel and release URLs on unmount or authority changes. The CLI requests the same representation explicitly. Mobile currently offers the original as a file action and does not automatically load the video cover. Audio and direct native media messages remain subsequent qualification scopes.

The candidate's automated regressions do not establish native intake, rendering, playback or installed-package qualification. Those gates and the required first-slice Human feedback remain tracked in [#1023](https://github.com/BotHarness/DeepSeekBot/issues/1023).

Opening a Bot Inbox source inspector must not bypass the Lark video's explicit acquisition policy. That existing inspector keeps original-download actions for video and cover, but excludes both from its legacy automatic image/video preview loop. Existing qualified WeChat video previews and unrelated Lark images keep their own presentation behavior.

The #1022 Lark parent-file candidate extends the same authority, without a new persistence or Provider contract. Its checked descriptor has an exact parent message association and no MIME or size. Projection and access share one eligibility function, expose a generic file card and retain unknown metadata. Explicit download uses the existing bounded attachment HTTP path. The application-defined `channelMediaTarget` RPC reuses that source access and resolves only the canonical Attachment file target; Client application discovery and opening use the existing DSH-native file capability, never a new launcher. A read-only capability check acquires no bytes, and the menu is mounted only after explicit action. The CLI supplies matching target and download commands. Native file/message/Client qualification remains separate from source tests.

Automated tests verify native association/order, exact placement and access, stop/revoke/restart, cancellation, UI visibility/retry and absence of new Inbox/model activity. These are regression evidence, not real platform acceptance. The PR must separately record fresh native-message bytes and actual matched-theme UI evidence; current blockers remain explicit until Human QA completes. Official resource semantics require the original message ID and its matching image key ([Lark reference](https://github.com/larksuite/cli/blob/main/skills/lark-im/references/lark-im-messages-resources-download.md)).
2 changes: 2 additions & 0 deletions docs/architecture/botharness-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,8 @@ The #1155 ordinary-text extension uses the same exclusive checked QQ inbound con
[ADR-0137](../adr/0137-human-bridge-media-uses-channel-source-authority.md) places Human external-image access in the existing Messaging and Attachment owners. The Source Event retains trusted native descriptors and order; realtime and historical Channel placement projections expose media references without resource keys. The existing attachment route under the authenticated API Gateway rechecks Human Channel access and independent recorded reception paths using Channel, Source Event and attachment IDs. The Provider verifies the original message/resource association before acquisition. Visible images acquire automatically, reuse the deterministic Attachment identity and enlarge in the native Modal. Reception stop refuses new acquisition; unbind/revocation refuses acquisition and cached serving through that path. Missing or purged originals are not recreated. Human viewing creates no Observation, Inbox Admission, model image input or send permission. #1021 is the Lark image tracer; files, audio/video and other providers extend the contract in subsequent slices, with real platform qualification tracked in each issue/PR.

#1022 沿同一权威接通 Lark 经核验的单个引用文件:投影与取得共享媒体资格判断,未知 MIME/大小不作推断。Human 明确下载才取得有界原件;支持原生打开的 Host 复用既有应用菜单,`channelMediaTarget` 每次按当前 Channel/来源/文件授权解析 canonical Attachment 目标。CLI 提供同一授权路径的目标和下载命令;没有新增 Provider、消息存储或 Bot 注意力。直接文件、多文件/混排与真实平台验收保持独立资格边界。

#1023 的首个视频候选将这一边界扩展到真实 @ 请求引用的原生 Lark `media` 父消息。Provider 明确协商 `sourceVideos`,仅保留准确的视频资源、平台已知时长及同一原生消息给出的可选封面;普通文件名不授予视频资格。Channel 只投影不透明资源引用。Human 明确准备后,既有 attachment HTTP owner 在当前授权下取得不超过 25 MiB 的原件,MP4 字节才可作为 `playback` 返回,浏览器另行确认解码且不自动播放。封面独立受检,缺失或失败不阻断播放;不转码、不生成封面、不调用模型转写。CLI 可明确请求同一播放表示;移动端暂以原件文件操作呈现,不自动加载封面。真实平台、安装包和 Human 首片反馈仍是资格门槛,音频与直接媒体消息未在本片完成。
[#908](https://github.com/BotHarness/BotHarness/issues/908) extends the same WeChat Source Event authority with opt-in one-level native quote evidence: server message ID, item ID, embedded text, display summary and partial-reference metadata remain distinct. Missing bodies resolve only from readable canonical sources in the same currently authorized account/private conversation, or accepted Outbox records with genuine native server receipts; WeChat client acknowledgements never qualify. No Provider transcript store or standalone Session lookup is introduced. `bridge_context(retained|retained-nearby)` returns bounded canonical local records with explicit `retained-local-sources` coverage, current own-identity/Grant/lease inspection, initial row-boundary snapshot and authorization-bound opaque keyset cursors. These reads audit the anchor Source Event without creating Admissions or wake; unavailable references do not imply remote deletion or a Thread. Client source details and Orchestrator input distinguish native quoted text, local resolution and unavailable content. Remote WeChat history/search remains unavailable.

[#909](https://github.com/BotHarness/BotHarness/issues/909) routes the explicitly authorized WeChat paired-owner DM through existing Channel connector records: local DM/Group placements and Inbox-only paths reference one canonical Source Event. Group members retain their independent Attention/harvest policy; private Inbox and explicitly connected local DM paths retain the `human-dm` policy. The native origin remains `dm`, never a fabricated WeChat group or Thread. Shared source readability does not transfer the receiving identity or its private continuation: each external reply still requires the acting Bot's own enabled identity and Grant. Existing intake boundaries, placement deduplication, membership checks and dispatch fences apply to switches, deletion, revocation and restart. No schema, Provider transcript store or new Session lifecycle is added. Fresh installed-product qualification is recorded on #909.
Expand Down
3 changes: 3 additions & 0 deletions docs/bot-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ deepseekbot group-create "Skill Lab" [--member <bot-id> ...] [--operation-key <s
deepseekbot channel-rename <channel> --name "Skill Lab" [--expected-channel <channelFingerprint>]
deepseekbot channel-media-target <channel> --source-event <source-event-id> --attachment <attachment-id>
deepseekbot channel-media-download <channel> --source-event <source-event-id> --attachment <attachment-id> --out ./original.csv
deepseekbot channel-media-download <channel> --source-event <source-event-id> --attachment <video-id> --representation playback --out ./playback.mp4
deepseekbot group-avatar-set <channel> --avatar-file ./avatar.png [--expected-channel <channelFingerprint>]
deepseekbot group-avatar-set <channel> --clear [--expected-channel <channelFingerprint>]
deepseekbot group-invite <channel> --bot <bot-id> [--operation-key <stable-key>] [--expected-channel <channelFingerprint>]
Expand Down Expand Up @@ -279,6 +280,8 @@ Renaming a Bot DM renames that Bot, as in the Web UI. `group-avatar-set` reads a

`channel-media-target` acquires checked source media through current Human Channel authorization and returns its canonical Host file target. `channel-media-download` uses the same authenticated attachment route as the card, writes the original bytes to a new local `--out` file and returns size, MIME and SHA-256 in its JSON result. It refuses an existing output instead of overwriting it. Both commands use `channel + source-event + attachment` without a Bot selector or model call; source revocation remains a coded refusal. The default bound is 25 MiB.

Use `--representation playback` on `channel-media-download` to request the same explicit preparation as the Channel player. For the #1023 Lark parent-video candidate this serves an original whose bytes identify MP4; it does not transcode or validate browser decoding. Omitting the flag always keeps original-download semantics. Unsupported formats return a coded refusal, and the original remains separately downloadable. Real native and installed-product qualification is recorded in [the candidate checkpoint](dev/verification/lark-1023-channel-video.md).

Busy Groups allow an initial hold and at most two rechecks. `budget.exhausted` returns
`requires-review`, `draftVersion` and choices; new source messages do not reset the existing
draft. To defer, drop without `resolve_inbox_ids`, then use `inbox_snooze` for the observed
Expand Down
3 changes: 3 additions & 0 deletions docs/bot-cli.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ deepseekbot group-create "Skill Lab" [--member <bot-id> ...] [--operation-key <s
deepseekbot channel-rename <channel> --name "Skill Lab" [--expected-channel <channelFingerprint>]
deepseekbot channel-media-target <channel> --source-event <source-event-id> --attachment <attachment-id>
deepseekbot channel-media-download <channel> --source-event <source-event-id> --attachment <attachment-id> --out ./original.csv
deepseekbot channel-media-download <channel> --source-event <source-event-id> --attachment <video-id> --representation playback --out ./playback.mp4
deepseekbot group-avatar-set <channel> --avatar-file ./avatar.png [--expected-channel <channelFingerprint>]
deepseekbot group-avatar-set <channel> --clear [--expected-channel <channelFingerprint>]
deepseekbot group-invite <channel> --bot <bot-id> [--operation-key <stable-key>] [--expected-channel <channelFingerprint>]
Expand Down Expand Up @@ -269,6 +270,8 @@ Host 重启后,先读取当前群聊,再用 `recover:true`、`draft_id`、

`channel-media-target` 通过当前 Human Channel 授权取得受检来源媒体,返回其 canonical Host 文件目标。`channel-media-download` 使用与卡片相同的认证附件路径,将原件字节写入新的本地 `--out` 文件,并在 JSON 中返回大小、MIME 和 SHA-256;目标已存在时拒绝覆盖。两条命令只使用 Channel、Source Event 和 attachment,不选择 Bot,也不调用模型;来源撤销仍返回明确错误码。默认上限为 25 MiB。

对 `channel-media-download` 明确传入 `--representation playback`,可使用与 Channel 播放器相同的准备路径。#1023 Lark 引用视频候选只返回字节识别为 MP4 的原件,不转码,也不能代替浏览器解码验收。不传该参数始终下载原件;不支持播放的格式返回明确错误码,原件仍可独立下载。真实平台和安装包资格见[候选检查点](dev/verification/lark-1023-channel-video.md)。

忙群的草稿初次 held 后最多复核两次。预算耗尽返回 `requires-review`、`budget.exhausted`、
`draftVersion` 和可选动作;新来源消息不能重置已有草稿预算。需要延后时,不带
`resolve_inbox_ids` 放弃草稿,再通过既有 `inbox_snooze` 为已观察且未完成的事项设置时间和说明。
Expand Down
Loading
Loading