dsh-mobile-plugin 0.1.87 → 0.1.88

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.
@@ -138,6 +138,10 @@ function parseBlocks(blocks, toolResults) {
138
138
  // does not expose chain-of-thought. They are known blocks, not unsupported
139
139
  // content; silently omit empty placeholders and avoid leaking their JSON.
140
140
  if (typeof b.text === 'string' && b.text) out.push({ type: 'reasoning', text: b.text });
141
+ } else if (b.type === 'image' && b.dataUnavailable === true) {
142
+ // The image attachment resolver marked this block as unavailable. Keep it out of
143
+ // visible text; pushMessage projects its retry metadata onto unavailableImages.
144
+ continue;
141
145
  } else if (b.type === 'image' && typeof b.data === 'string' && typeof b.mediaType === 'string') {
142
146
  out.push({ type: 'image', mediaType: b.mediaType, data: b.data, name: typeof b.name === 'string' ? b.name : undefined });
143
147
  } else if (b.type === 'tool-call') {
@@ -321,12 +325,35 @@ function deriveMessages(events) {
321
325
 
322
326
  // 普通消息(user/assistant)沿用原折叠逻辑。
323
327
  const pushMessage = (msg, e) => {
324
- const blocks = parseBlocks(msg.content, toolResults);
328
+ const parsed = parseBlocks(msg.content, toolResults);
329
+ // 工具产出的图挂成这条消息的图片附件:App 把它画在气泡上方(缩略图 + 文件名,点开全屏),
330
+ // 与用户自己发的图同一种形状。工具卡里不再重复画同一张图 —— 一张图两处画会把字节也翻倍。
331
+ const toolImages = [];
332
+ const blocks = parsed.map((block) => {
333
+ if (block.type !== 'tool-call' || !Array.isArray(block.images) || block.images.length === 0) return block;
334
+ toolImages.push(...block.images);
335
+ const { images: _movedToAttachments, ...rest } = block;
336
+ return rest;
337
+ });
325
338
  const text = blocksToText(blocks);
326
- const images = blocks.filter((b) => b.type === 'image').map(({ mediaType, data, name }) => ({ mediaType, data, name }));
339
+ const images = [
340
+ ...blocks.filter((b) => b.type === 'image'),
341
+ ...toolImages,
342
+ ].map(({ mediaType, data, name }) => ({ mediaType, data, name }));
343
+ const unavailableImages = (Array.isArray(msg.content) ? msg.content : [])
344
+ .filter((block) => block && block.type === 'image' && block.dataUnavailable === true)
345
+ .map((block) => {
346
+ const attachment = block.attachment && typeof block.attachment === 'object' ? block.attachment : {};
347
+ return {
348
+ attachmentId: typeof attachment.attachmentId === 'string' ? attachment.attachmentId : '',
349
+ ...(typeof block.mediaType === 'string' ? { mediaType: block.mediaType } : {}),
350
+ ...(typeof block.name === 'string' ? { name: block.name } : {}),
351
+ };
352
+ })
353
+ .filter((image) => image.attachmentId !== '');
327
354
  // 只要存在可识别或保留的 block 就入列表;纯推理/工具调用也必须保留,
328
- // 否则流式标签在重同步后会消失。
329
- const hasContent = blocks.length > 0;
355
+ // 否则流式标签在重同步时会消失。
356
+ const hasContent = blocks.length > 0 || unavailableImages.length > 0;
330
357
  if (!hasContent) return;
331
358
  const clientMessageId = msg.role === 'user' ? clientMessageIdFromSource(msg.source, msg) : undefined;
332
359
  const item = {
@@ -335,6 +362,7 @@ function deriveMessages(events) {
335
362
  text,
336
363
  blocks,
337
364
  images: images.length > 0 ? images : undefined,
365
+ ...(unavailableImages.length > 0 ? { unavailableImages } : {}),
338
366
  ...(clientMessageId ? { clientMessageId } : {}),
339
367
  seq: e.seq,
340
368
  time: e.time,
@@ -62,8 +62,9 @@ const ENDPOINTS = {
62
62
  'sessions.updateQueue': inv('session', 'updateQueue'),
63
63
  // 宿主的 fork 请求体只有 sessionId / atSeq,increaseTitle 在 0.2.0 已删除。
64
64
  'sessions.fork': inv('session', 'fork', (p) => ({ request: pick(p, ['sessionId', 'atSeq']) })),
65
- // 宿主的模型目录是全局的(不再按 session 查),这里翻译成插件/移动端要的形状。
66
- 'sessions.models': (ctx, payload, signal) => modelCatalog(ctx, signal),
65
+ // 宿主的模型目录是全局的(不再按 session 查),这里翻译成插件/移动端要的形状;
66
+ // payload 带 sessionId,供 modelCatalog 叠加会话自己的 modelSelection 投影。
67
+ 'sessions.models': (ctx, payload, signal) => modelCatalog(ctx, payload, signal),
67
68
 
68
69
  // agentPresets(namespace 是复数 agentPresets)
69
70
  'agentPresets.list': inv('agentPresets', 'list', () => ({})),
@@ -47,23 +47,49 @@ async function historyViaPage(ctx, payload, signal) {
47
47
  }
48
48
 
49
49
  /**
50
- * `session.models` → `session.modelCatalog`。
50
+ * `session.models` → `session.modelCatalog` + 会话自己的 `session/projections`。
51
51
  *
52
52
  * 宿主改成全局目录了(args 为空),字段也换了名:default → current、
53
53
  * routableProviders 变成「能不能用这个 provider」。移动端要的 groups 结构两边一致,
54
54
  * 原样带过去即可。
55
+ *
56
+ * 但 `default` 是**全局**默认,不是「这个会话实际用的模型」。会话自己的模型记在
57
+ * `session/projections` 的 `modelSelection` 投影里(`next` 是「下一次请求要用」,
58
+ * 落到 `lastUsed` 是「最近一次真正跑过的」);只有会话从没选过模型时,投影里才没有
59
+ * 这一项,此时才回落到全局默认。桌面端「切到某个对话显示那个对话的模型」读的正是
60
+ * 这份会话级投影,不是全局默认。移动端这里也要对同一口径,否则 A、B 两个会话各选
61
+ * 了不同模型,手机却都显示同一个「默认模型」。
62
+ *
63
+ * 读不到投影(会话不存在 / 宿主没挂投影能力)时不升级成错误:退回全局默认即可,
64
+ * 让模型目录照常下发,宁可少一次会话级精修也不让整个模型面板消失。
55
65
  */
56
- async function modelCatalog(ctx, signal) {
57
- const result = await invoke(ctx, 'session', 'modelCatalog', {}, signal);
66
+ async function modelCatalog(ctx, payload, signal) {
67
+ const sessionId = payload && typeof payload.sessionId === 'string' ? payload.sessionId : '';
68
+ const [result, projection] = await Promise.all([
69
+ invoke(ctx, 'session', 'modelCatalog', {}, signal),
70
+ sessionId ? invoke(ctx, 'session', 'projections', { request: { sessionId } }, signal) : Promise.resolve({ error: undefined, value: undefined }),
71
+ ]);
58
72
  if (result.error) return fail(result.error);
59
73
  const catalog = result.value && typeof result.value === 'object' ? result.value : {};
60
74
  const current = catalog.default && typeof catalog.default === 'object'
61
75
  ? pick(catalog.default, ['provider', 'model', 'reasoningEffort'])
62
76
  : null;
77
+ // 会话自己选过模型时,用它覆盖全局默认;读不到或没选过才保留全局默认。
78
+ const sessionModel = !projection.error && projection.value && typeof projection.value === 'object'
79
+ ? projection.value.values && typeof projection.value.values === 'object' && projection.value.values.modelSelection
80
+ : undefined;
81
+ const sessionSelection = sessionModel && typeof sessionModel === 'object'
82
+ ? (sessionModel.next && typeof sessionModel.next === 'object'
83
+ ? pick(sessionModel.next, ['provider', 'model', 'reasoningEffort'])
84
+ : sessionModel.lastUsed && typeof sessionModel.lastUsed === 'object'
85
+ ? pick(sessionModel.lastUsed, ['provider', 'model', 'reasoningEffort'])
86
+ : null)
87
+ : null;
88
+ const effective = sessionSelection && sessionSelection.provider && sessionSelection.model ? sessionSelection : current;
63
89
  const routable = Array.isArray(catalog.routableProviders) ? catalog.routableProviders : [];
64
90
  return ok({
65
- ...(current ? { current } : {}),
66
- routable: Boolean(current && routable.includes(current.provider)),
91
+ ...(effective ? { current: effective } : {}),
92
+ routable: Boolean(effective && routable.includes(effective.provider)),
67
93
  groups: Array.isArray(catalog.groups) ? catalog.groups : [],
68
94
  });
69
95
  }