dsh-connect-minimaxcode 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -357,7 +357,7 @@ A picture *can* reach a conversation: the host publishes
357
357
  store → durable reference" on its own. But the attachment entry point is a
358
358
  browser `File` upload and there is **no attachment library** to pick from,
359
359
  so it still has to travel as part of the next prompt. That last hop is
360
- planned for the next release.
360
+ done in 0.6.1 (see below).
361
361
 
362
362
  ### About token refresh
363
363
 
@@ -384,6 +384,19 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
384
384
 
385
385
  ## Changelog
386
386
 
387
+ ### 0.6.2 (2026-11-11)
388
+
389
+ - **Fixed "could not send it to the conversation"**: the previous version looked for the session among the slot's props, but the host hands the session to the slot's **injection callback** (the slot is session-scoped), so the identifier was always missing and the picture had nowhere to go. The session is now taken the way the host gives it.
390
+ - **One more way to deliver**: when the host offers no session face to send with, the plugin falls back to the input box's own attachment channel — the very road a picture dragged in by hand takes, so it appears in the input box and you press send.
391
+ - The picture's arrival is the feedback, so nothing is left on screen and the composer stays clean.
392
+ - `tests/generate-dock.spec.ts` rewritten to drive the plugin **the way the host does** (6 cases): the session identifier arrives through the injection, the picture travels as `{type:"image", mediaType, data}` with the prompt, the fallback into the input box when there is no way to send, and the message shown when neither is there.
393
+
394
+ ### 0.6.1 (2026-11-11)
395
+
396
+ - **A generated picture can now go straight into the conversation**: the composer tool row gained a **Generate an image** entry. Write a prompt, press generate, and the picture is sent as part of this turn — through the host's own road (`ctx.sessions.binding(id).session.prompt(content, mode, signal, requestId)`, the very call the send button makes), so the host validates it, commits it as a durable attachment and treats it as an ordinary message.
397
+ - A host that exposes no session face gets a plain "could not send it to the conversation" instead of a silent failure, and the plugin page's panel keeps working regardless.
398
+ - Added `tests/generate-dock.spec.ts` (4 cases) pinning: the entry is registered in the tool row, it declares the `sessions` service, the picture travels as `{type:"image", mediaType, data}` (raw base64, no data-url prefix) together with the prompt, and the message shown when there is no session to send to. All 252 cases pass.
399
+
387
400
  ### 0.6.0 (2026-11-11)
388
401
 
389
402
  - **Added H3 image and video generation**: the plugin page gained a **Generate** panel that drives MiniMax's `MiniMax-H3-Image` (text to image) and `MiniMax-H3` (video) directly. It goes through the endpoints the open platform's documentation gives (`POST /v1/images/generations`, `POST /v2/video_generation` plus polling `/v2/query/video_generation/{id}`), and **no key has to be configured**: the generation endpoints want the account's own open-platform key, and the plugin gets it by asking `token_plan_credit` with the OAuth bearer — the endpoint it already reads for the balance.
package/README.md CHANGED
@@ -368,7 +368,7 @@ GET https://api.minimax.cn/v2/query/video_generation/{id} # 轮询任务
368
368
  1. **不能像普通模型那样出现在模型选择器里。** DSH 宿主的 `ctx.llm` 只有对话适配器这一个注册点(`registerAdapter`),没有图像 provider 的挂载处;模型描述符 `LlmModelInfo` 也没有「输出模态」字段,选择器无从区分图像模型。pi-ai 自己虽有图像生成 API(`ImagesProvider` / `generateImages`),但 DSH 从不构造 `ImagesModels`,插件自己 `new` 一个也没有人会读。这三点都是宿主层面的事,插件改不动。
369
369
  2. **视频在 DSH 侧目前没有承载。** `pi-ai`、`dsh-llm`、`dsh-attachment`、`dsh-llm-pi-ai` 四个包里关于 video 的声明各为零处 —— 没有视频内容块、没有附件媒体类型、没有渲染。视频因此只能生成后在面板里播放与下载,不能像图片那样进对话。
