@eddyskywalker/dsh-chatgpt-subscription 0.29.0 → 0.31.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +11 -2
  3. package/lib/client.js +2923 -210
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +4270 -561
  6. package/lib/types/client/index.d.ts.map +1 -1
  7. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts +8 -5
  8. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts.map +1 -1
  9. package/lib/types/client/minimax-code/MinimaxVideoDocumentBody.d.ts +69 -0
  10. package/lib/types/client/minimax-code/MinimaxVideoDocumentBody.d.ts.map +1 -0
  11. package/lib/types/client/minimax-code/MinimaxVideoToolView.d.ts +83 -0
  12. package/lib/types/client/minimax-code/MinimaxVideoToolView.d.ts.map +1 -0
  13. package/lib/types/client/minimax-code/VideoFormDialog.d.ts +90 -0
  14. package/lib/types/client/minimax-code/VideoFormDialog.d.ts.map +1 -0
  15. package/lib/types/client/minimax-code/VideoSettingsSections.d.ts +73 -0
  16. package/lib/types/client/minimax-code/VideoSettingsSections.d.ts.map +1 -0
  17. package/lib/types/client/minimax-code/commands-contract.d.ts +35 -0
  18. package/lib/types/client/minimax-code/commands-contract.d.ts.map +1 -0
  19. package/lib/types/client/minimax-code/document-preview.d.ts +45 -0
  20. package/lib/types/client/minimax-code/document-preview.d.ts.map +1 -0
  21. package/lib/types/client/minimax-code/locales.d.ts +212 -8
  22. package/lib/types/client/minimax-code/locales.d.ts.map +1 -1
  23. package/lib/types/client/minimax-code/styles.d.ts.map +1 -1
  24. package/lib/types/client/minimax-code/video-command-host.d.ts +61 -0
  25. package/lib/types/client/minimax-code/video-command-host.d.ts.map +1 -0
  26. package/lib/types/client/minimax-code/video-command.d.ts +126 -0
  27. package/lib/types/client/minimax-code/video-command.d.ts.map +1 -0
  28. package/lib/types/client/minimax-code/video-contract.d.ts +172 -0
  29. package/lib/types/client/minimax-code/video-contract.d.ts.map +1 -0
  30. package/lib/types/client/minimax-code/video-defaults-store.d.ts +61 -0
  31. package/lib/types/client/minimax-code/video-defaults-store.d.ts.map +1 -0
  32. package/lib/types/client/minimax-code/video-form-rules.d.ts +288 -0
  33. package/lib/types/client/minimax-code/video-form-rules.d.ts.map +1 -0
  34. package/lib/types/client/minimax-code/video-media.d.ts +61 -0
  35. package/lib/types/client/minimax-code/video-media.d.ts.map +1 -0
  36. package/lib/types/client/minimax-code/video-slots.d.ts +87 -0
  37. package/lib/types/client/minimax-code/video-slots.d.ts.map +1 -0
  38. package/lib/types/host/common/context-overflow.d.ts.map +1 -1
  39. package/lib/types/host/minimax-code/adapter.d.ts +14 -0
  40. package/lib/types/host/minimax-code/adapter.d.ts.map +1 -1
  41. package/lib/types/host/minimax-code/checkin-gateway.d.ts +36 -0
  42. package/lib/types/host/minimax-code/checkin-gateway.d.ts.map +1 -1
  43. package/lib/types/host/minimax-code/checkin.d.ts +60 -2
  44. package/lib/types/host/minimax-code/checkin.d.ts.map +1 -1
  45. package/lib/types/host/minimax-code/client.d.ts.map +1 -1
  46. package/lib/types/host/minimax-code/iso-bmff-duration.d.ts +84 -0
  47. package/lib/types/host/minimax-code/iso-bmff-duration.d.ts.map +1 -0
  48. package/lib/types/host/minimax-code/mapper.d.ts +51 -19
  49. package/lib/types/host/minimax-code/mapper.d.ts.map +1 -1
  50. package/lib/types/host/minimax-code/matrix-client.d.ts +168 -0
  51. package/lib/types/host/minimax-code/matrix-client.d.ts.map +1 -0
  52. package/lib/types/host/minimax-code/media-store.d.ts +153 -0
  53. package/lib/types/host/minimax-code/media-store.d.ts.map +1 -0
  54. package/lib/types/host/minimax-code/model-catalog.d.ts +40 -15
  55. package/lib/types/host/minimax-code/model-catalog.d.ts.map +1 -1
  56. package/lib/types/host/minimax-code/routes.d.ts.map +1 -1
  57. package/lib/types/host/minimax-code/token-store.d.ts.map +1 -1
  58. package/lib/types/host/minimax-code/types.d.ts.map +1 -1
  59. package/lib/types/host/minimax-code/video-history.d.ts +240 -0
  60. package/lib/types/host/minimax-code/video-history.d.ts.map +1 -0
  61. package/lib/types/host/minimax-code/video-submit-rules.d.ts +40 -0
  62. package/lib/types/host/minimax-code/video-submit-rules.d.ts.map +1 -0
  63. package/lib/types/host/minimax-code/video-tool.d.ts +757 -0
  64. package/lib/types/host/minimax-code/video-tool.d.ts.map +1 -0
  65. package/lib/types/host/progress-updates/index.d.ts +6 -0
  66. package/lib/types/host/progress-updates/index.d.ts.map +1 -1
  67. package/lib/types/host/progress-updates/projection.d.ts +214 -0
  68. package/lib/types/host/progress-updates/projection.d.ts.map +1 -0
  69. package/lib/types/host/proxy-manager.d.ts.map +1 -1
  70. package/lib/types/index.d.ts.map +1 -1
  71. package/lib/types/shared/minimax-code-contracts.d.ts +17 -3
  72. package/lib/types/shared/minimax-code-contracts.d.ts.map +1 -1
  73. package/package.json +13 -4
package/lib/client.js CHANGED
@@ -29,6 +29,7 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
+ let react_dom = require("react-dom");
32
33
  //#region src/shared/minimax-code-contracts.ts
33
34
  /**
34
35
  * 全线共用的思考档位,按从小到大的顺序排列。
@@ -36,10 +37,9 @@ window.__ModuleLoader__.load({
36
37
  * 设置卡片只渲染这一份列表:档位是线路级的词汇表(`isMinimaxCodeReasoningEffort`
37
38
  * 就是按它校验的),而不是单个模型的能力表。某个模型实际接受哪些档位由
38
39
  * `MinimaxCodeModelOption.reasoningEfforts` 单独给出——把两者合成一份会让卡片
39
- * 在给一个只认 `default` 的模型显示 `max` 时无从判断是模型不支持还是文案没写。
40
+ * 在给一个只认 high 的模型显示 max 时无从判断是模型不支持还是文案没写。
40
41
  */
