dsh-recall-plugin 2.3.20 → 2.3.22

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,22 @@
2
2
 
3
3
  本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
4
4
 
5
+ ## [2.3.22] - 2026-09-16
6
+
7
+ ### 修复
8
+
9
+ - **win32 上宿主把 `ctx.shell` 配成 bash 时功能面整个死亡(issue #15)**:官方 shell 是提供方注册制——一个 composition 恰好一个 `ctx.shell` 实现,win32 的 profile 可以只启用 `bash-sandbox`(禁用 `pwsh-sandbox`);而插件按 `process.platform` 单选 pwsh 模板,两者之间没有契约保证,于是模板被 bash 执行、第一行编码前导即语法错误(`bash: -c: line 1: syntax error near unexpected token '('`),`ensureGit` 起每一步都失败:快照从未成功、撤回按钮不可用。官方 `ShellExecutor` 公开面(`resolve`/`run`/`start` + `sandboxMode`)没有任何方言标识,**不能查询只能探测**,故修复=行为探针判方言 + 判成 bash 时改走直连通道:
10
+ - **S1 方言探针**:`runShellMeta` 首调内联执行 `Write-Output <罕见 ASCII 哨兵>`(不带 UTF8_PRELUDE、30s 短超时、直调 `ctx.shell` 不触发失败清扫);exit 0 且回显哨兵判 pwsh,非零退出(bash 下 command-not-found 即 127)/无输出/reject 一律判 bash。结果按进程缓存并以 in-flight promise 去重(突发 `session/event` 只探一次)。POSIX 不探测——bash 模板与 bash 执行器天然一致。
11
+ - **S2 直连通道**:判 bash 时 `runShellMeta` 分流到 Node `spawn` 直连 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NonInteractive -Command <单 argv>`(PS 5.1 全平台自带,不赌 PS7;模板本就按 5.1 兼容写),保留官方四项语义:stdin 字节透传(PS 5.1 代码页坑由字节流绕开)、stdout 超截断保留尾部并置 `truncated`(索引读取靠它区分截断与损坏)、超时 `child.kill()`、非零退出仍走 `cleanupAfterGitFailure`;env 复刻官方清洗(剥凭证形状名与全部 `DSH_*`,叠 `NO_COLOR`/`PAGER`/`GIT_PAGER`),cwd 取 `sandboxPolicy.workspaceRoot || process.cwd()`,`windowsHide` 防桌面端闪窗。带 `RECALL_CLEANUP` 哨兵的失败清扫脚本不触发探针(只用缓存)——它只在真实命令失败后被调用,方言早已判定,善后路径不再叠一条探测进程。
12
+ - **默认路径零变化**:`ctx.shell` 即 pwsh 的常规部署(探针判 pwsh)与 POSIX 都不触达直连通道;探针误判的代价也低——两条通道对 pwsh 模板都兼容,判成 bash 只是绕开官方托管环境(无 `dshEnv`/PATH 注入、无进程树级终止)。
13
+ - 新增 `tests/unit/store-shell-dialect.test.js`(31 例:判定/收集/env 清洗/可执行路径四个纯函数 + 假 child 覆盖 stdin 字节透传、截断、超时 kill、spawn error + 分流接线与 in-flight 去重);`tests/probe/api-surface.test.js` 新增「win32 shell 方言」3 例(ShellExecutor 无方言字段、直连复刻的 PS 5.1 候选路径与 argv 旗标、env 清洗口径);compat-audit 新增 I36。
14
+
15
+ ## [2.3.21] - 2026-09-15
16
+
17
+ ### 变更
18
+
19
+ - **dsh 0.1.6-alpha.1 兼容性声明与台账同步**:全局实装 `@deepseek-ai/dsh@0.1.6-alpha.1`(npm dist-tag `alpha`,0.1.6 线首个预发布)后跑三层门禁——`verify:host` 装配断言通过、`npm test` 330/330、`test:probe` 32 例中 1 红。红点即本版唯一行为级变化:**官方修复 `sessions.fork` 按轮次分叉的切点**——由「`cut` 从 `boundary+1` 向后推进到下一个 `turn/start` 之前」改为「`cut` 固定 `boundary.seq + 1`、精确切到选中 `turn/end`」,结束事件之后的排队输入、标题、模型设置均不再复制进子会话 seed,2.3.17 起插件侧 `scanStaleQueueItemIds` 清理的残留排队消息问题(I35)在 0.1.6 线上从源头消失(探针按 2.3.20 预留的「好消息变红」路径改钉新锚点后 32/32 复绿)。**清理逻辑不退役**:peer 范围保留 0.1.5 线段,该线上 fork 切点未修复、清理仍必要;0.1.6 上退化为 `queue-item-not-found` 吞掉的无害空操作。其余消费面零破坏:`snapshotEvents`/`eventAt`/`ownEvents` 仅标 `@deprecated` 未移除(内存跳行为不变,列为前瞻观察项);`ShellExecutor.start` 异步化不涉及插件(只用 `resolve`+`run`);`agent/session-start`→`agent/created` 事件改名与插件无关;回填链与 chat.node/settings slot 全部在位。tag 对比(800 commits / 300 文件)按消费面包过滤后唯一源码命中即 fork 实现;reference/ 镜像按 alpha.1 tag 重拉 13 源——10 份内容相同、05/09/13 三文件有实质差异(09 不变式措辞补「纯消息投影」、新增 `agent/created`、桌面 profile 重写;13 钩子行改名;均不触及插件消费的槽位/契约)。**兼容声明同步**:0.1.6 为新 minor 线,7 个 dsh-* peer 各追加 `>=0.1.6-alpha.1 <0.1.7` 段;`dsh.compatibility.dshReleases` 补 `0.1.6-alpha.1: compatible`;README 双语安装兼容声明与 badge、reference/README 归档字段、dsh-contract.md「对应版本」同步。评估实证见 `docs/upgrade-assessments/dsh-0.1.6-alpha.1.md`,compat-audit 新增 0.1.6-alpha.1 核验段并更新 I35 条目。本插件源码零功能变更。
20
+
5
21
  ## [2.3.20] - 2026-09-14
6
22
 
7
23
  ### 新增
package/README.en.md CHANGED
@@ -7,7 +7,7 @@
7
7
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue)
8
8
  ![Build](https://img.shields.io/badge/pure%20JS-green)
9
9
 
10
- [![DSH](https://img.shields.io/badge/DSH-0.1.5--rc.2-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.5-rc.2)
10
+ [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.1-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.1)
11
11
  ![DSH](https://img.shields.io/badge/DSH-Desktop-blue)
12
12
  ---
13
13
  Under any message you've sent, click "↶ Recall" — **your workspace files and the conversation history roll back to just before that message was sent**.
@@ -67,7 +67,7 @@ Boundaries accepted by design and edge cases not yet covered — worth checking
67
67
 
68
68
  ## Installation
69
69
 
70
- Prerequisites: git CLI (without it the recall button won't appear and a notice shows at the top of the page — DSH itself keeps running); PowerShell 5.1 / 7 on Windows, bash + git on Linux/macOS; DSH `0.1.1-rc.2` through `0.1.5-rc.2` (peerDependencies open a window per minor line: `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6`, consistent with the `dsh.compatibility.dshReleases` declaration; each line is anchored at its first verified version with an exclusive upper bound at the next minor, so later prereleases/releases within a verified line are admitted without touching the peer declaration, while unverified new minor lines (e.g. 0.1.6) remain blocked).
70
+ Prerequisites: git CLI (without it the recall button won't appear and a notice shows at the top of the page — DSH itself keeps running); PowerShell 5.1 / 7 on Windows, bash + git on Linux/macOS; DSH `0.1.1-rc.2` through `0.1.6-alpha.1` (peerDependencies open a window per minor line: `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`, consistent with the `dsh.compatibility.dshReleases` declaration; each line is anchored at its first verified version with an exclusive upper bound at the next minor, so later prereleases/releases within a verified line are admitted without touching the peer declaration, while unverified new minor lines (e.g. 0.1.7) remain blocked).
71
71
 
72
72
  - Official DSH plugin command: install and auto-mount into the web profile
73
73
  ```powershell
@@ -137,6 +137,7 @@ The plugin manages disk usage automatically — no manual housekeeping needed:
137
137
  When each user message is sent (before the agent touches any files), the workspace is snapshotted into an independent shadow git repository; on recall, a "pre-rollback" safety snapshot is taken first, then files are restored via `git archive` and the conversation is rewound through DSH's official `sessions.fork` mechanism. Binary- and line-ending-safe, and your project's own git state is never touched.
138
138
 
139
139
  - Snapshot storage: `dsh-recall-snapshots/<SHA256(project absolute path)>/` under home, containing the shadow git repository (`git/`, tags named `snap-<messageID>`), the index file `index.json` (message ID → snapshot time / session), and the recall chain `lineage.json`. Scripts run via PowerShell on Windows and bash on Linux/macOS (selected automatically by platform).
140
+ - **Works on Windows even when the host configures its shell as bash** (2.3.22+): DSH's shell is registered by the profile, and on win32 you can enable only `bash-sandbox` — the PowerShell templates would then be executed by bash and fail across the board. Before running its first command the plugin probes whether the executor can run pwsh (a sentinel command); if it turns out to be bash, the plugin switches to spawning the system PowerShell 5.1 directly, so snapshots and recall keep working. Deployments where `ctx.shell` is pwsh behave exactly as before (one probe, then everything as usual).
140
141
  - To browse historical snapshots directly:
141
142
 
142
143
  ```powershell
@@ -162,7 +163,7 @@ Note: all source lives in `src/` (host in `src/host/`, browser side in `src/clie
162
163
 
163
164
  ### Tests
164
165
 
165
- - `npm test`: pure-logic unit tests (vitest, 17 files / 227 cases, no DSH dependency, runs identically in CI and locally) — config parsing, snapshot parsers, rescue orchestration, error classification, script-template same-name-export contract, client pure functions, published-package layout, snapshot index persistence, storage caps and retention, etc.;
166
+ - `npm test`: pure-logic unit tests (vitest, 28 files / 361 cases, no DSH dependency, runs identically in CI and locally) — config parsing, snapshot parsers, rescue orchestration, error classification, script-template same-name-export contract, client pure functions, published-package layout, snapshot index persistence, storage caps and retention, etc.;
166
167
  - `npm run test:probe`: official-API field probes (requires a local dsh installation; **must run after any dsh upgrade**) — pins fields like `renderMessageImages`/`node`/`cwd`, `atSeq`/`increaseTitle` of `sessions.fork`, `listSessions` record shape, `AgentRegistry`, and goes red on violation;
167
168
  - `npm run verify:host`: assembly gate (requires a local dsh installation) — boots the plugin with a real cordis context, asserting inject declarations, endpoint registration, Config schema, and teardown cleanliness, catching assembly regressions before release;
168
169
  - `npm run build`: full host+client build (mandatory after any `src/` change); `npm run check:dsh`: dsh version inspection (pre-release).
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-recall-plugin?style=flat-square&label=npm&color=3178C6)](https://www.npmjs.com/package/dsh-recall-plugin)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/dsh-recall-plugin?style=flat-square&label=downloads&color=1F883D)](https://www.npmjs.com/package/dsh-recall-plugin)
7
7
  ![License](https://img.shields.io/badge/license-MIT-blue)
8
- [![DSH](https://img.shields.io/badge/DSH-0.1.5--rc.2-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.5-rc.2)
8
+ [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.1-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.1)
9
9
  ---
10
10
  在任意一条你发过的消息下方点「↶ 撤回」——**工作区文件和对话历史一起回到那条消息发出之前的状态**。
11
11
  ---
@@ -65,7 +65,7 @@
65
65
 
66
66
  ## 安装
67
67
 
68
- 前置:git CLI(未装时撤回按钮不出现,页面顶部会提示安装 git,不影响 DSH 运行);Windows 上 PowerShell 5.1 / 7 均可,Linux/macOS 需 bash + git;DSH `0.1.1-rc.2` 至 `0.1.5-rc.2`(peerDependencies 为按 minor 版本线开窗 `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6`,与 `dsh.compatibility.dshReleases` 声明一致;每条线以首个核验版本为下限、开一条上界到下一 minor 的区间,同线内后续 prerelease/正式版自动放行、无需改 peer 声明,未验证的新 minor 线(如 0.1.6)仍被拦截)。
68
+ 前置:git CLI(未装时撤回按钮不出现,页面顶部会提示安装 git,不影响 DSH 运行);Windows 上 PowerShell 5.1 / 7 均可,Linux/macOS 需 bash + git;DSH `0.1.1-rc.2` 至 `0.1.6-alpha.1`(peerDependencies 为按 minor 版本线开窗 `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`,与 `dsh.compatibility.dshReleases` 声明一致;每条线以首个核验版本为下限、开一条上界到下一 minor 的区间,同线内后续 prerelease/正式版自动放行、无需改 peer 声明,未验证的新 minor 线(如 0.1.7)仍被拦截)。
69
69
 
70
70
 
71
71
  - DSH 官方插件命令:安装并自动挂载进 web profile
@@ -140,6 +140,7 @@ dsh plugin --profile web remove dsh-recall-plugin
140
140
  每条用户消息发送时(agent 动文件之前),工作区被快照进一个独立的影子 git 仓库;撤回时先打「回退前」安全快照、再用 `git archive` 恢复文件、通过 DSH 官方 `sessions.fork` 机制把会话切到该消息之前。二进制与换行符安全,全程不触碰项目自身的 git 状态。
141
141
 
142
142
  - 快照存储:home 下 `dsh-recall-snapshots/<SHA256(项目绝对路径)>/`,内含影子 git 仓库(`git/`,tag 名为 `snap-<消息ID>`)、索引文件 `index.json`(消息 ID → 快照时间 / 会话)与撤回链 `lineage.json`。Windows 上脚本走 PowerShell,Linux/macOS 走 bash(按平台自动分叉)。
143
+ - **Windows 上宿主把 shell 配成 bash 也照常工作**(2.3.22+):官方 shell 由 profile 注册,win32 上可以只启用 `bash-sandbox`——此时 pwsh 脚本会被 bash 执行而全盘失败。插件首次执行命令前会先探测执行器能否跑 pwsh(哨兵命令),确认是 bash 后改用系统 PowerShell 5.1 直连执行,快照与撤回不受影响;`ctx.shell` 即 pwsh 的常规部署行为不变(探测一次、判为 pwsh 后一切照旧)。
143
144
  - 想直接翻历史快照:
144
145
 
145
146
  ```powershell
@@ -165,7 +166,7 @@ pnpm install
165
166
 
166
167
  ### 测试
167
168
 
168
- - `npm test`:纯逻辑单测(vitest,17 个文件 227 例,无 DSH 依赖,CI 与本地同跑)——配置解析、快照解析器、救援编排、错误分类、脚本模板同名导出契约、客户端纯函数、发布包内容布局、快照索引持久化、存储上限与保留天数等;
169
+ - `npm test`:纯逻辑单测(vitest,28 个文件 361 例,无 DSH 依赖,CI 与本地同跑)——配置解析、快照解析器、救援编排、错误分类、脚本模板同名导出契约、客户端纯函数、发布包内容布局、快照索引持久化、存储上限与保留天数等;
169
170
  - `npm run test:probe`:官方 API 字段探针(依赖本机 dsh 安装;dsh 升级后本地必跑)——钉住 `renderMessageImages`/`node`/`cwd`、`sessions.fork` 的 `atSeq`/`increaseTitle`、`listSessions` 记录结构、`AgentRegistry` 等字段,违反即红;
170
171
  - `npm run verify:host`:装配门禁(依赖本机 dsh 安装)——用真实 cordis 起插件,断言 inject 声明、端点注册、Config schema、卸载清理,装配回归发版前即可拦截;
171
172
  - `npm run build`:host+client 全量打包(改任何 `src/` 后必跑);`npm run check:dsh`:dsh 版本巡检(发布前)。
package/lib/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import os from "node:os";
2
2
  import crypto from "node:crypto";
3
+ import { spawn as childSpawn } from "node:child_process";
3
4
  import * as pwshScripts from "./scripts.pwsh.js";
4
5
  import * as posixScripts from "./scripts.posix.js";
5
6
  import { classifyEnvError } from "./diagnostics.js";
@@ -37,9 +38,100 @@ function isTmpConsumedError(error, basename) {
37
38
  if (!basename || s.indexOf(basename) < 0) return false;
38
39
  return /No such file/i.test(s) || /does not exist/i.test(s) || /cannot find path/i.test(s);
39
40
  }
40
- function createRuntime(ctx, config) {
41
+ const SHELL_PROBE_SENTINEL = "RCL_DIALECT_PROBE_9f4a2e";
42
+ const SHELL_PROBE_COMMAND = "Write-Output " + SHELL_PROBE_SENTINEL;
43
+ function judgeShellDialect(res) {
44
+ if (!res || res.exitCode !== 0) return "bash";
45
+ const text = res.stdout && res.stdout.text || "";
46
+ return text.indexOf(SHELL_PROBE_SENTINEL) >= 0 ? "pwsh" : "bash";
47
+ }
48
+ const SENSITIVE_ENV = /KEY|PASSWORD|SECRET|TOKEN/i;
49
+ const STDERR_KEEP_BYTES = 65536;
50
+ function scrubChildEnv(env) {
51
+ const out = {};
52
+ for (const key of Object.keys(env)) {
53
+ const value = env[key];
54
+ if (value === void 0) continue;
55
+ if (SENSITIVE_ENV.test(key) || key.toUpperCase().indexOf("DSH_") === 0) continue;
56
+ out[key] = value;
57
+ }
58
+ out.NO_COLOR = "1";
59
+ out.PAGER = "cat";
60
+ out.GIT_PAGER = "cat";
61
+ return out;
62
+ }
63
+ function collectStdout(chunks, maxBytes) {
64
+ const total = chunks.reduce((n, c) => n + c.length, 0);
65
+ if (total <= maxBytes) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
66
+ const all = Buffer.concat(chunks);
67
+ return { text: all.subarray(all.length - maxBytes).toString("utf8"), truncated: true };
68
+ }
69
+ function directPwshPath(env) {
70
+ return (env.SystemRoot || env.windir || "C:\\Windows") + "\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
71
+ }
72
+ async function runViaSpawn(spawn, req) {
73
+ const child = spawn(directPwshPath(process.env), ["-NoProfile", "-NonInteractive", "-Command", req.command], {
74
+ cwd: req.cwd,
75
+ env: scrubChildEnv(process.env),
76
+ windowsHide: true
77
+ });
78
+ return await new Promise((resolve, reject) => {
79
+ const sink = (maxBytes) => {
80
+ const chunks = [];
81
+ let total = 0;
82
+ return {
83
+ push(chunk) {
84
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
85
+ chunks.push(buf);
86
+ total += buf.length;
87
+ while (chunks.length > 1 && total - chunks[0].length > maxBytes) {
88
+ total -= chunks[0].length;
89
+ chunks.shift();
90
+ }
91
+ },
92
+ take() {
93
+ return collectStdout(chunks, maxBytes);
94
+ }
95
+ };
96
+ };
97
+ const outSink = sink(req.stdoutMaxBytes);
98
+ const errSink = sink(STDERR_KEEP_BYTES);
99
+ let timedOut = false;
100
+ let settled = false;
101
+ const timer = setTimeout(() => {
102
+ timedOut = true;
103
+ try {
104
+ child.kill();
105
+ } catch (error) {
106
+ }
107
+ }, Math.max(1, req.timeoutMs));
108
+ const finish = (code) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(timer);
112
+ const stdout = outSink.take();
113
+ resolve({ text: stdout.text, truncated: stdout.truncated, exitCode: code, stderr: errSink.take().text, timedOut });
114
+ };
115
+ if (child.stdout) child.stdout.on("data", (chunk) => outSink.push(chunk));
116
+ if (child.stderr) child.stderr.on("data", (chunk) => errSink.push(chunk));
117
+ child.on("error", (error) => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(timer);
121
+ reject(error);
122
+ });
123
+ child.on("close", (code) => finish(code === void 0 ? null : code));
124
+ if (child.stdin) {
125
+ child.stdin.on("error", () => {
126
+ });
127
+ child.stdin.end(Buffer.from(req.stdin === void 0 ? "" : req.stdin, "utf8"));
128
+ }
129
+ });
130
+ }
131
+ function createRuntime(ctx, config, deps) {
41
132
  const shell = ctx.shell;
42
133
  const sessions = ctx.sessions;
134
+ const spawn = deps && deps.spawn || childSpawn;
43
135
  const isWin = process.platform === "win32";
44
136
  const SEP = isWin ? "\\" : "/";
45
137
  const scripts = isWin ? pwshScripts : posixScripts;
@@ -66,6 +158,10 @@ function createRuntime(ctx, config) {
66
158
  gcCount: /* @__PURE__ */ new Map(),
67
159
  gitExe: null,
68
160
  posixHomeBase: null,
161
+ // win32 方言探针缓存(issue #15):判定结果 + in-flight promise(并发首调
162
+ // 只跑一次探针);POSIX 不探测,两字段恒为 null
163
+ shellDialect: null,
164
+ shellDialectProbe: null,
69
165
  homeContainer: null,
70
166
  errors: [],
71
167
  // 逐消息的快照反馈(issue #7 失败可见性):失败 {failed,error} 或
@@ -95,28 +191,71 @@ function createRuntime(ctx, config) {
95
191
  const missing = pwshKeys.filter((k) => posixKeys.indexOf(k) < 0);
96
192
  if (missing.length) recordError("recall script parity: posix \u7F3A\u5C11\u5BFC\u51FA " + missing.join(", "));
97
193
  })();
194
+ function resolveShellDialect() {
195
+ if (state.shellDialect) return Promise.resolve(state.shellDialect);
196
+ if (!state.shellDialectProbe) {
197
+ state.shellDialectProbe = (async () => {
198
+ let res = null;
199
+ try {
200
+ const sp = ctx.get("sandboxPolicy");
201
+ res = await shell.run(shell.resolve({
202
+ command: SHELL_PROBE_COMMAND,
203
+ timeoutMs: 3e4,
204
+ stdoutMaxBytes: 4096,
205
+ sandboxPolicy: { mode: "danger-full-access", workspaceRoot: sp && sp.workspaceRoot || process.cwd() }
206
+ }));
207
+ } catch (error) {
208
+ res = null;
209
+ }
210
+ const dialect = judgeShellDialect(res);
211
+ state.shellDialect = dialect;
212
+ console.error("recall shell dialect probe: " + dialect + (dialect === "bash" ? "\uFF08ctx.shell \u975E pwsh\uFF0C\u6539\u7528\u76F4\u8FDE powershell.exe \u901A\u9053\uFF09" : ""));
213
+ return dialect;
214
+ })();
215
+ }
216
+ return state.shellDialectProbe;
217
+ }
218
+ async function shellDialectFor(command) {
219
+ if (command.indexOf("RECALL_CLEANUP") >= 0) return state.shellDialect || "pwsh";
220
+ return await resolveShellDialect();
221
+ }
222
+ async function throwShellFailure(command, stderr, exitCode) {
223
+ await cleanupAfterGitFailure(command);
224
+ const detail = String(stderr || "").trim() || "exit " + String(exitCode);
225
+ throw new Error(detail.slice(0, 1500));
226
+ }
227
+ async function runShellDirect(command, req) {
228
+ const res = await runViaSpawn(spawn, req);
229
+ if (res.timedOut) {
230
+ await throwShellFailure(command, res.stderr + "\n\u76F4\u8FDE powershell.exe \u8D85\u65F6\uFF08" + String(req.timeoutMs) + "ms\uFF09", null);
231
+ }
232
+ if (res.exitCode !== 0) await throwShellFailure(command, res.stderr, res.exitCode);
233
+ return { text: res.text, truncated: res.truncated };
234
+ }
98
235
  async function runShellMeta(command, opts) {
99
236
  const sp = ctx.get("sandboxPolicy");
237
+ const workspaceRoot = sp && sp.workspaceRoot || process.cwd();
238
+ const timeoutMs = opts && opts.timeoutMs || 3e5;
239
+ const stdoutMaxBytes = opts && opts.stdoutMaxBytes || 4194304;
240
+ const fullCommand = scripts.UTF8_PRELUDE + "\n" + command;
241
+ if (isWin && await shellDialectFor(command) === "bash") {
242
+ return await runShellDirect(command, { command: fullCommand, timeoutMs, stdoutMaxBytes, cwd: workspaceRoot, stdin: opts && opts.stdin });
243
+ }
100
244
  const spec = shell.resolve({
101
- // 编码前导:pwsh 侧统一 UTF-8 输出(中文机器 GBK 代码页不再乱码);
102
- // bash 侧 LC_ALL=C 确定序。各模板自带,这里统一前置注入。
103
- command: scripts.UTF8_PRELUDE + "\n" + command,
104
- timeoutMs: opts && opts.timeoutMs || 3e5,
105
- stdoutMaxBytes: opts && opts.stdoutMaxBytes || 4194304,
245
+ command: fullCommand,
246
+ timeoutMs,
247
+ stdoutMaxBytes,
106
248
  // stdin 是官方 ShellExecRequest 契约字段(bash-local/pwsh 均实现),
107
249
  // POSIX 侧用它传 index.json 全文,绕开 argv 长度上限
108
250
  ...opts && opts.stdin !== void 0 ? { stdin: opts.stdin } : {},
109
- sandboxPolicy: { mode: "danger-full-access", workspaceRoot: sp && sp.workspaceRoot || process.cwd() }
251
+ sandboxPolicy: { mode: "danger-full-access", workspaceRoot }
110
252
  });
111
253
  const res = await shell.run(spec);
112
- const out = res && res.stdout && res.stdout.text || "";
113
254
  if (res && res.exitCode !== 0) {
114
- await cleanupAfterGitFailure(command);
115
- const err = (res && res.stderr && res.stderr.text || "").trim() || "exit " + String(res.exitCode);
116
- throw new Error(err.slice(0, 1500));
255
+ await throwShellFailure(command, res && res.stderr && res.stderr.text || "", res.exitCode);
117
256
  }
118
257
  return {
119
- text: out,
258
+ text: res && res.stdout && res.stdout.text || "",
120
259
  truncated: Boolean(res && res.stdout && res.stdout.truncated)
121
260
  };
122
261
  }
@@ -356,9 +495,16 @@ function createRuntime(ctx, config) {
356
495
  return { state, isWin, scripts, recordError, runShell, runShellMeta, writeTextViaShell, resolveRoot, resolveGit, homeDirFor, resolveHomeContainer, resolveStore, storeFromDir, tryUpgradeToHome, ensureGit, cleanupLegacy, cleanupAfterGitFailure };
357
496
  }
358
497
  export {
498
+ SHELL_PROBE_COMMAND,
499
+ SHELL_PROBE_SENTINEL,
500
+ collectStdout,
359
501
  createRuntime,
502
+ directPwshPath,
360
503
  isTmpConsumedError,
504
+ judgeShellDialect,
361
505
  parseCleanupResult,
362
506
  resolvePosixHomeBase,
507
+ runViaSpawn,
508
+ scrubChildEnv,
363
509
  selectPosixHomeBase
364
510
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recall-plugin",
3
- "version": "2.3.20",
3
+ "version": "2.3.22",
4
4
  "description": "在 DSH 中撤回自己发过的消息:工作区文件与对话历史一起回到那条消息发出之前,消息的文本与附件自动放回输入框,改完即可重发。文件走独立影子 git 快照(不碰项目自身的 git),对话走官方 sessions.fork,原会话归档可找回。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -56,18 +56,19 @@
56
56
  "0.1.5-alpha.1": "compatible",
57
57
  "0.1.5-alpha.2": "compatible",
58
58
  "0.1.5-rc.1": "compatible",
59
- "0.1.5-rc.2": "compatible"
59
+ "0.1.5-rc.2": "compatible",
60
+ "0.1.6-alpha.1": "compatible"
60
61
  }
61
62
  }
62
63
  },
63
64
  "peerDependencies": {
64
65
  "@deepseek-ai/cordis": "^4.0.1",
65
- "@deepseek-ai/dsh-host-webserver": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
66
- "@deepseek-ai/dsh-sandbox-policy": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
67
- "@deepseek-ai/dsh-session": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
68
- "@deepseek-ai/dsh-session-query": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
69
- "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
70
- "@deepseek-ai/dsh-shell": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6",
66
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
67
+ "@deepseek-ai/dsh-sandbox-policy": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
68
+ "@deepseek-ai/dsh-session": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
69
+ "@deepseek-ai/dsh-session-query": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
70
+ "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
71
+ "@deepseek-ai/dsh-shell": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
71
72
  "@deepseek-ai/schemastery": "^3.18.1",
72
73
  "react": "^18.2.0"
73
74
  },