@vibedev-si/dsh-vibedev 0.2.7 → 0.3.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 CHANGED
@@ -3,10 +3,10 @@
3
3
  在 DeepSeek Harness 里登录 VibeDev 账号,和 DeepSeek 账号并存:
4
4
 
5
5
  - **VibeDev 的模型**:登录后,模型选择器里会多出「VibeDev」一组(Claude、GPT 等网关提供的模型),和 DeepSeek 的模型并列,选中就能用;
6
- - **媒体生成**:Agent 可以生成图片、视频、音乐和播客,并把语音转成文字,结果保存在工作区的 `media/` 目录下;
6
+ - **媒体生成**:Agent 可以生成图片、视频、音乐、播客和语音,并把语音转成文字,结果保存在工作区的 `media/` 目录下;
7
7
  - **网页搜索**:提供走 VibeDev 网关的网页搜索,见下面的「网页搜索」。
8
8
 
9
- 费用都从你的 VibeDev 余额里扣,DeepSeek 账号照常由 DeepSeek Harness 自己管理,两边互不影响。还没有 VibeDev 账号的话,可以在 [vibedev.jzsaas.com](https://vibedev.jzsaas.com) 注册。
9
+ 费用都从你的 VibeDev 余额里扣,DeepSeek 账号照常由 DeepSeek Harness 自己管理,两边互不影响。还没有 VibeDev 账号的话,可以在 [VibeDev](https://api.vibedev.studio) 注册。
10
10
 
11
11
  本插件由 `dsh-media` 改名而来,见下面的「从 dsh-media 升级」。
12
12
 
@@ -43,6 +43,12 @@
43
43
 
44
44
  ## VibeDev 的模型
45
45
 
46
+ 正式默认 API 为 `https://api.vibedev.studio`。从旧版升级后须重新登录,才会获取本站模型;登录仍使用 S256 PKCE、state、随机 localhost 回调和 app token,不使用共享 HMAC secret。充值、注册、用量链接跟随所选 API origin,网页搜索也使用该 API 的 `/v1/vibedev/web-search`。
47
+
48
+ 所有网关的凭据键、设备记录和媒体任务目录均按规范化 gateway origin 隔离,只接受带匹配 origin 标记的登录记录。原 `dsh-media/vibedev-session` 和未标注的数据目录保留在本地,但不再读取、导入或自动恢复、重新提交其中的旧视频、音频与上传状态。DeepSeek 等其它账号、聊天、工作区及历史媒体 URL 不改写;已有美国 origin 登录记录可继续使用。
49
+
50
+ 媒体创建、完成、刷新请求走所选 API;上传和下载使用服务器返回的完整签名 URL,保留 host、path、query,不携带 VibeDev app token。返回签名链接时下载不再先探测旧 `/content` 接口;未返回链接的旧接口仍可直接返回文件。API 和下载请求拒绝重定向,不把失败或503当作跨区域重发理由。现有同站同键重试、SSE、心跳和取消语义保留。默认不读取开发密钥,旧 `VIBEDEV_GATEWAY_API_KEY` 变量不再使用;开发调试需显式指定本站专用环境变量。本地验证版不解除服务器503挡板,不发送真实模型或付费媒体测试。
51
+
46
52
  - 模型列表来自 VibeDev 网关,按你的账号下发,所以**未登录时不显示**,登录后立即出现,不用重启。
47
53
  - 模型在选择器里的「VibeDev」组;默认模型仍是 DeepSeek Harness 原来的,想用 VibeDev 的模型就在选择器里选。
48
54
  - 没登录就用了 VibeDev 的模型,或登录已失效时,对话里会提示到侧边栏或设置里登录;余额不足时会提示并给出充值链接。这些提示和 DeepSeek 账号的登录、充值提示是分开的。
@@ -59,8 +65,8 @@
59
65
  | --- | --- |
60
66
  | `image_generate` | 生成图片;带参考图时用来改图、换风格或把产品放进场景。生成的图片 Agent 自己能看到。 |
61
67
  | `video_generate` | 生成视频,在后台运行,一般 1 到 10 分钟。完成后会通知 Agent,文件保存在 `media/videos/`。 |
62
- | `audio_generate` | 生成音乐(可以自带歌词)或一期播客(围绕一个话题或一个网页)。 |
63
- | `audio_transcribe` | 把语音转成文字(目前只支持普通话)。 |
68
+ | `audio_generate` | 生成音乐(可以自带歌词)、一期播客(围绕一个话题或一个网页),或一段语音(朗读文字,可以用参考音频指定音色)。 |
69
+ | `audio_transcribe` | 把语音转成文字(以文件上传,语言和时长限制以模型声明为准,目前常见为普通话)。 |
64
70
  | `media_models` | 列出账号可用的媒体模型和价格;视频模型还会列出每种模式接受哪些输入。 |
65
71
  | `media_tasks` | 查看进行中和已完成的视频、音频任务。 |
66
72
  | `media_account` | 查看用的是哪个 VibeDev 账号;登录或退出(和侧边栏、设置里的登录是同一个)。 |
@@ -75,11 +81,22 @@
75
81
 
76
82
  每个视频模型支持的模式、参考素材数量、时长、画面比例和分辨率都不一样。插件会按网关下发的模型能力逐项检查:模型不支持的请求,会在上传和扣费之前就被拒绝,并说明这个模型支持什么。所有参考素材会先上传到 VibeDev 网关的素材库,网关会检查格式,并测量参考视频的时长。
77
83
 
84
+ ### 音频模型
85
+
86
+ 音频模型是音乐、播客、语音还是转写,只看网关模型目录里 `audio.generation_kind` 的声明,不按模型名称猜;没有声明类型的模型会列出来,但不能用来生成。每个模型只提交它在 `request_fields` 里声明的字段,正文写进 `prompt_field` 指定的字段:
87
+
88
+ - 音乐:`prompt`、`lyrics`、`genre`、`mood`、`gender`;
89
+ - 播客:`prompt`、`source_url`;
90
+ - 语音:按模型声明,例如 Seed 是 `text_prompt`、`references`(参考音频)、`audio_config`(输出格式和采样率)、`watermark`。
91
+
92
+ 参考音频的数量、大小、时长、不能同时使用的字段,以及 WAV、MP3 等格式各自支持的采样率,都以网关下发的能力为准。音乐和播客不接受参考音频、格式、采样率或时长参数。
93
+
78
94
  ## 费用
79
95
 
80
96
  - VibeDev 的聊天模型按用量计费,价格以 VibeDev 网关为准。
81
- - 图片按张计费,视频按秒计费(参考视频的时长也计入),音乐和播客按次计费。`media_models` 会列出当前价格。
82
- - 视频和音频只在成功时扣费,失败不扣。视频提交后无法取消。
97
+ - 图片按张计费,视频按秒计费(参考视频的时长也计入),音乐和播客按次计费,语音按原始秒计费(按服务商返回的原始时长,可能和播放时长不同),转写按分钟计费。`media_models` 会列出当前价格;目录没有给出价格的模型显示「未定价」,不代表免费。
98
+ - 视频和音频只在成功时扣费,失败和取消的任务不扣。视频提交后无法取消。
99
+ - 同一次操作只会生成一次:提交没有得到确认时,会用同一个请求键向网关确认结果;查询或下载失败只会重新查询、刷新链接,不会重新提交。结果已经生成、但费用还在结算(`settlement_pending`)时,结果照常保存,不会重新生成。
83
100
  - 插件设置里的「每次付费生成前先询问」默认关闭。打开后,每次付费的媒体请求之前都会先弹出确认,并显示预估金额。
84
101
 
85
102
  ## 设置
@@ -90,7 +107,7 @@
90
107
  | --- | --- | --- |
91
108
  | 每次付费生成前先询问 | 关 | 打开后,每次媒体生成前先确认费用。 |
92
109
  | 保存目录 | `media` | 相对于工作区。 |
93
- | 默认生图 / 视频 / 音乐 / 播客 / 转写模型 | 空 | 留空时由 Agent 按模型目录选择。 |
110
+ | 默认生图 / 视频 / 音乐 / 播客 / 语音合成 / 转写模型 | 空 | 留空时由 Agent 按模型目录选择;可以填模型 id 或别名。 |
94
111
 
95
112
  另有几项只能在配置文件(`cordis.patch.yml` 中 `dsh-vibedev` 一行)里改:`displayName`(模型选择器里显示的组名,默认 `VibeDev`)、`preferredModels`(排在最前的 VibeDev 模型)、`openBrowserOnSignIn`(登录时是否自动打开系统浏览器,默认开)、`catalogRefreshMinutes`(模型目录刷新间隔,默认 30 分钟)、`client`(登录时报给网关的客户端名称,默认 `vibedev-plugin`)、`primary`(作为应用的主账号显示,默认关,VibeDev 应用里开)。
96
113
 
@@ -126,12 +143,14 @@ npm run build
126
143
 
127
144
  ## English
128
145
 
146
+ **American API default**: production defaults to `https://api.vibedev.studio`. Upgrades from unscoped historical state require a new app-token login before listing models. Every origin has separate credentials, devices and durable media tasks; grants must carry a matching origin. Historical unmarked files and the old credential record stay intact but are never read, imported, resumed or resubmitted. Existing American-origin grants remain usable. Other accounts, chats, workspaces and historical media URLs are unchanged. API control requests, account web links and web search follow the selected origin; uploads/downloads preserve the server's complete signed URL without an app token. Redirects and cross-region fallback are refused. S256 PKCE, state, random localhost callback, SSE, heartbeats, cancellation and same-origin idempotency remain. Developer-key fallback is disabled by default and the old shared environment variable is ignored; development requires an explicitly named gateway-specific variable. Local verification neither removes the server's 503 gate nor sends real model or paid media tests.
147
+
129
148
  Sign in to VibeDev in DeepSeek Harness, next to the DeepSeek account:
130
149
 
131
150
  - **VibeDev models**: once signed in, the model picker gains a "VibeDev" group (Claude, GPT and the other models of the VibeDev gateway) next to DeepSeek's;
132
- - **Media generation**: the agent generates images, videos, music and podcasts and transcribes speech; results are saved in the workspace under `media/`.
151
+ - **Media generation**: the agent generates images, videos, music, podcasts and speech, and transcribes speech; results are saved in the workspace under `media/`.
133
152
 
134
- Everything is paid from your VibeDev balance; the DeepSeek account stays DeepSeek Harness's own, and the two do not affect each other. To create an account, register at [vibedev.jzsaas.com](https://vibedev.jzsaas.com). This plugin was renamed from `dsh-media`.
153
+ Everything is paid from your VibeDev balance; the DeepSeek account stays DeepSeek Harness's own, and the two do not affect each other. To create an account, register at [VibeDev](https://api.vibedev.studio). This plugin was renamed from `dsh-media`.
135
154
 
136
155
  **Install**: install `@vibedev-si/dsh-vibedev` by package name on the Plugins page (the `@vibedev-si/` scope is required), or from the [VibeDev Plugin Center](https://github.com/VibeDev-Si/dsh-ecosystem); from the command line, `dsh plugin add @vibedev-si/dsh-vibedev`, or `dsh plugin --profile desktop add @vibedev-si/dsh-vibedev` for the desktop app (quit it fully first). The VibeDev app ships with the plugin built in, and there the VibeDev account is the main account (the bottom row of the sidebar and the first Settings section), with the DeepSeek account as the second one. Within 24 hours of a release, name the version (e.g. `@vibedev-si/dsh-vibedev@0.2.5`): pnpm does not install versions younger than a day by default.
137
156
 
@@ -141,13 +160,17 @@ Everything is paid from your VibeDev balance; the DeepSeek account stays DeepSee
141
160
 
142
161
  **Creator tools withdrawn (0.2.5)**: the Creator tools line 0.2.4 added at the end of Settings → VibeDev account is gone. It was a cross-plugin recommendation slot, and in the settings page it hurt the layout; a redesign can revisit it later. Only that interface change is reverted: the account, the balance, the model list, the sign-in and saved screenplays, boards and film files are untouched.
143
162
 
163
+ **Gateway audio (0.3.1)**: audio models are recognized from the catalog's structured `audio` block (music, podcast, speech, transcription), never from their names; speech generation (Seed) joins music and podcasts; prices show their unit (per generation, per second of original audio, per minute) and "not priced" when the catalog states none; the `vibedevMedia` service gains `startAudio` with an operation key and query-only result reading.
164
+
144
165
  **Models**: the list comes from the VibeDev gateway for your account, so nothing is listed before you sign in; models appear right after, without a restart. Using a VibeDev model while signed out, or after the sign-in has ended, asks you to sign in; a used-up balance says so with the top-up link. These messages are separate from the DeepSeek account's. An expired access token is renewed and the request sent again, without repeating any output.
145
166
 
146
167
  **Web search**: the plugin registers the web search provider `vibedev-gateway`, signed in with your VibeDev account. Registering does not select it: DeepSeek Harness keeps its own search unless the profile sets `searchProvider: vibedev-gateway` on the `web` row (the VibeDev app does). Searching is free, but the gateway refuses every request, searches included, while the balance is zero.
147
168
 
148
- **Media tools**: `image_generate`, `video_generate` (runs as a background job), `audio_generate` (music or a podcast), `audio_transcribe` (Mandarin only for now), `media_models`, `media_tasks` and `media_account`. Reference images, videos and audio, and first and last frames, can each be a workspace path, an http(s) link, or `chat:1` for the most recent matching file you attached. A request the model cannot serve is refused before anything is uploaded or charged.
169
+ **Media tools**: `image_generate`, `video_generate` (runs as a background job), `audio_generate` (music, a podcast or speech), `audio_transcribe` (a multipart file upload; languages and limits as each model declares), `media_models`, `media_tasks` and `media_account`. Reference images, videos and audio, and first and last frames, can each be a workspace path, an http(s) link, or `chat:1` for the most recent matching file you attached. A request the model cannot serve is refused before anything is uploaded or charged.
170
+
171
+ **Audio models**: whether an audio model makes music, a podcast, speech or a transcription comes from its catalog declaration (`audio.generation_kind`), never from its name; a model that declares no kind is listed but cannot generate. Each model is sent only the fields it declares in `request_fields`, with the text in its `prompt_field`: music takes `prompt`, `lyrics`, `genre`, `mood` and `gender`; a podcast `prompt` and `source_url`; speech what it declares (Seed: `text_prompt`, `references`, `audio_config`, `watermark`). Speech reference limits (count, size, length, exclusions) and the sample rates of each output format come from the gateway. Music and podcasts take no references, format, sample rate or duration.
149
172
 
150
- **Cost**: chat models are billed by usage at the gateway's prices; images per image, video per second (reference video length counts), music and podcasts per request. Failed video and audio tasks are not charged, and a submitted video cannot be cancelled. "Ask before each paid generation" is off by default.
173
+ **Cost**: chat models are billed by usage at the gateway's prices; images per image, video per second (reference video length counts), music and podcasts per request, speech per second of original audio (the length the provider reports, which can differ from playback), transcription per minute. A model without a catalog price is shown as "not priced" (未定价), never as free. Failed and cancelled video and audio tasks are not charged, and a submitted video cannot be cancelled. One operation generates once: an unconfirmed submission is settled under the same key, a failed query or download only queries again or renews the link, and a result whose charge is still settling (`settlement_pending`) is kept, never generated again. "Ask before each paid generation" is off by default.
151
174
 
152
175
  **Settings**: open "VibeDev Account & Models" on the Plugins page for the spending confirmation, the output folder and the default media models. `displayName`, `preferredModels`, `openBrowserOnSignIn`, `catalogRefreshMinutes`, `client` (the client name sent with the sign-in, default `vibedev-plugin`) and `primary` (show the account as the app's main account; off by default, on in the VibeDev app) are set in the profile's `cordis.patch.yml` row `dsh-vibedev`.
153
176
 
@@ -155,10 +178,16 @@ Everything is paid from your VibeDev balance; the DeepSeek account stays DeepSee
155
178
 
156
179
  **Privacy**: prompts and reference media are sent to the VibeDev gateway, which forwards them to the model provider. The plugin's own sign-in is kept in the DeepSeek Harness credential store, and task records under `dsh-media/` in the DeepSeek Harness data folder (the former name, kept so an upgrade loses nothing).
157
180
 
158
- **For other plugins**: the `vibedevMedia` service generates images and videos through the same model choice, reference checks, idempotent submission and background saving; the film workbench ([dsh-film](https://github.com/VibeDev-Si/dsh-film)) storyboard uses it.
181
+ **For other plugins**: the `vibedevMedia` service generates images, videos and audio (`startAudio`: music, podcasts and speech) through the same model choice, reference checks, idempotent submission and background saving; the film workbench ([dsh-film](https://github.com/VibeDev-Si/dsh-film)) storyboard uses it. `audioForms()` describes the form of each audio model (only its declared fields, choices and limits, and its price). Every task is read the same way: `task` (as recorded), `refreshTask` (ask the gateway again — fresh links, settlement; a query, never a submission), `onTask` (each change) and `result` (the files to play or download). Pass the same `operationId` when one click is retried: its task is returned and nothing is submitted twice.
159
182
 
160
183
  **Third-party code**: the model route in `src/llm/` is ported from the VibeDev app; `context.ts`, `replay.ts` and `stream.ts` are copied from DeepSeek Harness's `@deepseek-ai/dsh-llm-pi-ai` (MIT). The licences of the bundled pi-ai, Anthropic SDK, OpenAI SDK and their dependencies are in `THIRD-PARTY-NOTICES.txt`.
161
184
 
185
+ ## Login diagnostics (0.2.8-us.3) · 登录诊断
186
+
187
+ 登录状态区分等待浏览器、交换登录凭据、保存凭据与成功、失败、取消、过期;设置页面和新版 VibeDev Next 欢迎页显示网络、超时、授权拒绝、服务不可用、响应异常、本地存储或回调错误。折叠的诊断信息及 Host 日志只记录尝试编号、阶段、目标 origin、HTTP 状态和受控网络错误码,不记录账号密码、授权码、PKCE verifier、token、响应正文或完整回调链接。保持 PKCE、state 与随机 localhost 回调;交换失败不会自动重发或切换网关。美国网页公开配置仍需服务维护方单独核对。
188
+
189
+ Login distinguishes waiting for the browser, exchanging credentials, saving credentials, and successful, failed, cancelled or expired outcomes. Settings and the updated VibeDev Next welcome page show classified failures. Collapsible support details and Host logs contain only the attempt reference, phase, origin, HTTP status and allowlisted network codes. Passwords, authorization codes, PKCE verifiers, tokens, response bodies and full callback URLs are excluded. PKCE, state and the random localhost callback remain; a failed exchange is never automatically repeated or sent to another gateway. The gateway operator must separately verify the American web page's public configuration.
190
+
162
191
  ## Maintainer releases
163
192
 
164
193
  Build, typecheck and test locally, pack the tested archive, tag the matching source version, and publish a GitHub Release with `vibedev-si-dsh-vibedev-<version>.tgz` attached. `.github/workflows/publish.yml` publishes that exact archive with npm OIDC; it does not rebuild the package. The job checks the source/tag/archive identities and Release asset digest, refuses an existing version with different bytes, and skips an identical already-published version. It verifies the complete npm tarball after publication. For a manual retry of an unpublished version, run the workflow on the version's tag ref.
@@ -1,4 +1,4 @@
1
- THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.2.7
1
+ THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.3.1
2
2
 
3
3
  This package bundles code from the projects below. Each is used under the licence that
4
4
  follows its name; the licence texts are reproduced as the projects ship them.
package/client/client.js CHANGED
@@ -131,10 +131,10 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
131
131
  const body = await response.json().catch(() => void 0);
132
132
  if (!response.ok || typeof body?.url !== "string") throw new Error(typeof body?.error?.message === "string" ? body.error.message : `HTTP ${response.status}`);
133
133
  if (body.opened !== true) this.options.openWindow?.(body.url);
134
- } catch (error) {
134
+ } catch {
135
135
  this.set({ actionError: {
136
136
  kind: "sign-in",
137
- message: error instanceof Error ? error.message : String(error)
137
+ message: "Sign-in request failed."
138
138
  } });
139
139
  } finally {
140
140
  this.set({ busy: void 0 });
@@ -179,6 +179,71 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
179
179
  }
180
180
  };
181
181
  //#endregion
182
+ //#region src/client/sign-in-presentation.ts
183
+ /** @param attempt - latest safe Host snapshot. @returns a locale key for a terminal login failure. */
184
+ function failureKey(attempt) {
185
+ if (attempt?.phase === "expired") return "signInExpired";
186
+ if (attempt?.phase !== "failed") return void 0;
187
+ const keys = {
188
+ network: "signInNetwork",
189
+ timeout: "signInTimeout",
190
+ "gateway-refused": "signInRefused",
191
+ "gateway-unavailable": "signInUnavailable",
192
+ protocol: "signInProtocol",
193
+ storage: "signInStorage",
194
+ callback: "signInCallback"
195
+ };
196
+ return attempt.errorCode !== void 0 && Object.hasOwn(keys, attempt.errorCode) ? keys[attempt.errorCode] : "signInUnknown";
197
+ }
198
+ /** @param attempt - latest snapshot. @returns support reference without query, body, credentials or raw error. */
199
+ function safeSupportDetails(attempt) {
200
+ if (attempt === void 0) return void 0;
201
+ const id = /^[a-zA-Z0-9_-]{1,80}$/.test(attempt.id) ? attempt.id : void 0;
202
+ let origin;
203
+ try {
204
+ const url = new URL(attempt.gatewayOrigin);
205
+ if (["https:", "http:"].includes(url.protocol) && !url.username && !url.password) origin = url.origin;
206
+ } catch {}
207
+ const stage = [
208
+ "listen",
209
+ "device",
210
+ "redeem",
211
+ "persist"
212
+ ].includes(attempt.stage ?? "") ? attempt.stage : void 0;
213
+ return [
214
+ [
215
+ "network",
216
+ "timeout",
217
+ "gateway-refused",
218
+ "gateway-unavailable",
219
+ "protocol",
220
+ "storage",
221
+ "callback"
222
+ ].includes(attempt.errorCode ?? "") ? attempt.errorCode : void 0,
223
+ stage,
224
+ Number.isInteger(attempt.httpStatus) && Number(attempt.httpStatus) >= 100 && Number(attempt.httpStatus) <= 599 ? `HTTP ${attempt.httpStatus}` : void 0,
225
+ [
226
+ "ECONNRESET",
227
+ "ECONNREFUSED",
228
+ "ETIMEDOUT",
229
+ "ENOTFOUND",
230
+ "EAI_AGAIN",
231
+ "ENETUNREACH",
232
+ "EHOSTUNREACH",
233
+ "UND_ERR_CONNECT_TIMEOUT",
234
+ "UND_ERR_HEADERS_TIMEOUT",
235
+ "UND_ERR_BODY_TIMEOUT",
236
+ "UND_ERR_SOCKET",
237
+ "CERT_HAS_EXPIRED",
238
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
239
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
240
+ "ERR_TLS_CERT_ALTNAME_INVALID"
241
+ ].includes(attempt.networkCode ?? "") ? attempt.networkCode : void 0,
242
+ origin,
243
+ id
244
+ ].filter((value) => value !== void 0).join(" · ");
245
+ }
246
+ //#endregion
182
247
  //#region src/client/locales.ts
183
248
  /**
184
249
  * Copy for the dsh-vibedev pages (the plugin's settings page, the VibeDev
@@ -198,6 +263,7 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
198
263
  videoModel: "视频",
199
264
  musicModel: "音乐",
200
265
  podcastModel: "播客",
266
+ speechModel: "语音合成",
201
267
  transcriptionModel: "语音转写",
202
268
  modelPlaceholder: "留空自动选择",
203
269
  account: "账号",
@@ -239,6 +305,18 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
239
305
  signOut: "退出登录",
240
306
  signOutFailed: "退出没有成功,请重试。",
241
307
  signInFailed: "没能开始登录:{message}",
308
+ signInExchanging: "正在交换登录凭据…",
309
+ signInCommitting: "正在保存登录信息…",
310
+ signInExpired: "登录已超时,请重新登录。",
311
+ signInNetwork: "无法连接登录服务,请检查网络后重新登录。",
312
+ signInTimeout: "登录服务响应超时,请重新登录。",
313
+ signInRefused: "登录服务拒绝了本次授权,请重新发起登录。",
314
+ signInUnavailable: "登录服务暂时不可用,请稍后重试。",
315
+ signInProtocol: "登录服务返回内容异常,请将诊断信息提供给支持人员。",
316
+ signInStorage: "无法在本机保存登录信息,请检查应用数据目录的访问权限。",
317
+ signInCallback: "无法启动本地登录回调,请检查安全软件后重试。",
318
+ signInUnknown: "登录未完成,请重新登录。",
319
+ signInDiagnostic: "登录诊断信息",
242
320
  loadFailed: "读取 VibeDev 账号状态失败,请稍后再试。",
243
321
  sidebarSignedIn: "VibeDev 已登录",
244
322
  sidebarSignedOut: "登录 VibeDev",
@@ -261,6 +339,7 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
261
339
  videoModel: "Video",
262
340
  musicModel: "Music",
263
341
  podcastModel: "Podcasts",
342
+ speechModel: "Speech",
264
343
  transcriptionModel: "Speech to text",
265
344
  modelPlaceholder: "Empty: chosen automatically",
266
345
  account: "Account",
@@ -302,6 +381,18 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
302
381
  signOut: "Sign out",
303
382
  signOutFailed: "Signing out did not work. Try again.",
304
383
  signInFailed: "Could not start the sign-in: {message}",
384
+ signInExchanging: "Exchanging sign-in credentials…",
385
+ signInCommitting: "Saving sign-in credentials…",
386
+ signInExpired: "Sign-in expired. Please sign in again.",
387
+ signInNetwork: "Could not connect to the sign-in service. Check your network and try signing in again.",
388
+ signInTimeout: "The sign-in service did not respond in time. Please sign in again.",
389
+ signInRefused: "The sign-in service rejected this authorization. Please start a new sign-in.",
390
+ signInUnavailable: "The sign-in service is temporarily unavailable. Please try again later.",
391
+ signInProtocol: "The sign-in service returned an invalid response. Please share the diagnostic details with support.",
392
+ signInStorage: "Could not save the sign-in locally. Check access to the app data folder.",
393
+ signInCallback: "Could not start the local sign-in callback. Check local security software and try again.",
394
+ signInUnknown: "Could not complete sign-in. Please sign in again.",
395
+ signInDiagnostic: "Sign-in diagnostic details",
305
396
  loadFailed: "The VibeDev account could not be read. Try again later.",
306
397
  sidebarSignedIn: "VibeDev signed in",
307
398
  sidebarSignedOut: "Sign in to VibeDev",
@@ -402,6 +493,7 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
402
493
  function statusDot(state) {
403
494
  const view = state.view;
404
495
  if (view === void 0) return "idle";
496
+ if (view.source === "none" && view.attempt?.phase === "failed") return "error";
405
497
  if (view.pending !== void 0) return "ongoing";
406
498
  return view.source === "none" ? "idle" : "done";
407
499
  }
@@ -432,8 +524,8 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
432
524
  let actions = null;
433
525
  if (view === void 0) status = state.loadFailed ? t("loadFailed") : t("loading");
434
526
  else if (view.pending !== void 0) {
435
- status = t("signingIn");
436
- details = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
527
+ status = view.attempt?.phase === "committing" ? t("signInCommitting") : view.attempt?.phase === "exchanging" ? t("signInExchanging") : t("signingIn");
528
+ details = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: view.attempt === void 0 || view.attempt.phase === "waiting-browser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
437
529
  style: styles$1.line,
438
530
  children: t("pendingStatus")
439
531
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -443,10 +535,11 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
443
535
  store.openPending();
444
536
  },
445
537
  children: t("pendingLink")
446
- })] });
538
+ })] }) : null });
447
539
  actions = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
448
540
  variant: "outline",
449
541
  size: "sm",
542
+ disabled: view.attempt?.phase === "committing",
450
543
  onClick: () => {
451
544
  store.cancel();
452
545
  },
@@ -529,7 +622,9 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
529
622
  }) : null
530
623
  ] });
531
624
  }
532
- const error = state.actionError === void 0 ? void 0 : state.actionError.kind === "sign-in" ? fill(t("signInFailed"), { message: state.actionError.message }) : t("signOutFailed");
625
+ const error = state.actionError === void 0 ? void 0 : state.actionError.kind === "sign-in" ? t("signInUnknown") : t("signOutFailed");
626
+ const loginFailure = view?.source === "none" ? failureKey(view.attempt) : void 0;
627
+ const supportDetails = loginFailure === void 0 ? void 0 : safeSupportDetails(view?.attempt);
533
628
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
534
629
  style: styles$1.page,
535
630
  "data-dsh-vibedev-account": true,
@@ -562,6 +657,15 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
562
657
  style: styles$1.error,
563
658
  role: "alert",
564
659
  children: error
660
+ }),
661
+ loginFailure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
662
+ style: styles$1.error,
663
+ role: "alert",
664
+ children: t(loginFailure)
665
+ }),
666
+ supportDetails === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
667
+ style: styles$1.quiet,
668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("signInDiagnostic") }), supportDetails]
565
669
  })
566
670
  ]
567
671
  })
@@ -578,6 +682,7 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
578
682
  "videoModel",
579
683
  "musicModel",
580
684
  "podcastModel",
685
+ "speechModel",
581
686
  "transcriptionModel"
582
687
  ];
583
688
  /** Every field the page edits. */
@@ -636,8 +741,6 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
636
741
  * output folder, the pinned models, and how the account works. Edits are
637
742
  * staged and written together by the save, like every settings page there.
638
743
  */
639
- /** Where new users register and everyone tops up. */
640
- const VIBEDEV_SITE = "https://vibedev.jzsaas.com";
641
744
  const styles = {
642
745
  section: {
643
746
  padding: "12px 0",
@@ -714,11 +817,12 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
714
817
  }
715
818
  /**
716
819
  * Render the settings page.
717
- * @param props - the dictionary, the form store and its actions.
820
+ * @param props - the dictionary, form store, actions and selected gateway account view.
718
821
  * @returns the page.
719
822
  */
720
- function MediaSettingsPage({ t, store, actions }) {
823
+ function MediaSettingsPage({ t, store, actions, account }) {
721
824
  const state = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
825
+ const site = (0, react.useSyncExternalStore)(account.subscribe, account.getSnapshot).view?.links.register;
722
826
  if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
723
827
  style: styles.quiet,
724
828
  role: "status",
@@ -801,9 +905,9 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
801
905
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
802
906
  style: styles.hint,
803
907
  children: t("accountHint")
804
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
908
+ }), site === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
805
909
  style: styles.link,
806
- href: VIBEDEV_SITE,
910
+ href: site,
807
911
  target: "_blank",
808
912
  rel: "noreferrer",
809
913
  children: t("register")
@@ -960,15 +1064,16 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
960
1064
  {
961
1065
  type: "label",
962
1066
  id: "pending",
963
- text: t("pendingStatus")
1067
+ text: view?.attempt?.phase === "committing" ? t("signInCommitting") : view?.attempt?.phase === "exchanging" ? t("signInExchanging") : t("pendingStatus")
964
1068
  },
965
- {
1069
+ ...view?.attempt === void 0 || view.attempt.phase === "waiting-browser" ? [{
966
1070
  id: "open-pending",
967
1071
  label: t("pendingLink")
968
- },
1072
+ }] : [],
969
1073
  {
970
1074
  id: "cancel",
971
- label: t("cancelSignIn")
1075
+ label: t("cancelSignIn"),
1076
+ disabled: view?.attempt?.phase === "committing"
972
1077
  },
973
1078
  {
974
1079
  type: "separator",
@@ -1136,15 +1241,16 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
1136
1241
  {
1137
1242
  type: "label",
1138
1243
  id: "pending",
1139
- text: t("pendingStatus")
1244
+ text: view.attempt?.phase === "committing" ? t("signInCommitting") : view.attempt?.phase === "exchanging" ? t("signInExchanging") : t("pendingStatus")
1140
1245
  },
1141
- {
1246
+ ...view.attempt === void 0 || view.attempt.phase === "waiting-browser" ? [{
1142
1247
  id: "open-pending",
1143
1248
  label: t("pendingLink")
1144
- },
1249
+ }] : [],
1145
1250
  {
1146
1251
  id: "cancel",
1147
- label: t("cancelSignIn")
1252
+ label: t("cancelSignIn"),
1253
+ disabled: view.attempt?.phase === "committing"
1148
1254
  }
1149
1255
  ] : [
1150
1256
  {
@@ -1331,7 +1437,8 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
1331
1437
  }, (owner = {}) => owner.view === "summary" ? null : (0, react.createElement)(MediaSettingsPage, {
1332
1438
  t,
1333
1439
  store,
1334
- actions
1440
+ actions,
1441
+ account
1335
1442
  })));
1336
1443
  let section;
1337
1444
  const placeSection = () => {
@@ -1345,7 +1452,8 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
1345
1452
  }, () => (0, react.createElement)(MediaSettingsPage, {
1346
1453
  t,
1347
1454
  store,
1348
- actions
1455
+ actions,
1456
+ account
1349
1457
  }))));
1350
1458
  };
1351
1459
  const unsubscribe = account.subscribe(placeSection);
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { ConnectionFetchRoute } from '@deepseek-ai/dsh-client-connection';
21
21
  import type { CredentialChain } from '../auth/credentials.js';
22
- import type { PluginLogin } from '../auth/login.js';
22
+ import type { PluginLogin, SignInAttempt } from '../auth/login.js';
23
23
  /** Path prefix of the plugin's Host routes. */
24
24
  export declare const ACCOUNT_ROUTE_PREFIX = "/api/dsh-vibedev/account";
25
25
  /** Where the active credential comes from. */
@@ -43,6 +43,8 @@ export interface AccountView {
43
43
  readonly url: string;
44
44
  readonly expiresAt: number;
45
45
  };
46
+ /** Safe sign-in progress or the most recent terminal diagnostic. */
47
+ readonly attempt?: SignInAttempt;
46
48
  /** VibeDev chat models the pickers list now, and why there are none. */
47
49
  readonly models: {
48
50
  readonly count: number;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Audio generation requests — music, podcasts and speech — checked against what
3
+ * the chosen model declares in the catalog's `audio` block, and turned into the
4
+ * body of `POST /v1/audio/generations`.
5
+ *
6
+ * Each kind sends only its own fields:
7
+ *
8
+ * | kind | wire fields |
9
+ * | ------- | --------------------------------------------------------------- |
10
+ * | music | model, prompt, lyrics, genre, mood, gender |
11
+ * | podcast | model, prompt, source_url |
12
+ * | speech | as declared; Seed: model, text_prompt, references, audio_config, watermark |
13
+ *
14
+ * The main text goes in the model's `prompt_field`. A field the model does not
15
+ * declare in `request_fields` is refused before anything is uploaded or charged;
16
+ * music and podcasts never take speech references, output formats, sample rates
17
+ * or a duration. Speech references, formats and sample rates come from the
18
+ * catalog only (count, size, length, mutual exclusions, the sample rates of
19
+ * each format), never from a fixed list.
20
+ * @module dsh-vibedev/audio/request
21
+ */
22
+ import type { AudioCapabilities, AudioGenerationKind, MediaModel } from '../gateway/catalog.js';
23
+ import type { AudioQuote } from '../pricing.js';
24
+ import type { MediaCall, MediaRuntime } from '../runtime.js';
25
+ import type { TaskRecord } from '../tasks/store.js';
26
+ /** The kinds of audio a generation request makes. */
27
+ export type AudioMakeKind = Exclude<AudioGenerationKind, 'transcription'>;
28
+ export declare const AUDIO_MAKE_KINDS: readonly AudioMakeKind[];
29
+ /** What to generate. Unset fields are not sent. */
30
+ export interface AudioRequest {
31
+ /** What to make; with a named model it must match the model's declared kind. */
32
+ readonly kind?: AudioMakeKind;
33
+ /** A model id or alias; otherwise the pinned or first model of the kind. */
34
+ readonly model?: string;
35
+ /** The main text: a song description, a podcast topic, or the words to speak. */
36
+ readonly prompt?: string;
37
+ /** Music. */
38
+ readonly lyrics?: string;
39
+ readonly genre?: string;
40
+ readonly mood?: string;
41
+ readonly gender?: string;
42
+ /** Podcast: an http(s) page for the hosts to discuss. */
43
+ readonly sourceUrl?: string;
44
+ /** Speech: reference audio (a voice to imitate) — workspace paths, links or `chat:` attachments. */
45
+ readonly references?: readonly string[];
46
+ /** Speech: an output format the model declares (`wav`, `mp3`, …). */
47
+ readonly format?: string;
48
+ /** Speech: a sample rate the model declares for that format. */
49
+ readonly sampleRate?: number;
50
+ /** Speech: whether the provider adds its watermark, when the model takes the field. */
51
+ readonly watermark?: boolean;
52
+ /** Not an audio-generation parameter; refused for music and podcasts, and for speech unless declared. */
53
+ readonly duration?: number;
54
+ /**
55
+ * One user operation's key: the same key always answers with the same task,
56
+ * so a repeated click or a retried call never creates a second one. Letters,
57
+ * digits, `-` and `_`, at most 80. Generated when absent.
58
+ */
59
+ readonly operationId?: string;
60
+ }
61
+ /**
62
+ * What an audio model makes, from its declaration only.
63
+ * @param model - the model.
64
+ * @returns the kind, or undefined when the catalog does not declare it.
65
+ */
66
+ export declare function audioKindOf(model: MediaModel): AudioMakeKind | undefined;
67
+ /** The wire fields a model takes. */
68
+ export declare function requestFieldsOf(model: MediaModel): readonly string[];
69
+ /** The field the main text goes in. */
70
+ export declare function promptFieldOf(model: MediaModel): string | undefined;
71
+ /** The output format and sample rate a speech request uses, from the declared formats only. */
72
+ export declare function audioConfigOf(model: MediaModel, format: string | undefined, sampleRate: number | undefined): {
73
+ format: string;
74
+ sample_rate?: number;
75
+ } | undefined;
76
+ /**
77
+ * Check a request against the model and build the body, without references
78
+ * (they are uploaded first; their URLs are added by {@link prepareAudioTask}).
79
+ * @param model - the chosen model.
80
+ * @param request - what to generate.
81
+ * @returns the body without references, and the wire fields the request uses.
82
+ * @throws {@link MediaError} for a field the model does not take, a missing text, or a value outside the declared choices.
83
+ */
84
+ export declare function audioRequestBody(model: MediaModel, request: AudioRequest): Record<string, unknown>;
85
+ /**
86
+ * Choose the model for a request: the one named (an id or an alias), else the
87
+ * pinned or first model of the kind. A named model must make the kind asked for.
88
+ */
89
+ export declare function chooseAudioModel(runtime: MediaRuntime, request: Pick<AudioRequest, 'kind' | 'model'>, signal?: AbortSignal): Promise<MediaModel>;
90
+ /** A checked, priced audio task ready to submit. */
91
+ export interface PreparedAudio {
92
+ readonly model: MediaModel;
93
+ readonly kind: AudioMakeKind;
94
+ readonly quote: AudioQuote;
95
+ readonly draft: TaskRecord;
96
+ }
97
+ /**
98
+ * Everything before an audio submission: choose the model, check every field
99
+ * against its declaration, upload and measure speech references, price it,
100
+ * confirm the spending, and build the task under the operation's key.
101
+ * @param runtime - the media runtime.
102
+ * @param call - the tool call or host call.
103
+ * @param request - what to generate.
104
+ * @param save - where the result goes.
105
+ * @returns the prepared task.
106
+ */
107
+ export declare function prepareAudioTask(runtime: MediaRuntime, call: MediaCall, request: AudioRequest, save: {
108
+ readonly folder: string;
109
+ readonly stem: string;
110
+ }): Promise<PreparedAudio>;
111
+ /** One field of an audio form, for interfaces that build their form from the catalog. */
112
+ export interface AudioFormField {
113
+ /** The {@link AudioRequest} key. */
114
+ readonly key: 'prompt' | 'lyrics' | 'genre' | 'mood' | 'gender' | 'sourceUrl' | 'references' | 'format' | 'sampleRate' | 'watermark';
115
+ /** The wire field it is sent as. */
116
+ readonly wire: string;
117
+ readonly required: boolean;
118
+ /** Declared choices. */
119
+ readonly choices?: readonly string[];
120
+ /** For `sampleRate`: the rates of each format. */
121
+ readonly sampleRates?: Readonly<Record<string, readonly number[]>>;
122
+ /** For `references`: the declared limits. */
123
+ readonly limits?: NonNullable<AudioCapabilities['references']>;
124
+ readonly maxChars?: number;
125
+ }
126
+ /** What a form for one audio model shows. */
127
+ export interface AudioForm {
128
+ readonly model: string;
129
+ readonly name: string;
130
+ readonly kind: AudioMakeKind;
131
+ readonly fields: readonly AudioFormField[];
132
+ readonly price: AudioQuote;
133
+ readonly defaultFormat?: string;
134
+ }
135
+ /**
136
+ * The form an interface shows for one model: only the fields the model
137
+ * declares, with their declared choices and limits.
138
+ * @param model - the model.
139
+ * @returns the form, or undefined for a model that cannot generate audio.
140
+ */
141
+ export declare function audioFormOf(model: MediaModel): AudioForm | undefined;
@@ -36,13 +36,15 @@ export declare class CredentialChain {
36
36
  */
37
37
  reject(credential: GatewayCredential): Promise<void>;
38
38
  }
39
- export declare const GRANT_KEY: import("@deepseek-ai/dsh-credentials").CredentialKey;
39
+ /** Credential identity for one gateway; never consult another gateway's record. */
40
+ export declare function gatewayGrantKey(origin: string): import("@deepseek-ai/dsh-credentials").CredentialKey;
40
41
  /**
41
42
  * Grant storage: the host's credential store when one is loaded (a grant
42
43
  * record the store keeps verbatim), else a private file in the state
43
44
  * directory. The device id is not a secret and lives in the state directory.
44
- * @param stateDir - the plugin state directory.
45
+ * @param root - the plugin state root; the storage selects an origin-specific directory itself.
45
46
  * @param credentials - the host's credential store, when loaded.
47
+ * @param selectedOrigin - the issuing gateway; unmarked historical state is never adopted.
46
48
  * @returns the storage.
47
49
  */
48
- export declare function grantStorage(stateDir: string, credentials: () => CredentialProvider | undefined): GrantStorage;
50
+ export declare function grantStorage(root: string, credentials: () => CredentialProvider | undefined, selectedOrigin?: string): GrantStorage;