41
42
  const MINIMAX_CODE_REASONING_EFFORTS = [
42
- "default",
43
43
  "low",
44
44
  "medium",
45
45
  "high",
@@ -158,10 +158,10 @@ window.__ModuleLoader__.load({
158
158
  function CodexImageToolView({ block, loadImage, t }) {
159
159
  const settled = "kind" in block;
160
160
  const isError = settled ? block.isError : false;
161
- const prompt = promptFromBlock(block);
161
+ const prompt = promptFromBlock$1(block);
162
162
  const images = settled && !isError ? imageBlocks(block.content) : [];
163
163
  const label = isError ? t("imageToolFailed") : settled ? t("imageToolDone") : t("imageToolRunning");
164
- const summary = prompt ?? textSummary(settled ? block.content : []);
164
+ const summary = prompt ?? textSummary$1(settled ? block.content : []);
165
165
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
166
166
  className: "dsh-codex-image-tool",
167
167
  "data-state": isError ? "error" : settled ? "done" : "running",
@@ -222,7 +222,7 @@ window.__ModuleLoader__.load({
222
222
  alt: label
223
223
  });
224
224
  }
225
- function promptFromBlock(block) {
225
+ function promptFromBlock$1(block) {
226
226
  const partial = partialPromptFromBlock(block);
227
227
  if (partial !== null) return partial;
228
228
  const raw = "kind" in block ? block.call?.argsRaw : dispatchedArgsRaw(block);
@@ -263,7 +263,7 @@ window.__ModuleLoader__.load({
263
263
  }
264
264
  return result;
265
265
  }
266
- function textSummary(blocks) {
266
+ function textSummary$1(blocks) {
267
267
  const text = blocks.map((block) => block.type === "text" || block.type === "reasoning" ? block.text : "").filter(Boolean).join(" ").trim();
268
268
  return text === "" ? null : text;
269
269
  }
@@ -6800,9 +6800,9 @@ window.__ModuleLoader__.load({
6800
6800
  regionCn: "中国大陆 (minimax.cn)",
6801
6801
  regionGlobal: "国际 (minimax.io)",
6802
6802
  expiresAt: "令牌到期",
6803
- expiresRemaining: "剩余 {hours} 小时",
6803
+ expiresRemaining: "剩余 {minutes} 分钟",
6804
6804
  expiresExpired: "已过期",
6805
- expiresHint: "访问令牌约 24 小时有效;Host 会在令牌过期或上游返回 401 时自动刷新。刷新被拒绝时请回到 MiniMax Code 重新登录。",
6805
+ expiresHint: "访问令牌约 1 小时有效;Host 会在令牌过期或上游返回 401 时自动刷新。刷新被拒绝时请回到 MiniMax Code 重新登录。",
6806
6806
  signIn: "登录 MiniMax Code",
6807
6807
  signingIn: "正在申请设备码...",
6808
6808
  signOut: "登出",
@@ -6894,7 +6894,75 @@ window.__ModuleLoader__.load({
6894
6894
  capText: "文本",
6895
6895
  capImage: "图片",
6896
6896
  loading: "加载中...",
6897
- unknown: "—"
6897
+ unknown: "—",
6898
+ videoToolTitle: "视频",
6899
+ videoToolRunning: "正在生成视频",
6900
+ videoToolDone: "视频已生成",
6901
+ videoToolFailed: "视频生成失败",
6902
+ videoGenerating: "上游还在生成,完成后会自动出现",
6903
+ videoReading: "正在读取视频",
6904
+ videoTooLarge: "这个视频超出了 {limit} 的整文件读取上限,用系统播放器打开仍然可以看",
6905
+ videoUnreadable: "读不到这个视频的字节,文件可能已经被移动或删除",
6906
+ videoUnplayable: "这个视频的字节解不开,无法播放",
6907
+ videoNoMeta: "这次调用没有留下视频信息,无法定位文件",
6908
+ videoChannelOff: "当前环境没有打开文件读取通道,暂时无法播放",
6909
+ videoFailed: "这次生成没有成功,没有可播放的文件",
6910
+ videoSize: "{size}",
6911
+ videoTitle: "视频",
6912
+ videoCommandLabel: "生成视频",
6913
+ videoCommandDesc: "挑选参数,填成一句话再发送",
6914
+ videoPresetGroup: "常用档位",
6915
+ videoPresetCustom: "自定义…",
6916
+ videoPresetCustomDesc: "打开完整参数表单",
6917
+ videoDialogTitle: "生成视频参数",
6918
+ videoDialogDesc: "填好后会写入输入框,确认无误再发送",
6919
+ videoPromptLabel: "提示词",
6920
+ videoPromptPlaceholder: "描述你想要的画面",
6921
+ videoPromptRequired: "请先填提示词",
6922
+ videoModelLabel: "模型",
6923
+ videoRatioLabel: "比例",
6924
+ videoRatioLockedKeyframes: "给了首帧或末帧,比例固定为自适应",
6925
+ videoRatioLockedTextOnly: "纯文本不能选自适应,服务端没有可自适应的对象",
6926
+ videoDurationLabel: "时长",
6927
+ videoDurationUnit: "秒",
6928
+ videoResolutionLabel: "分辨率",
6929
+ videoWatermarkLabel: "AIGC 水印",
6930
+ videoMediaSection: "参考媒体",
6931
+ videoMediaHint: "填绝对路径或工作区相对路径,一行一个",
6932
+ videoInputImageLabel: "首帧图",
6933
+ videoLastFrameLabel: "末帧图",
6934
+ videoReferenceImagesLabel: "参考图",
6935
+ videoReferenceVideosLabel: "参考视频",
6936
+ videoReferenceVideoDurationsLabel: "参考视频时长",
6937
+ videoReferenceAudiosLabel: "参考音频",
6938
+ videoMediaUnsupported: "{model} 不接受参考图、参考视频或参考音频",
6939
+ videoLastFrameUnsupported: "{model} 不接受末帧图",
6940
+ videoTooManyImages: "参考图有 {count} 张,最多 {limit} 张",
6941
+ videoTooManyVideos: "参考视频有 {count} 个,最多 {limit} 个",
6942
+ videoTooManyAudios: "参考音频有 {count} 个,最多 {limit} 个",
6943
+ videoTooManyTotal: "参考媒体共 {count} 个,最多 {limit} 个",
6944
+ videoDurationInvalid: "{model} 不支持 {duration} 秒",
6945
+ videoSubmit: "填入输入框",
6946
+ videoCancel: "取消",
6947
+ videoClose: "关闭",
6948
+ videoDraftFilled: "参数已填入输入框,确认后发送",
6949
+ videoDraftFailed: "没有找到这个会话的输入框,参数未填入",
6950
+ videoNotMinimax: "这个命令只在 MiniMax Code 会话里可用",
6951
+ videoDefaultsSection: "视频默认参数",
6952
+ videoDefaultsHint: "打开 /video 时表单的初始值,只影响表单,不会自动生成",
6953
+ videoDefaultsSaved: "已保存",
6954
+ videoHistorySection: "视频生成历史",
6955
+ videoHistoryHint: "最近生成的视频,只记录路径不保存视频内容",
6956
+ videoHistoryEmpty: "还没有生成记录",
6957
+ videoHistoryUnavailable: "暂时读不到生成历史",
6958
+ videoHistoryRefresh: "刷新",
6959
+ videoHistoryRefreshing: "读取中…",
6960
+ videoHistoryAt: "{time}",
6961
+ videoHistoryMeta: "{model} · {ratio} · {duration} 秒 · {size}",
6962
+ videoHistoryNoRatio: "未指定比例",
6963
+ videoHistoryStorage: "记录位置:{path}",
6964
+ videoHistoryEncrypted: "已由系统凭据后端加密",
6965
+ videoHistoryPlain: "本机没有可用的凭据后端,记录为明文"
6898
6966
  };
6899
6967
  const en$6 = {
6900
6968
  ...accountPoolEn,
@@ -6910,9 +6978,9 @@ window.__ModuleLoader__.load({
6910
6978
  regionCn: "Mainland China (minimax.cn)",
6911
6979
  regionGlobal: "Global (minimax.io)",
6912
6980
  expiresAt: "Token expires",
6913
- expiresRemaining: "{hours} h left",
6981
+ expiresRemaining: "{minutes} min left",
6914
6982
  expiresExpired: "Expired",
6915
- expiresHint: "The access token lives about 24 hours; the Host refreshes it once it expires or the upstream answers 401. When a refresh is rejected, sign in again from MiniMax Code.",
6983
+ expiresHint: "The access token lives about 1 hour; the Host refreshes it once it expires or the upstream answers 401. When a refresh is rejected, sign in again from MiniMax Code.",
6916
6984
  signIn: "Sign in to MiniMax Code",
6917
6985
  signingIn: "Requesting a device code...",
6918
6986
  signOut: "Sign out",
@@ -7004,7 +7072,75 @@ window.__ModuleLoader__.load({
7004
7072
  capText: "Text",
7005
7073
  capImage: "Image",
7006
7074
  loading: "Loading...",
7007
- unknown: "—"
7075
+ unknown: "—",
7076
+ videoToolTitle: "Video",
7077
+ videoToolRunning: "Generating video",
7078
+ videoToolDone: "Video generated",
7079
+ videoToolFailed: "Video generation failed",
7080
+ videoGenerating: "Still generating upstream; the video appears here once it lands.",
7081
+ videoReading: "Reading the video",
7082
+ videoTooLarge: "This video is past the {limit} full-file read cap; your system player can still open it.",
7083
+ videoUnreadable: "The video bytes could not be read — the file may have been moved or deleted.",
7084
+ videoUnplayable: "The video bytes could not be opened for playback.",
7085
+ videoNoMeta: "This call recorded no video metadata, so the file cannot be located.",
7086
+ videoChannelOff: "No file-read channel is available here, so playback is unavailable.",
7087
+ videoFailed: "This generation did not succeed, so there is no file to play.",
7088
+ videoSize: "{size}",
7089
+ videoTitle: "Video",
7090
+ videoCommandLabel: "Generate video",
7091
+ videoCommandDesc: "Pick parameters, then fill them into the composer",
7092
+ videoPresetGroup: "Presets",
7093
+ videoPresetCustom: "Custom…",
7094
+ videoPresetCustomDesc: "Open the full parameter form",
7095
+ videoDialogTitle: "Video parameters",
7096
+ videoDialogDesc: "The form writes into the composer; review it before sending",
7097
+ videoPromptLabel: "Prompt",
7098
+ videoPromptPlaceholder: "Describe the clip you want",
7099
+ videoPromptRequired: "A prompt is required",
7100
+ videoModelLabel: "Model",
7101
+ videoRatioLabel: "Aspect ratio",
7102
+ videoRatioLockedKeyframes: "A first or last frame is set, so the ratio is fixed to adaptive",
7103
+ videoRatioLockedTextOnly: "Text-only cannot use adaptive: there is nothing to adapt to",
7104
+ videoDurationLabel: "Duration",
7105
+ videoDurationUnit: "s",
7106
+ videoResolutionLabel: "Resolution",
7107
+ videoWatermarkLabel: "AIGC watermark",
7108
+ videoMediaSection: "Reference media",
7109
+ videoMediaHint: "Absolute or workspace-relative paths, one per line",
7110
+ videoInputImageLabel: "First frame",
7111
+ videoLastFrameLabel: "Last frame",
7112
+ videoReferenceImagesLabel: "Reference images",
7113
+ videoReferenceVideosLabel: "Reference videos",
7114
+ videoReferenceVideoDurationsLabel: "Reference video durations",
7115
+ videoReferenceAudiosLabel: "Reference audio",
7116
+ videoMediaUnsupported: "{model} does not accept reference images, videos or audio",
7117
+ videoLastFrameUnsupported: "{model} does not accept a last-frame image",
7118
+ videoTooManyImages: "{count} reference images, at most {limit} are accepted",
7119
+ videoTooManyVideos: "{count} reference videos, at most {limit} are accepted",
7120
+ videoTooManyAudios: "{count} reference audio files, at most {limit} are accepted",
7121
+ videoTooManyTotal: "{count} reference media files in total, at most {limit} are accepted",
7122
+ videoDurationInvalid: "{model} does not accept {duration} seconds",
7123
+ videoSubmit: "Fill the composer",
7124
+ videoCancel: "Cancel",
7125
+ videoClose: "Close",
7126
+ videoDraftFilled: "Parameters are in the composer; send when ready",
7127
+ videoDraftFailed: "This session has no composer to fill, so nothing was inserted",
7128
+ videoNotMinimax: "This command is only available in a MiniMax Code session",
7129
+ videoDefaultsSection: "Video defaults",
7130
+ videoDefaultsHint: "Initial values for the /video form; they only prefill it and never generate",
7131
+ videoDefaultsSaved: "Saved",
7132
+ videoHistorySection: "Video history",
7133
+ videoHistoryHint: "Recent generations; paths only, never the video content",
7134
+ videoHistoryEmpty: "No generations yet",
7135
+ videoHistoryUnavailable: "Video history is unreadable right now",
7136
+ videoHistoryRefresh: "Refresh",
7137
+ videoHistoryRefreshing: "Reading…",
7138
+ videoHistoryAt: "{time}",
7139
+ videoHistoryMeta: "{model} · {ratio} · {duration}s · {size}",
7140
+ videoHistoryNoRatio: "no ratio",
7141
+ videoHistoryStorage: "Stored at {path}",
7142
+ videoHistoryEncrypted: "Encrypted by the platform credential backend",
7143
+ videoHistoryPlain: "No credential backend on this host; stored as plain text"
7008
7144
  };
7009
7145
  const dictionaries$6 = {
7010
7146
  zh: zh$6,
@@ -7024,7 +7160,7 @@ window.__ModuleLoader__.load({
7024
7160
  * rather than inferred from the JSX.
7025
7161
  */
7026
7162
  /** The card's own dictionary; stable identity keeps effect dependencies intact. */
7027
- const fallbackTranslate$3 = (key) => zh$6[key];
7163
+ const fallbackTranslate$4 = (key) => zh$6[key];
7028
7164
  /**
7029
7165
  * The shared pool card's labels, resolved through this card's dictionary.
7030
7166
  *
@@ -7120,7 +7256,7 @@ window.__ModuleLoader__.load({
7120
7256
  /** Device-code poll cadence. The host owns the real expiry. */
7121
7257
  const LOGIN_POLL_INTERVAL_MS$2 = 2e3;
7122
7258
  function useMinimaxCodeSection({ onModelChange }) {
7123
- const t = fallbackTranslate$3;
7259
+ const t = fallbackTranslate$4;
7124
7260
  const [status, setStatus] = (0, react.useState)(null);
7125
7261
  const [loading, setLoading] = (0, react.useState)(true);
7126
7262
  const [busy, setBusy] = (0, react.useState)(null);
@@ -7584,86 +7720,1048 @@ window.__ModuleLoader__.load({
7584
7720
  };
7585
7721
  }
7586
7722
  //#endregion
7587
- //#region src/client/minimax-code/MinimaxCodeSection.tsx
7723
+ //#region src/client/minimax-code/video-contract.ts
7588
7724
  /**
7589
- * The MiniMax Code settings card.
7725
+ * MiniMax 视频能力在浏览器半边的共享契约与纯函数。
7590
7726
  *
7591
- * Markup only: every piece of state, every effect and every request this card
7592
- * makes lives in 'useMinimaxCodeSection.ts', which returns one named shape the
7593
- * JSX below reads. The two files are the split of a single 915-line component —
7594
- * the behaviour is unchanged, and this one should stay render-shaped.
7727
+ * ## 为什么形状要在这里再写一遍
7728
+ *
7729
+ * host 那一份(`MinimaxVideoPresentationMeta`)住在 `src/host/minimax-code/` 下。
7730
+ * 客户端 tsconfig 的 `include` 只覆盖 `src/client/**` 与 `src/shared/**`——
7731
+ * 从浏览器半边 import 宿主模块既越界,也会把宿主的 node 依赖拖进浏览器闭包。
7732
+ * 两份形状必须逐字一致(字段名、可选性),由
7733
+ * `test/minimax-code-video-toolview.test.tsx` 的赋值断言钉住。
7734
+ *
7735
+ * ## 视频怎么从工作区走到 `<video>`
7736
+ *
7737
+ * meta 里的 `videoPath` 是**工作区相对路径**,而浏览器只有一个取字节通道:
7738
+ * `ctx.remote.workspaceFiles.readBytes(sessionId, path, options, signal)`
7739
+ * (宿主侧边栏图像预览走的是同一条,见
7740
+ * `ui-sidebar-documentpreview/src/client/rpc.ts` 的 `ReadDocumentBytes`)。
7741
+ * 拿到的 `Uint8Array` 包成 Blob,再 `URL.createObjectURL` 交给 `<video>`:
7742
+ * 视频不能像图片那样走 `loadImage`(那是附件专用的),也不能直接写文件路径
7743
+ * ——浏览器的 `<video src>` 读不到工作区。
7744
+ *
7745
+ * 对象 URL 是**手动生命周期**:拿到就建、卸载就 `revokeObjectURL`,见两个组件里的
7746
+ * `useObjectUrl`。漏掉那一步等于把整段视频留在内存里。
7747
+ *
7748
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-contract
7595
7749
  */
7596
- /** Human label per thinking mode, so the card states what the model does. */
7597
- function thinkingLabel(mode, t) {
7598
- if (mode === "always-on") return t("thinkingAlwaysOn");
7599
- if (mode === "forced-effort") return t("thinkingForced");
7600
- return t("thinkingToggle");
7750
+ /**
7751
+ * `minimax_generate_video` 的线上工具名。
7752
+ *
7753
+ * `tool.call.toolview` 是按这个字符串 keyed 分发的,拼错一个字视图就永远不渲染
7754
+ * (槽位不会报错,它只是永远命不中),所以两边共用这一个常量。
7755
+ */
7756
+ const MINIMAX_VIDEO_TOOL_NAME = "minimax_generate_video";
7757
+ /**
7758
+ * 从工具结果的 `meta` 里读出视频事实,读不到就是 `undefined`。
7759
+ *
7760
+ * `meta` 在类型上是 `unknown`,且它由**另一个运行时**写入——会话日志可能是
7761
+ * 旧版本插件写的、可能缺字段、也可能根本不是这个工具的对象。所以这里逐字段
7762
+ * 校验,任何一项不合格就整体当作「没有 meta」,由卡片改画占位块,
7763
+ * 而不是让一次审计过的 JSON 把整个对话流带崩。
7764
+ *
7765
+ * @param meta - `ToolResultNode.meta`(通常是 `output.presentationMeta`)。
7766
+ * @returns 通过校验的视频事实;不是本工具的 meta 时 undefined。
7767
+ */
7768
+ function readVideoMeta(meta) {
7769
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
7770
+ const candidate = meta;
7771
+ const videoPath = candidate.videoPath;
7772
+ const mediaType = candidate.mediaType;
7773
+ const bytes = candidate.bytes;
7774
+ const model = candidate.model;
7775
+ const taskId = candidate.taskId;
7776
+ const durationSeconds = candidate.durationSeconds;
7777
+ if (typeof videoPath !== "string" || videoPath === "") return void 0;
7778
+ if (typeof mediaType !== "string" || !mediaType.startsWith("video/")) return void 0;
7779
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return void 0;
7780
+ if (typeof model !== "string") return void 0;
7781
+ if (typeof taskId !== "string") return void 0;
7782
+ if (durationSeconds !== void 0 && (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || durationSeconds <= 0)) return;
7783
+ return {
7784
+ videoPath,
7785
+ mediaType,
7786
+ bytes,
7787
+ model,
7788
+ taskId,
7789
+ ...durationSeconds === void 0 ? {} : { durationSeconds }
7790
+ };
7601
7791
  }
7602
7792
  /**
7603
- * Hours still left on the access token, or null once it has run out.
7793
+ * 把工作区相对路径拼成绝对路径。
7604
7794
  *
7605
- * MiniMax Code's token lives about 24 hours, so hours are the unit the card
7606
- * reports; a value worth a fraction of an hour keeps one decimal instead of
7607
- * rounding down to 0.
7795
+ * 分隔符**按 posix 拼**,不按宿主平台拼:这段代码跑在浏览器里,而工作区路径
7796
+ * 的规范形是 `/` 分隔(宿主的 `parseFileAddress` 也按这个认)。反斜杠进来的
7797
+ * 相对路径先归一化,避免拼出 `C:\\ws\\clips\\a.mp4` 这种混合形态。
7798
+ *
7799
+ * `cwd` 缺省时(宿主从不认识这个会话的工作区)**原样返回相对路径**:它仍然是
7800
+ * 这个会话地址空间里的合法路径,`openFile` 与 `readBytes` 都能用;只是别再声称
7801
+ * 它是绝对的——调用方要的是「能打开的路径」,不是「看起来像绝对路径的字符串」。
7802
+ *
7803
+ * @param cwd - 会话工作区根;未提供时按相对路径处理。
7804
+ * @param videoPath - meta 里的工作区相对路径。
7805
+ * @returns 绝对路径,或 `cwd` 缺省时的相对路径。
7608
7806
  */
7609
- function remainingHours(expiresAtMs) {
7610
- if (expiresAtMs === void 0 || !Number.isFinite(expiresAtMs) || expiresAtMs <= 0) return null;
7611
- const hours = (expiresAtMs - Date.now()) / 36e5;
7612
- if (hours <= 0) return null;
7613
- return hours >= 10 ? hours.toFixed(0) : hours.toFixed(1);
7807
+ function absoluteVideoPath(cwd, videoPath) {
7808
+ const relative = videoPath.replaceAll("\\", "/");
7809
+ if (cwd === void 0 || cwd === "") return relative;
7810
+ const root = cwd.replaceAll("\\", "/").replace(/\/+$/, "");
7811
+ if (root === "") return relative;
7812
+ if (/^(?:[A-Za-z]:\/|\/)/.test(relative)) return relative;
7813
+ return `${root}/${relative}`;
7614
7814
  }
7615
- function MinimaxCodeSection({ onModelChange }) {
7616
- const { state, derived, actions } = useMinimaxCodeSection({ onModelChange });
7617
- const { status, loading, busy, error, flow, copied, connectionNotice, contextDrafts, savingModel, pool, resetCards, resetNotice, selectedResetCardId } = state;
7618
- const { authenticated, account, quota, models, contextModels, overrideCount, poolLabels, resetCardList, resetCardFlows } = derived;
7619
- const { login, cancelLogin, copyCode, logout, poolAction, setStrategy, toggleModel, setAllModels, setCheckinEnabled, runCheckinNow, updateEffort, saveContextWindow, resetContextWindow, resetAllContextWindows, refreshQuota, refreshResetCards, selectResetCard, useResetCard, testConnection, updateContextDraft } = actions;
7620
- const t = fallbackTranslate$3;
7621
- if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7622
- className: "dsha-page",
7623
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7624
- className: "dsha-empty",
7625
- children: t("loading")
7626
- })
7815
+ /**
7816
+ * 把一次取字节的异常读成「太大」还是「读不到」。
7817
+ *
7818
+ * 判定**只看错误上的 `code`**,与宿主自己的文档预览同款做法
7819
+ * (`ui-sidebar-documentpreview/src/client/failure-line.ts`):远端失败的形状是
7820
+ * `{ code, message, details }`,`code` 就是那条稳定的判别字段,不要去解析
7821
+ * `message` 里的英文,也不要靠 `instanceof`——跨包、跨 realm 都不成立。
7822
+ *
7823
+ * 任何拿不到 `code` 的东西(通道根本没装上、浏览器抛的 TypeError、undefined)
7824
+ * 一律归入「读不到」:那是默认的那句话,不能因为一个畸形异常就把文案换成
7825
+ * 「文件太大」。
7826
+ *
7827
+ * @param error - `catch` 到的任意值。
7828
+ * @returns `'too-large'` 仅当宿主明确报了这个码,否则 `'unreadable'`。
7829
+ */
7830
+ function videoReadFailureOf(error) {
7831
+ return typeof error === "object" && error !== null && error.code === "workspace-file/too-large" ? "too-large" : "unreadable";
7832
+ }
7833
+ /**
7834
+ * 从一次 too-large 失败里读出那个上限。
7835
+ *
7836
+ * 宿主把它放在 `details.limit`(`packages/api/workspace-files/src/index.ts` 的
7837
+ * `RemoteError('workspace-file/too-large', …, { path, limit })`)。读不到就不显示
7838
+ * 上限——文案里留一个「—」比编一个数字出来好。
7839
+ *
7840
+ * @param error - `catch` 到的任意值。
7841
+ * @returns 上限字节数,或 undefined。
7842
+ */
7843
+ function videoReadLimitOf(error) {
7844
+ if (typeof error !== "object" || error === null) return void 0;
7845
+ const details = error.details;
7846
+ if (typeof details !== "object" || details === null) return void 0;
7847
+ const limit = details.limit;
7848
+ return typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? limit : void 0;
7849
+ }
7850
+ /**
7851
+ * 字节数转人读的体积。
7852
+ *
7853
+ * 只有 11px 的位置放它,所以最多三位有效数字:`986 KB`、`12.4 MB`、`1.02 GB`。
7854
+ * 单位之间是普通空格,不引小数点前的零。
7855
+ * @param bytes - 文件字节数。
7856
+ * @returns 形如 `12.4 MB` 的短标签。
7857
+ */
7858
+ function formatBytes(bytes) {
7859
+ if (!Number.isFinite(bytes) || bytes < 0) return "—";
7860
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
7861
+ const units = [
7862
+ "KB",
7863
+ "MB",
7864
+ "GB",
7865
+ "TB"
7866
+ ];
7867
+ let value = bytes / 1024;
7868
+ let unit = 0;
7869
+ while (value >= 1024 && unit < units.length - 1) {
7870
+ value /= 1024;
7871
+ unit += 1;
7872
+ }
7873
+ const rounded = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2);
7874
+ return `${rounded.includes(".") ? rounded.replace(/0+$/, "").replace(/\.$/, "") : rounded} ${units[unit]}`;
7875
+ }
7876
+ /**
7877
+ * 「文件太大,读不出来」那一句,带上宿主报的上限。
7878
+ *
7879
+ * 宿主把 `details.limit` 交给文案(`failureLine` 的
7880
+ * `t('error.tooLarge', { limit: humanBytes(failure.details.limit) })`),这里照做,
7881
+ * 只是换成本插件自己的 {@link formatBytes}——两边对同一个数字的写法必须与本卡片
7882
+ * 右下角那行体积一致。
7883
+ *
7884
+ * 上限缺失时退回「—」(`formatBytes` 对非数字的既有行为):一句话里少一个数字,
7885
+ * 好过为了凑数字去猜一个上限。
7886
+ *
7887
+ * @param t - 命名空间化的翻译。
7888
+ * @param limit - 宿主报的上限字节数;undefined 时按未知处理。
7889
+ * @returns 一句占位文案(中文一句、结尾不加句号)。
7890
+ */
7891
+ function videoTooLargeText(t, limit) {
7892
+ return t("videoTooLarge").replace("{limit}", formatBytes(limit ?? NaN));
7893
+ }
7894
+ /**
7895
+ * 秒数转 `m:ss` / `h:mm:ss`。
7896
+ *
7897
+ * 视频时长是元数据,不是大数字:只补零到分钟位,超过一小时才出现小时段。
7898
+ * @param seconds - 时长秒数。
7899
+ * @returns 形如 `0:07` 或 `1:02:03` 的短标签。
7900
+ */
7901
+ function formatDuration(seconds) {
7902
+ const total = Math.max(0, Math.round(seconds));
7903
+ const hours = Math.floor(total / 3600);
7904
+ const minutes = Math.floor(total % 3600 / 60);
7905
+ const rest = total % 60;
7906
+ const tail = `${minutes}:${String(rest).padStart(2, "0")}`;
7907
+ return hours === 0 ? tail : `${hours}:${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
7908
+ }
7909
+ /**
7910
+ * 从路径里取文件名,用于侧边栏那一行元数据。
7911
+ * @param path - 绝对或相对路径,`/` 与 `\` 都认。
7912
+ * @returns 最后一个分隔符之后的部分;没有分隔符时返回原串。
7913
+ */
7914
+ function fileNameOf(path) {
7915
+ const normalized = path.replaceAll("\\", "/");
7916
+ const name = normalized.slice(normalized.lastIndexOf("/") + 1);
7917
+ return name === "" ? normalized : name;
7918
+ }
7919
+ //#endregion
7920
+ //#region src/client/minimax-code/video-form-rules.ts
7921
+ /**
7922
+ * MiniMax 视频参数的**浏览器侧规则副本**与表单状态机。
7923
+ *
7924
+ * ## 为什么这里又抄了一遍宿主那张表
7925
+ *
7926
+ * 每个模型的合法时长/分辨率、以及比例随首帧/参考媒体被改写的联动,都写在宿主
7927
+ * 的 `src/host/minimax-code/video-tool.ts` 里。浏览器半边 **import 不到**它:
7928
+ * 客户端 tsconfig 只覆盖 `src/client/**` 与 `src/shared/**`,跨过去会把宿主的
7929
+ * node 依赖拖进浏览器闭包(同 `video-contract.ts` 顶部那条注释的理由)。
7930
+ *
7931
+ * 所以这里是**逐字抄写**,不是重新推导。抄的是那一份**实测**表
7932
+ * (`docs/minimax-video-model-rules.md`),不是文档或猜测:
7933
+ *
7934
+ * | model | duration | resolutions |
7935
+ * | -------------------- | ----------------- | ------------ |
7936
+ * | `MiniMax-H3` | 4-15 | 768P, 2K |
7937
+ * | `MiniMax-H3-Max` | 5-15 | 480P, 768P |
7938
+ * | `MiniMax-Hailuo-2.3` | 6 或 10 | 768P, 1080P |
7939
+ *
7940
+ * 三张表**两两不同,且没有一张等于并集**——2K 只有 H3 有、480P 只有 H3-Max 有、
7941
+ * 1080P 只有 Hailuo 有。所以客户端也**不能**用一张并集去校验:那会让 H3-Max 的
7942
+ * 用户选到 2K,然后被服务端在计费之后拒掉。
7943
+ *
7944
+ * `test/minimax-code-video-form-rules.test.ts` 里的一个用例把这三行钉死,改任何
7945
+ * 一边而忘了另一边都会红。
7946
+ *
7947
+ * ## 比例联动为什么必须在表单层就做掉
7948
+ *
7949
+ * 宿主的 `resolveVideoSubmissionRatio` 是照上游逐分支抄的:
7950
+ *
7951
+ * ```ts
7952
+ * isH3(model)
7953
+ * ? hasKeyframes ? 'adaptive'
7954
+ * : hasReferences ? (ratio ?? 'adaptive')
7955
+ * : (ratio ?? '16:9')
7956
+ * : ratio
7957
+ * ```
7958
+ *
7959
+ * 也就是说:**给了首帧,用户选的比例会被无声改写成 `adaptive`**;纯文本又
7960
+ * **不允许** `adaptive`(没有可自适应的对象,宿主会直接拒绝)。
7961
+ *
7962
+ * 让用户先选一个「16:9」再被改写成「adaptive」,是这个入口最不该犯的错——用户
7963
+ * 看到的和发出去的不是一个东西。所以这里把「最终比例」直接算出来给用户看,
7964
+ * 并且**不给选择**:`resolveVideoRatioChoice` 返回的 `locked` 就是那个意思。
7965
+ *
7966
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-form-rules
7967
+ */
7968
+ /** 三个视频模型,顺序与宿主 `MINIMAX_VIDEO_MODELS` 一致。 */
7969
+ const MINIMAX_VIDEO_MODELS = [
7970
+ "MiniMax-H3",
7971
+ "MiniMax-H3-Max",
7972
+ "MiniMax-Hailuo-2.3"
7973
+ ];
7974
+ /** 服务端认识的每一个比例拼写,顺序照上游。 */
7975
+ const MINIMAX_VIDEO_RATIOS = [
7976
+ "21:9",
7977
+ "16:9",
7978
+ "4:3",
7979
+ "1:1",
7980
+ "3:4",
7981
+ "9:16",
7982
+ "adaptive"
7983
+ ];
7984
+ /** 首帧/末帧在场时宿主**强制**发送的比例。 */
7985
+ const MINIMAX_KEYFRAME_RATIO = "adaptive";
7986
+ /**
7987
+ * 实测的每模型规则,逐字抄自宿主 `MINIMAX_VIDEO_MODEL_RULES`。
7988
+ *
7989
+ * 抄错一个字的表现是「客户端放行 → 服务端计费后拒绝」,所以这里连顺序都不改。
7990
+ */
7991
+ const VIDEO_MODEL_RULES = [
7992
+ {
7993
+ model: "MiniMax-H3",
7994
+ duration: {
7995
+ kind: "range",
7996
+ min: 4,
7997
+ max: 15
7998
+ },
7999
+ resolutions: ["768P", "2K"],
8000
+ supportsReferenceMedia: true,
8001
+ isH3Family: true
8002
+ },
8003
+ {
8004
+ model: "MiniMax-H3-Max",
8005
+ duration: {
8006
+ kind: "range",
8007
+ min: 5,
8008
+ max: 15
8009
+ },
8010
+ resolutions: ["480P", "768P"],
8011
+ supportsReferenceMedia: true,
8012
+ isH3Family: true
8013
+ },
8014
+ {
8015
+ model: "MiniMax-Hailuo-2.3",
8016
+ duration: {
8017
+ kind: "values",
8018
+ values: [6, 10]
8019
+ },
8020
+ resolutions: ["768P", "1080P"],
8021
+ supportsReferenceMedia: false,
8022
+ isH3Family: false
8023
+ }
8024
+ ];
8025
+ /** @param model - 模型 id。 @returns 该模型的规则,未知时为 undefined。 */
8026
+ function videoModelRule(model) {
8027
+ return VIDEO_MODEL_RULES.find((rule) => rule.model === model);
8028
+ }
8029
+ /**
8030
+ * 一个模型可以选的时长,**从短到长、去重后**给选择器用。
8031
+ *
8032
+ * 区间型的模型给的是 1 秒一档的整数刻度(服务端接受区间内任意整数),取值集型的
8033
+ * 模型只有那几个值。两者都返回数组,表单因此不必分支。
8034
+ *
8035
+ * @param model - 模型 id。
8036
+ * @returns 合法时长列表;模型未知时为空。
8037
+ */
8038
+ function durationChoices(model) {
8039
+ const rule = videoModelRule(model);
8040
+ if (rule === void 0) return [];
8041
+ if (rule.duration.kind === "values") return [...rule.duration.values];
8042
+ const values = [];
8043
+ for (let value = rule.duration.min; value <= rule.duration.max; value += 1) values.push(value);
8044
+ return values;
8045
+ }
8046
+ /**
8047
+ * 一个模型的默认时长。
8048
+ *
8049
+ * 区间型的模型取**下界**:那是最便宜的一次生成,也是用户第一次点开表单时最不
8050
+ * 容易心疼的值。取值集型的模型取第一个值(Hailuo 的 6)。
8051
+ *
8052
+ * @param model - 模型 id。
8053
+ * @returns 默认时长;模型未知时为 undefined。
8054
+ */
8055
+ function defaultDuration(model) {
8056
+ const rule = videoModelRule(model);
8057
+ if (rule === void 0) return void 0;
8058
+ return rule.duration.kind === "values" ? rule.duration.values[0] : rule.duration.min;
8059
+ }
8060
+ /**
8061
+ * 一个模型的默认分辨率。
8062
+ *
8063
+ * 取**该模型自己的**第一项,而不是宿主的 `2K`:2K 是 H3 专属,H3-Max 与 Hailuo
8064
+ * 都没有,硬套过去就是一个必被拒绝的默认值。
8065
+ *
8066
+ * @param model - 模型 id。
8067
+ * @returns 默认分辨率;模型未知时为 undefined。
8068
+ */
8069
+ function defaultResolution(model) {
8070
+ return videoModelRule(model)?.resolutions[0];
8071
+ }
8072
+ /** 路径是不是「真的给了」:空白字符不算给。 */
8073
+ function hasPath(value) {
8074
+ return typeof value === "string" && value.trim() !== "";
8075
+ }
8076
+ /** 非空路径的条数。 */
8077
+ function countPaths(values) {
8078
+ if (!Array.isArray(values)) return 0;
8079
+ return values.filter(hasPath).length;
8080
+ }
8081
+ /**
8082
+ * 这份媒体输入决定的两件事:有没有首帧、有没有参考媒体。
8083
+ *
8084
+ * 两个布尔直接对应用户能看见的那句提示,以及比例能不能选。
8085
+ *
8086
+ * @param media - 表单里的媒体路径。
8087
+ * @returns 两个判定。
8088
+ */
8089
+ function mediaPresence(media) {
8090
+ return {
8091
+ hasKeyframes: hasPath(media.inputImagePath) || hasPath(media.lastFrameImagePath),
8092
+ hasReferences: countPaths(media.referenceImagePaths) > 0 || countPaths(media.referenceVideoPaths) > 0 || countPaths(media.referenceAudioPaths) > 0
8093
+ };
8094
+ }
8095
+ /**
8096
+ * 决定比例这一栏的形态,**在表单层就把联动做掉**。
8097
+ *
8098
+ * 三条规则与宿主 {@link resolveVideoSubmissionRatio} 一一对应,只是把「静默改写」
8099
+ * 换成「当场显示」:
8100
+ *
8101
+ * 1. **有首帧** → 强制 `adaptive`,不给选。用户选了也会被改写,所以干脆不给。
8102
+ * 2. **有参考媒体** → 默认 `adaptive`,但**可以改**:上游对这个分支是
8103
+ * `ratio ?? 'adaptive'`,用户给了就照发。所以这里列出的每一个拼写都是
8104
+ * 真正会发出去的那个。
8105
+ * 3. **纯文本** → 默认 `16:9`(H3 家族)或「不发」(Hailuo),**`adaptive` 不可选**
8106
+ * ——服务端没有可自适应的对象,选了必被拒。
8107
+ *
8108
+ * @param input - 模型、用户当前选的比例、媒体在场情况。
8109
+ * @returns 这一栏的形态。
8110
+ */
8111
+ function resolveVideoRatioChoice(input) {
8112
+ const { model, requested, hasKeyframes, hasReferences } = input;
8113
+ const isH3 = videoModelRule(model)?.isH3Family === true;
8114
+ if (hasKeyframes) return {
8115
+ ratio: MINIMAX_KEYFRAME_RATIO,
8116
+ selectable: false,
8117
+ lockedReason: "keyframes",
8118
+ options: []
8119
+ };
8120
+ if (hasReferences) return {
8121
+ ratio: requested ?? "adaptive",
8122
+ selectable: true,
8123
+ options: MINIMAX_VIDEO_RATIOS
8124
+ };
8125
+ const textOptions = MINIMAX_VIDEO_RATIOS.filter((entry) => entry !== "adaptive");
8126
+ return {
8127
+ ratio: requested ?? (isH3 ? "16:9" : void 0),
8128
+ selectable: true,
8129
+ lockedReason: "text-only",
8130
+ options: textOptions
8131
+ };
8132
+ }
8133
+ /**
8134
+ * 把多行文本框切成路径数组。
8135
+ *
8136
+ * 一行一条,去掉首尾空白,丢掉空行。**不做**通配符展开、不做存在性检查:路径
8137
+ * 由宿主在提交时解析,浏览器里没有工作区文件系统。
8138
+ *
8139
+ * @param text - 多行文本。
8140
+ * @returns 路径数组。
8141
+ */
8142
+ function parsePathLines(text) {
8143
+ return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "");
8144
+ }
8145
+ /**
8146
+ * 校验整个表单。
8147
+ *
8148
+ * 顺序照着服务端自己的检查顺序(model → duration → resolution → prompt → ratio),
8149
+ * 因为报告一条服务端根本不会提的规则,比不报告更糟。
8150
+ *
8151
+ * **提示词为空是唯一挡住提交的理由**;其余都是本地就拦下的、必然被拒的组合
8152
+ * (参考媒体给了 Hailuo、条数超限),拦下来只为省一次计费请求。
8153
+ *
8154
+ * @param values - 表单值。
8155
+ * @returns 问题列表;空数组表示可以提交。
8156
+ */
8157
+ function validateVideoForm(values) {
8158
+ const issues = [];
8159
+ const rule = videoModelRule(values.model);
8160
+ const { hasKeyframes } = mediaPresence(values.media);
8161
+ const images = countPaths(values.media.referenceImagePaths);
8162
+ const videos = countPaths(values.media.referenceVideoPaths);
8163
+ const audios = countPaths(values.media.referenceAudioPaths);
8164
+ if (rule !== void 0 && !rule.supportsReferenceMedia) {
8165
+ if (images + videos + audios > 0) issues.push({
8166
+ code: "reference-unsupported",
8167
+ params: { model: values.model }
8168
+ });
8169
+ if (hasPath(values.media.lastFrameImagePath)) issues.push({
8170
+ code: "last-frame-unsupported",
8171
+ params: { model: values.model }
8172
+ });
8173
+ }
8174
+ if (images > 9) issues.push({
8175
+ code: "too-many-images",
8176
+ params: {
8177
+ count: images,
8178
+ limit: 9
8179
+ }
7627
8180
  });
7628
- const hours = remainingHours(account?.expiresAtMs);
7629
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7630
- className: "dsha-page",
8181
+ if (videos > 3) issues.push({
8182
+ code: "too-many-videos",
8183
+ params: {
8184
+ count: videos,
8185
+ limit: 3
8186
+ }
8187
+ });
8188
+ if (audios > 3) issues.push({
8189
+ code: "too-many-audios",
8190
+ params: {
8191
+ count: audios,
8192
+ limit: 3
8193
+ }
8194
+ });
8195
+ if (images + videos + audios > 12) issues.push({
8196
+ code: "too-many-total",
8197
+ params: {
8198
+ count: images + videos + audios,
8199
+ limit: 12
8200
+ }
8201
+ });
8202
+ if (values.durationSeconds !== void 0 && !durationChoices(values.model).includes(values.durationSeconds)) issues.push({
8203
+ code: "duration-invalid",
8204
+ params: {
8205
+ model: values.model,
8206
+ duration: values.durationSeconds
8207
+ }
8208
+ });
8209
+ if (values.prompt.trim() === "") issues.push({ code: "prompt-empty" });
8210
+ return issues;
8211
+ }
8212
+ /**
8213
+ * 表单是否满足**提交**的最低条件。
8214
+ *
8215
+ * 比 {@link validateVideoForm} 宽:那个列表里除了提示词以外的问题都会在提交时被
8216
+ * 服务端拒绝,而这里只回答「按钮能不能亮」。真正挡住生成的是提示词——那一条
8217
+ * 是任务书里点名的:**缺提示词时不能提交**。
8218
+ *
8219
+ * @param values - 表单值。
8220
+ * @returns 能否提交。
8221
+ */
8222
+ function canSubmitVideoForm(values) {
8223
+ return values.prompt.trim() !== "";
8224
+ }
8225
+ /**
8226
+ * 把表单拼成一句**给模型看**的话,填进输入框。
8227
+ *
8228
+ * ## 为什么是这个形状
8229
+ *
8230
+ * 工具是模型自主调用的,所以这段文字要同时做到三件事:点名工具、把参数写成模型
8231
+ * 抄得下来的形态、说清「只发一次」。用等号写字段是刻意的——模型最容易照抄的就是
8232
+ * 这种形如签名的行。
8233
+ *
8234
+ * 比例**只在真的会发出去时才写**:纯文本 + Hailuo 的最终比例是 undefined(宿主
8235
+ * 不发这个字段,服务端用自己的默认),这时候替用户编一个 16:9 就是在描述一次
8236
+ * 不会发生的请求。
8237
+ *
8238
+ * ## 「只发一次」为什么必须写进去
8239
+ *
8240
+ * 提交是**计费**且**终态**的:重发不是续传,是再买一次生成。宿主自己的守卫也只
8241
+ * 在同一 session 同一 turn 内生效(`videoSubmissionKey`),换个 turn 就拦不住了。
8242
+ * 把这句话交给模型,是这道防线上客户端能出力的那一半。
8243
+ *
8244
+ * @param values - 表单值(应已通过 {@link resolveVideoRatioChoice} 归一化)。
8245
+ * @returns 填进输入框的整段文本。
8246
+ */
8247
+ function buildVideoDraftText(values) {
8248
+ const { hasKeyframes, hasReferences } = mediaPresence(values.media);
8249
+ const ratio = resolveVideoRatioChoice({
8250
+ model: values.model,
8251
+ ...values.ratio === void 0 ? {} : { requested: values.ratio },
8252
+ hasKeyframes,
8253
+ hasReferences
8254
+ }).ratio;
8255
+ const lines = [];
8256
+ lines.push("用 minimax_generate_video 生成视频:");
8257
+ lines.push("prompt=" + values.prompt.trim());
8258
+ lines.push("model=" + values.model);
8259
+ if (ratio !== void 0) lines.push("ratio=" + ratio);
8260
+ if (values.durationSeconds !== void 0) lines.push("durationSeconds=" + values.durationSeconds);
8261
+ if (values.resolution !== void 0) lines.push("resolution=" + values.resolution);
8262
+ const inputImagePath = values.media.inputImagePath?.trim();
8263
+ if (inputImagePath !== void 0 && inputImagePath !== "") lines.push("inputImagePath=" + inputImagePath);
8264
+ const lastFrameImagePath = values.media.lastFrameImagePath?.trim();
8265
+ if (lastFrameImagePath !== void 0 && lastFrameImagePath !== "") lines.push("lastFrameImagePath=" + lastFrameImagePath);
8266
+ const images = (values.media.referenceImagePaths ?? []).map((path) => path.trim()).filter((path) => path !== "");
8267
+ if (images.length > 0) lines.push("referenceImagePaths=" + images.join(","));
8268
+ const videos = (values.media.referenceVideoPaths ?? []).map((path) => path.trim()).filter((path) => path !== "");
8269
+ if (videos.length > 0) lines.push("referenceVideoPaths=" + videos.join(","));
8270
+ const videoDurations = (values.media.referenceVideoDurationSeconds ?? []).filter((value) => Number.isFinite(value) && value > 0);
8271
+ if (videos.length > 0 && videoDurations.length === videos.length) lines.push("referenceVideoDurationSeconds=" + videoDurations.join(","));
8272
+ const audios = (values.media.referenceAudioPaths ?? []).map((path) => path.trim()).filter((path) => path !== "");
8273
+ if (audios.length > 0) lines.push("referenceAudioPaths=" + audios.join(","));
8274
+ if (values.watermark !== void 0) lines.push("aigc_watermark=" + (values.watermark ? "true" : "false"));
8275
+ lines.push("");
8276
+ lines.push("这些参数已经按服务端规则校验过,请原样使用,不要改动或补充");
8277
+ lines.push("提交是计费的终态操作:只调用一次,失败也不重发");
8278
+ return lines.join("\n");
8279
+ }
8280
+ /**
8281
+ * 归一化一份默认参数。
8282
+ *
8283
+ * 存在的理由是**模型换了而时长没跟着换**:用户把默认模型从 H3 改成 Hailuo,原来
8284
+ * 的 8 秒就不合法了。这里当场把它换成新模型的默认值,而不是把非法值一路带到表单
8285
+ * 里,再让用户在弹窗里看到一条自己没设过的错误。
8286
+ *
8287
+ * @param value - 任意解析出来的对象。
8288
+ * @returns 一份自洽的默认参数。
8289
+ */
8290
+ function normalizeVideoDefaults(value) {
8291
+ const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
8292
+ const model = MINIMAX_VIDEO_MODELS.find((known) => known === record.model) ?? "MiniMax-H3";
8293
+ const requestedDuration = typeof record.durationSeconds === "number" && Number.isFinite(record.durationSeconds) ? record.durationSeconds : void 0;
8294
+ const durationSeconds = requestedDuration !== void 0 && durationChoices(model).includes(requestedDuration) ? requestedDuration : defaultDuration(model);
8295
+ const ratio = MINIMAX_VIDEO_RATIOS.find((known) => known === record.ratio);
8296
+ return {
8297
+ model,
8298
+ ...ratio === void 0 ? {} : { ratio },
8299
+ ...durationSeconds === void 0 ? {} : { durationSeconds },
8300
+ watermark: record.watermark === true
8301
+ };
8302
+ }
8303
+ //#endregion
8304
+ //#region src/client/minimax-code/VideoSettingsSections.tsx
8305
+ /**
8306
+ * 设置页里的两个视频分区:**默认参数**与**生成历史**。
8307
+ *
8308
+ * 视觉语言逐条照抄 `MinimaxCodeSection.tsx` 的既有分区(`.dsha-group` /
8309
+ * `.dsha-grouphead` / `.dsha-row` / `.dsha-muted` / `.dsha-btn`),不新增任何
8310
+ * 颜色、字号刻度或字重。
8311
+ *
8312
+ * ## 一次加载只用一个加载态
8313
+ *
8314
+ * 历史是**列表**,所以它的加载态是骨架(`.dshm-video-skeleton` 三条灰条);
8315
+ * 默认参数不是列表,它直接从 localStorage 同步读出来,根本没有加载态。这条是
8316
+ * `dsh-client-ui-ux` 的硬约束:列表用骨架、其他用裸 spinner,且**不写 loading 文案**。
8317
+ *
8318
+ * ## 破坏性操作保留数据
8319
+ *
8320
+ * 读历史失败时**不清空已经拿到的行**:上一次成功的列表留在屏幕上,只在上方加一句
8321
+ * 提示(`videoHistoryUnavailable`)。清空会让一次网络抖动看起来像「历史被删了」。
8322
+ *
8323
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/VideoSettingsSections
8324
+ */
8325
+ /**
8326
+ * 校验一份历史载荷。
8327
+ *
8328
+ * 宿主那一半可能比浏览器这一半旧(同样的「两半不同版本」问题 `line-api.ts` 开头
8329
+ * 那一大段注释在讲),所以每一行都逐字段过一遍,坏行**丢掉**而不是让整个列表渲染成
8330
+ * 一堆 `undefined`。
8331
+ *
8332
+ * @param value - 路由的 `value`。
8333
+ * @returns 规范化后的载荷。
8334
+ */
8335
+ function parseVideoHistoryDto(value) {
8336
+ if (typeof value !== "object" || value === null) return {
8337
+ entries: [],
8338
+ storagePath: "",
8339
+ encryptedAtRest: false
8340
+ };
8341
+ const record = value;
8342
+ const entries = [];
8343
+ if (Array.isArray(record.entries)) for (const raw of record.entries) {
8344
+ if (typeof raw !== "object" || raw === null) continue;
8345
+ const row = raw;
8346
+ if (typeof row.createdAtMs !== "number" || !Number.isFinite(row.createdAtMs)) continue;
8347
+ if (typeof row.model !== "string" || row.model === "") continue;
8348
+ if (typeof row.videoPath !== "string" || row.videoPath === "") continue;
8349
+ if (typeof row.bytes !== "number" || !Number.isFinite(row.bytes)) continue;
8350
+ if (typeof row.taskId !== "string") continue;
8351
+ entries.push({
8352
+ createdAtMs: row.createdAtMs,
8353
+ model: row.model,
8354
+ videoPath: row.videoPath,
8355
+ bytes: row.bytes,
8356
+ taskId: row.taskId,
8357
+ ...typeof row.ratio === "string" && row.ratio !== "" ? { ratio: row.ratio } : {},
8358
+ ...typeof row.durationSeconds === "number" && Number.isFinite(row.durationSeconds) ? { durationSeconds: row.durationSeconds } : {}
8359
+ });
8360
+ }
8361
+ return {
8362
+ entries,
8363
+ storagePath: typeof record.storagePath === "string" ? record.storagePath : "",
8364
+ encryptedAtRest: record.encryptedAtRest === true
8365
+ };
8366
+ }
8367
+ /**
8368
+ * 生成历史分区。
8369
+ *
8370
+ * 数据只来自宿主路由:浏览器侧没有任何事件能知道「刚刚生成了一段视频」——那次调用发
8371
+ * 生在宿主的工具执行里。所以这份列表是**主动去读**的,第一次挂载读一次,用户可以
8372
+ * 手动刷新。
8373
+ */
8374
+ function VideoHistorySection({ t }) {
8375
+ const [data, setData] = (0, react.useState)(null);
8376
+ const [failure, setFailure] = (0, react.useState)(null);
8377
+ const [loading, setLoading] = (0, react.useState)(true);
8378
+ const alive = (0, react.useRef)(true);
8379
+ (0, react.useEffect)(() => () => {
8380
+ alive.current = false;
8381
+ }, []);
8382
+ const load = (0, react.useCallback)(async () => {
8383
+ try {
8384
+ const value = await get$2("/video-history");
8385
+ if (!alive.current) return;
8386
+ setData(parseVideoHistoryDto(value));
8387
+ setFailure(null);
8388
+ } catch (error) {
8389
+ if (!alive.current) return;
8390
+ setFailure(messageOf$2(error));
8391
+ } finally {
8392
+ if (alive.current) setLoading(false);
8393
+ }
8394
+ }, []);
8395
+ (0, react.useEffect)(() => {
8396
+ load();
8397
+ }, [load]);
8398
+ const rows = (0, react.useMemo)(() => data?.entries ?? [], [data]);
8399
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8400
+ className: "dsha-group",
7631
8401
  children: [
8402
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8403
+ className: "dsha-grouphead",
8404
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("videoHistorySection") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8405
+ className: "dsha-btn",
8406
+ disabled: loading,
8407
+ onClick: () => {
8408
+ setLoading(true);
8409
+ load();
8410
+ },
8411
+ children: loading ? t("videoHistoryRefreshing") : t("videoHistoryRefresh")
8412
+ })]
8413
+ }),
7632
8414
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7633
8415
  className: "dsha-muted",
7634
- children: t("pageDesc")
8416
+ children: t("videoHistoryHint")
7635
8417
  }),
7636
- pool !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPoolSection, {
7637
- labels: poolLabels,
7638
- accounts: pool.accounts,
7639
- rotationStrategy: pool.rotationStrategy,
7640
- ...pool.activeAccountId === void 0 ? {} : { activeAccountId: pool.activeAccountId },
7641
- busy,
7642
- onLogin: () => void login(),
7643
- onSetPrimary: (accountId) => void poolAction("set-primary", accountId),
7644
- onDelete: (accountId) => void poolAction("delete", accountId),
7645
- onClearCooldown: (accountId) => void poolAction("clear-cooldown", accountId),
7646
- onRelogin: (accountId) => void poolAction("relogin", accountId),
7647
- onSetStrategy: (strategy) => void setStrategy(strategy),
7648
- showQuota: true
8418
+ failure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8419
+ className: "dsha-notice",
8420
+ children: t("videoHistoryUnavailable")
7649
8421
  }),
7650
- status?.checkin != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7651
- className: "dsha-group",
8422
+ loading && data === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8423
+ className: "dshm-video-skeleton",
8424
+ "aria-hidden": "true",
7652
8425
  children: [
7653
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7654
- className: "dsha-grouphead",
7655
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("dailyCheckin") })
7656
- }),
7657
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7658
- className: "dsha-muted",
7659
- children: t("checkinHint")
7660
- }),
7661
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7662
- className: "dsha-row",
7663
- style: { marginBottom: 8 },
7664
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7665
- className: "dsha-label",
7666
- style: { fontWeight: 600 },
8426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
8427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
8428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
8429
+ ]
8430
+ }) : rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8431
+ className: "dsha-empty",
8432
+ children: t("videoHistoryEmpty")
8433
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8434
+ className: "dshm-video-history",
8435
+ children: rows.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8436
+ className: "dsha-row",
8437
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8438
+ className: "dshm-video-history-path dshm-mono",
8439
+ title: entry.videoPath,
8440
+ children: entry.videoPath
8441
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8442
+ className: "dsha-value dshm-video-history-meta",
8443
+ children: t("videoHistoryMeta", {
8444
+ model: entry.model,
8445
+ ratio: entry.ratio ?? t("videoHistoryNoRatio"),
8446
+ duration: entry.durationSeconds === void 0 ? t("unknown") : formatDuration(entry.durationSeconds),
8447
+ size: formatBytes(entry.bytes)
8448
+ })
8449
+ })]
8450
+ }, entry.taskId + ":" + String(entry.createdAtMs)))
8451
+ }),
8452
+ data !== null && data.storagePath !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8453
+ className: "dsha-muted dshm-quota-note",
8454
+ children: t("videoHistoryStorage", { path: data.storagePath })
8455
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8456
+ className: "dsha-muted",
8457
+ children: data.encryptedAtRest ? t("videoHistoryEncrypted") : t("videoHistoryPlain")
8458
+ })] })
8459
+ ]
8460
+ });
8461
+ }
8462
+ /**
8463
+ * 默认参数分区。
8464
+ *
8465
+ * 改动立即写回,不设保存按钮:这四项都只是 `/video` 表单的起手值,没有「暂存」与
8466
+ * 「已生效」的区别要表达;一个保存按钮反而会让用户以为不点就不生效。
8467
+ */
8468
+ function VideoDefaultsSection({ t, initial, onSave }) {
8469
+ const [defaults, setDefaults] = (0, react.useState)(() => normalizeVideoDefaults(initial));
8470
+ const [notice, setNotice] = (0, react.useState)(null);
8471
+ const apply = (0, react.useCallback)((next) => {
8472
+ const normalized = normalizeVideoDefaults(next);
8473
+ setDefaults(normalized);
8474
+ setNotice(onSave(normalized) ? t("videoDefaultsSaved") : null);
8475
+ }, [onSave, t]);
8476
+ const rule = videoModelRule(defaults.model);
8477
+ const durations = durationChoices(defaults.model);
8478
+ const ratios = MINIMAX_VIDEO_RATIOS.filter((entry) => entry !== "adaptive");
8479
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8480
+ className: "dsha-group",
8481
+ children: [
8482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8483
+ className: "dsha-grouphead",
8484
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("videoDefaultsSection") })
8485
+ }),
8486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8487
+ className: "dsha-muted",
8488
+ children: t("videoDefaultsHint")
8489
+ }),
8490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8491
+ className: "dsha-row",
8492
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8493
+ className: "dsha-label",
8494
+ children: t("videoModelLabel")
8495
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
8496
+ className: "dsha-select",
8497
+ "aria-label": t("videoModelLabel"),
8498
+ value: defaults.model,
8499
+ onChange: (event) => {
8500
+ const model = event.currentTarget.value;
8501
+ apply({
8502
+ ...defaults,
8503
+ model
8504
+ });
8505
+ },
8506
+ children: MINIMAX_VIDEO_MODELS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
8507
+ value: entry,
8508
+ children: entry
8509
+ }, entry))
8510
+ })]
8511
+ }),
8512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8513
+ className: "dsha-row",
8514
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8515
+ className: "dsha-label",
8516
+ children: t("videoDurationLabel")
8517
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
8518
+ className: "dsha-select",
8519
+ "aria-label": t("videoDurationLabel"),
8520
+ value: defaults.durationSeconds === void 0 ? "" : String(defaults.durationSeconds),
8521
+ onChange: (event) => {
8522
+ const value = Number(event.currentTarget.value);
8523
+ apply({
8524
+ ...defaults,
8525
+ durationSeconds: Number.isFinite(value) ? value : void 0
8526
+ });
8527
+ },
8528
+ children: durations.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
8529
+ value: String(value),
8530
+ children: [
8531
+ value,
8532
+ " ",
8533
+ t("videoDurationUnit")
8534
+ ]
8535
+ }, value))
8536
+ })]
8537
+ }),
8538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8539
+ className: "dsha-row",
8540
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8541
+ className: "dsha-label",
8542
+ children: t("videoRatioLabel")
8543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
8544
+ className: "dsha-select",
8545
+ "aria-label": t("videoRatioLabel"),
8546
+ value: defaults.ratio ?? "",
8547
+ onChange: (event) => {
8548
+ const value = event.currentTarget.value;
8549
+ const ratio = ratios.find((entry) => entry === value);
8550
+ apply({
8551
+ ...defaults,
8552
+ ratio
8553
+ });
8554
+ },
8555
+ children: ratios.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
8556
+ value: entry,
8557
+ children: entry
8558
+ }, entry))
8559
+ })]
8560
+ }),
8561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8562
+ className: "dsha-row",
8563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
8564
+ className: "dsha-label",
8565
+ htmlFor: "dshm-video-default-watermark",
8566
+ children: t("videoWatermarkLabel")
8567
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8568
+ id: "dshm-video-default-watermark",
8569
+ type: "checkbox",
8570
+ checked: defaults.watermark,
8571
+ onChange: (event) => {
8572
+ apply({
8573
+ ...defaults,
8574
+ watermark: event.currentTarget.checked
8575
+ });
8576
+ }
8577
+ })]
8578
+ }),
8579
+ rule !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
8580
+ className: "dsha-muted dshm-quota-note",
8581
+ children: [
8582
+ t("videoResolutionLabel"),
8583
+ ": ",
8584
+ defaultResolution(defaults.model) ?? t("unknown")
8585
+ ]
8586
+ }),
8587
+ notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8588
+ className: "dsha-muted",
8589
+ role: "status",
8590
+ children: notice
8591
+ })
8592
+ ]
8593
+ });
8594
+ }
8595
+ //#endregion
8596
+ //#region src/client/minimax-code/video-defaults-store.ts
8597
+ /**
8598
+ * 「视频默认参数」的浏览器侧存储。
8599
+ *
8600
+ * ## 为什么存在浏览器里,而不是 `$DSH_HOME/storages`
8601
+ *
8602
+ * 这份数据是**表单的初始值**,不是凭据、不是配额、也不影响任何一次请求的内容:
8603
+ * 用户没打开设置页时它什么也不做,打开 `/video` 时它只决定三个下拉框的起手值。
8604
+ * 存到宿主就要新开一条路由、一个新文件和一套平台加密后端,而那条路由的每一个
8605
+ * 读者都是同一个浏览器的同一个页面。
8606
+ *
8607
+ * 对照的是**生成历史**:那个必须落在宿主,因为写它的是宿主(视频工具在生成成功
8608
+ * 之后落一行),浏览器根本没有那次事件。默认参数相反,它是纯 UI 偏好。
8609
+ *
8610
+ * ## 读不到就当没有
8611
+ *
8612
+ * localStorage 在三种情况下会抛或返回垃圾:隐私模式下禁用、被别的代码写坏、以及
8613
+ * 存储配额满。三种都退化成「一份出厂默认」,而不是让设置页打不开——这是整个仓库
8614
+ * 对可选依赖的一贯处理(见 `progress-updates/index.ts` 的 `serviceOf`)。
8615
+ *
8616
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-defaults-store
8617
+ */
8618
+ /** localStorage 的键。带命名空间前缀,避免与宿主其它插件的键撞名。 */
8619
+ const VIDEO_DEFAULTS_STORAGE_KEY = "dsh-minimax-code.video-defaults";
8620
+ /**
8621
+ * 取浏览器 localStorage,取不到就是 undefined。
8622
+ *
8623
+ * 走 `globalThis` 而不是直接写 `localStorage`:这段代码也会在 jsdom 与 SSR 风格的
8624
+ * 渲染里跑,那种环境下这个全局可能根本不存在。
8625
+ *
8626
+ * @returns storage,或 undefined。
8627
+ */
8628
+ function browserStorage() {
8629
+ try {
8630
+ const storage = globalThis.localStorage;
8631
+ return typeof storage?.getItem === "function" ? storage : void 0;
8632
+ } catch {
8633
+ return;
8634
+ }
8635
+ }
8636
+ /**
8637
+ * 读一份默认参数。
8638
+ *
8639
+ * 任何解析失败都退化成出厂默认,并且**不写回**:一个被写坏的值留在原处,用户下一次
8640
+ * 保存就覆盖掉它了,中途重建一份没有意义。
8641
+ *
8642
+ * @param storage - 存储;缺省取浏览器 localStorage。
8643
+ * @returns 自洽的默认参数(见 `normalizeVideoDefaults`)。
8644
+ */
8645
+ function readVideoDefaults(storage = browserStorage()) {
8646
+ if (storage === void 0) return normalizeVideoDefaults({});
8647
+ try {
8648
+ const raw = storage.getItem(VIDEO_DEFAULTS_STORAGE_KEY);
8649
+ if (raw === null || raw.trim() === "") return normalizeVideoDefaults({});
8650
+ return normalizeVideoDefaults(JSON.parse(raw));
8651
+ } catch {
8652
+ return normalizeVideoDefaults({});
8653
+ }
8654
+ }
8655
+ /**
8656
+ * 写一份默认参数。
8657
+ *
8658
+ * 写入被拒绝(配额满、隐私模式)时**返回 false 而不是抛**:设置页要把这件事显示成
8659
+ * 一句提示,而不是让一次点击把整页带崩。
8660
+ *
8661
+ * @param defaults - 要保存的值。
8662
+ * @param storage - 存储;缺省取浏览器 localStorage。
8663
+ * @returns 是否真的写进去了。
8664
+ */
8665
+ function writeVideoDefaults(defaults, storage = browserStorage()) {
8666
+ if (storage === void 0) return false;
8667
+ try {
8668
+ storage.setItem(VIDEO_DEFAULTS_STORAGE_KEY, JSON.stringify(defaults));
8669
+ return true;
8670
+ } catch {
8671
+ return false;
8672
+ }
8673
+ }
8674
+ //#endregion
8675
+ //#region src/client/minimax-code/MinimaxCodeSection.tsx
8676
+ /**
8677
+ * The MiniMax Code settings card.
8678
+ *
8679
+ * Markup only: every piece of state, every effect and every request this card
8680
+ * makes lives in 'useMinimaxCodeSection.ts', which returns one named shape the
8681
+ * JSX below reads. The two files are the split of a single 915-line component —
8682
+ * the behaviour is unchanged, and this one should stay render-shaped.
8683
+ */
8684
+ /** Human label per thinking mode, so the card states what the model does. */
8685
+ function thinkingLabel(mode, t) {
8686
+ if (mode === "always-on") return t("thinkingAlwaysOn");
8687
+ if (mode === "forced-effort") return t("thinkingForced");
8688
+ return t("thinkingToggle");
8689
+ }
8690
+ /**
8691
+ * Minutes still left on the access token, or null once it has run out.
8692
+ *
8693
+ * MiniMax Code's token lives about ONE HOUR — measured, not the day an earlier
8694
+ * note assumed — so minutes are the unit the card reports: hours would turn the
8695
+ * whole remaining life into a single digit and hide the point at which the Host
8696
+ * renews the credential. One decimal is kept, so a countdown that is running
8697
+ * out reads as 0.4 rather than as nothing; below one minute the token is
8698
+ * treated as already gone.
8699
+ */
8700
+ function remainingMinutes(expiresAtMs) {
8701
+ if (expiresAtMs === void 0 || !Number.isFinite(expiresAtMs) || expiresAtMs <= 0) return null;
8702
+ const minutes = (expiresAtMs - Date.now()) / 6e4;
8703
+ if (minutes < 1) return null;
8704
+ return minutes.toFixed(1);
8705
+ }
8706
+ function MinimaxCodeSection({ onModelChange }) {
8707
+ const { state, derived, actions } = useMinimaxCodeSection({ onModelChange });
8708
+ const { status, loading, busy, error, flow, copied, connectionNotice, contextDrafts, savingModel, pool, resetCards, resetNotice, selectedResetCardId } = state;
8709
+ const { authenticated, account, quota, models, contextModels, overrideCount, poolLabels, resetCardList, resetCardFlows } = derived;
8710
+ const { login, cancelLogin, copyCode, logout, poolAction, setStrategy, toggleModel, setAllModels, setCheckinEnabled, runCheckinNow, updateEffort, saveContextWindow, resetContextWindow, resetAllContextWindows, refreshQuota, refreshResetCards, selectResetCard, useResetCard, testConnection, updateContextDraft } = actions;
8711
+ const t = fallbackTranslate$4;
8712
+ const videoDefaultsRef = react.default.useRef(null);
8713
+ if (videoDefaultsRef.current === null) videoDefaultsRef.current = readVideoDefaults();
8714
+ const saveVideoDefaults = react.default.useCallback((next) => {
8715
+ if (!writeVideoDefaults(next)) return false;
8716
+ videoDefaultsRef.current = next;
8717
+ return true;
8718
+ }, []);
8719
+ if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8720
+ className: "dsha-page",
8721
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8722
+ className: "dsha-empty",
8723
+ children: t("loading")
8724
+ })
8725
+ });
8726
+ const minutes = remainingMinutes(account?.expiresAtMs);
8727
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8728
+ className: "dsha-page",
8729
+ children: [
8730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8731
+ className: "dsha-muted",
8732
+ children: t("pageDesc")
8733
+ }),
8734
+ pool !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPoolSection, {
8735
+ labels: poolLabels,
8736
+ accounts: pool.accounts,
8737
+ rotationStrategy: pool.rotationStrategy,
8738
+ ...pool.activeAccountId === void 0 ? {} : { activeAccountId: pool.activeAccountId },
8739
+ busy,
8740
+ onLogin: () => void login(),
8741
+ onSetPrimary: (accountId) => void poolAction("set-primary", accountId),
8742
+ onDelete: (accountId) => void poolAction("delete", accountId),
8743
+ onClearCooldown: (accountId) => void poolAction("clear-cooldown", accountId),
8744
+ onRelogin: (accountId) => void poolAction("relogin", accountId),
8745
+ onSetStrategy: (strategy) => void setStrategy(strategy),
8746
+ showQuota: true
8747
+ }),
8748
+ status?.checkin != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8749
+ className: "dsha-group",
8750
+ children: [
8751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8752
+ className: "dsha-grouphead",
8753
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("dailyCheckin") })
8754
+ }),
8755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8756
+ className: "dsha-muted",
8757
+ children: t("checkinHint")
8758
+ }),
8759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8760
+ className: "dsha-row",
8761
+ style: { marginBottom: 8 },
8762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8763
+ className: "dsha-label",
8764
+ style: { fontWeight: 600 },
7667
8765
  children: t("checkinAuto")
