dsh-recall-plugin 2.3.14 → 2.3.15
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 +6 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +88 -10
- package/lib/config.js +1 -1
- package/package.json +1 -1
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
|
+
## [2.3.15] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **撤回回填扩展为「文本 + 附件」**:此前撤回只把被撤回消息的文本回填到输入框,消息里的附件(图片/文件)不会回来;现在两者一起回填,改完可直接重发。实现沿用官方 composer 自己的附件链路(`dsh-client-ui-conversation` 的 `addFiles` 实证):**早读**——撤回执行一开始(源会话仍在册、附件引用可解析时)经 `sessions.binding(sessionId).session.readAttachment(attachmentId)` 取回原始字节与 mediaType,重建为浏览器 `File`(缺文件名按 mediaType 派生 `attachment-N.<subtype>`);**后写**——fork + open 完成后在子会话里 `conversation.createDrafts(sessionId, files)` 注册草稿附件、`shell.actions.addAttachments(ids)` 进入输入态(未接纳时 `releaseDraftAttachments` 释放,与官方 `addFiles` 的失败回滚一致)。附件读取授权绑定「消息所在会话」,故必须用源会话早读——被撤回消息不在 fork 出的子会话日志中,直读子会话拿不到授权。附件读取与草稿注册均逐项 try/catch、服务面全部 typeof 探测:任一步缺失只降级附件,文本回填与撤回主流程不受影响(旧版 dsh 无 `conversation`/`createDrafts`/`readAttachment` 时恒降级)。`refillDraft` 开关语义同步为「文本与附件」(设置描述与 README 双语同步);新增 `attachmentRefsFromBlocks` / `defaultAttachmentName` 纯函数与 3 例单测,compat-audit 新增 I34。客户端 bundle 结构与既有行为零变化。
|
|
10
|
+
|
|
5
11
|
## [2.3.14] - 2026-09-11
|
|
6
12
|
|
|
7
13
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -107,7 +107,7 @@ All options can be edited visually in the "**Settings → Plugin Config → Reca
|
|
|
107
107
|
| `maxSnapshotsPerWorkspace` | 500 | Maximum snapshots kept per workspace; oldest pruned beyond the cap. 0 = unlimited |
|
|
108
108
|
| `retentionDays` | 0 | Keep snapshots for this many days; older ones are deleted. 0 = disabled (works independently of the cap) |
|
|
109
109
|
| `baseExcludes` | `.git`, `node_modules/`, `.dsh-recall-snapshots/`, `dsh-recall-snapshots/` | Base exclusion list (gitignore syntax, lower priority than exclude.txt) |
|
|
110
|
-
| `refillDraft` | true | Refill the recalled message text into the input box after a recall |
|
|
110
|
+
| `refillDraft` | true | Refill the recalled message (text and attachments) into the input box after a recall |
|
|
111
111
|
| `snapshotEnabled` | true | Master snapshot switch (off = no new snapshots; existing snapshots remain recallable) |
|
|
112
112
|
| `archiveOriginal` | true | Archive the original session after a recall (off = the original session stays in the session list) |
|
|
113
113
|
|
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ dsh plugin --profile web remove dsh-recall-plugin
|
|
|
109
109
|
| `maxSnapshotsPerWorkspace` | 500 | 每个工作区保留的最大快照数,超限自动删除最旧的;0 = 不限制 |
|
|
110
110
|
| `retentionDays` | 0 | 按天数保留快照,超期自动删除;0 = 不启用(与条数上限各自独立生效) |
|
|
111
111
|
| `baseExcludes` | `.git`、`node_modules/`、`.dsh-recall-snapshots/`、`dsh-recall-snapshots/` | 基础排除表(gitignore 语法,优先级低于 exclude.txt) |
|
|
112
|
-
| `refillDraft` | true |
|
|
112
|
+
| `refillDraft` | true | 撤回后把被撤回的消息(文本与附件)回填到输入框 |
|
|
113
113
|
| `snapshotEnabled` | true | 快照总开关(关闭只冻结新建,已有快照仍可撤回) |
|
|
114
114
|
| `archiveOriginal` | true | 撤回后归档原会话(关闭后原会话保留在会话列表中) |
|
|
115
115
|
|
package/lib/client.js
CHANGED
|
@@ -380,6 +380,24 @@ function buildUtil() {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
// src/client/recall-node.ts
|
|
383
|
+
function attachmentRefsFromBlocks(blocks) {
|
|
384
|
+
const refs = [];
|
|
385
|
+
for (const block of blocks) {
|
|
386
|
+
if (!block || block.type !== "image" && block.type !== "file") continue;
|
|
387
|
+
const ref = block.attachment;
|
|
388
|
+
if (!ref || typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
|
|
389
|
+
const entry = { attachmentId: ref.attachmentId };
|
|
390
|
+
if (typeof ref.name === "string" && ref.name !== "") entry.name = ref.name;
|
|
391
|
+
refs.push(entry);
|
|
392
|
+
}
|
|
393
|
+
return refs;
|
|
394
|
+
}
|
|
395
|
+
function defaultAttachmentName(mediaType, index) {
|
|
396
|
+
const text = String(mediaType || "");
|
|
397
|
+
const slash = text.indexOf("/");
|
|
398
|
+
const sub = (slash >= 0 ? text.slice(slash + 1) : text).replace(/[^a-z0-9.+-]/gi, "");
|
|
399
|
+
return "attachment-" + String(index + 1) + "." + (sub || "bin");
|
|
400
|
+
}
|
|
383
401
|
var KIND_INFO = {
|
|
384
402
|
modified: { label: "\u4FEE\u6539", cls: "modified" },
|
|
385
403
|
restored: { label: "\u6062\u590D", cls: "restored" },
|
|
@@ -507,31 +525,89 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
|
|
|
507
525
|
}
|
|
508
526
|
return null;
|
|
509
527
|
}
|
|
510
|
-
function fillDraft(targetSessionId, draftText) {
|
|
511
|
-
if (!
|
|
528
|
+
function fillDraft(targetSessionId, draftText, attachmentFiles) {
|
|
529
|
+
if (!targetSessionId || !draftText && attachmentFiles === null) return;
|
|
512
530
|
let attempts = 0;
|
|
531
|
+
let attachStarted = false;
|
|
513
532
|
const attempt = () => {
|
|
533
|
+
let textDone = draftText === "";
|
|
514
534
|
try {
|
|
515
535
|
const conversation = ctx.get("conversation");
|
|
516
536
|
if (conversation && conversation.input && typeof conversation.input.shell === "function") {
|
|
517
537
|
const shell = conversation.input.shell(targetSessionId);
|
|
518
538
|
if (shell) {
|
|
519
|
-
if (
|
|
520
|
-
shell.actions.setDraft
|
|
521
|
-
|
|
539
|
+
if (!textDone) {
|
|
540
|
+
if (shell.actions && typeof shell.actions.setDraft === "function") {
|
|
541
|
+
shell.actions.setDraft(draftText);
|
|
542
|
+
textDone = true;
|
|
543
|
+
} else if (typeof shell.setDraft === "function") {
|
|
544
|
+
shell.setDraft(draftText);
|
|
545
|
+
textDone = true;
|
|
546
|
+
}
|
|
522
547
|
}
|
|
523
|
-
if (
|
|
524
|
-
shell
|
|
525
|
-
return;
|
|
548
|
+
if (!attachStarted && attachmentFiles !== null) {
|
|
549
|
+
attachStarted = startAttachDrafts(conversation, shell, targetSessionId, attachmentFiles);
|
|
526
550
|
}
|
|
527
551
|
}
|
|
528
552
|
}
|
|
529
553
|
} catch (e) {
|
|
530
554
|
}
|
|
531
|
-
|
|
555
|
+
const attachDone = attachmentFiles === null || attachStarted;
|
|
556
|
+
if ((!textDone || !attachDone) && attempts++ < 8) setTimeout(attempt, 150);
|
|
532
557
|
};
|
|
533
558
|
attempt();
|
|
534
559
|
}
|
|
560
|
+
function startAttachDrafts(conversation, shell, sessionId, files) {
|
|
561
|
+
if (typeof conversation.createDrafts !== "function") return false;
|
|
562
|
+
const actions = shell.actions;
|
|
563
|
+
let add = null;
|
|
564
|
+
if (actions && typeof actions.addAttachments === "function") add = (ids) => actions.addAttachments ? actions.addAttachments(ids) : false;
|
|
565
|
+
else if (typeof shell.addAttachments === "function") add = (ids) => shell.addAttachments ? shell.addAttachments(ids) : false;
|
|
566
|
+
if (add === null) return false;
|
|
567
|
+
(async () => {
|
|
568
|
+
let list = [];
|
|
569
|
+
try {
|
|
570
|
+
list = await files;
|
|
571
|
+
} catch (e) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (!Array.isArray(list) || list.length === 0) return;
|
|
575
|
+
try {
|
|
576
|
+
const drafts = conversation.createDrafts ? conversation.createDrafts(sessionId, list) : [];
|
|
577
|
+
const descriptors = Array.isArray(drafts) ? drafts : [];
|
|
578
|
+
const ids = descriptors.map((draft) => draft && draft.id).filter((id) => typeof id === "string" && id !== "");
|
|
579
|
+
if (ids.length === 0) return;
|
|
580
|
+
const accepted = add(ids);
|
|
581
|
+
if (accepted === false && typeof conversation.releaseDraftAttachments === "function") conversation.releaseDraftAttachments(descriptors);
|
|
582
|
+
} catch (e) {
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
function preloadAttachmentFiles(sourceSessionId, refs) {
|
|
588
|
+
return (async () => {
|
|
589
|
+
const sessions = ctx.sessions;
|
|
590
|
+
const binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sourceSessionId) : void 0;
|
|
591
|
+
const session = binding && binding.session;
|
|
592
|
+
if (!session || typeof session.readAttachment !== "function") return [];
|
|
593
|
+
const out = [];
|
|
594
|
+
for (const ref of refs) {
|
|
595
|
+
try {
|
|
596
|
+
const result = await session.readAttachment(ref.attachmentId);
|
|
597
|
+
if (!result || result.ok !== true || !result.value || result.value.data === void 0 || result.value.data === null) continue;
|
|
598
|
+
const raw = result.value.data;
|
|
599
|
+
const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw);
|
|
600
|
+
const meta = result.value.attachment;
|
|
601
|
+
const mediaType = meta && typeof meta.mediaType === "string" && meta.mediaType !== "" ? meta.mediaType : "application/octet-stream";
|
|
602
|
+
const name = ref.name || (meta && typeof meta.name === "string" && meta.name !== "" ? meta.name : defaultAttachmentName(mediaType, out.length));
|
|
603
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
604
|
+
out.push(new File([buffer], name, { type: mediaType }));
|
|
605
|
+
} catch (e) {
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
535
611
|
function UserRecallNode(props) {
|
|
536
612
|
const node = props && props.node;
|
|
537
613
|
const renderMessageImages = props && props.renderMessageImages;
|
|
@@ -542,6 +618,7 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
|
|
|
542
618
|
const text = blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
543
619
|
const imageBlocks = blocks.filter((b) => b && b.type === "image" && b.attachment).map((b) => ({ attachment: b.attachment }));
|
|
544
620
|
const rest = blocks.filter((b) => !b || !(b.type === "text" && typeof b.text === "string") && !(b.type === "image" && b.attachment));
|
|
621
|
+
const attachmentRefs = attachmentRefsFromBlocks(blocks);
|
|
545
622
|
const [copied, setCopied] = React.useState(false);
|
|
546
623
|
const [hasSnapshot, setHasSnapshot] = React.useState(false);
|
|
547
624
|
const [recall, setRecall] = React.useState({ stage: "idle" });
|
|
@@ -625,6 +702,7 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
|
|
|
625
702
|
const changes = recall.changes || [];
|
|
626
703
|
const previewCut = typeof recall.cutSeq === "number" ? recall.cutSeq : null;
|
|
627
704
|
const previewTotal = typeof recall.total === "number" ? recall.total : changes.length;
|
|
705
|
+
const attachmentFiles = pluginConfig.refillDraft && attachmentRefs.length > 0 && sessionId ? preloadAttachmentFiles(sessionId, attachmentRefs) : null;
|
|
628
706
|
setRecall({ stage: "executing", changes });
|
|
629
707
|
api("execute", { messageId, sessionId, previewTotal, previewTreeId: recall.treeId || void 0, previewAt: Date.now() }).then(async (res) => {
|
|
630
708
|
if (!res || !res.ok) {
|
|
@@ -676,7 +754,7 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
|
|
|
676
754
|
chatError = String(error);
|
|
677
755
|
}
|
|
678
756
|
}
|
|
679
|
-
if (pluginConfig.refillDraft) fillDraft(fillTarget, text);
|
|
757
|
+
if (pluginConfig.refillDraft) fillDraft(fillTarget, text, attachmentFiles);
|
|
680
758
|
setHasSnapshot(false);
|
|
681
759
|
setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
|
|
682
760
|
}).catch((error) => {
|
package/lib/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const Config = Schema.object({
|
|
|
9
9
|
// HOME 时(容器 root=/root 等)它落在工作区内,漏排除会让 git add -A
|
|
10
10
|
// 把影子仓库自己吞进去、快照全部失败(issue #6)
|
|
11
11
|
baseExcludes: Schema.array(Schema.string()).default([".git", "node_modules/", ".dsh-recall-snapshots/", "dsh-recall-snapshots/"]).description("\u57FA\u7840\u6392\u9664\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E exclude.txt\uFF09"),
|
|
12
|
-
refillDraft: Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846"),
|
|
12
|
+
refillDraft: Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\uFF08\u6587\u672C\u4E0E\u9644\u4EF6\uFF09\u56DE\u586B\u5230\u8F93\u5165\u6846"),
|
|
13
13
|
snapshotEnabled: Schema.boolean().default(true).description("\u542F\u7528\u6D88\u606F\u5FEB\u7167\uFF08\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\uFF0C\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09"),
|
|
14
14
|
archiveOriginal: Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD\uFF08\u5173\u95ED\u540E\u539F\u4F1A\u8BDD\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF09"),
|
|
15
15
|
retentionDays: Schema.number().default(0).description("\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528")
|