dsh-plugin-subscriptions 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/README.zh.md +11 -2
- package/lib/auth/rpc.d.ts +14 -0
- package/lib/auth/rpc.js +18 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +178 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +10 -0
- package/lib/client/locales.d.ts +6 -0
- package/lib/client/locales.js +6 -0
- package/lib/client.js +282 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +408 -41
- package/lib/providers/codex.d.ts +5 -2
- package/lib/providers/codex.js +34 -4
- package/lib/tools/image-generate.d.ts +55 -19
- package/lib/tools/image-generate.js +130 -31
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +255 -0
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ The `image_generate` tool renders its result inline in the conversation:
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
Its `provider` parameter picks the image backend — the same prompt through GPT (`gpt-image-2`, top) and Grok (`grok-imagine-image-2.0`, bottom):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
The `video_generate` tool plays the generated clip inline:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Providers
|
|
26
34
|
|
|
27
35
|
| Route | Subscription | Models |
|
|
@@ -37,7 +45,8 @@ Logged-in cards also show **subscription usage** — per rate-limit window (5-ho
|
|
|
37
45
|
Also included, registered when the matching provider is enabled:
|
|
38
46
|
|
|
39
47
|
- **`x_search`** tool (Grok) — xAI's hosted X search, returning `{ answer, citations }`.
|
|
40
|
-
- **`image_generate`** tool (ChatGPT) — `gpt-image-2` via the Codex backend;
|
|
48
|
+
- **`image_generate`** tool (ChatGPT or Grok) — `gpt-image-2` via the Codex backend, or `grok-imagine-image-2.0` via `api.x.ai/v1/images/generations`. The `provider` argument picks the preferred provider (`gpt`, the default, or `grok`); when the preferred one is logged out the other serves as fallback. Images are saved under `~/.dsh/plugins/subscriptions/images/` and the paths returned. The `size`/`quality` arguments map onto Grok's `aspect_ratio`/`quality` on the Grok path.
|
|
49
|
+
- **`video_generate`** tool (Grok) — `grok-imagine-video-1.5` via `api.x.ai/v1/videos` (async submit + poll); MP4s are saved under `~/.dsh/plugins/subscriptions/videos/`, the path returned, and the clip plays inline in the conversation. Supports duration (1–15 s), aspect ratio, resolution, and image-to-video via `image_url`.
|
|
41
50
|
|
|
42
51
|
## Install
|
|
43
52
|
|
|
@@ -128,5 +137,5 @@ After `pnpm build`, restart `dsh web` to pick up changes.
|
|
|
128
137
|
- `src/auth/` — PKCE/JWT helpers, token store, OAuth flow engine (temp loopback callback server), `/subscriptions-auth` RPC channel
|
|
129
138
|
- `src/providers/` — per-provider OAuth constants/exchange/refresh + `LlmAdapter`s
|
|
130
139
|
- `src/translate/` — dsh `Message[]` ⟷ OpenAI Responses / Anthropic Messages wire formats, SSE → `StreamChunk`
|
|
131
|
-
- `src/tools/` — `x_search` and `
|
|
140
|
+
- `src/tools/` — `x_search`, `image_generate`, and `video_generate`
|
|
132
141
|
- `src/client/` — the Settings → Subscriptions page (browser half, zh/en, theme-token aware)
|
package/README.zh.md
CHANGED
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
`provider` 参数可选择生图后端——同一条提示词分别走 GPT(`gpt-image-2`,上)和 Grok(`grok-imagine-image-2.0`,下):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
`video_generate` 工具生成的视频直接内联播放:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Provider 一览
|
|
26
34
|
|
|
27
35
|
| 路由 | 订阅 | 模型 |
|
|
@@ -37,7 +45,8 @@
|
|
|
37
45
|
随 provider 启用自动注册的工具:
|
|
38
46
|
|
|
39
47
|
- **`x_search`**(Grok)—— xAI 托管的 X 搜索,返回 `{ answer, citations }`。
|
|
40
|
-
- **`image_generate`**(ChatGPT)—— 经 Codex 后端调用 `gpt-image-2
|
|
48
|
+
- **`image_generate`**(ChatGPT 或 Grok)—— 经 Codex 后端调用 `gpt-image-2`,或经 `api.x.ai/v1/images/generations` 调用 `grok-imagine-image-2.0`。`provider` 参数指定首选提供方(`gpt` 为默认值,可选 `grok`);首选方未登录时自动回退到另一方。图片保存到 `~/.dsh/plugins/subscriptions/images/` 并返回路径。Grok 路径上 `size`/`quality` 参数会映射为 Grok 的 `aspect_ratio`/`quality`。
|
|
49
|
+
- **`video_generate`**(Grok)—— 经 `api.x.ai/v1/videos` 调用 `grok-imagine-video-1.5`(异步提交 + 轮询);MP4 保存到 `~/.dsh/plugins/subscriptions/videos/` 并返回路径,视频直接在对话里内联播放。支持时长(1–15 秒)、宽高比、分辨率,以及通过 `image_url` 做图生视频。
|
|
41
50
|
|
|
42
51
|
## 安装
|
|
43
52
|
|
|
@@ -128,5 +137,5 @@ pnpm test # 编译后跑 node --test 单测
|
|
|
128
137
|
- `src/auth/` —— PKCE/JWT 工具、token 存储、OAuth 流程引擎(临时本地回调服务)、`/subscriptions-auth` RPC 通道
|
|
129
138
|
- `src/providers/` —— 各 provider 的 OAuth 常量/换发/刷新 + `LlmAdapter` 实现
|
|
130
139
|
- `src/translate/` —— dsh `Message[]` 与 OpenAI Responses / Anthropic Messages 格式互转,SSE → `StreamChunk`
|
|
131
|
-
- `src/tools/` —— `x_search` 与 `
|
|
140
|
+
- `src/tools/` —— `x_search`、`image_generate` 与 `video_generate`
|
|
132
141
|
- `src/client/` —— 设置 → 订阅页面(浏览器面,中英文,跟随明暗主题)
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface ImageBytesResult {
|
|
|
15
15
|
mediaType: string;
|
|
16
16
|
dataBase64: string;
|
|
17
17
|
}
|
|
18
|
+
/** Decoded video bytes returned by the `video` endpoint. */
|
|
19
|
+
export interface VideoBytesResult {
|
|
20
|
+
mediaType: string;
|
|
21
|
+
dataBase64: string;
|
|
22
|
+
}
|
|
18
23
|
/** Login state of one provider, as rendered by the Settings page. */
|
|
19
24
|
export interface ProviderStatus {
|
|
20
25
|
/** Whether a session exists in the store. */
|
|
@@ -64,6 +69,15 @@ export interface AuthController {
|
|
|
64
69
|
* @throws when no attachment service is mounted or the read fails.
|
|
65
70
|
*/
|
|
66
71
|
readImage(ref: ImageAttachmentRef, signal: AbortSignal): Promise<ImageBytesResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Read one generated video's bytes for inline playback.
|
|
74
|
+
* @param name - bare MP4 file name inside the plugin's videos directory
|
|
75
|
+
* (validated against {@link VIDEO_NAME_PATTERN}; never a path).
|
|
76
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
77
|
+
* @returns the media type and base64-encoded bytes.
|
|
78
|
+
* @throws when the file does not exist or cannot be read.
|
|
79
|
+
*/
|
|
80
|
+
readVideo(name: string, signal: AbortSignal): Promise<VideoBytesResult>;
|
|
67
81
|
}
|
|
68
82
|
/**
|
|
69
83
|
* Register the `/subscriptions-auth` RPC channel when a host connection exists.
|
package/lib/auth/rpc.js
CHANGED
|
@@ -10,6 +10,8 @@ import { PROVIDER_IDS } from './store.js';
|
|
|
10
10
|
export const SUBSCRIPTIONS_AUTH_CHANNEL = '/subscriptions-auth';
|
|
11
11
|
/** Media types the attachment store accepts (ImageMediaType). */
|
|
12
12
|
const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
13
|
+
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
14
|
+
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
13
15
|
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
14
16
|
class BadRequest extends Error {
|
|
15
17
|
}
|
|
@@ -72,6 +74,20 @@ function readImageRef(payload) {
|
|
|
72
74
|
...name === undefined ? {} : { name: name },
|
|
73
75
|
};
|
|
74
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Validate the `video` endpoint's payload into a bare file name. Rejecting
|
|
79
|
+
* anything with a path separator (the pattern allows none) pins every read
|
|
80
|
+
* inside the plugin's videos directory.
|
|
81
|
+
*/
|
|
82
|
+
function readVideoName(payload) {
|
|
83
|
+
if (typeof payload !== 'object' || payload === null)
|
|
84
|
+
throw new BadRequest('payload must be an object');
|
|
85
|
+
const name = payload.name;
|
|
86
|
+
if (typeof name !== 'string' || !VIDEO_NAME_PATTERN.test(name)) {
|
|
87
|
+
throw new BadRequest('payload.name must be a bare .mp4 file name');
|
|
88
|
+
}
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
75
91
|
async function dispatch(controller, endpoint, payload, signal) {
|
|
76
92
|
switch (endpoint) {
|
|
77
93
|
case 'status': {
|
|
@@ -95,6 +111,8 @@ async function dispatch(controller, endpoint, payload, signal) {
|
|
|
95
111
|
return ok(await controller.usage(readProvider(payload), signal));
|
|
96
112
|
case 'image':
|
|
97
113
|
return ok(await controller.readImage(readImageRef(payload), signal));
|
|
114
|
+
case 'video':
|
|
115
|
+
return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
98
116
|
default:
|
|
99
117
|
throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
100
118
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
|
|
2
|
+
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
|
|
3
|
+
import type { SubscriptionsKey } from './locales.js';
|
|
4
|
+
/** Mirror of ui-tool's ToolCallOwnerProps (see ImageGenerateToolview). */
|
|
5
|
+
interface ToolCallOwnerProps {
|
|
6
|
+
callId: string;
|
|
7
|
+
toolName: string;
|
|
8
|
+
block: ToolCallBlock;
|
|
9
|
+
cwd?: string | undefined;
|
|
10
|
+
openFile: (path: string) => void;
|
|
11
|
+
inspect?: (() => void) | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Decoded video bytes as the node half's `video` endpoint answers them. */
|
|
14
|
+
export interface VideoBytes {
|
|
15
|
+
mediaType: string;
|
|
16
|
+
dataBase64: string;
|
|
17
|
+
}
|
|
18
|
+
/** Injected dependencies of {@link VideoGenerateToolview} (slot `inject`). */
|
|
19
|
+
export interface VideoGenerateToolviewInjected {
|
|
20
|
+
/** Loads one generated video's bytes by bare file name. */
|
|
21
|
+
loadVideo: (name: string) => Promise<VideoBytes>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Props delivered by the toolview outlet: the owner share plus the inject
|
|
25
|
+
* face and the framework locale seat, spread flat.
|
|
26
|
+
*/
|
|
27
|
+
export type VideoGenerateToolviewProps = Partial<ToolCallOwnerProps> & Partial<VideoGenerateToolviewInjected> & {
|
|
28
|
+
t?: ((key: SubscriptionsKey, params?: Record<string, unknown>) => string) | undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Build the video loader over the `/subscriptions-auth` `video` endpoint.
|
|
32
|
+
* @param rpc - Connection RPC caller.
|
|
33
|
+
* @returns loader resolving a bare file name to the decoded bytes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createVideoLoader(rpc: ConnectionHandle['rpc']): (name: string) => Promise<VideoBytes>;
|
|
36
|
+
/**
|
|
37
|
+
* The `video_generate` keyed toolview component.
|
|
38
|
+
* @param props - owner share, inject face, and locale seat (spread flat).
|
|
39
|
+
* @returns the call row plus, once settled, the player / text / error body.
|
|
40
|
+
*/
|
|
41
|
+
export declare function VideoGenerateToolview(props: VideoGenerateToolviewProps): import("react").JSX.Element | null;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Keyed toolview for the `video_generate` tool: renders the generated video
|
|
4
|
+
* inline in the conversation. The row shows the call's prompt while running
|
|
5
|
+
* and after settling; a settled success loads the MP4 bytes through the node
|
|
6
|
+
* half's `/subscriptions-auth` `video` endpoint (by bare file name), builds a
|
|
7
|
+
* Blob URL, and plays it in a native `<video controls>` element. The file
|
|
8
|
+
* name comes from the result's presentation meta when the dispatch was
|
|
9
|
+
* top-level, and is recovered from the "Saved video to …" text line for
|
|
10
|
+
* nested (Code Mode) dispatches, where the harness computes no meta. A result
|
|
11
|
+
* with neither renders its text; an error result renders the first error line.
|
|
12
|
+
*
|
|
13
|
+
* The 'tool.call.toolview' SlotMap entry is declared by
|
|
14
|
+
* ImageGenerateToolview.tsx in this same package (one declaration per
|
|
15
|
+
* augmentation), so this file only mirrors the owner-props shape.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import { IconSparkle16 } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
19
|
+
import { en } from './locales.js';
|
|
20
|
+
/** Logical RPC channel served by the node half of this plugin. */
|
|
21
|
+
const SUBSCRIPTIONS_AUTH_CHANNEL = '/subscriptions-auth';
|
|
22
|
+
/** Title prompt truncation budget (characters). */
|
|
23
|
+
const PROMPT_MAX_LENGTH = 60;
|
|
24
|
+
/**
|
|
25
|
+
* Build the video loader over the `/subscriptions-auth` `video` endpoint.
|
|
26
|
+
* @param rpc - Connection RPC caller.
|
|
27
|
+
* @returns loader resolving a bare file name to the decoded bytes.
|
|
28
|
+
*/
|
|
29
|
+
export function createVideoLoader(rpc) {
|
|
30
|
+
return async (name) => {
|
|
31
|
+
const result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL, 'video', { name });
|
|
32
|
+
if (!result.ok)
|
|
33
|
+
throw new Error(result.error.message);
|
|
34
|
+
return result.value;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* English-dictionary fallback for a missing locale seat (standalone renders);
|
|
39
|
+
* the framework always supplies the namespace-bound one.
|
|
40
|
+
*/
|
|
41
|
+
function fallbackTranslate(key, params) {
|
|
42
|
+
let text = en[key];
|
|
43
|
+
for (const [name, value] of Object.entries(params ?? {})) {
|
|
44
|
+
text = text.replaceAll(`{${name}}`, String(value));
|
|
45
|
+
}
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
/** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
|
|
49
|
+
function derivePrompt(argsRaw) {
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(argsRaw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Non-JSON args (mid-stream truncation): fall back to the raw string below.
|
|
56
|
+
parsed = undefined;
|
|
57
|
+
}
|
|
58
|
+
let prompt;
|
|
59
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
60
|
+
const args = parsed;
|
|
61
|
+
if (typeof args.prompt === 'string' && args.prompt !== '')
|
|
62
|
+
prompt = args.prompt;
|
|
63
|
+
else {
|
|
64
|
+
for (const value of Object.values(args)) {
|
|
65
|
+
if (typeof value === 'string' && value !== '') {
|
|
66
|
+
prompt = value;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const line = (prompt ?? argsRaw).split('\n', 1)[0] ?? '';
|
|
73
|
+
return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
|
|
74
|
+
}
|
|
75
|
+
/** Flatten a settled result's text blocks (the fallback body and the error line). */
|
|
76
|
+
function resultText(block) {
|
|
77
|
+
if (!('kind' in block))
|
|
78
|
+
return '';
|
|
79
|
+
const parts = [];
|
|
80
|
+
for (const part of block.content) {
|
|
81
|
+
if (part.type === 'text')
|
|
82
|
+
parts.push(part.text);
|
|
83
|
+
}
|
|
84
|
+
if (parts.length === 0 && block.error !== undefined)
|
|
85
|
+
parts.push(`${block.error.name}: ${block.error.code}`);
|
|
86
|
+
return parts.join('\n');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The generated video's bare file name: presentation meta first (top-level
|
|
90
|
+
* dispatches), then the render text's "Saved video to …" line (nested
|
|
91
|
+
* dispatches compute no meta).
|
|
92
|
+
*/
|
|
93
|
+
function resolveFileName(block) {
|
|
94
|
+
if (!('kind' in block))
|
|
95
|
+
return undefined;
|
|
96
|
+
const meta = block.meta;
|
|
97
|
+
if (typeof meta === 'object' && meta !== null) {
|
|
98
|
+
const fileName = meta.fileName;
|
|
99
|
+
if (typeof fileName === 'string' && fileName.length > 0)
|
|
100
|
+
return fileName;
|
|
101
|
+
}
|
|
102
|
+
const match = /^Saved video to (.+\.mp4)/m.exec(resultText(block));
|
|
103
|
+
if (match === null)
|
|
104
|
+
return undefined;
|
|
105
|
+
const path = match[1];
|
|
106
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
107
|
+
}
|
|
108
|
+
/** Decode a base64 payload into bytes (browser-side; no Buffer). */
|
|
109
|
+
function base64Bytes(dataBase64) {
|
|
110
|
+
const binary = atob(dataBase64);
|
|
111
|
+
const bytes = new Uint8Array(binary.length);
|
|
112
|
+
for (let index = 0; index < binary.length; index++)
|
|
113
|
+
bytes[index] = binary.charCodeAt(index);
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
|
116
|
+
const styles = {
|
|
117
|
+
container: { display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0' },
|
|
118
|
+
row: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
|
|
119
|
+
icon: { display: 'inline-flex', flexShrink: 0, color: 'var(--dsw-alias-label-tertiary)' },
|
|
120
|
+
title: {
|
|
121
|
+
fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)',
|
|
122
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
123
|
+
},
|
|
124
|
+
subtle: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
125
|
+
output: {
|
|
126
|
+
margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)',
|
|
127
|
+
whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
|
|
128
|
+
},
|
|
129
|
+
error: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
130
|
+
video: {
|
|
131
|
+
display: 'block', maxWidth: 480, width: '100%', borderRadius: 8,
|
|
132
|
+
backgroundColor: 'var(--dsw-alias-fill-tertiary)',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The `video_generate` keyed toolview component.
|
|
137
|
+
* @param props - owner share, inject face, and locale seat (spread flat).
|
|
138
|
+
* @returns the call row plus, once settled, the player / text / error body.
|
|
139
|
+
*/
|
|
140
|
+
export function VideoGenerateToolview(props) {
|
|
141
|
+
const { block, loadVideo } = props;
|
|
142
|
+
const t = props.t ?? fallbackTranslate;
|
|
143
|
+
const settled = block !== undefined && 'kind' in block;
|
|
144
|
+
const isError = settled && block.isError;
|
|
145
|
+
const fileName = block !== undefined && settled && !isError ? resolveFileName(block) : undefined;
|
|
146
|
+
const [load, setLoad] = useState({ phase: 'loading' });
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (fileName === undefined || loadVideo === undefined)
|
|
149
|
+
return;
|
|
150
|
+
let cancelled = false;
|
|
151
|
+
let objectUrl;
|
|
152
|
+
setLoad({ phase: 'loading' });
|
|
153
|
+
loadVideo(fileName).then((video) => {
|
|
154
|
+
if (cancelled)
|
|
155
|
+
return;
|
|
156
|
+
// Uint8Array#slice detaches a plain ArrayBuffer view for BlobPart.
|
|
157
|
+
objectUrl = URL.createObjectURL(new Blob([base64Bytes(video.dataBase64).slice()], { type: video.mediaType }));
|
|
158
|
+
setLoad({ phase: 'ready', url: objectUrl });
|
|
159
|
+
}, (error) => {
|
|
160
|
+
if (cancelled)
|
|
161
|
+
return;
|
|
162
|
+
setLoad({ phase: 'failed', message: error instanceof Error ? error.message : String(error) });
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
cancelled = true;
|
|
166
|
+
if (objectUrl !== undefined)
|
|
167
|
+
URL.revokeObjectURL(objectUrl);
|
|
168
|
+
};
|
|
169
|
+
}, [fileName, loadVideo]);
|
|
170
|
+
if (block === undefined)
|
|
171
|
+
return null;
|
|
172
|
+
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? '';
|
|
173
|
+
const title = `video_generate: ${derivePrompt(argsRaw)}`;
|
|
174
|
+
const text = settled ? resultText(block) : '';
|
|
175
|
+
return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.row, children: [_jsx("span", { style: styles.icon, children: _jsx(IconSparkle16, { size: 14 }) }), _jsx("span", { style: styles.title, children: title })] }), !settled && _jsx("p", { style: styles.subtle, children: t('generatingVideo') }), settled && isError && text !== '' && (_jsx("p", { style: styles.error, children: text.split('\n', 1)[0] })), settled && !isError && fileName !== undefined && load.phase === 'loading' && (_jsx("p", { style: styles.subtle, children: t('videoLoading') })), settled && !isError && fileName !== undefined && load.phase === 'failed' && (_jsx("p", { style: styles.error, children: t('videoLoadFailed', { message: load.message }) })), settled && !isError && fileName !== undefined && load.phase === 'ready' && (
|
|
176
|
+
// eslint-disable-next-line jsx-a11y/media-has-caption -- generated clip; no caption track exists.
|
|
177
|
+
_jsx("video", { style: styles.video, src: load.url, controls: true, preload: "metadata" })), settled && !isError && fileName === undefined && text !== '' && (_jsx("p", { style: styles.output, children: text }))] }));
|
|
178
|
+
}
|
package/lib/client/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
|
10
10
|
import type { SubscriptionsKey } from './locales.js';
|
|
11
11
|
export type { SubscriptionsSectionInjected, SubscriptionsSectionProps } from './SubscriptionsSection.js';
|
|
12
12
|
export type { ImageGenerateToolviewInjected, ImageGenerateToolviewProps } from './ImageGenerateToolview.js';
|
|
13
|
+
export type { VideoGenerateToolviewInjected, VideoGenerateToolviewProps } from './VideoGenerateToolview.js';
|
|
13
14
|
export type { SubscriptionsKey } from './locales.js';
|
|
14
15
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
15
16
|
interface LocaleNamespaceMap {
|
package/lib/client/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// nodenext the .js specifier resolves to the .tsx source (see README note).
|
|
4
4
|
import { SubscriptionsSection } from './SubscriptionsSection.js';
|
|
5
5
|
import { ImageGenerateToolview, createImageLoader } from './ImageGenerateToolview.js';
|
|
6
|
+
import { VideoGenerateToolview, createVideoLoader } from './VideoGenerateToolview.js';
|
|
6
7
|
import { en, zh } from './locales.js';
|
|
7
8
|
/** Dictionary namespace owned by this plugin. */
|
|
8
9
|
const NS = 'settings.subscriptions';
|
|
@@ -43,4 +44,13 @@ export function apply(ctx) {
|
|
|
43
44
|
locale: NS,
|
|
44
45
|
inject: toolviewInjected,
|
|
45
46
|
}, ImageGenerateToolview));
|
|
47
|
+
// The video_generate keyed toolview plays the saved MP4 inline; its bytes
|
|
48
|
+
// ride the same channel's `video` endpoint through the injected loader.
|
|
49
|
+
const videoToolviewInjected = () => ({ loadVideo: createVideoLoader(connection.rpc) });
|
|
50
|
+
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
51
|
+
name: 'tool.call.toolview',
|
|
52
|
+
key: 'video_generate',
|
|
53
|
+
locale: NS,
|
|
54
|
+
inject: videoToolviewInjected,
|
|
55
|
+
}, VideoGenerateToolview));
|
|
46
56
|
}
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export declare const en: {
|
|
|
37
37
|
imageLoadFailed: string;
|
|
38
38
|
imagePreview: string;
|
|
39
39
|
imageClose: string;
|
|
40
|
+
generatingVideo: string;
|
|
41
|
+
videoLoading: string;
|
|
42
|
+
videoLoadFailed: string;
|
|
40
43
|
};
|
|
41
44
|
/** zh strings, one per {@link en} key. */
|
|
42
45
|
export declare const zh: {
|
|
@@ -76,6 +79,9 @@ export declare const zh: {
|
|
|
76
79
|
imageLoadFailed: string;
|
|
77
80
|
imagePreview: string;
|
|
78
81
|
imageClose: string;
|
|
82
|
+
generatingVideo: string;
|
|
83
|
+
videoLoading: string;
|
|
84
|
+
videoLoadFailed: string;
|
|
79
85
|
};
|
|
80
86
|
/** The Subscriptions namespace key union (en is the key-set source of truth). */
|
|
81
87
|
export type SubscriptionsKey = keyof typeof en;
|
package/lib/client/locales.js
CHANGED
|
@@ -37,6 +37,9 @@ export const en = {
|
|
|
37
37
|
imageLoadFailed: 'Retry',
|
|
38
38
|
imagePreview: 'Image preview',
|
|
39
39
|
imageClose: 'Close',
|
|
40
|
+
generatingVideo: 'Generating video…',
|
|
41
|
+
videoLoading: 'Loading video…',
|
|
42
|
+
videoLoadFailed: 'Video failed to load: {message}',
|
|
40
43
|
};
|
|
41
44
|
/** zh strings, one per {@link en} key. */
|
|
42
45
|
export const zh = {
|
|
@@ -76,4 +79,7 @@ export const zh = {
|
|
|
76
79
|
imageLoadFailed: '重试',
|
|
77
80
|
imagePreview: '图片预览',
|
|
78
81
|
imageClose: '关闭',
|
|
82
|
+
generatingVideo: '正在生成视频…',
|
|
83
|
+
videoLoading: '视频加载中…',
|
|
84
|
+
videoLoadFailed: '视频加载失败:{message}',
|
|
79
85
|
};
|