7668
8766
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7669
8767
  type: "checkbox",
@@ -7748,7 +8846,7 @@ window.__ModuleLoader__.load({
7748
8846
  children: t("expiresAt")
7749
8847
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7750
8848
  className: "dsha-value",
7751
- children: [formatDate$1(account?.expiresAtMs), hours === null ? ` · ${t("expiresExpired")}` : ` · ${t("expiresRemaining").replace("{hours}", hours)}`]
8849
+ children: [formatDate$1(account?.expiresAtMs), minutes === null ? ` · ${t("expiresExpired")}` : ` · ${t("expiresRemaining").replace("{minutes}", minutes)}`]
7752
8850
  })]
7753
8851
  })
7754
8852
  ] }),
@@ -8092,6 +9190,12 @@ window.__ModuleLoader__.load({
8092
9190
  }, `${entry.cardId}:${entry.createdAtMs}:${index}`))
8093
9191
  ]
8094
9192
  }),
9193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoDefaultsSection, {
9194
+ t,
9195
+ initial: videoDefaultsRef.current,
9196
+ onSave: saveVideoDefaults
9197
+ }),
9198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoHistorySection, { t }),
8095
9199
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8096
9200
  className: "dsha-error",
8097
9201
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: error })
@@ -8382,7 +9486,7 @@ window.__ModuleLoader__.load({
8382
9486
  /** 登录状态机的轮询节奏。真实超时与间隔由 host 拥有,卡片只是跟随。 */
8383
9487
  const LOGIN_POLL_INTERVAL_MS$1 = 2e3;
8384
9488
  /** 本卡片的字典;身份稳定,依赖它的 effect 才不会每次渲染重建。 */
8385
- const fallbackTranslate$2 = (key) => zh$5[key];
9489
+ const fallbackTranslate$3 = (key) => zh$5[key];
8386
9490
  /**
8387
9491
  * 共享账号池卡片需要的标签集,按本卡片的字典逐键查出来。
8388
9492
  *
@@ -8447,7 +9551,7 @@ window.__ModuleLoader__.load({
8447
9551
  });
8448
9552
  }
8449
9553
  function useGlmCodingPlanSection({ onModelChange } = {}) {
8450
- const t = fallbackTranslate$2;
9554
+ const t = fallbackTranslate$3;
8451
9555
  const [status, setStatus] = (0, react.useState)(null);
8452
9556
  const [loading, setLoading] = (0, react.useState)(true);
8453
9557
  const [busy, setBusy] = (0, react.useState)(null);
@@ -11536,7 +12640,7 @@ window.__ModuleLoader__.load({
11536
12640
  /** 登录状态机的轮询节奏。真实超时与间隔由 host 拥有,卡片只是跟随。 */
11537
12641
  const LOGIN_POLL_INTERVAL_MS = 2e3;
11538
12642
  /** 本卡片的字典;身份稳定,依赖它的 effect 才不会每次渲染重建。 */
11539
- const fallbackTranslate$1 = (key) => zh$2[key];
12643
+ const fallbackTranslate$2 = (key) => zh$2[key];
11540
12644
  /**
11541
12645
  * 共享账号池卡片需要的标签集,按本卡片的字典逐键查出来。
11542
12646
  *
@@ -11666,7 +12770,7 @@ window.__ModuleLoader__.load({
11666
12770
  /** 写侧的线路常量;卡片用它渲染说明,而不是渲染一条永远为 0 的进度条。 */
11667
12771
  const CACHE_WRITE_NOTE = STEP_CODE_CACHE_WRITE_NOTE;
11668
12772
  function useStepCodeSection({ onModelChange } = {}) {
11669
- const t = fallbackTranslate$1;
12773
+ const t = fallbackTranslate$2;
11670
12774
  const [status, setStatus] = (0, react.useState)(null);
11671
12775
  const [loading, setLoading] = (0, react.useState)(true);
11672
12776
  const [busy, setBusy] = (0, react.useState)(null);
@@ -14470,79 +15574,733 @@ window.__ModuleLoader__.load({
14470
15574
  });
14471
15575
  }
14472
15576
  //#endregion
14473
- //#region src/client/minimax-code/styles.ts
14474
- const STYLE_ID$3 = "dsh-minimax-code-settings-style";
15577
+ //#region src/client/minimax-code/video-media.tsx
14475
15578
  /**
14476
- * MiniMax Code-only additions to the shared settings design system.
15579
+ * 视频字节 → `<video>` 的那一段:对象 URL 的生命周期,以及两个界面共用的
15580
+ * 「视频台」外框。
14477
15581
  *
14478
- * The base control styles (.dsha-page, .dsha-group, .dsha-row, .dsha-models,
14479
- * the device-code box, the meter, the composer badge, ...) come from the
14480
- * stylesheet the sibling routes install, in the same order the client installs
14481
- * them, so this file declares only what stays unique to this card: the mono
14482
- * credential path and the model-name list.
15582
+ * ## 为什么对象 URL 必须由这里管
15583
+ *
15584
+ * `URL.createObjectURL` 生成的地址把整段字节钉在内存里,直到显式
15585
+ * `revokeObjectURL`。视频动辄几十 MB,漏一次就是一次长期泄漏;而 `<video>` 一旦
15586
+ * 拿到 `src` 又会自己缓存。所以生命周期只写在 `useObjectUrl` 里一处:
15587
+ * 字节变了就换新地址并撤旧的,组件卸载就撤当前的。
15588
+ *
15589
+ * ## 为什么两个界面共用一个外框
15590
+ *
15591
+ * 对话流卡片只有约 520px 宽,侧边栏是全尺寸,两者尺寸差一倍——但观众看到的应该
15592
+ * 是**同一个东西的两个尺寸**,不是两套设计。所以 16:9 的视频台只定义一次
15593
+ * (`.dshm-video-stage`),两处都渲染 {@link VideoStage},差别只剩容器的宽度。
15594
+ *
15595
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-media
14483
15596
  */
14484
- function installMinimaxCodeStyles() {
14485
- installPluginStyle("minimax-code", `
14486
- .dshm-mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}
14487
- .dshm-model-list{display:flex;flex-wrap:wrap;gap:7px;padding-top:8px}
14488
- .dshm-model-list code{background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;padding:5px 8px;white-space:nowrap}
14489
- .dshm-quota-note{margin-top:12px}
14490
- .dshm-reset-list{display:flex;flex-direction:column;gap:6px;padding-top:8px}
14491
- .dshm-reset-row{align-items:center;justify-content:space-between}
14492
- .dshm-reset-pick{background:none;border:0.5px solid var(--dsw-alias-border-l2);cursor:pointer;padding:4px 8px}
14493
- .dshm-reset-pick[aria-pressed="true"]{border-color:var(--dsw-alias-border-focus,var(--dsw-alias-border-l3));background:var(--dsw-alias-bg-layer-2)}
14494
- `, STYLE_ID$3);
14495
- }
14496
- //#endregion
14497
- //#region src/client/workbuddy/WorkBuddyComposerQuota.tsx
14498
- const api = createLineApi("/workbuddy/api", "WorkBuddy");
14499
15597
  /**
14500
- * Pick the most meaningful number for the badge.
15598
+ * 把一段字节变成一个可播放的对象 URL,并在字节变化或组件卸载时撤销它。
14501
15599
  *
14502
- * A bounded allowance is what a user spends down, so it wins over a bare credit
14503
- * balance; the balance shows only when no percentage is reported. The billing
14504
- * payload reports both a cycle meter and a package meter, and the cycle meter
14505
- * is the one that actually resets, so it is preferred when both exist.
15600
+ * 失败(浏览器不支持 Blob URL、或构造抛错)时返回 `failed`,由调用方把占位块
15601
+ * 的文字换成失败措辞——**不抛异常**:一块视频读不出来,不该把整段对话流带崩。
15602
+ *
15603
+ * @param data - 视频字节;`undefined` 表示还没拿到。
15604
+ * @param mediaType - Blob 的 MIME 类型,如 `video/mp4`。
15605
+ * @returns 就绪时的 `url`,或 `failed`,或两者皆无(仍在等字节)。
14506
15606
  */
14507
- function selectBadgeFacts$2(status) {
14508
- const quota = status?.quota;
14509
- if (quota === null || quota === void 0) return {
14510
- text: "—",
14511
- tooltip: "WorkBuddy quota unavailable — click to refresh",
14512
- level: "normal"
14513
- };
14514
- const meter = (quota.meters ?? []).find((candidate) => candidate.id === "cycle" && candidate.remainingFraction !== null) ?? (quota.meters ?? []).find((candidate) => candidate.remainingFraction !== null);
14515
- if (meter !== void 0 && meter.remainingFraction !== null) {
14516
- const remaining = Math.round(meter.remainingFraction * 100);
14517
- return {
14518
- text: `${remaining}%`,
14519
- tooltip: `[WorkBuddy] ${meter.label}: ${remaining}% left`,
14520
- level: quotaLevel(remaining)
15607
+ function useObjectUrl(data, mediaType) {
15608
+ const [source, setSource] = (0, react.useState)({
15609
+ url: null,
15610
+ failed: false
15611
+ });
15612
+ (0, react.useEffect)(() => {
15613
+ if (data === void 0) {
15614
+ setSource({
15615
+ url: null,
15616
+ failed: false
15617
+ });
15618
+ return;
15619
+ }
15620
+ let url;
15621
+ try {
15622
+ url = URL.createObjectURL(new Blob([data], { type: mediaType }));
15623
+ } catch {
15624
+ setSource({
15625
+ url: null,
15626
+ failed: true
15627
+ });
15628
+ return;
15629
+ }
15630
+ setSource({
15631
+ url,
15632
+ failed: false
15633
+ });
15634
+ return () => {
15635
+ URL.revokeObjectURL(url);
14521
15636
  };
14522
- }
14523
- if (quota.remainingCredits !== null) return {
14524
- text: String(quota.remainingCredits),
14525
- tooltip: `[WorkBuddy] Remaining credits: ${quota.remainingCredits}`,
14526
- level: "normal"
14527
- };
14528
- return {
14529
- text: "—",
14530
- tooltip: "WorkBuddy quota unavailable — click to refresh",
14531
- level: "normal"
14532
- };
15637
+ }, [data, mediaType]);
15638
+ return source;
14533
15639
  }
14534
- function WorkBuddyComposerQuota({ directory, loadModelDirectory, t }) {
14535
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerQuotaBadge, {
14536
- directory,
14537
- loadModelDirectory,
14538
- providerId: "workbuddy-subscription",
14539
- readStatus: (refresh) => refresh ? api.post("/quota") : api.get("/status"),
14540
- selectFacts: selectBadgeFacts$2,
14541
- label: t("composerLabel")
15640
+ /**
15641
+ * 16:9 的视频台:深色底、发丝线、letterbox。
15642
+ *
15643
+ * 就绪时是 `<video controls preload="metadata">`(`preload="metadata"` 让浏览器
15644
+ * 先拿时长与首帧,而不是立刻开始整段下载);否则是同一个外框的虚线形态,
15645
+ * 里面一行 tertiary 说明。**失败时外框不变**——数据仍在屏幕上,只是它读不出来。
15646
+ *
15647
+ * @param props - 对象 URL、占位文案与无障碍名。
15648
+ * @returns 视频台。
15649
+ */
15650
+ function VideoStage({ url, placeholder, label }) {
15651
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15652
+ className: "dshm-video-stage",
15653
+ "data-state": url === null ? "empty" : "ready",
15654
+ children: url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15655
+ className: "dshm-video-placeholder",
15656
+ "data-tone": placeholder.tone,
15657
+ children: placeholder.text
15658
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
15659
+ className: "dshm-video-el",
15660
+ src: url,
15661
+ controls: true,
15662
+ preload: "metadata",
15663
+ "aria-label": label
15664
+ })
14542
15665
  });
14543
15666
  }
14544
15667
  //#endregion
14545
- //#region src/client/workbuddy/styles.ts
15668
+ //#region src/client/minimax-code/MinimaxVideoDocumentBody.tsx
15669
+ /**
15670
+ * 右侧栏里的视频渲染器:把工作区里的一个 mp4/webm/mov/m4v 播成一段视频。
15671
+ *
15672
+ * ## 它是怎么被选中的
15673
+ *
15674
+ * 两个注册各管一半,缺一不可(形状照抄宿主
15675
+ * `ui-sidebar-documentpreview/src/client/image/index.ts`):
15676
+ *
15677
+ * 1. `ctx.documentPreviews.register(…)` 声明「mp4 等后缀归我」;
15678
+ * `priority: 'extension'` 让外部实现排在内置实现之前
15679
+ * (`document/registry.ts` 的排序规则:档位 → 后缀长度 → 注册先后)。
15680
+ * 2. `sidebar.right.tab.document` 的 keyed 槽用**同一个 id** 注册正文。
15681
+ * 侧边栏按地址选渲染器,选中谁就渲染谁的 keyed 槽。
15682
+ *
15683
+ * `binaryExtensions` 必须与 `extensions` 是同一组后缀:`register` 会拒绝
15684
+ * `binaryExtensions` 里出现在 `extensions` 之外的项。
15685
+ *
15686
+ * ## 字节从哪来
15687
+ *
15688
+ * 宿主的内置图像渲染器不自己取字节——文档预览插件读好了通过 `content` 发下来。
15689
+ * 我们的定义是**后注册的外部实现**,拿不到那条 `bytes-complete` 通路,
15690
+ * 所以按任务要求自己取:`ctx.remote.workspaceFiles.readBytes`,与宿主
15691
+ * `ReadDocumentBytes` 调的是同一个远端。会话 id 取自页签的
15692
+ * `resourceAddress`(`dsh-resource://file/session/<id>/<path>`),路径就是地址里
15693
+ * 那一段——它已经按该会话的工作区根解析过。
15694
+ *
15695
+ * ## 为什么和对话流卡片长得一样
15696
+ *
15697
+ * 两处共用 `.dshm-video-stage` 与 `useObjectUrl`:视频台是**同一个东西的两个尺寸**,
15698
+ * 差别只有容器宽度。侧边栏多一行安静的元数据(文件名 + 体积),因为这里的视频台
15699
+ * 是全尺寸的,下面空着反而显眼。
15700
+ *
15701
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/MinimaxVideoDocumentBody
15702
+ */
15703
+ /** 兜底翻译:只在没有 locale 座位时用到。 */
15704
+ function translate$1(key) {
15705
+ return zh$6[key] ?? key;
15706
+ }
15707
+ /**
15708
+ * 一个文件地址里的会话与路径。
15709
+ *
15710
+ * 只认 `session` 作用域:`absolute` 地址没有会话,取字节通道需要会话身份,
15711
+ * 所以那种地址这里当作「没有可读的文件」,由视频台画占位块。
15712
+ * @param address - `dsh-resource://file/…` 地址。
15713
+ * @returns 会话 id 与路径;不是会话地址时 undefined。
15714
+ */
15715
+ function sessionFileOf(address) {
15716
+ if (address === void 0 || !address.startsWith("dsh-resource://file/")) return void 0;
15717
+ const end = address.search(/[?#]/);
15718
+ const [scope, id, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
15719
+ if (scope !== "session" || id === void 0 || id === "" || rest.length === 0) return void 0;
15720
+ try {
15721
+ return {
15722
+ sessionId: decodeURIComponent(id),
15723
+ path: rest.map(decodeURIComponent).join("/")
15724
+ };
15725
+ } catch {
15726
+ return;
15727
+ }
15728
+ }
15729
+ /**
15730
+ * 侧边栏视频正文。
15731
+ * @param props - 地址、会话、取字节通道与文案。
15732
+ * @returns 16:9 视频台与下方一行元数据。
15733
+ */
15734
+ function MinimaxVideoDocumentBody(props) {
15735
+ const t = props.t ?? translate$1;
15736
+ const file = sessionFileOf(props.resourceAddress);
15737
+ const sessionId = props.sessionId ?? file?.sessionId;
15738
+ const path = file?.path;
15739
+ const data = useDocumentBytes({
15740
+ readVideo: props.readVideo,
15741
+ sessionId,
15742
+ path
15743
+ });
15744
+ const mediaType = mediaTypeOf(path);
15745
+ const source = useObjectUrl(data.kind === "ready" ? data.data : void 0, mediaType);
15746
+ const placeholder = placeholderFor$1(data, t);
15747
+ const bytes = data.kind === "ready" ? data.data.byteLength : void 0;
15748
+ const name = path === void 0 ? void 0 : fileName(path);
15749
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15750
+ className: "dshm-video-document",
15751
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoStage, {
15752
+ url: source.failed ? null : source.url,
15753
+ placeholder,
15754
+ label: name ?? t("videoTitle")
15755
+ }), name === void 0 || bytes === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15756
+ className: "dshm-video-doc-meta",
15757
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15758
+ className: "dshm-video-doc-name",
15759
+ title: name,
15760
+ children: name
15761
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15762
+ className: "dshm-video-doc-size",
15763
+ children: t("videoSize").replace("{size}", formatBytes(bytes))
15764
+ })]
15765
+ })]
15766
+ });
15767
+ }
15768
+ /**
15769
+ * 取一次整段视频字节。
15770
+ *
15771
+ * 没有「加载中」这一态:视频台在拿到字节前本来就是虚线占位,多一个中间态只是
15772
+ * 多一次重渲染。卸载或换文件时 abort。
15773
+ * @param input - 通道、会话与路径。
15774
+ * @returns 当前字节状态。
15775
+ */
15776
+ function useDocumentBytes(input) {
15777
+ const { readVideo, sessionId, path } = input;
15778
+ const [state, setState] = (0, react.useState)({ kind: "idle" });
15779
+ (0, react.useEffect)(() => {
15780
+ if (readVideo === void 0 || sessionId === void 0 || path === void 0) {
15781
+ setState({ kind: "idle" });
15782
+ return;
15783
+ }
15784
+ const controller = new AbortController();
15785
+ let live = true;
15786
+ readVideo(sessionId, path, controller.signal).then((data) => {
15787
+ if (live) setState({
15788
+ kind: "ready",
15789
+ data
15790
+ });
15791
+ }, (error) => {
15792
+ if (live) setState({
15793
+ kind: "failed",
15794
+ reason: videoReadFailureOf(error),
15795
+ limit: videoReadLimitOf(error)
15796
+ });
15797
+ });
15798
+ return () => {
15799
+ live = false;
15800
+ controller.abort();
15801
+ };
15802
+ }, [
15803
+ readVideo,
15804
+ sessionId,
15805
+ path
15806
+ ]);
15807
+ return state;
15808
+ }
15809
+ /**
15810
+ * 视频台此刻该说什么。
15811
+ *
15812
+ * 与对话流卡片的同名字面同一套措辞,但这里只有字节这一条信息:侧边栏的地址就是
15813
+ * 用户点开的那个文件,没有「还在生成」这个阶段。**中文最多两句、结尾不加句号**
15814
+ * (DSH 文案规则)。
15815
+ *
15816
+ * `too-large` 是唯一一个「文件没坏、只是不交给浏览器」的失败:那时说成「可能被
15817
+ * 移动或删除」是错的,所以单独一句,并带上宿主报的上限。
15818
+ *
15819
+ * @param data - 当前字节状态。
15820
+ * @param t - 命名空间化的翻译。
15821
+ * @returns 占位文案与色调。
15822
+ */
15823
+ function placeholderFor$1(data, t) {
15824
+ if (data.kind === "failed") {
15825
+ if (data.reason === "too-large") return {
15826
+ text: videoTooLargeText((key) => t(key), data.limit),
15827
+ tone: "danger"
15828
+ };
15829
+ return {
15830
+ text: t("videoUnreadable"),
15831
+ tone: "danger"
15832
+ };
15833
+ }
15834
+ if (data.kind === "ready") return {
15835
+ text: t("videoUnplayable"),
15836
+ tone: "danger"
15837
+ };
15838
+ return {
15839
+ text: t("videoReading"),
15840
+ tone: "quiet"
15841
+ };
15842
+ }
15843
+ /**
15844
+ * 后缀 → MIME。
15845
+ *
15846
+ * 只覆盖本实现认领的四个后缀,所以查表就是穷举:不认识的进来是编程错误,
15847
+ * 退回 `video/mp4` 而不是抛——一个后缀不该让侧边栏白屏。
15848
+ * @param path - 文件路径。
15849
+ * @returns 该后缀的 MIME 类型。
15850
+ */
15851
+ function mediaTypeOf(path) {
15852
+ const extension = path === void 0 ? "" : path.slice(path.lastIndexOf(".") + 1).toLowerCase();
15853
+ if (extension === "webm") return "video/webm";
15854
+ if (extension === "mov") return "video/quicktime";
15855
+ if (extension === "m4v") return "video/x-m4v";
15856
+ return "video/mp4";
15857
+ }
15858
+ /**
15859
+ * 路径里的文件名。
15860
+ * @param path - 文件路径。
15861
+ * @returns 最后一个分隔符之后的部分。
15862
+ */
15863
+ function fileName(path) {
15864
+ const normalized = path.replaceAll("\\", "/");
15865
+ const name = normalized.slice(normalized.lastIndexOf("/") + 1);
15866
+ return name === "" ? normalized : name;
15867
+ }
15868
+ //#endregion
15869
+ //#region src/client/minimax-code/MinimaxVideoToolView.tsx
15870
+ /**
15871
+ * `minimax_generate_video` 在对话流里的工具卡片。
15872
+ *
15873
+ * ## 解剖结构照抄同一仓库的生图卡片
15874
+ *
15875
+ * 状态点 + 标题 + 摘要 + 媒体区,与 `src/client/CodexImageToolView.tsx` 一字不差的
15876
+ * 骨架(`.dsh-codex-image-tool` 那一族)。同一个对话里两张卡片是**同一种东西**:
15877
+ * 一条发丝线、8px 圆角、13px 标题、8px 状态点。差别只在媒体区——那边是图片格,
15878
+ * 这边是 16:9 的视频台。
15879
+ *
15880
+ * ## 三个阶段都要画
15881
+ *
15882
+ * `tool.call.toolview` 交来的 `block` 是阶段相关的:
15883
+ * - `phase: 'preparing'` —— 参数还在流式生成,结果无从谈起;
15884
+ * - `phase: 'start'` —— 参数完整,上游还在生成(**进行中**,视频台空着);
15885
+ * - `'kind' in block` —— 已结算,`isError` 决定成功还是失败。
15886
+ *
15887
+ * 判断写法与生图卡片同款(`'kind' in block`),因为要面对的正是同一批阶段形状。
15888
+ *
15889
+ * ## 数据只来自 meta
15890
+ *
15891
+ * 视频路径在 `ToolResultNode.meta`(宿主写的 `output.presentationMeta`)里。工具的
15892
+ * 结构化 `value` 不随会话日志持久化,所以这里**不读 `value`**,也不读结果正文
15893
+ * 里的自然语言——读不到 meta 就画占位块,卡片照常显示标题与状态。
15894
+ *
15895
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/MinimaxVideoToolView
15896
+ */
15897
+ /** 兜底翻译:只在没有 locale 座位时用到,走同一份 zh 字典。 */
15898
+ function translate(key) {
15899
+ return zh$6[key] ?? key;
15900
+ }
15901
+ /**
15902
+ * 一张视频工具卡片。
15903
+ * @param props - 阶段块、工作区根、打开/取字节通道与文案。
15904
+ * @returns 卡片。
15905
+ */
15906
+ function MinimaxVideoToolView(props) {
15907
+ const { block, cwd, openFile, readVideo, sessionId } = props;
15908
+ const t = props.t ?? translate;
15909
+ const settled = block.kind !== void 0;
15910
+ const isError = settled && block.isError === true;
15911
+ const meta = settled && !isError ? readVideoMeta(block.meta) : void 0;
15912
+ const summary = promptFromBlock(block) ?? textSummary(block.content);
15913
+ const label = isError ? t("videoToolFailed") : settled ? t("videoToolDone") : t("videoToolRunning");
15914
+ const absolute = meta === void 0 ? void 0 : absoluteVideoPath(cwd, meta.videoPath);
15915
+ const data = useVideoBytes({
15916
+ readVideo,
15917
+ sessionId,
15918
+ path: absolute,
15919
+ enabled: meta !== void 0
15920
+ });
15921
+ const source = useObjectUrl(data.kind === "ready" ? data.data : void 0, meta?.mediaType ?? "video/mp4");
15922
+ const placeholder = placeholderFor({
15923
+ settled,
15924
+ isError,
15925
+ meta,
15926
+ data,
15927
+ source,
15928
+ t
15929
+ });
15930
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15931
+ className: "dshm-video-tool",
15932
+ "data-state": isError ? "error" : settled ? "done" : "running",
15933
+ children: [
15934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15935
+ className: "dshm-video-head",
15936
+ children: [
15937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15938
+ className: "dshm-video-dot",
15939
+ "aria-hidden": "true"
15940
+ }),
15941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15942
+ className: "dshm-video-title",
15943
+ children: label
15944
+ }),
15945
+ summary === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15946
+ className: "dshm-video-summary",
15947
+ children: summary
15948
+ })
15949
+ ]
15950
+ }),
15951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoStage, {
15952
+ url: source.failed ? null : source.url,
15953
+ placeholder,
15954
+ label: meta === void 0 ? t("videoToolTitle") : fileNameOf(meta.videoPath)
15955
+ }),
15956
+ meta === void 0 || absolute === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15957
+ className: "dshm-video-foot",
15958
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15959
+ type: "button",
15960
+ className: "dshm-video-path",
15961
+ onClick: () => {
15962
+ openFile?.(absolute, { line: 1 });
15963
+ },
15964
+ title: absolute,
15965
+ children: meta.videoPath
15966
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15967
+ className: "dshm-video-meta",
15968
+ children: [meta.durationSeconds === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatDuration(meta.durationSeconds) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("videoSize").replace("{size}", formatBytes(meta.bytes)) })]
15969
+ })]
15970
+ })
15971
+ ]
15972
+ });
15973
+ }
15974
+ /**
15975
+ * 取一次视频字节。
15976
+ *
15977
+ * `enabled` 为假、或通道缺席时什么都不做——一张没有 meta 的卡片不该发出请求。
15978
+ * 卸载或换文件时 abort:面板关掉之后不该还有字节在路上。
15979
+ *
15980
+ * @param input - 通道、会话、路径与开关。
15981
+ * @returns 当前字节状态。
15982
+ */
15983
+ function useVideoBytes(input) {
15984
+ const { readVideo, sessionId, path, enabled } = input;
15985
+ const [state, setState] = (0, react.useState)({ kind: "idle" });
15986
+ (0, react.useEffect)(() => {
15987
+ if (!enabled || path === void 0 || readVideo === void 0 || sessionId === void 0) {
15988
+ setState({ kind: "idle" });
15989
+ return;
15990
+ }
15991
+ const controller = new AbortController();
15992
+ let live = true;
15993
+ setState({ kind: "loading" });
15994
+ readVideo(sessionId, path, controller.signal).then((data) => {
15995
+ if (live) setState({
15996
+ kind: "ready",
15997
+ data
15998
+ });
15999
+ }, (error) => {
16000
+ if (live) setState({
16001
+ kind: "failed",
16002
+ reason: videoReadFailureOf(error),
16003
+ limit: videoReadLimitOf(error)
16004
+ });
16005
+ });
16006
+ return () => {
16007
+ live = false;
16008
+ controller.abort();
16009
+ };
16010
+ }, [
16011
+ readVideo,
16012
+ sessionId,
16013
+ path,
16014
+ enabled
16015
+ ]);
16016
+ return state;
16017
+ }
16018
+ /**
16019
+ * 视频台此刻该说什么。
16020
+ *
16021
+ * 四句话按「谁的问题」排:读不出来是产物的问题,通道缺席是本插件没接上,
16022
+ * 还在生成是时间的问题。**中文最多两句、结尾不加句号**(DSH 文案规则),
16023
+ * 英文同理用句点收尾。
16024
+ *
16025
+ * `too-large` 单独一句:那时文件好好的,只是宿主的整文件上限没把它交出来,
16026
+ * 说成「可能被移动或删除」是在冤枉它。
16027
+ *
16028
+ * @param input - 阶段、meta、字节状态与对象 URL 状态。
16029
+ * @returns 占位文案与色调。
16030
+ */
16031
+ function placeholderFor(input) {
16032
+ const { settled, isError, meta, data, source, t } = input;
16033
+ if (isError) return {
16034
+ text: t("videoFailed"),
16035
+ tone: "danger"
16036
+ };
16037
+ if (!settled) return {
16038
+ text: t("videoGenerating"),
16039
+ tone: "quiet"
16040
+ };
16041
+ if (meta === void 0) return {
16042
+ text: t("videoNoMeta"),
16043
+ tone: "danger"
16044
+ };
16045
+ if (source.failed) return {
16046
+ text: t("videoUnplayable"),
16047
+ tone: "danger"
16048
+ };
16049
+ if (data.kind === "failed") {
16050
+ if (data.reason === "too-large") return {
16051
+ text: videoTooLargeText((key) => t(key), data.limit),
16052
+ tone: "danger"
16053
+ };
16054
+ return {
16055
+ text: t("videoUnreadable"),
16056
+ tone: "danger"
16057
+ };
16058
+ }
16059
+ if (data.kind === "idle") return {
16060
+ text: t("videoChannelOff"),
16061
+ tone: "danger"
16062
+ };
16063
+ return {
16064
+ text: t("videoReading"),
16065
+ tone: "quiet"
16066
+ };
16067
+ }
16068
+ /**
16069
+ * 从参数里读提示词。
16070
+ *
16071
+ * 三个阶段的读法不同:流式阶段只有前缀视图(`args.text`),完整阶段才有
16072
+ * `argsRaw`。与生图卡片的 `promptFromBlock` 同款。
16073
+ * @param block - 当前阶段块。
16074
+ * @returns 提示词,或 `null`。
16075
+ */
16076
+ function promptFromBlock(block) {
16077
+ const partial = partialPrompt(block);
16078
+ if (partial !== null) return partial;
16079
+ const raw = block.argsRaw;
16080
+ if (typeof raw !== "string" || raw.trim() === "") return null;
16081
+ try {
16082
+ const value = JSON.parse(raw);
16083
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.prompt === "string" ? value.prompt : null;
16084
+ } catch {
16085
+ return null;
16086
+ }
16087
+ }
16088
+ /**
16089
+ * 流式阶段的提示词前缀。
16090
+ * @param block - 当前阶段块。
16091
+ * @returns 已到达的提示词前缀,或 `null`。
16092
+ */
16093
+ function partialPrompt(block) {
16094
+ const read = block.args?.text;
16095
+ if (typeof read !== "function") return null;
16096
+ try {
16097
+ const prompt = read.call(block.args, "prompt");
16098
+ return typeof prompt === "string" && prompt !== "" ? prompt : null;
16099
+ } catch {
16100
+ return null;
16101
+ }
16102
+ }
16103
+ /**
16104
+ * 结果正文的第一行文字,当作摘要。
16105
+ * @param blocks - 结算结果的内容块。
16106
+ * @returns 一行摘要,或 `null`。
16107
+ */
16108
+ function textSummary(blocks) {
16109
+ if (blocks === void 0) return null;
16110
+ const text = blocks.map((content) => content.type === "text" ? content.text : "").filter((line) => typeof line === "string" && line.trim() !== "").join(" ").replace(/s+/g, " ").trim();
16111
+ return text === "" ? null : text;
16112
+ }
16113
+ //#endregion
16114
+ //#region src/client/minimax-code/document-preview.ts
16115
+ /**
16116
+ * MiniMax 视频渲染器的实现身份与 `documentPreviews` 定义。
16117
+ *
16118
+ * 定义形状照抄宿主 `ui-sidebar-documentpreview/src/client/image/index.ts` 的
16119
+ * `imageBodyDefinition`:id / extensions / binaryExtensions / priority / title /
16120
+ * loading / wrap。**id 同时是 keyed 槽的键**——定义与正文必须用同一个字符串,
16121
+ * 否则侧边栏选中了我们、却渲染不出正文。
16122
+ *
16123
+ * 这里是纯数据,不 import 任何槽位或 cordis 类型:定义要能在测试里直接断言形状,
16124
+ * 而 `priority` / `loading` 是字符串字面量联合,写宽了编译器就抓不住笔误。
16125
+ *
16126
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/document-preview
16127
+ */
16128
+ /** 本实现在文档预览系统里的身份,也是正文 keyed 槽的键。 */
16129
+ const MINIMAX_VIDEO_PREVIEW_ID = "@eddyskywalker/dsh-chatgpt-subscription/video";
16130
+ /** 本实现认领的文件后缀。 */
16131
+ const MINIMAX_VIDEO_EXTENSIONS = [
16132
+ "mp4",
16133
+ "webm",
16134
+ "mov",
16135
+ "m4v"
16136
+ ];
16137
+ /**
16138
+ * 构造视频渲染器的定义。
16139
+ *
16140
+ * `binaryExtensions` 与 `extensions` 用同一个数组:这四个后缀的字节都不能当文本
16141
+ * 读,`register` 也要求前者是后者的子集。
16142
+ *
16143
+ * @param title - 惰性求值的实现名(走本线路的 locale 命名空间)。
16144
+ * @returns 可交给 `ctx.documentPreviews.register` 的定义。
16145
+ */
16146
+ function minimaxVideoPreviewDefinition(title) {
16147
+ return {
16148
+ id: MINIMAX_VIDEO_PREVIEW_ID,
16149
+ extensions: MINIMAX_VIDEO_EXTENSIONS,
16150
+ binaryExtensions: MINIMAX_VIDEO_EXTENSIONS,
16151
+ priority: "extension",
16152
+ title,
16153
+ loading: "bytes-complete",
16154
+ wrap: false
16155
+ };
16156
+ }
16157
+ //#endregion
16158
+ //#region src/client/minimax-code/styles.ts
16159
+ const STYLE_ID$3 = "dsh-minimax-code-settings-style";
16160
+ /**
16161
+ * MiniMax Code-only additions to the shared settings design system.
16162
+ *
16163
+ * The base control styles (.dsha-page, .dsha-group, .dsha-row, .dsha-models,
16164
+ * the device-code box, the meter, the composer badge, ...) come from the
16165
+ * stylesheet the sibling routes install, in the same order the client installs
16166
+ * them, so this file declares only what stays unique to this card: the mono
16167
+ * credential path and the model-name list.
16168
+ */
16169
+ function installMinimaxCodeStyles() {
16170
+ installPluginStyle("minimax-code", `
16171
+ .dshm-mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}
16172
+ .dshm-model-list{display:flex;flex-wrap:wrap;gap:7px;padding-top:8px}
16173
+ .dshm-model-list code{background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;padding:5px 8px;white-space:nowrap}
16174
+ .dshm-quota-note{margin-top:12px}
16175
+ .dshm-reset-list{display:flex;flex-direction:column;gap:6px;padding-top:8px}
16176
+ .dshm-reset-row{align-items:center;justify-content:space-between}
16177
+ .dshm-reset-pick{background:none;border:0.5px solid var(--dsw-alias-border-l2);cursor:pointer;padding:4px 8px}
16178
+ .dshm-reset-pick[aria-pressed="true"]{border-color:var(--dsw-alias-border-focus,var(--dsw-alias-border-l3));background:var(--dsw-alias-bg-layer-2)}
16179
+
16180
+ /* ---- MiniMax 视频工具卡片与侧边栏视频台 ----
16181
+ * 解剖结构照抄同仓库的 .dsh-codex-image-tool 一族(发丝线、radius 8、13px 标题、
16182
+ * 8px 状态点、虚线占位块),所以两族卡片放在一起是同一个东西。字重全部 ≤500:
16183
+ * 强调靠字号与墨色,不靠加粗。颜色只用语义 token,不新增任何色值。
16184
+ *
16185
+ * 视频台是两个界面共用的同一个外框:对话流里约 460px 宽,侧边栏里通栏。16:9 的
16186
+ * 固定比例让它在两个尺寸下都是同一个形状。 */
16187
+ .dshm-video-tool{border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;corner-shape:round;display:flex;flex-direction:column;gap:8px;margin:4px 0 4px 4px;max-width:520px;min-width:0;padding:10px 12px}
16188
+ .dshm-video-head{align-items:center;display:flex;font-size:13px;gap:8px;min-width:0}
16189
+ .dshm-video-dot{background:var(--dsw-alias-button-info-fill,#397ee8);border-radius:50%;corner-shape:round;display:inline-block;flex:none;height:8px;width:8px}
16190
+ .dshm-video-tool[data-state=error] .dshm-video-dot{background:var(--dsw-alias-label-danger,#d94b4b)}
16191
+ .dshm-video-title{font-weight:500;white-space:nowrap}
16192
+ .dshm-video-summary{color:var(--dsw-alias-label-tertiary);font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
16193
+ .dshm-video-stage{aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;corner-shape:round;display:flex;min-width:0;overflow:hidden;position:relative;width:100%}
16194
+ .dshm-video-stage[data-state=empty]{align-items:center;border-style:dashed;border-width:1px;justify-content:center;padding:16px}
16195
+ .dshm-video-el{background:var(--dsw-alias-bg-layer-1,#000);display:block;height:100%;object-fit:contain;width:100%}
16196
+ .dshm-video-placeholder{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;text-align:center}
16197
+ .dshm-video-placeholder[data-tone=danger]{color:var(--dsw-alias-label-danger,#d94b4b)}
16198
+ .dshm-video-foot{align-items:center;display:flex;font-size:11px;gap:10px;justify-content:space-between;min-width:0}
16199
+ .dshm-video-path{background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-secondary);cursor:pointer;flex:0 1 auto;font:inherit;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.35;min-width:0;overflow:hidden;padding:3px 7px;text-align:left;text-overflow:ellipsis;white-space:nowrap}
16200
+ .dshm-video-path:hover{background:var(--dsw-alias-interactive-bg-hover)}
16201
+ .dshm-video-path:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#397ee8);outline-offset:2px}
16202
+ .dshm-video-meta{color:var(--dsw-alias-label-tertiary);display:flex;flex:none;font-size:11px;font-variant-numeric:tabular-nums;gap:8px;white-space:nowrap}
16203
+ .dshm-video-document{display:flex;flex-direction:column;gap:10px;min-width:0;padding:16px}
16204
+ .dshm-video-doc-meta{align-items:baseline;color:var(--dsw-alias-label-tertiary);display:flex;font-size:11px;gap:12px;justify-content:space-between;min-width:0}
16205
+ .dshm-video-doc-name{color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
16206
+ .dshm-video-doc-size{flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}
16207
+ @media(max-width:560px){.dshm-video-tool{margin-left:0;max-width:100%}.dshm-video-foot{align-items:flex-start;flex-direction:column;gap:4px}.dshm-video-document{padding:12px 10px}}
16208
+ @media(prefers-reduced-motion:reduce){.dshm-video-path{transition:none}}
16209
+ /* ---- /video 参数弹窗与设置页的视频分区 ----
16210
+ * 解剖结构照抄同文件的 .dshm-video-* 一族与设置卡的 .dsha-* 基础类:发丝线 0.5px、
16211
+ * radius 8 配 corner-shape:round、字号只用 11/12/13 三档、字重 ≤500、颜色只走
16212
+ * --dsw-alias-* 语义 token,不新增任何色值。
16213
+ *
16214
+ * 弹窗本身是 body portal:输入框所在的容器有 overflow,就地渲染会被裁掉。 */
16215
+ .dshm-video-modal-root{align-items:center;display:flex;inset:0;justify-content:center;position:fixed;z-index:1000}
16216
+ .dshm-video-modal-mask{background:color-mix(in srgb,var(--dsw-alias-label-primary,#000) 32%,transparent);inset:0;position:absolute}
16217
+ .dshm-video-modal{background:var(--dsw-alias-bg-layer-1,#fff);border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;corner-shape:round;box-shadow:0 12px 40px color-mix(in srgb,var(--dsw-alias-label-primary,#000) 18%,transparent);display:flex;flex-direction:column;max-height:82vh;max-width:560px;min-width:0;position:relative;width:calc(100% - 32px)}
16218
+ .dshm-video-modal:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#397ee8);outline-offset:2px}
16219
+ .dshm-video-modal-head{align-items:center;border-bottom:0.5px solid var(--dsw-alias-border-l2);display:flex;gap:12px;justify-content:space-between;padding:12px 16px}
16220
+ .dshm-video-modal-title{font-size:13px;font-weight:500;margin:0}
16221
+ .dshm-video-modal-close{background:none;border:0;border-radius:6px;corner-shape:round;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px}
16222
+ .dshm-video-modal-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
16223
+ .dshm-video-modal-close:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#397ee8);outline-offset:2px}
16224
+ .dshm-video-modal-desc{color:var(--dsw-alias-label-tertiary);font-size:11px;margin:0;padding:8px 16px 0}
16225
+ .dshm-video-modal-body{display:flex;flex-direction:column;gap:10px;min-height:0;overflow:auto;padding:12px 16px}
16226
+ .dshm-video-modal-foot{border-top:0.5px solid var(--dsw-alias-border-l2);display:flex;gap:8px;justify-content:flex-end;padding:12px 16px}
16227
+ .dshm-video-field{display:flex;flex-direction:column;gap:5px;min-width:0}
16228
+ .dshm-video-field-row{display:flex;flex-wrap:wrap;gap:10px}
16229
+ .dshm-video-field-row .dshm-video-field{flex:1 1 140px}
16230
+ .dshm-video-field-label{color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500}
16231
+ .dshm-video-input{background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;corner-shape:round;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;min-width:0;padding:6px 8px;width:100%}
16232
+ .dshm-video-input:focus-visible{outline:2px solid var(--dsw-alias-button-info-fill,#397ee8);outline-offset:1px}
16233
+ .dshm-video-textarea{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.5;resize:vertical}
16234
+ .dshm-video-ratio-locked{align-items:center;display:flex;flex-wrap:wrap;gap:8px}
16235
+ .dshm-video-hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}
16236
+ .dshm-video-watermark{align-items:center;flex-direction:row;gap:8px}
16237
+ .dshm-video-media{border-top:0.5px solid var(--dsw-alias-border-l2);display:flex;flex-direction:column;gap:10px;padding-top:12px}
16238
+ .dshm-video-media-title{font-size:12px;font-weight:500;margin:0}
16239
+ .dshm-video-issue{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.5;margin:0}
16240
+ .dshm-video-issue[data-tone=danger]{color:var(--dsw-alias-label-danger,#d94b4b)}
16241
+ /* 列表的加载态:骨架,不是 loading 文案。 */
16242
+ .dshm-video-skeleton{display:flex;flex-direction:column;gap:6px;padding:10px 0}
16243
+ .dshm-video-skeleton span{animation:dshm-video-pulse 1.4s ease-in-out infinite;background:var(--dsw-alias-bg-layer-2);border-radius:6px;corner-shape:round;display:block;height:14px}
16244
+ .dshm-video-skeleton span:nth-child(2){opacity:.7;width:82%}
16245
+ .dshm-video-skeleton span:nth-child(3){opacity:.45;width:64%}
16246
+ @keyframes dshm-video-pulse{0%,100%{opacity:.55}50%{opacity:1}}
16247
+ .dshm-video-history{display:flex;flex-direction:column;min-width:0}
16248
+ .dshm-video-history-path{color:var(--dsw-alias-label-secondary);flex:1 1 auto;font-size:11px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
16249
+ .dshm-video-history-meta{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11px;font-variant-numeric:tabular-nums}
16250
+ @media(max-width:560px){.dshm-video-modal{max-height:88vh;width:calc(100% - 16px)}.dshm-video-field-row{flex-direction:column;gap:8px}}
16251
+ @media(prefers-reduced-motion:reduce){.dshm-video-skeleton span{animation:none;opacity:.6}}
16252
+ `, STYLE_ID$3);
16253
+ }
16254
+ //#endregion
16255
+ //#region src/client/workbuddy/WorkBuddyComposerQuota.tsx
16256
+ const api = createLineApi("/workbuddy/api", "WorkBuddy");
16257
+ /**
16258
+ * Pick the most meaningful number for the badge.
16259
+ *
16260
+ * A bounded allowance is what a user spends down, so it wins over a bare credit
16261
+ * balance; the balance shows only when no percentage is reported. The billing
16262
+ * payload reports both a cycle meter and a package meter, and the cycle meter
16263
+ * is the one that actually resets, so it is preferred when both exist.
16264
+ */
16265
+ function selectBadgeFacts$2(status) {
16266
+ const quota = status?.quota;
16267
+ if (quota === null || quota === void 0) return {
16268
+ text: "—",
16269
+ tooltip: "WorkBuddy quota unavailable — click to refresh",
16270
+ level: "normal"
16271
+ };
16272
+ const meter = (quota.meters ?? []).find((candidate) => candidate.id === "cycle" && candidate.remainingFraction !== null) ?? (quota.meters ?? []).find((candidate) => candidate.remainingFraction !== null);
16273
+ if (meter !== void 0 && meter.remainingFraction !== null) {
16274
+ const remaining = Math.round(meter.remainingFraction * 100);
16275
+ return {
16276
+ text: `${remaining}%`,
16277
+ tooltip: `[WorkBuddy] ${meter.label}: ${remaining}% left`,
16278
+ level: quotaLevel(remaining)
16279
+ };
16280
+ }
16281
+ if (quota.remainingCredits !== null) return {
16282
+ text: String(quota.remainingCredits),
16283
+ tooltip: `[WorkBuddy] Remaining credits: ${quota.remainingCredits}`,
16284
+ level: "normal"
16285
+ };
16286
+ return {
16287
+ text: "—",
16288
+ tooltip: "WorkBuddy quota unavailable — click to refresh",
16289
+ level: "normal"
16290
+ };
16291
+ }
16292
+ function WorkBuddyComposerQuota({ directory, loadModelDirectory, t }) {
16293
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerQuotaBadge, {
16294
+ directory,
16295
+ loadModelDirectory,
16296
+ providerId: "workbuddy-subscription",
16297
+ readStatus: (refresh) => refresh ? api.post("/quota") : api.get("/status"),
16298
+ selectFacts: selectBadgeFacts$2,
16299
+ label: t("composerLabel")
16300
+ });
16301
+ }
16302
+ //#endregion
16303
+ //#region src/client/workbuddy/styles.ts
14546
16304
  const STYLE_ID$2 = "dsh-workbuddy-settings-style";
14547
16305
  /**
14548
16306
  * WorkBuddy-only additions to the shared settings design system.
@@ -15056,81 +16814,890 @@ window.__ModuleLoader__.load({
15056
16814
  }, 1500);
15057
16815
  } catch {}
15058
16816
  });
15059
- const handleZoom = (e) => {
15060
- e.stopPropagation();
15061
- openZoomModal(cleanSvg);
16817
+ const handleZoom = (e) => {
16818
+ e.stopPropagation();
16819
+ openZoomModal(cleanSvg);
16820
+ };
16821
+ wrapper.querySelector("[data-btn=\"zoom\"]")?.addEventListener("click", handleZoom);
16822
+ chartEl?.addEventListener("click", handleZoom);
16823
+ host.style.display = "none";
16824
+ host.parentNode?.insertBefore(wrapper, host);
16825
+ host.setAttribute("data-dsh-mermaid-status", "done");
16826
+ } catch {
16827
+ processedHosts.delete(host);
16828
+ host.removeAttribute("data-dsh-mermaid-status");
16829
+ const errContainer = document.getElementById(renderId);
16830
+ if (errContainer) errContainer.remove();
16831
+ }
16832
+ }
16833
+ function scanAndRenderMermaid() {
16834
+ if (typeof document === "undefined") return;
16835
+ const codeElements = document.querySelectorAll("pre code, .md-code-block code");
16836
+ for (const codeEl of codeElements) {
16837
+ const candidate = findMermaidCandidate(codeEl);
16838
+ if (candidate) renderMermaidHost(candidate.host, candidate.code);
16839
+ }
16840
+ }
16841
+ function setupMermaidObserver() {
16842
+ if (typeof document === "undefined") return () => {};
16843
+ installMermaidStyles();
16844
+ let timer;
16845
+ const debouncedScan = () => {
16846
+ window.clearTimeout(timer);
16847
+ timer = window.setTimeout(() => {
16848
+ scanAndRenderMermaid();
16849
+ }, 250);
16850
+ };
16851
+ debouncedScan();
16852
+ const observer = new MutationObserver((mutations) => {
16853
+ let shouldScan = false;
16854
+ for (const m of mutations) {
16855
+ for (const node of m.addedNodes) if (node.nodeType === 1) {
16856
+ const el = node;
16857
+ if (el.classList?.contains("dsh-mermaid-wrapper") || el.closest?.(".dsh-mermaid-wrapper")) continue;
16858
+ shouldScan = true;
16859
+ break;
16860
+ }
16861
+ if (shouldScan) break;
16862
+ }
16863
+ if (shouldScan) debouncedScan();
16864
+ });
16865
+ observer.observe(document.body, {
16866
+ childList: true,
16867
+ subtree: true
16868
+ });
16869
+ const themeObserver = new MutationObserver(() => {
16870
+ initMermaidConfig();
16871
+ for (const el of document.querySelectorAll("[data-dsh-mermaid-status=\"done\"]")) {
16872
+ processedHosts.delete(el);
16873
+ el.removeAttribute("data-dsh-mermaid-status");
16874
+ const prevWrapper = el.previousElementSibling;
16875
+ if (prevWrapper && prevWrapper.classList.contains("dsh-mermaid-wrapper")) prevWrapper.remove();
16876
+ el.style.display = "";
16877
+ }
16878
+ debouncedScan();
16879
+ });
16880
+ themeObserver.observe(document.documentElement, {
16881
+ attributes: true,
16882
+ attributeFilter: [
16883
+ "class",
16884
+ "data-theme",
16885
+ "data-color-mode"
16886
+ ]
16887
+ });
16888
+ return () => {
16889
+ observer.disconnect();
16890
+ themeObserver.disconnect();
16891
+ window.clearTimeout(timer);
16892
+ };
16893
+ }
16894
+ //#endregion
16895
+ //#region src/client/minimax-code/VideoFormDialog.tsx
16896
+ /**
16897
+ * `/video` 的参数弹窗:把「有哪些参数」和「哪些组合合法」一次讲清楚。
16898
+ *
16899
+ * ## 为什么是一个自建弹窗,而不是宿主的 popupSelect
16900
+ *
16901
+ * 宿主的 popupSelect 只有 `options()` 与 `onSelect()`(`ui-commands/src/client/
16902
+ * contract.ts:64-75`),**单选**,装不下「提示词 + 模型 + 比例 + 时长 + 分辨率 +
16903
+ * 四类参考文件 + 水印」。而 `ActionSpec.run(session)` 的契约明写它「不提交任何
16904
+ * 东西」,所以命令自己也发不出去。`/video` 因此是两级:popupSelect 给预设档
16905
+ * (见 `video-command.ts`),「自定义…」这一档打开本组件。
16906
+ *
16907
+ * ## 关键行为:它**不发送**,只把参数写进输入框
16908
+ *
16909
+ * 这是这次入口的核心价值。视频参数有联动规则(给了首帧→比例被强制改成
16910
+ * `adaptive`;纯文本→`adaptive` 不可选),如果直接发起生成,用户永远看不到
16911
+ * 自己填的东西被改成了什么。所以这里只调用 `setDraft`,把
16912
+ * {@link buildVideoDraftText} 拼出来的那段话放进输入框,剩下的交给用户。
16913
+ *
16914
+ * ## 弹窗怎么落地
16915
+ *
16916
+ * 宿主有 `@deepseek-ai/dsh-client-ui-primitives` 的 `Modal`(`Modal.tsx:48`,
16917
+ * 内部 `createPortal(..., document.body)` 在 `:57-90`)。**本插件不依赖那个包的
16918
+ * 运行时**(`progress-updates/markdown-lite.tsx:6` 记录了这个决定:它的 `lib`
16919
+ * 是宿主装配产物,插件装不到),所以这里按同一形状自己 portal 到 `document.body`:
16920
+ * 遮罩 + 居中卡片 + `role="dialog"` + `aria-modal` + Escape 关闭 + 点遮罩关闭。
16921
+ * portal 到 body 是必须的——输入框所在的那片容器有 `overflow`,就地渲染会被裁掉。
16922
+ *
16923
+ * ## 键盘可达性
16924
+ *
16925
+ * 卡片带 `tabIndex={-1}` 并在打开时聚焦,Tab 因此从卡片内部开始按 DOM 顺序走遍
16926
+ * 每个字段;Escape 关闭(在 `keydown` 捕获阶段处理,输入框里的 Escape 也生效);
16927
+ * 关闭后焦点还给打开它的元素,见 {@link VideoFormDialog} 里的 `restoreFocus`。
16928
+ *
16929
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-form-dialog
16930
+ */
16931
+ /** 把校验问题翻成一句话。中文最多两句、结尾不加句号。 */
16932
+ function issueText(issue, t) {
16933
+ const params = issue.params ?? {};
16934
+ switch (issue.code) {
16935
+ case "prompt-empty": return t("videoPromptRequired");
16936
+ case "reference-unsupported": return t("videoMediaUnsupported", params);
16937
+ case "last-frame-unsupported": return t("videoLastFrameUnsupported", params);
16938
+ case "too-many-images": return t("videoTooManyImages", params);
16939
+ case "too-many-videos": return t("videoTooManyVideos", params);
16940
+ case "too-many-audios": return t("videoTooManyAudios", params);
16941
+ case "too-many-total": return t("videoTooManyTotal", params);
16942
+ case "duration-invalid": return t("videoDurationInvalid", params);
16943
+ default: return t("unknown");
16944
+ }
16945
+ }
16946
+ /** 一个带标签的表单行。标签与控件用 `useId` 关联,屏幕阅读器读得到。 */
16947
+ function Field({ label, id, children }) {
16948
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16949
+ className: "dshm-video-field",
16950
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
16951
+ className: "dshm-video-field-label",
16952
+ htmlFor: id,
16953
+ children: label
16954
+ }), children]
16955
+ });
16956
+ }
16957
+ /**
16958
+ * 参数弹窗。
16959
+ *
16960
+ * 受控到组件内部:`/video` 只负责开与关,表单状态活在这里,关闭即丢弃——一次
16961
+ * 生成填一次参数,没有「上次填到一半」的语义要维护。
16962
+ */
16963
+ function VideoFormDialog({ open, onClose, onSubmit, defaults, t }) {
16964
+ const seeded = (0, react.useMemo)(() => normalizeVideoDefaults(defaults ?? {}), [defaults]);
16965
+ const [prompt, setPrompt] = (0, react.useState)("");
16966
+ const [model, setModel] = (0, react.useState)(seeded.model);
16967
+ const [ratio, setRatio] = (0, react.useState)(seeded.ratio);
16968
+ const [durationSeconds, setDurationSeconds] = (0, react.useState)(seeded.durationSeconds);
16969
+ const [resolution, setResolution] = (0, react.useState)(defaultResolution(seeded.model));
16970
+ const [watermark, setWatermark] = (0, react.useState)(seeded.watermark);
16971
+ const [inputImagePath, setInputImagePath] = (0, react.useState)("");
16972
+ const [lastFrameImagePath, setLastFrameImagePath] = (0, react.useState)("");
16973
+ const [referenceImages, setReferenceImages] = (0, react.useState)("");
16974
+ const [referenceVideos, setReferenceVideos] = (0, react.useState)("");
16975
+ const [referenceVideoDurations, setReferenceVideoDurations] = (0, react.useState)("");
16976
+ const [referenceAudios, setReferenceAudios] = (0, react.useState)("");
16977
+ const [notice, setNotice] = (0, react.useState)(null);
16978
+ const dialog = (0, react.useRef)(null);
16979
+ const restoreFocus = (0, react.useRef)(null);
16980
+ const promptRef = (0, react.useRef)(null);
16981
+ const promptId = "dshm-video-prompt";
16982
+ const modelId = "dshm-video-model";
16983
+ const ratioId = "dshm-video-ratio";
16984
+ const durationId = "dshm-video-duration";
16985
+ const resolutionId = "dshm-video-resolution";
16986
+ (0, react.useEffect)(() => {
16987
+ if (!open) return;
16988
+ const seed = normalizeVideoDefaults(defaults ?? {});
16989
+ setPrompt("");
16990
+ setModel(seed.model);
16991
+ setRatio(seed.ratio);
16992
+ setDurationSeconds(seed.durationSeconds);
16993
+ setResolution(defaultResolution(seed.model));
16994
+ setWatermark(seed.watermark);
16995
+ setInputImagePath("");
16996
+ setLastFrameImagePath("");
16997
+ setReferenceImages("");
16998
+ setReferenceVideos("");
16999
+ setReferenceVideoDurations("");
17000
+ setReferenceAudios("");
17001
+ setNotice(null);
17002
+ restoreFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
17003
+ const frame = requestAnimationFrame(() => {
17004
+ promptRef.current?.focus();
17005
+ });
17006
+ return () => {
17007
+ cancelAnimationFrame(frame);
17008
+ restoreFocus.current?.focus();
17009
+ };
17010
+ }, [open]);
17011
+ (0, react.useEffect)(() => {
17012
+ if (!open) return;
17013
+ const onKeyDown = (event) => {
17014
+ if (event.key !== "Escape") return;
17015
+ event.stopPropagation();
17016
+ onClose();
17017
+ };
17018
+ document.addEventListener("keydown", onKeyDown, true);
17019
+ return () => {
17020
+ document.removeEventListener("keydown", onKeyDown, true);
17021
+ };
17022
+ }, [open, onClose]);
17023
+ const media = (0, react.useMemo)(() => ({
17024
+ ...inputImagePath.trim() === "" ? {} : { inputImagePath },
17025
+ ...lastFrameImagePath.trim() === "" ? {} : { lastFrameImagePath },
17026
+ referenceImagePaths: parsePathLines(referenceImages),
17027
+ referenceVideoPaths: parsePathLines(referenceVideos),
17028
+ referenceVideoDurationSeconds: parsePathLines(referenceVideoDurations).map((line) => Number(line)).filter((value) => Number.isFinite(value) && value > 0),
17029
+ referenceAudioPaths: parsePathLines(referenceAudios)
17030
+ }), [
17031
+ inputImagePath,
17032
+ lastFrameImagePath,
17033
+ referenceImages,
17034
+ referenceVideos,
17035
+ referenceVideoDurations,
17036
+ referenceAudios
17037
+ ]);
17038
+ const presence = (0, react.useMemo)(() => mediaPresence(media), [media]);
17039
+ const ratioChoice = (0, react.useMemo)(() => resolveVideoRatioChoice({
17040
+ model,
17041
+ ...ratio === void 0 ? {} : { requested: ratio },
17042
+ hasKeyframes: presence.hasKeyframes,
17043
+ hasReferences: presence.hasReferences
17044
+ }), [
17045
+ model,
17046
+ ratio,
17047
+ presence.hasKeyframes,
17048
+ presence.hasReferences
17049
+ ]);
17050
+ const values = (0, react.useMemo)(() => ({
17051
+ prompt,
17052
+ model,
17053
+ ...ratioChoice.ratio === void 0 ? {} : { ratio: ratioChoice.ratio },
17054
+ ...durationSeconds === void 0 ? {} : { durationSeconds },
17055
+ ...resolution === void 0 ? {} : { resolution },
17056
+ watermark,
17057
+ media
17058
+ }), [
17059
+ prompt,
17060
+ model,
17061
+ ratioChoice.ratio,
17062
+ durationSeconds,
17063
+ resolution,
17064
+ watermark,
17065
+ media
17066
+ ]);
17067
+ const issues = (0, react.useMemo)(() => validateVideoForm(values), [values]);
17068
+ const submittable = canSubmitVideoForm(values);
17069
+ const rule = videoModelRule(model);
17070
+ const durations = durationChoices(model);
17071
+ const ratios = ratioChoice.options;
17072
+ const onModelChange = (0, react.useCallback)((next) => {
17073
+ setModel(next);
17074
+ const allowedDurations = durationChoices(next);
17075
+ setDurationSeconds((current) => current !== void 0 && allowedDurations.includes(current) ? current : defaultDuration(next));
17076
+ const allowedResolutions = videoModelRule(next)?.resolutions ?? [];
17077
+ setResolution((current) => current !== void 0 && allowedResolutions.includes(current) ? current : defaultResolution(next));
17078
+ setRatio(void 0);
17079
+ }, []);
17080
+ const submit = (0, react.useCallback)(() => {
17081
+ if (!submittable) {
17082
+ setNotice({
17083
+ tone: "danger",
17084
+ text: t("videoPromptRequired")
17085
+ });
17086
+ return;
17087
+ }
17088
+ if (!onSubmit(buildVideoDraftText({
17089
+ ...values,
17090
+ ...resolution === void 0 ? {} : { resolution }
17091
+ }))) {
17092
+ setNotice({
17093
+ tone: "danger",
17094
+ text: t("videoDraftFailed")
17095
+ });
17096
+ return;
17097
+ }
17098
+ onClose();
17099
+ }, [
17100
+ onClose,
17101
+ onSubmit,
17102
+ submittable,
17103
+ t,
17104
+ values
17105
+ ]);
17106
+ if (!open) return null;
17107
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17108
+ className: "dshm-video-modal-root",
17109
+ role: "presentation",
17110
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
17111
+ className: "dshm-video-modal-mask",
17112
+ "aria-hidden": "true",
17113
+ onClick: onClose
17114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17115
+ ref: dialog,
17116
+ className: "dshm-video-modal",
17117
+ role: "dialog",
17118
+ "aria-modal": "true",
17119
+ "aria-label": t("videoDialogTitle"),
17120
+ tabIndex: -1,
17121
+ children: [
17122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17123
+ className: "dshm-video-modal-head",
17124
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
17125
+ className: "dshm-video-modal-title",
17126
+ children: t("videoDialogTitle")
17127
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17128
+ type: "button",
17129
+ className: "dshm-video-modal-close",
17130
+ "aria-label": t("videoClose"),
17131
+ onClick: onClose,
17132
+ children: "×"
17133
+ })]
17134
+ }),
17135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17136
+ className: "dshm-video-modal-desc",
17137
+ children: t("videoDialogDesc")
17138
+ }),
17139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17140
+ className: "dshm-video-modal-body",
17141
+ children: [
17142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17143
+ label: t("videoPromptLabel"),
17144
+ id: promptId,
17145
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
17146
+ ref: promptRef,
17147
+ id: promptId,
17148
+ className: "dshm-video-input dshm-video-textarea",
17149
+ rows: 3,
17150
+ placeholder: t("videoPromptPlaceholder"),
17151
+ value: prompt,
17152
+ onChange: (event) => {
17153
+ setPrompt(event.currentTarget.value);
17154
+ setNotice(null);
17155
+ }
17156
+ })
17157
+ }),
17158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17159
+ className: "dshm-video-field-row",
17160
+ children: [
17161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17162
+ label: t("videoModelLabel"),
17163
+ id: modelId,
17164
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
17165
+ id: modelId,
17166
+ className: "dshm-video-input",
17167
+ value: model,
17168
+ onChange: (event) => {
17169
+ onModelChange(event.currentTarget.value);
17170
+ },
17171
+ children: MINIMAX_VIDEO_MODELS.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
17172
+ value: entry,
17173
+ children: entry
17174
+ }, entry))
17175
+ })
17176
+ }),
17177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17178
+ label: t("videoDurationLabel"),
17179
+ id: durationId,
17180
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
17181
+ id: durationId,
17182
+ className: "dshm-video-input",
17183
+ value: durationSeconds === void 0 ? "" : String(durationSeconds),
17184
+ onChange: (event) => {
17185
+ const next = Number(event.currentTarget.value);
17186
+ setDurationSeconds(Number.isFinite(next) ? next : void 0);
17187
+ },
17188
+ children: durations.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
17189
+ value: String(value),
17190
+ children: [
17191
+ value,
17192
+ " ",
17193
+ t("videoDurationUnit")
17194
+ ]
17195
+ }, value))
17196
+ })
17197
+ }),
17198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17199
+ label: t("videoResolutionLabel"),
17200
+ id: resolutionId,
17201
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
17202
+ id: resolutionId,
17203
+ className: "dshm-video-input",
17204
+ value: resolution ?? defaultResolution(model) ?? "",
17205
+ onChange: (event) => {
17206
+ const next = event.currentTarget.value;
17207
+ setResolution(rule?.resolutions.includes(next) === true ? next : void 0);
17208
+ },
17209
+ children: (rule?.resolutions ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
17210
+ value: entry,
17211
+ children: entry
17212
+ }, entry))
17213
+ })
17214
+ })
17215
+ ]
17216
+ }),
17217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
17218
+ label: t("videoRatioLabel"),
17219
+ id: ratioId,
17220
+ children: [ratioChoice.selectable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
17221
+ id: ratioId,
17222
+ className: "dshm-video-input",
17223
+ value: ratioChoice.ratio ?? "",
17224
+ onChange: (event) => {
17225
+ const next = event.currentTarget.value;
17226
+ setRatio(ratios.includes(next) ? next : void 0);
17227
+ },
17228
+ children: ratios.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
17229
+ value: entry,
17230
+ children: entry
17231
+ }, entry))
17232
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17233
+ className: "dshm-video-ratio-locked",
17234
+ id: ratioId,
17235
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
17236
+ className: "dshm-mono",
17237
+ children: ratioChoice.ratio ?? t("unknown")
17238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17239
+ className: "dshm-video-hint",
17240
+ children: t("videoRatioLockedKeyframes")
17241
+ })]
17242
+ }), ratioChoice.selectable && ratioChoice.lockedReason === "text-only" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17243
+ className: "dshm-video-hint",
17244
+ children: t("videoRatioLockedTextOnly")
17245
+ })]
17246
+ }),
17247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17248
+ className: "dshm-video-field-row dshm-video-watermark",
17249
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
17250
+ className: "dshm-video-field-label",
17251
+ htmlFor: "dshm-video-watermark",
17252
+ children: t("videoWatermarkLabel")
17253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17254
+ id: "dshm-video-watermark",
17255
+ type: "checkbox",
17256
+ checked: watermark,
17257
+ onChange: (event) => {
17258
+ setWatermark(event.currentTarget.checked);
17259
+ }
17260
+ })]
17261
+ }),
17262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17263
+ className: "dshm-video-media",
17264
+ children: [
17265
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
17266
+ className: "dshm-video-media-title",
17267
+ children: t("videoMediaSection")
17268
+ }),
17269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17270
+ className: "dshm-video-hint",
17271
+ children: t("videoMediaHint")
17272
+ }),
17273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17274
+ label: t("videoInputImageLabel"),
17275
+ id: "dshm-video-input-image",
17276
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17277
+ id: "dshm-video-input-image",
17278
+ className: "dshm-video-input",
17279
+ type: "text",
17280
+ value: inputImagePath,
17281
+ onChange: (event) => {
17282
+ setInputImagePath(event.currentTarget.value);
17283
+ }
17284
+ })
17285
+ }),
17286
+ rule?.supportsReferenceMedia === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17287
+ label: t("videoLastFrameLabel"),
17288
+ id: "dshm-video-last-frame",
17289
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17290
+ id: "dshm-video-last-frame",
17291
+ className: "dshm-video-input",
17292
+ type: "text",
17293
+ value: lastFrameImagePath,
17294
+ onChange: (event) => {
17295
+ setLastFrameImagePath(event.currentTarget.value);
17296
+ }
17297
+ })
17298
+ }),
17299
+ rule?.supportsReferenceMedia === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
17300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17301
+ label: t("videoReferenceImagesLabel"),
17302
+ id: "dshm-video-ref-images",
17303
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
17304
+ id: "dshm-video-ref-images",
17305
+ className: "dshm-video-input dshm-video-textarea",
17306
+ rows: 2,
17307
+ value: referenceImages,
17308
+ onChange: (event) => {
17309
+ setReferenceImages(event.currentTarget.value);
17310
+ }
17311
+ })
17312
+ }),
17313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17314
+ label: t("videoReferenceVideosLabel"),
17315
+ id: "dshm-video-ref-videos",
17316
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
17317
+ id: "dshm-video-ref-videos",
17318
+ className: "dshm-video-input dshm-video-textarea",
17319
+ rows: 2,
17320
+ value: referenceVideos,
17321
+ onChange: (event) => {
17322
+ setReferenceVideos(event.currentTarget.value);
17323
+ }
17324
+ })
17325
+ }),
17326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17327
+ label: t("videoReferenceVideoDurationsLabel"),
17328
+ id: "dshm-video-ref-video-durations",
17329
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
17330
+ id: "dshm-video-ref-video-durations",
17331
+ className: "dshm-video-input dshm-video-textarea",
17332
+ rows: 2,
17333
+ value: referenceVideoDurations,
17334
+ onChange: (event) => {
17335
+ setReferenceVideoDurations(event.currentTarget.value);
17336
+ }
17337
+ })
17338
+ }),
17339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
17340
+ label: t("videoReferenceAudiosLabel"),
17341
+ id: "dshm-video-ref-audios",
17342
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
17343
+ id: "dshm-video-ref-audios",
17344
+ className: "dshm-video-input dshm-video-textarea",
17345
+ rows: 2,
17346
+ value: referenceAudios,
17347
+ onChange: (event) => {
17348
+ setReferenceAudios(event.currentTarget.value);
17349
+ }
17350
+ })
17351
+ })
17352
+ ] })
17353
+ ]
17354
+ }),
17355
+ issues.filter((issue) => issue.code !== "prompt-empty").map((issue) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17356
+ className: "dshm-video-issue",
17357
+ "data-tone": "danger",
17358
+ children: issueText(issue, t)
17359
+ }, issue.code)),
17360
+ notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
17361
+ className: "dshm-video-issue",
17362
+ "data-tone": notice.tone,
17363
+ role: "status",
17364
+ children: notice.text
17365
+ })
17366
+ ]
17367
+ }),
17368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17369
+ className: "dshm-video-modal-foot",
17370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17371
+ type: "button",
17372
+ className: "dsha-btn",
17373
+ onClick: onClose,
17374
+ children: t("videoCancel")
17375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
17376
+ type: "button",
17377
+ className: "dsha-btn dsha-btn-primary",
17378
+ disabled: !submittable,
17379
+ onClick: submit,
17380
+ children: t("videoSubmit")
17381
+ })]
17382
+ })
17383
+ ]
17384
+ })]
17385
+ }), document.body);
17386
+ }
17387
+ /**
17388
+ * 预设档表。
17389
+ *
17390
+ * 每一行都是「模型 × 比例 × 时长 × 分辨率」的一个**实测合法**组合:
17391
+ *
17392
+ * - H3:4-15 秒、768P/2K;H3-Max:5-15 秒、480P/768P;Hailuo:6 或 10 秒、768P/1080P;
17393
+ * - 比例是纯文本分支,所以 `adaptive` 一律不出现(那是首帧/参考媒体的分支才会有的值)。
17394
+ *
17395
+ * 时长都取各模型的下界,理由与 {@link defaultDuration} 相同:第一档应当是最便宜的
17396
+ * 一次生成。用户要别的组合,走「自定义…」。
17397
+ */
17398
+ function videoPresets(t) {
17399
+ const rows = [];
17400
+ for (const rule of VIDEO_MODEL_RULES) {
17401
+ const duration = defaultDuration(rule.model);
17402
+ const resolution = defaultResolution(rule.model);
17403
+ if (duration === void 0 || resolution === void 0) continue;
17404
+ const ratio = rule.isH3Family ? "16:9" : void 0;
17405
+ rows.push({
17406
+ preset: {
17407
+ id: rule.model + ":" + String(duration) + ":" + resolution,
17408
+ model: rule.model,
17409
+ ratio,
17410
+ durationSeconds: duration,
17411
+ resolution
17412
+ },
17413
+ label: rule.model + " · " + String(duration) + t("videoDurationUnit") + " · " + resolution + (ratio === void 0 ? "" : " · " + ratio)
17414
+ });
17415
+ }
17416
+ return rows;
17417
+ }
17418
+ //#endregion
17419
+ //#region src/client/minimax-code/video-command.ts
17420
+ /** 本线路的 provider id(宿主 `PROVIDER_ID` 的同值副本)。 */
17421
+ const MINIMAX_CODE_PROVIDER = "minimax-code";
17422
+ /** `/video` 里「自定义…」那一行的 id。 */
17423
+ const CUSTOM_OPTION_ID = "custom";
17424
+ /** 本模块登记的命令名。 */
17425
+ const VIDEO_COMMAND_NAME = "video";
17426
+ /**
17427
+ * 这条会话的模型路由,读不到就是 undefined。
17428
+ *
17429
+ * 路径与宿主 `submissionState` 一致:投影 → `next` 优先、`lastUsed` 兜底 →
17430
+ * provider。**全程可选链**:投影没注册、快照是 undefined、字段缺失,都在这里
17431
+ * 变成 undefined,而不是一次异常。
17432
+ *
17433
+ * @param ctx - 浏览器根上下文(或注入了 `sessions` 的 scope)。
17434
+ * @param sessionId - 会话 id。
17435
+ * @returns 形如 `{ provider, model }` 的路由,或 undefined。
17436
+ */
17437
+ function sessionRouteOf(ctx, sessionId) {
17438
+ try {
17439
+ const snapshot = ((ctx.sessions?.binding?.(sessionId))?.session?.projections?.faceOf?.("modelSelection"))?.getSnapshot?.();
17440
+ if (typeof snapshot !== "object" || snapshot === null) return void 0;
17441
+ const record = snapshot;
17442
+ const pick = (value) => {
17443
+ if (typeof value !== "object" || value === null) return void 0;
17444
+ const candidate = value;
17445
+ if (typeof candidate.provider !== "string" || candidate.provider === "") return void 0;
17446
+ if (typeof candidate.model !== "string" || candidate.model === "") return void 0;
17447
+ return {
17448
+ provider: candidate.provider,
17449
+ model: candidate.model
17450
+ };
15062
17451
  };
15063
- wrapper.querySelector("[data-btn=\"zoom\"]")?.addEventListener("click", handleZoom);
15064
- chartEl?.addEventListener("click", handleZoom);
15065
- host.style.display = "none";
15066
- host.parentNode?.insertBefore(wrapper, host);
15067
- host.setAttribute("data-dsh-mermaid-status", "done");
17452
+ return pick(record.next) ?? pick(record.lastUsed);
15068
17453
  } catch {
15069
- processedHosts.delete(host);
15070
- host.removeAttribute("data-dsh-mermaid-status");
15071
- const errContainer = document.getElementById(renderId);
15072
- if (errContainer) errContainer.remove();
17454
+ return;
15073
17455
  }
15074
17456
  }
15075
- function scanAndRenderMermaid() {
15076
- if (typeof document === "undefined") return;
15077
- const codeElements = document.querySelectorAll("pre code, .md-code-block code");
15078
- for (const codeEl of codeElements) {
15079
- const candidate = findMermaidCandidate(codeEl);
15080
- if (candidate) renderMermaidHost(candidate.host, candidate.code);
17457
+ /**
17458
+ * 把一段文本写进该会话的输入框。
17459
+ *
17460
+ * 走宿主的 `SessionInput.setDraft`(`ui-conversation/src/client/contract/input.ts:186`),
17461
+ * 不是 `prompt`:后者会绕过输入框直接把一条用户消息发出去,用户就看不见自己填了
17462
+ * 什么。`for(actx)` 要求一个**会话作用域的 Context**,正是
17463
+ * `ctx.sessions.binding(id).ctx`(见 `session-controller/src/client/sessions/service.ts:101-108`)。
17464
+ *
17465
+ * @param ctx - 浏览器根上下文。
17466
+ * @param sessionId - 目标会话。
17467
+ * @param text - 要填入的文本。
17468
+ * @returns 是否真的填进去了。
17469
+ */
17470
+ function fillComposer(ctx, sessionId, text) {
17471
+ try {
17472
+ const actx = (ctx.sessions?.binding?.(sessionId))?.ctx;
17473
+ if (actx === void 0) return false;
17474
+ const input = ctx.conversation?.input?.for?.(actx);
17475
+ if (typeof input?.setDraft !== "function") return false;
17476
+ input.setDraft(text);
17477
+ return true;
17478
+ } catch {
17479
+ return false;
15081
17480
  }
15082
17481
  }
15083
- function setupMermaidObserver() {
15084
- if (typeof document === "undefined") return () => {};
15085
- installMermaidStyles();
15086
- let timer;
15087
- const debouncedScan = () => {
15088
- window.clearTimeout(timer);
15089
- timer = window.setTimeout(() => {
15090
- scanAndRenderMermaid();
15091
- }, 250);
15092
- };
15093
- debouncedScan();
15094
- const observer = new MutationObserver((mutations) => {
15095
- let shouldScan = false;
15096
- for (const m of mutations) {
15097
- for (const node of m.addedNodes) if (node.nodeType === 1) {
15098
- const el = node;
15099
- if (el.classList?.contains("dsh-mermaid-wrapper") || el.closest?.(".dsh-mermaid-wrapper")) continue;
15100
- shouldScan = true;
15101
- break;
17482
+ /** 兜底翻译:只在没有 locale 服务时用到(测试、以及菜单渲染早于字典注册)。 */
17483
+ function fallbackTranslate$1(key, params) {
17484
+ const template = zh$6[key] ?? key;
17485
+ if (params === void 0) return template;
17486
+ return template.replace(/\{(\w+)\}/g, (_match, name) => String(params[name] ?? ""));
17487
+ }
17488
+ /**
17489
+ * 把一条预设档拼成填进输入框的那段话。
17490
+ *
17491
+ * 提示词留一条**待填的占位**,而不是随便编一句:用户选了「H3 · 4秒 · 768P」之后,
17492
+ * 真正缺的只有画面描述,那一条应当由他来写。
17493
+ *
17494
+ * @param preset - 选中的预设档。
17495
+ * @returns 填进输入框的文本。
17496
+ */
17497
+ function presetDraftText(preset) {
17498
+ return buildVideoDraftText({
17499
+ prompt: "(在这里写画面描述)",
17500
+ model: preset.model,
17501
+ ...preset.ratio === void 0 ? {} : { ratio: preset.ratio },
17502
+ durationSeconds: preset.durationSeconds,
17503
+ resolution: preset.resolution,
17504
+ media: {}
17505
+ });
17506
+ }
17507
+ /**
17508
+ * 构造 `/video` 的 `CommandContribution`。
17509
+ *
17510
+ * 注册在插件的 `ctx.inject(['commandUi'], scope => …)` 里,与宿主
17511
+ * `ui-message-feedback` 的 `/feedback` 装饰同一种接线方式。
17512
+ *
17513
+ * @param ctx - 浏览器根上下文。
17514
+ * @param options - 打开弹窗与填输入框的通道。
17515
+ * @returns 一份可直接交给 `commandUi.register` 的贡献。
17516
+ */
17517
+ function createVideoCommand(ctx, options) {
17518
+ const t = options.translate ?? fallbackTranslate$1;
17519
+ const fill = options.fill ?? ((sessionId, text) => fillComposer(ctx, sessionId, text));
17520
+ const defaultsOf = options.defaults ?? (() => normalizeVideoDefaults({}));
17521
+ return {
17522
+ name: VIDEO_COMMAND_NAME,
17523
+ label: () => t("videoCommandLabel"),
17524
+ description: () => t("videoCommandDesc"),
17525
+ available: (session) => sessionRouteOf(ctx, session.sessionId)?.provider === MINIMAX_CODE_PROVIDER,
17526
+ ui: {
17527
+ kind: "popupSelect",
17528
+ options: async () => {
17529
+ const rows = videoPresets(t).map(({ preset, label }) => ({
17530
+ id: preset.id,
17531
+ label,
17532
+ group: {
17533
+ name: "preset",
17534
+ label: t("videoPresetGroup")
17535
+ }
17536
+ }));
17537
+ rows.push({
17538
+ id: CUSTOM_OPTION_ID,
17539
+ label: t("videoPresetCustom"),
17540
+ detail: t("videoPresetCustomDesc"),
17541
+ group: {
17542
+ name: "custom",
17543
+ label: t("videoPresetCustom")
17544
+ }
17545
+ });
17546
+ return rows;
17547
+ },
17548
+ onSelect: async (option, session) => {
17549
+ const defaults = defaultsOf();
17550
+ if (option.id === "custom") {
17551
+ options.openForm(session.sessionId, defaults);
17552
+ return;
17553
+ }
17554
+ const preset = videoPresets(t).find((row) => row.preset.id === option.id)?.preset;
17555
+ if (preset === void 0) return;
17556
+ fill(session.sessionId, presetDraftText(preset));
15102
17557
  }
15103
- if (shouldScan) break;
15104
17558
  }
15105
- if (shouldScan) debouncedScan();
15106
- });
15107
- observer.observe(document.body, {
15108
- childList: true,
15109
- subtree: true
15110
- });
15111
- const themeObserver = new MutationObserver(() => {
15112
- initMermaidConfig();
15113
- for (const el of document.querySelectorAll("[data-dsh-mermaid-status=\"done\"]")) {
15114
- processedHosts.delete(el);
15115
- el.removeAttribute("data-dsh-mermaid-status");
15116
- const prevWrapper = el.previousElementSibling;
15117
- if (prevWrapper && prevWrapper.classList.contains("dsh-mermaid-wrapper")) prevWrapper.remove();
15118
- el.style.display = "";
17559
+ };
17560
+ }
17561
+ //#endregion
17562
+ //#region src/client/minimax-code/video-command-host.tsx
17563
+ /**
17564
+ * 「/video 命令 + 参数弹窗」的挂载点,由 `src/client/index.tsx` 调用。
17565
+ *
17566
+ * 挂两样东西:
17567
+ *
17568
+ * 1. **弹窗的渲染座位**:一个客户端容器,portal 到 `document.body`。它不挂在任何
17569
+ * 会话槽位上——弹窗是**全局**的,而 `/video` 命令的回调在一段没有 React 树的
17570
+ * 代码里;把两者连起来的是这个容器持有的 `open` 状态加一个模块级句柄。
17571
+ * 2. **命令本体**:`createVideoCommand` 产出的 popupSelect 贡献,注册到
17572
+ * `ctx.commandUi`,与宿主 `ui-message-feedback` 的 `/feedback` 装饰同一形状。
17573
+ *
17574
+ * ## 为什么命令回调要经过一个句柄
17575
+ *
17576
+ * `CommandUiSpec` 的回调是普通函数,不是 React 组件,拿不到 setState。所以容器把
17577
+ * 自己的 `openForm` 登记到一个模块级的可变引用上,命令回调通过它打开弹窗。容器
17578
+ * 卸载时把这个引用清掉(只清自己那一个),回调因此永远调不到一个已卸载的组件。
17579
+ *
17580
+ * ## 注册失败了怎么办
17581
+ *
17582
+ * 与家族其它模块一致:吞掉、不抛。web shell 在插件 apply 抛错时整页启动失败,
17583
+ * 一个外部插件不能因为「宿主这一版没有 commandUi」就把 GUI 带下水。
17584
+ *
17585
+ * @module @eddyskywalker/dsh-chatgpt-subscription/client/minimax-code/video-command-host
17586
+ */
17587
+ /**
17588
+ * 当前挂着的容器句柄。
17589
+ *
17590
+ * 模块级的可变引用是刻意的:命令回调由宿主持有,它没有别的通道能碰到 React 树。
17591
+ * 只允许一个值,因为一个浏览器页只有一棵树、一个弹窗。
17592
+ */
17593
+ let activeHandle;
17594
+ /**
17595
+ * 打开参数弹窗。
17596
+ *
17597
+ * 没有容器挂着时什么也不做——插件被禁用、或页面还没挂载时调到这里,静默即可;
17598
+ * 抛错只会让一次斜杠菜单点击变成控制台里的一条异常。
17599
+ *
17600
+ * @param sessionId - 会话 id。
17601
+ * @param defaults - 表单起手值。
17602
+ */
17603
+ function openVideoForm(sessionId, defaults) {
17604
+ activeHandle?.(sessionId, defaults);
17605
+ }
17606
+ /**
17607
+ * 构造弹窗容器。
17608
+ *
17609
+ * 常驻挂载、`open` 为 false 时渲染出 null,而不是按需创建:请求来自命令回调,那时
17610
+ * 组件树已经渲染完了,没有第二次渲染去创建它。
17611
+ *
17612
+ * @param ctx - 浏览器根上下文;提交时用它把文本写进对应会话的输入框。
17613
+ * @returns 一个不需要 props 的组件。
17614
+ */
17615
+ function createVideoCommandHost(ctx) {
17616
+ return function VideoCommandHost() {
17617
+ const [request, setRequest] = (0, react.useState)(null);
17618
+ const sessionRef = (0, react.useRef)(null);
17619
+ const handle = (0, react.useCallback)((sessionId, defaults) => {
17620
+ sessionRef.current = sessionId;
17621
+ setRequest({ defaults: normalizeVideoDefaults(defaults) });
17622
+ }, []);
17623
+ (0, react.useEffect)(() => {
17624
+ activeHandle = handle;
17625
+ return () => {
17626
+ if (activeHandle === handle) activeHandle = void 0;
17627
+ };
17628
+ }, [handle]);
17629
+ const close = (0, react.useCallback)(() => {
17630
+ setRequest(null);
17631
+ }, []);
17632
+ const submit = (0, react.useCallback)((text) => {
17633
+ const sessionId = sessionRef.current;
17634
+ if (sessionId === null) return false;
17635
+ return fillComposer(ctx, sessionId, text);
17636
+ }, []);
17637
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoFormDialog, {
17638
+ open: request !== null,
17639
+ onClose: close,
17640
+ onSubmit: submit,
17641
+ defaults: request?.defaults ?? normalizeVideoDefaults({}),
17642
+ t: fallbackTranslate$4
17643
+ });
17644
+ };
17645
+ }
17646
+ /**
17647
+ * 从上下文里取 `commandUi`。
17648
+ *
17649
+ * 属性读只在 `'commandUi'` 已写进当前 scope 的 `inject` 名单时合法;本插件的静态
17650
+ * `inject` 不包含它(以免无 `ui-commands` 的旧宿主卡在 pending),直接读
17651
+ * `ctx.commandUi` 会被 Cordis 的属性守卫当场抛错、使整个插件 fiber 落入
17652
+ * `failed`。因此这里先试属性(供 `ctx.inject(['commandUi'], scope => …)` 的子
17653
+ * scope 与测试替身使用)、失败再退回 `get('commandUi')`。
17654
+ */
17655
+ function commandUiOf(scope) {
17656
+ try {
17657
+ const direct = scope.commandUi;
17658
+ if (direct !== void 0) return direct;
17659
+ } catch {}
17660
+ try {
17661
+ return scope.get?.("commandUi");
17662
+ } catch {
17663
+ return;
17664
+ }
17665
+ }
17666
+ /**
17667
+ * 把 `/video` 命令挂到宿主的命令服务上。
17668
+ *
17669
+ * 与宿主 `ui-message-feedback` 的 `/feedback` 装饰同一种接线方式:若 `commandUi`
17670
+ * 已经在位(或当前是无 `inject` 能力的测试替身),就地注册;若 `commandUi` 晚于
17671
+ * 本插件激活,则经 `ctx.inject(['commandUi'], scope => …)` 在它上线时补注册。
17672
+ *
17673
+ * 注册失败只吞掉:宿主这一版可能没有 `commandUi`(它由 `ui-commands` 提供),一个
17674
+ * 可选依赖缺席不该让整个插件 apply 失败。
17675
+ *
17676
+ * @param ctx - 浏览器根上下文。
17677
+ * @returns 卸载函数;服务缺席时是一个空操作。
17678
+ */
17679
+ function installVideoCommand(ctx) {
17680
+ const registerOn = (scope) => {
17681
+ const commandUi = commandUiOf(scope);
17682
+ if (typeof commandUi?.register !== "function") return () => {};
17683
+ try {
17684
+ return commandUi.register(createVideoCommand(ctx, {
17685
+ openForm: openVideoForm,
17686
+ defaults: () => readVideoDefaults(),
17687
+ translate: fallbackTranslate$4
17688
+ }));
17689
+ } catch {
17690
+ return () => {};
15119
17691
  }
15120
- debouncedScan();
15121
- });
15122
- themeObserver.observe(document.documentElement, {
15123
- attributes: true,
15124
- attributeFilter: [
15125
- "class",
15126
- "data-theme",
15127
- "data-color-mode"
15128
- ]
17692
+ };
17693
+ const immediate = commandUiOf(ctx);
17694
+ const injectService = ctx.inject;
17695
+ if (typeof immediate?.register === "function" || typeof injectService !== "function") return registerOn(ctx);
17696
+ const child = injectService.call(ctx, ["commandUi"], (scope) => {
17697
+ scope.effect(() => registerOn(scope), "dsh-minimax-code: /video command");
15129
17698
  });
15130
17699
  return () => {
15131
- observer.disconnect();
15132
- themeObserver.disconnect();
15133
- window.clearTimeout(timer);
17700
+ child?.dispose?.();
15134
17701
  };
15135
17702
  }
15136
17703
  //#endregion
@@ -17233,7 +19800,31 @@ window.__ModuleLoader__.load({
17233
19800
  locale: NS,
17234
19801
  inject: (sessionId) => ({ loadImage: imageLoader(ctx, sessionId) })
17235
19802
  }, CodexImageToolView));
19803
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
19804
+ name: "tool.call.toolview",
19805
+ key: MINIMAX_VIDEO_TOOL_NAME,
19806
+ locale: NS_MINIMAX_CODE,
19807
+ inject: (sessionId) => ({
19808
+ sessionId,
19809
+ readVideo: videoBytesLoader(ctx)
19810
+ })
19811
+ }, MinimaxVideoToolView));
19812
+ installVideoDocumentPreview(ctx);
17236
19813
  installProgressUpdatesClient(ctx);
19814
+ ctx.effect(() => installVideoCommand(ctx), "dsh-minimax-code: /video command");
19815
+ const VideoCommandHost = createVideoCommandHost(ctx);
19816
+ ctx.slots.inject("conversation.input.overlay", () => {
19817
+ try {
19818
+ return ctx.slots.register({
19819
+ name: "conversation.input.overlay",
19820
+ id: "minimax-video-form",
19821
+ order: 3,
19822
+ locale: NS_MINIMAX_CODE
19823
+ }, VideoCommandHost);
19824
+ } catch {
19825
+ return () => {};
19826
+ }
19827
+ });
17237
19828
  }
17238
19829
  /**
17239
19830
  * The seats every composer badge is handed.
@@ -17287,6 +19878,128 @@ window.__ModuleLoader__.load({
17287
19878
  if (legacyResolve !== void 0) return (attachment) => legacyResolve(sessionId, attachment);
17288
19879
  return () => Promise.reject(/* @__PURE__ */ new Error("no client service resolves an image URL"));
17289
19880
  }
19881
+ /**
19882
+ * 从上下文里读一个**可选**的浏览器服务,读不到就当作没有。
19883
+ *
19884
+ * 只有 `get` 是 Cordis 的基础成员;没写进 `inject` 的服务名直接当属性读会抛错。
19885
+ * 而本文件要读的这几个(`remote` 的命名空间、宿主的 `documentPreviews`)都属于
19886
+ * 「装了才有」:文档预览插件不是本插件的依赖。
19887
+ *
19888
+ * 与 `./progress-updates/index.ts` 的 `serviceOf` 同形——那边这么写是因为
19889
+ * `sidebarRight*` 可选,这边同理。测试里的上下文替身没有 `get`,所以取值本身
19890
+ * 也必须包在 try 里,而不是只包 `ctx.get` 的调用。
19891
+ *
19892
+ * @param ctx - 浏览器根上下文。
19893
+ * @param name - 服务名。
19894
+ * @returns 服务实例,或 undefined。
19895
+ */
19896
+ function optionalService(ctx, name) {
19897
+ try {
19898
+ return ctx.get?.(name);
19899
+ } catch {
19900
+ return;
19901
+ }
19902
+ }
19903
+ /**
19904
+ * 从上下文里解析 `remote.workspaceFiles` 命名空间。
19905
+ *
19906
+ * 在真实宿主中,`remote.workspaceFiles` 是 `ClientRemoteService` 通过
19907
+ * `new RemoteNamespaceService(ctx, 'workspaceFiles')` 注册的独立 Cordis 服务
19908
+ * (服务名 `'remote.workspaceFiles'`)。由于本插件的静态 `inject` 只声明了
19909
+ * `'remote'` 与 `'remote.session'`,直接读 `ctx.remote.workspaceFiles` 属性会触发
19910
+ * Cordis 的属性守卫并抛出 `cannot get property "remote.workspaceFiles" without inject`。
19911
+ * 因此这里优先走 `optionalService(ctx, 'remote.workspaceFiles')`(即 `ctx.get`),
19912
+ * 仅在测试替身把 `workspaceFiles` 挂成普通对象属性时才在 `try/catch` 内回退读取。
19913
+ */
19914
+ function resolveWorkspaceFiles(ctx) {
19915
+ const direct = optionalService(ctx, "remote.workspaceFiles");
19916
+ if (typeof direct?.readBytes === "function") return direct;
19917
+ try {
19918
+ const nested = optionalService(ctx, "remote")?.workspaceFiles;
19919
+ if (typeof nested?.readBytes === "function") return nested;
19920
+ } catch {}
19921
+ }
19922
+ /**
19923
+ * 取视频字节:把 `ctx.remote.workspaceFiles.readBytes` 收成一条窄通道。
19924
+ *
19925
+ * 视频在浏览器里**没有别的取字节方式**:`loadImage` 是附件专用的,`<video src>`
19926
+ * 也读不到工作区。宿主侧边栏的图像预览走的正是这条远端
19927
+ * (`ui-sidebar-documentpreview/src/client/rpc.ts` 的 `ReadDocumentBytes`),
19928
+ * 这里照抄同一调用:不传 `range` 即整文件,上限由宿主 `maxFileBytes` 决定。
19929
+ *
19930
+ * 远端是**可选的**且按需解析:不在 `apply(ctx)` 瞬间把服务抓死,以免
19931
+ * `remote.workspaceFiles` 晚于本插件挂载时读到空值;取不到就拒绝这次读取——
19932
+ * 两个界面都会把它显示成占位块,而不是崩掉。
19933
+ *
19934
+ * @param ctx - 浏览器根上下文。
19935
+ * @returns 与 `readBytes` 同形的取字节通道。
19936
+ */
19937
+ function videoBytesLoader(ctx) {
19938
+ return async (sessionId, path, signal) => {
19939
+ const workspaceFiles = resolveWorkspaceFiles(ctx);
19940
+ if (typeof workspaceFiles?.readBytes !== "function") throw new Error("no remote workspaceFiles reader is mounted");
19941
+ return (await workspaceFiles.readBytes.bind(workspaceFiles)(sessionId, path, {}, signal)).data;
19942
+ };
19943
+ }
19944
+ /**
19945
+ * 从上下文里取 `documentPreviews` 注册表。
19946
+ *
19947
+ * 先试属性读(供 `ctx.inject(['documentPreviews'], scope => …)` 的子 scope 与测试
19948
+ * 替身使用),被 Cordis 拒绝时退回 `optionalService`(`ctx.get`)。
19949
+ */
19950
+ function documentPreviewsOf(scope) {
19951
+ try {
19952
+ const direct = scope.documentPreviews;
19953
+ if (direct !== void 0) return direct;
19954
+ } catch {}
19955
+ return optionalService(scope, "documentPreviews");
19956
+ }
19957
+ /**
19958
+ * 挂载 MiniMax 视频的侧边栏渲染器。
19959
+ *
19960
+ * 两个注册各管一半,形状逐字照抄宿主
19961
+ * `ui-sidebar-documentpreview/src/client/image/index.ts`:
19962
+ *
19963
+ * 1. `documentPreviews.register` 声明「mp4/webm/mov/m4v 归我」,`priority: 'extension'`
19964
+ * 让外部实现排在内置实现之前(`document/registry.ts` 的排序规则);
19965
+ * 2. `sidebar.right.tab.document` 的 keyed 槽用**同一个 id** 注册正文。
19966
+ *
19967
+ * 两段都包在 `ctx.effect` 里,返回注册器给的幂等卸载函数:插件行被禁用时定义与
19968
+ * 正文一起消失,不会留下一个注册了却渲染不出的实现。
19969
+ *
19970
+ * `documentPreviews` 是宿主文档预览插件通过 `ctx.reflect.provide` 给的服务,它的
19971
+ * `inject` 比本插件多 `sidebarRightTabs` / `remote.workspaceFiles` / `resources`,
19972
+ * 因此在浏览器启动时通常晚于本插件激活。若它在 `apply(ctx)` 瞬间尚未上线,则通过
19973
+ * `ctx.inject(['documentPreviews'], …)` 在它上线时自动补注册。
19974
+ *
19975
+ * @param ctx - 浏览器根上下文。
19976
+ */
19977
+ function installVideoDocumentPreview(ctx) {
19978
+ const readVideo = videoBytesLoader(ctx);
19979
+ const registerPreview = (scope) => {
19980
+ const registry = documentPreviewsOf(scope);
19981
+ if (typeof registry?.register !== "function") return () => {};
19982
+ try {
19983
+ const t = ctx.locale.bind(NS_MINIMAX_CODE);
19984
+ return registry.register(minimaxVideoPreviewDefinition(() => t("videoTitle")));
19985
+ } catch {
19986
+ return () => {};
19987
+ }
19988
+ };
19989
+ const immediate = documentPreviewsOf(ctx);
19990
+ const injectService = ctx.inject;
19991
+ if (typeof immediate?.register === "function" || typeof injectService !== "function") {
19992
+ if (typeof immediate?.register === "function") ctx.effect(() => registerPreview(ctx), "dsh-minimax-code: video preview definition");
19993
+ } else injectService.call(ctx, ["documentPreviews"], (scope) => {
19994
+ scope.effect(() => registerPreview(scope), "dsh-minimax-code: video preview definition");
19995
+ });
19996
+ ctx.slots.inject("sidebar.right.tab.document", () => ctx.slots.register({
19997
+ name: "sidebar.right.tab.document",
19998
+ key: MINIMAX_VIDEO_PREVIEW_ID,
19999
+ locale: NS_MINIMAX_CODE,
20000
+ inject: () => ({ readVideo })
20001
+ }, MinimaxVideoDocumentBody));
20002
+ }
17290
20003
  //#endregion
17291
20004
  exports.apply = apply;
17292
20005
  exports.inject = inject;