370
370
 
371
- 图片进对话是可行的:宿主公开了 `ctx.attachments.admitPromptContent()`,插件可以独立完成「生成 → 落库 → 拿到持久引用」;但附件入口是浏览器 `File` 上传,**没有附件库**可供挑选,因此要进对话仍需作为下一轮 prompt 的一部分提交。这一步的宿主接线待下一版完成。
371
+ 图片进对话是可行的:宿主公开了 `ctx.attachments.admitPromptContent()`,插件可以独立完成「生成 → 落库 → 拿到持久引用」;但附件入口是浏览器 `File` 上传,**没有附件库**可供挑选,因此要进对话仍需作为下一轮 prompt 的一部分提交。这一步已由 0.6.1 完成(见下)。
372
372
 
373
373
  ### 关于续期
374
374
 
@@ -395,6 +395,19 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
395
395
 
396
396
  ## 更新记录
397
397
 
398
+ ### 0.6.2(2026-11-11)
399
+
400
+ - **修复「无法发送到对话」**:上一版从插槽的 props 里找会话标识,但宿主是把会话交给**插槽的注入回调**的(该插槽是会话作用域的),于是标识始终为空,图片自然送不出去。现在按宿主的方式取会话标识。
401
+ - **多一条投递路径**:宿主若不提供直接发送的会话面,插件会退回到输入框自己的附件通道 —— 与手动拖一张图进输入框完全同路,图片出现在输入框里,由你按下发送。
402
+ - 图片到达本身就是反馈,成功后不再留提示条,输入框保持干净。
403
+ - `tests/generate-dock.spec.ts` 重写为**按宿主的真实投递方式**驱动插件(6 个用例):会话标识来自注入回调、图片作为 `{type:"image", mediaType, data}` 随提示词发出、没有发送通道时退回输入框、以及两条路都没有时的提示。
404
+
405
+ ### 0.6.1(2026-11-11)
406
+
407
+ - **生成的图片现在能直接进对话**:输入框工具行新增一个「生成图片」入口,写下提示词点生成,图片会作为**这一轮消息的一部分**发给模型 —— 走的是宿主自己那条路(`ctx.sessions.binding(id).session.prompt(content, mode, signal, requestId)`,与输入框按发送键完全相同),因此图片会被宿主校验、落成持久附件,并按普通消息参与对话。
408
+ - 宿主若不暴露会话句柄,该入口会明确提示「无法发送到对话」,而不是静默失败;插件页的生成面板仍可照常使用。
409
+ - 新增 `tests/generate-dock.spec.ts`(4 个用例)钉住:入口注册在工具行、声明了 `sessions` 服务、生成的图以 `{type:"image", mediaType, data}` 的形态(原始 base64,不带 data-url 前缀)随提示词一起发出、以及没有会话句柄时的提示。全量 252 个用例通过。
410
+
398
411
  ### 0.6.0(2026-11-11)
399
412
 
400
413
  - **新增 H3 图像与视频生成**:插件页多了一个「生成」面板,可直接调用 MiniMax 的 `MiniMax-H3-Image`(文生图)与 `MiniMax-H3`(视频生成)。走的是开放平台官方文档给出的接口(`POST /v1/images/generations`、`POST /v2/video_generation` + 轮询 `/v2/query/video_generation/{id}`),**用户无需配置任何密钥** —— 生成接口要的是账户自己的开放平台密钥,插件用 OAuth 令牌问本来就在读的 `token_plan_credit` 就能拿到。
package/lib/client.js CHANGED
@@ -94,6 +94,11 @@
94
94
  generateSaved: '已存入 DSH 附件库',
95
95
  generateFailed: '生成失败',
96
96
  generateHint: '生成走 MiniMax 的开放平台,用的是你账号自己的额度。图片会并存进 DSH 的附件库。',
