dsh-recall-plugin 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
4
4
 
5
+ ## [1.7.1] - 2026-08-25
6
+
7
+ ### 修复
8
+
9
+ - 历史会话中用户消息图片从未渲染([#9](https://github.com/limbo947/dsh-recall-plugin/issues/9)):插件渲染器读取的 `props.loadImage` 在官方 `conversation.chat.node` slot 契约中**从不存在**(实际入口是 `props.renderMessageImages`),自研加载链在守卫处直接 return——v1.6.2 的重试链、v1.7.0 的失败按钮全部从未执行,图片永久无声空白(v1.6.2/#8 的修复因此「修了却无效」)。用户消息图片改走官方 `renderMessageImages` 管线(自带鉴权、缓存、失败重试与灯箱预览),布局对齐官方(图片在上、气泡在下);自研 `ImageBox`/`useImageSrc` 及对应 CSS 作为死代码移除。
10
+
5
11
  ## [1.7.0] - 2026-08-25
6
12
 
7
13
  ### 新增
package/lib/client.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * dsh-recall-plugin — Client 半(持久插件形态,浏览器 bundle)
3
3
  *
4
4
  * 职责:注册 conversation.chat.node 的 user 渲染器,重绘用户消息气泡
5
- * (文本/图片/JSON 块),在复制按钮旁加「撤回」按钮;二次确认面板展示
5
+ * (文本/图片/JSON 块;图片经官方 props.renderMessageImages 管线渲染,
6
+ * 见 UserRecallNode 内注释),在复制按钮旁加「撤回」按钮;二次确认面板展示
6
7
  * 文件变更清单,确认后经 /api/recall/* 调用 Host 回退文件,并用官方
7
8
  * sessions.fork 把对话一并回退(新会话打开、原会话归档)。
8
9
  *
@@ -38,9 +39,6 @@ window.__ModuleLoader__.load({
38
39
  '.dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}',
39
40
  '.dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}',
40
41
  '.dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}',
41
- '.dsh-recall-img{max-width:100%;max-height:320px;border-radius:12px;object-fit:contain}',
42
- '.dsh-recall-img-failed{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:8px 14px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0}',
43
- '.dsh-recall-img-failed:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}',
44
42
  '.dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}',
45
43
  '.dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}',
46
44
  '.dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}',
@@ -268,52 +266,6 @@ window.__ModuleLoader__.load({
268
266
  )
269
267
  }
270
268
 
271
- // 图片加载失败的暂态判定(issue #8):会话回放时最早的图片消息先于
272
- // 会话 binding 就绪渲染,loadImage 以 unknown session 拒绝——这类
273
- // 失败靠短退避自动重试即可自愈;此前 `.catch(() => {})` 把失败静默
274
- // 吞掉且 src 恒为 null,图片永久空白且刷新无法恢复。自动重试耗尽
275
- // 后置 failed,由 ImageBox 提供「点击重试」(对齐官方 MessageImage
276
- // 的失败态语义),attempt 仅被手动重试递增以复用整条加载链。
277
- const IMAGE_RETRY_DELAYS = [400, 1500, 4000]
278
-
279
- function useImageSrc(attachment, loadImage) {
280
- const [src, setSrc] = React.useState(null)
281
- const [failed, setFailed] = React.useState(false)
282
- const [attempt, setAttempt] = React.useState(0)
283
- React.useEffect(() => {
284
- let alive = true
285
- let timer = null
286
- if (!attachment || typeof loadImage !== 'function') return undefined
287
- // 复位旧状态:attachment 变化或手动重试时清掉上一轮的 src/failed,
288
- // 避免加载期间短暂显示上一张图或残留的失败态
289
- setSrc(null)
290
- setFailed(false)
291
- const tryLoad = (retried) => {
292
- Promise.resolve(loadImage(attachment)).then((url) => {
293
- if (!alive) return
294
- if (url) setSrc(String(url))
295
- else if (retried < IMAGE_RETRY_DELAYS.length) timer = setTimeout(() => tryLoad(retried + 1), IMAGE_RETRY_DELAYS[retried])
296
- else setFailed(true)
297
- }).catch(() => {
298
- if (!alive) return
299
- if (retried < IMAGE_RETRY_DELAYS.length) timer = setTimeout(() => tryLoad(retried + 1), IMAGE_RETRY_DELAYS[retried])
300
- else setFailed(true)
301
- })
302
- }
303
- tryLoad(0)
304
- return () => { alive = false; if (timer) clearTimeout(timer) }
305
- }, [attachment, loadImage, attempt])
306
- return { src, failed, retry: () => setAttempt(attempt + 1) }
307
- }
308
-
309
- function ImageBox(props) {
310
- const { src, failed, retry } = useImageSrc(props.attachment, props.loadImage)
311
- if (src) return React.createElement('img', { className: 'dsh-recall-img', src, alt: '' })
312
- // 加载中保持原零占位语义(不撑气泡);只在确定失败后渲染可重试入口
313
- if (failed) return React.createElement('button', { type: 'button', className: 'dsh-recall-img-failed', onClick: retry }, '图片加载失败,点击重试')
314
- return null
315
- }
316
-
317
269
  // kind 语义单表承载(文案/徽章类名/汇总顺序):新增 kind 时只改
318
270
  // 这一处,避免出现「映射表改了、CSS 类名忘了」的半改状态
319
271
  const KIND_INFO = {
@@ -404,14 +356,21 @@ window.__ModuleLoader__.load({
404
356
 
405
357
  function UserRecallNode(props) {
406
358
  const node = props && props.node
407
- const loadImage = props && props.loadImage
359
+ // 图片渲染入口:官方 ChatNodeSeat 传给本 slot props 契约只有
360
+ // renderMessageImages(内部经 conversation.message.images slot 渲染
361
+ // 官方 MessageImages,自带鉴权、缓存、失败重试与灯箱预览)。
362
+ // 契约里从不存在 loadImage(issue #9):v1.6/v1.7 读该不存在的
363
+ // 字段导致自研加载链从未执行、用户消息图片永久无声空白。
364
+ const renderMessageImages = props && props.renderMessageImages
408
365
  const sessionId = props && props.sessionId
409
366
  const data = node && node.data ? node.data : {}
410
367
  // node.id 是会话事件匹配时写入的真实消息 ID;node.key 是位置键(如 13:input),不能用于快照查询
411
368
  const messageId = node ? String(node.id || node.key || '') : ''
412
369
  const blocks = Array.isArray(data.content) ? data.content : []
413
370
  const text = blocks.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
414
- const images = blocks.filter((b) => b && b.type === 'image' && b.attachment).map((b) => b.attachment)
371
+ // 官方契约:images image 块数组({attachment}),官方内部取
372
+ // image.attachment.attachmentId —— 不是裸 attachment 对象
373
+ const imageBlocks = blocks.filter((b) => b && b.type === 'image' && b.attachment).map((b) => ({ attachment: b.attachment }))
415
374
  const rest = blocks.filter((b) => !b || !(b.type === 'text' && typeof b.text === 'string') && !(b.type === 'image' && b.attachment))
416
375
 
417
376
  const [copied, setCopied] = React.useState(false)
@@ -597,10 +556,14 @@ window.__ModuleLoader__.load({
597
556
  }
598
557
 
599
558
  const bubbleChildren = []
600
- if (text !== '') bubbleChildren.push(React.createElement('div', { className: 'dsh-recall-bubble', key: 'text' }, text))
601
- for (let i = 0; i < images.length; i++) {
602
- bubbleChildren.push(React.createElement(ImageBox, { key: 'img-' + i, attachment: images[i], loadImage }))
559
+ // 图片在上、气泡在下:布局顺序对齐官方 UserStyleBubble(历史会话
560
+ // 里用户消息的观感与官方一致)。Fragment 包一层仅为挂 key。
561
+ if (imageBlocks.length && typeof renderMessageImages === 'function') {
562
+ bubbleChildren.push(React.createElement(React.Fragment, { key: 'images' },
563
+ renderMessageImages({ images: imageBlocks, align: 'end' })
564
+ ))
603
565
  }
566
+ if (text !== '') bubbleChildren.push(React.createElement('div', { className: 'dsh-recall-bubble', key: 'text' }, text))
604
567
  for (let i = 0; i < rest.length; i++) {
605
568
  bubbleChildren.push(React.createElement('pre', { className: 'dsh-recall-json', key: 'rest-' + i }, JSON.stringify(rest[i], null, 2)))
606
569
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recall-plugin",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "DSH 消息撤回插件:在用户消息气泡旁加「撤回」按钮,把项目文件(独立影子 git 仓库快照)与对话历史(官方 fork)一并回退到该消息发送之前。",
5
5
  "type": "module",
6
6
  "repository": {