97
+ generateToChat: '生成图片',
98
+ generateToChatHint: '生成的图片会作为这一轮消息的一部分发给模型。',
99
+ generateSent: '图片已发送到对话',
100
+ generateDrafted: '图片已放入输入框',
101
+ generateSendFailed: '无法发送到对话,请到插件页生成后下载。',
97
102
  };
98
103
  const en = {
99
104
  title: 'MiniMax Code credits',
@@ -161,6 +166,11 @@
161
166
  generateSaved: 'Saved to DSH attachments',
162
167
  generateFailed: 'Generation failed',
163
168
  generateHint: 'Generation runs on MiniMax\'s open platform with your own account\'s allowance. Images are also stored in DSH\'s attachment store.',
169
+ generateToChat: 'Generate an image',
170
+ generateToChatHint: 'The generated picture is sent as part of this turn.',
171
+ generateSent: 'The picture is on its way to the conversation',
172
+ generateDrafted: 'The picture is in the input box',
173
+ generateSendFailed: 'Could not send it to the conversation; generate on the plugin page and download it instead.',
164
174
  };
165
175
  const loader = globalThis.__ModuleLoader__;
166
176
  if (loader === undefined)
@@ -170,6 +180,13 @@
170
180
  factory(require) {
171
181
  const React = require('react');
172
182
  const h = React.createElement;
183
+ /**
184
+ * The host's client context, once the plugin is applied.
185
+ *
186
+ * The components are closures over the module, not over the context, so
187
+ * this is how they reach the host's services.
188
+ */
189
+ let clientContext;
173
190
  /** Format a credit amount without pretending to more precision. */
174
191
  function credits(value, digits) {
175
192
  if (value === undefined || !Number.isFinite(value))
@@ -419,6 +436,167 @@
419
436
  return null;
420
437
  return h(CreditPill, props);
421
438
  }
439
+ /**
440
+ * Generate a picture and send it to the conversation.
441
+ *
442
+ * The composer already knows how to send a message with an image in it,
443
+ * and the picture the upstream produces is an ordinary image file, so
444
+ * the dock does the same thing the composer does: it asks the session
445
+ * to accept a turn whose content is the picture plus the prompt.
446
+ *
447
+ * Nothing here is required of the host beyond what the composer itself
448
+ * uses. A host that does not expose the session face simply gets the
449
+ * message that says so, and the plugin page still offers the picture.
450
+ */
451
+ function GenerateDock(props) {
452
+ const t = props.t ?? ((key, params) => {
453
+ const template = zh[key] ?? key;
454
+ return template.replace(/\\{(\\w+)\\}/g, (_match, name) => String(params?.[name] ?? ''));
455
+ });
456
+ const [open, setOpen] = React.useState(false);
457
+ const [prompt, setPrompt] = React.useState('');
458
+ const [busy, setBusy] = React.useState(false);
459
+ const [note, setNote] = React.useState('');
460
+ /**
461
+ * The session the picture belongs to.
462
+ *
463
+ * The host hands the session id to the slot through its injection,
464
+ * because the slot is session-scoped; it is also carried on the
465
+ * standard props, so either may deliver it.
466
+ */
467
+ const sessionId = () => props.sessionId;
468
+ /**
469
+ * The session's own send path.
470
+ *
471
+ * `binding(id).session.prompt(content, mode, signal, requestId)` is
472
+ * what the composer calls when the person presses send, so a picture
473
+ * sent this way is a normal turn.
474
+ */
475
+ const sessionFace = () => {
476
+ const id = sessionId();
477
+ if (id === undefined)
478
+ return undefined;
479
+ const binding = clientContext?.sessions?.binding?.(id);
480
+ const send = binding?.session?.prompt;
481
+ return typeof send === 'function' ? { prompt: send } : undefined;
482
+ };
483
+ /**
484
+ * The conversation's own draft channel.
485
+ *
486
+ * The composer builds a draft from a browser File and hands its id to
487
+ * the input box, which is how a picture attached by hand appears
488
+ * there. Using the same two steps leaves the picture in the input box
489
+ * for the person to send, rather than sending on their behalf.
490
+ */
491
+ const draftFace = () => {
492
+ const id = sessionId();
493
+ if (id === undefined)
494
+ return undefined;
495
+ const scoped = clientContext?.sessions?.scope?.(id);
496
+ const conversation = scoped?.get?.('conversation');
497
+ const create = conversation?.createDrafts;
498
+ return typeof create === 'function'
499
+ ? { createDrafts: create.bind(conversation) }
500
+ : undefined;
501
+ };
502
+ /** The bytes behind a data URL. */
503
+ const base64Bytes = (dataUrl) => {
504
+ const comma = dataUrl.indexOf(',');
505
+ const base64 = comma < 0 ? dataUrl : dataUrl.slice(comma + 1);
506
+ const binary = atob(base64);
507
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
508
+ for (let index = 0; index < binary.length; index += 1) {
509
+ bytes[index] = binary.charCodeAt(index);
510
+ }
511
+ return bytes;
512
+ };
513
+ /** Turn a data URL into the parts the session's admission accepts. */
514
+ const imagePart = (dataUrl, mediaType) => {
515
+ const comma = dataUrl.indexOf(',');
516
+ if (comma < 0)
517
+ return undefined;
518
+ return { type: 'image', mediaType, data: dataUrl.slice(comma + 1) };
519
+ };
520
+ async function send() {
521
+ const text = prompt.trim();
522
+ if (text === '')
523
+ return;
524
+ setBusy(true);
525
+ setNote('');
526
+ try {
527
+ const response = await fetch(GENERATE_PATH, {
528
+ method: 'POST',
529
+ headers: { 'content-type': 'application/json' },
530
+ body: JSON.stringify({ kind: 'image', prompt: text, area: '1K' }),
531
+ });
532
+ const body = await response.json();
533
+ if (body.ok !== true || body.dataUrl === undefined) {
534
+ setNote(body.error ?? t('generateFailed'));
535
+ return;
536
+ }
537
+ const part = imagePart(body.dataUrl, body.mediaType ?? 'image/jpeg');
538
+ if (part === undefined) {
539
+ setNote(t('generateFailed'));
540
+ return;
541
+ }
542
+ // Sending the picture as a turn is what the dock promises; the
543
+ // draft channel is the fallback for a host that only offers that.
544
+ const face = sessionFace();
545
+ if (face !== undefined) {
546
+ await face.prompt([part, { type: 'text', text }], 'queue', undefined, `minimax-h3-${Date.now()}`);
547
+ setPrompt('');
548
+ setOpen(false);
549
+ // The picture's own arrival is the feedback: it is in the
550
+ // conversation now, so the composer needs no banner.
551
+ setNote('');
552
+ return;
553
+ }
554
+ const drafts = draftFace();
555
+ const addAttachments = props.inputActions?.addAttachments;
556
+ if (drafts !== undefined && typeof addAttachments === 'function') {
557
+ const file = new File([base64Bytes(body.dataUrl)], 'minimax-h3-image.jpg', { type: body.mediaType ?? 'image/jpeg' });
558
+ const created = drafts.createDrafts(sessionId(), [file], true);
559
+ addAttachments(created.map(draft => draft.id));
560
+ setPrompt('');
561
+ setOpen(false);
562
+ setNote('');
563
+ return;
564
+ }
565
+ setNote(t('generateSendFailed'));
566
+ }
567
+ catch {
568
+ setNote(t('generateSendFailed'));
569
+ }
570
+ finally {
571
+ setBusy(false);
572
+ }
573
+ }
574
+ if (!open) {
575
+ return h('button', {
576
+ type: 'button',
577
+ style: buttonStyle,
578
+ title: t('generateToChatHint'),
579
+ onClick: () => { setOpen(true); setNote(''); },
580
+ }, t('generateToChat'));
581
+ }
582
+ return h('div', { style: dockStyle, 'data-minimax-generate-dock': true }, h('input', {
583
+ value: prompt,
584
+ placeholder: t('generatePrompt'),
585
+ style: dockInputStyle,
586
+ disabled: busy,
587
+ onChange: (event) => setPrompt(event.target.value),
588
+ }), h('button', {
589
+ type: 'button',
590
+ style: buttonStyle,
591
+ disabled: busy || prompt.trim() === '',
592
+ onClick: () => { void send(); },
593
+ }, busy ? t('generateRunning') : t('generateRun')), h('button', {
594
+ type: 'button',
595
+ style: buttonStyle,
596
+ disabled: busy,
597
+ onClick: () => { setOpen(false); setPrompt(''); setNote(''); },
598
+ }, '✕'), note === '' ? null : h('span', { style: mutedStyle }, note));
599
+ }
422
600
  /** Ask the host to do something, and let the card read the result. */
423
601
  async function act(action, body) {
424
602
  try {
@@ -694,6 +872,21 @@
694
872
  opacity: 0.8,
695
873
  padding: '1px 8px',
696
874
  };
875
+ const dockStyle = {
876
+ display: 'flex',
877
+ alignItems: 'center',
878
+ gap: '4px',
879
+ };
880
+ const dockInputStyle = {
881
+ width: '160px',
882
+ padding: '2px 6px',
883
+ borderRadius: '999px',
884
+ border: '1px solid var(--dsh-composer-dock-inset, currentColor)',
885
+ background: 'transparent',
886
+ color: 'inherit',
887
+ font: 'inherit',
888
+ fontSize: '12px',
889
+ };
697
890
  const detailStyle = {
698
891
  position: 'absolute',
699
892
  bottom: 'calc(100% + 6px)',
@@ -828,9 +1021,15 @@
828
1021
  borderRadius: '8px',
829
1022
  };
830
1023
  return {
831
- inject: ['slots', 'locale'],
1024
+ // The sessions service is what lets a generated picture be delivered
1025
+ // into the conversation the composer belongs to.
1026
+ inject: ['slots', 'locale', 'sessions'],
832
1027
  apply(ctx) {
833
1028
  const context = ctx;
1029
+ // The components need the host's sessions service to deliver a
1030
+ // generated picture into the conversation, so it is kept where the
1031
+ // components can reach it.
1032
+ clientContext = context;
834
1033
  // Visible text goes through the host's locale service.
835
1034
  context.effect(() => context.locale.register(NAMESPACE, { zh, en }), `${BUNDLE_NAME}: locale`);
836
1035
  // The composer and plugin-page slots carry no translator in their
@@ -850,6 +1049,18 @@
850
1049
  locale: NAMESPACE,
851
1050
  inject: () => ({ t }),
852
1051
  }, CreditPillGate));
1052
+ // The image dock sits beside the balance: it is the same provider's
1053
+ // capability, and it needs the session the composer belongs to.
1054
+ context.slots.inject('conversation.input.left', () => context.slots.register({
1055
+ name: 'conversation.input.left',
1056
+ id: 'minimax-generate',
1057
+ order: 5,
1058
+ locale: NAMESPACE,
1059
+ // The slot is session-scoped, and its injection receives the
1060
+ // session it is scoped to — which is how the dock learns which
1061
+ // conversation a picture belongs to.
1062
+ inject: (sessionId) => ({ t, sessionId }),
1063
+ }, GenerateDock));
853
1064
  // The plugin's own page, keyed by the package name, where the
854
1065
  // balance and the expiry schedule are spelled out.
855
1066
  context.slots.inject('plugins.bundle.config', () => context.slots.register({
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-minimaxcode",
3
3
  "displayName": "DSH Connect MiniMax Code",
4
4
  "description": "把本机已登录的 MiniMax Code 桌面 App 模型接入 DeepSeek Harness:零配置、Anthropic Messages 兼容、动态模型目录,并显示积分余额与到期 — bring MiniMax Code desktop app models into DSH with a dynamic model catalog and a credit balance with expiry details.",
5
- "version": "0.6.0",
5
+ "version": "0.6.2",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {