dsh-vscode-mode 0.21.0 → 0.21.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/lib/index.js CHANGED
@@ -13114,6 +13114,147 @@ function reviewHandlers(ctx, registry) {
13114
13114
  };
13115
13115
  }
13116
13116
  //#endregion
13117
+ //#region src/missingReview.ts
13118
+ /** Missing historical files: frozen, whole-file deletion reviews. */
13119
+ const frozen = /* @__PURE__ */ new Map();
13120
+ const locks = /* @__PURE__ */ new Set();
13121
+ /** @private @author ddj 2026年10月28号 @param target Resolved absolute path. @returns Platform file identity. */
13122
+ function fileKey(target) {
13123
+ const normalized = resolve(target).replace(/\\/g, "/");
13124
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
13125
+ }
13126
+ /** @private @author ddj 2026年10月28号 @param records Ordered records. @returns Full evidence and decision revision, excluding transient conflict flags. */
13127
+ function revisionOf(records) {
13128
+ const evidence = records.map(({ callId, path, at, after, hunks, decisions, archived, superseded }) => ({
13129
+ callId,
13130
+ path,
13131
+ at,
13132
+ after,
13133
+ hunks,
13134
+ decisions,
13135
+ archived,
13136
+ superseded
13137
+ }));
13138
+ return createHash("sha256").update(JSON.stringify(evidence)).digest("hex");
13139
+ }
13140
+ /** @private @author ddj 2026年10月28号 @param ctx Host capability. @param cwd Workspace. @param bucket Records. @param target Resolved file. @returns Records for the same physical path spelling, oldest first. */
13141
+ async function fileRecords(ctx, cwd, bucket, target) {
13142
+ const matches = [];
13143
+ const identity = fileKey(target);
13144
+ for (const record of bucket.values()) {
13145
+ if (record.superseded) continue;
13146
+ if (fileKey(await resolveTargetIn(ctx, cwd, record.path)) === identity) matches.push(record);
13147
+ }
13148
+ return matches.sort((left, right) => left.at.localeCompare(right.at));
13149
+ }
13150
+ /** @public @author ddj 2026年10月28号 @param ctx Host. @param cwd Workspace. @param path Requested file. @returns True only for confirmed absence; directories and IO failures are not missing files. */
13151
+ async function fileAbsent(ctx, cwd, path) {
13152
+ const fs = ctx.get("fs");
13153
+ if (!fs) throw new Error("缺少 fs");
13154
+ const target = await resolveTargetIn(ctx, cwd, path);
13155
+ try {
13156
+ return !await fs.stat(target);
13157
+ } catch (error) {
13158
+ if (["ENOENT", "FS_NOT_FOUND"].includes(String(error.code))) return true;
13159
+ throw error;
13160
+ }
13161
+ }
13162
+ /** @public @author ddj 2026年10月28号 @param ctx Host. @param session Live session. @param cwd Workspace. @param bucket Records. @param path Requested file. @returns Frozen complete snapshot; never constructs text from hunks or writes files. @throws Missing evidence or nonmissing target. */
13163
+ async function missingReview(ctx, session, cwd, bucket, path) {
13164
+ const target = await resolveTargetIn(ctx, cwd, path);
13165
+ if (hasOfficial(cwd, path) || hasOfficial(cwd, target)) throw new Error("该文件已有官方审查记录,请使用官方审查入口");
13166
+ if (!await fileAbsent(ctx, cwd, target)) throw new Error("文件已重新出现,请刷新");
13167
+ const records = await fileRecords(ctx, cwd, bucket, target);
13168
+ const latest = records.at(-1);
13169
+ if (!records.some((record) => !record.archived && !recordResolved(record))) throw new Error("没有待审历史记录");
13170
+ if (!latest || typeof latest.after !== "string") throw new Error("缺少可靠的完整历史快照,无法展示或恢复;不会用局部差异拼接文件");
13171
+ if (Buffer.byteLength(latest.after, "utf8") > 8388608) throw new Error("历史快照过大,无法整文件预览");
13172
+ for (const [token, item] of frozen) if (item.expires < Date.now()) frozen.delete(token);
13173
+ if (frozen.size >= 256) throw new Error("缺失文件预览过多,请稍后重试");
13174
+ const view = {
13175
+ token: randomUUID(),
13176
+ path,
13177
+ content: latest.after,
13178
+ callIds: records.filter((record) => !record.archived && !recordResolved(record)).map((record) => record.callId),
13179
+ at: latest.at
13180
+ };
13181
+ frozen.set(view.token, {
13182
+ view,
13183
+ cwd,
13184
+ session,
13185
+ revision: revisionOf(records),
13186
+ expires: Date.now() + 18e5
13187
+ });
13188
+ return view;
13189
+ }
13190
+ /** @private @author ddj 2026年10月28号 @param ctx Host. @param item Frozen evidence. @param target Resolved path. @param undo Restore instead of keeping absence. @sideEffects Atomically creates absent file, verifies exact bytes; retries never overwrite an existing file. */
13191
+ async function applyDisk(ctx, item, target, undo) {
13192
+ const fs = ctx.get("fs");
13193
+ const absent = await fileAbsent(ctx, item.cwd, target);
13194
+ if (!undo) {
13195
+ if (!absent) throw new Error("文件已重新出现,未执行 Keep");
13196
+ return;
13197
+ }
13198
+ const operation = item.operation;
13199
+ if (!absent && !operation.restored) throw new Error("文件已重新出现,未执行 Undo");
13200
+ if (absent && !operation.restored) {
13201
+ await fs.writeText(target, item.view.content, { kind: "createIfAbsent" }, void 0, policyOf(ctx, item.session));
13202
+ operation.restored = true;
13203
+ }
13204
+ if (absent && operation.restored && !await fs.stat(target)) throw new Error("恢复后的文件再次缺失,未结清记录");
13205
+ if (await fs.readText(target) !== item.view.content) throw new Error("恢复内容校验失败,未结清记录");
13206
+ }
13207
+ /** @public @author ddj 2026年10月28号 @param ctx Host. @param session Requesting session. @param cwd Workspace. @param bucket Records. @param token Frozen preview token. @param operationId Stable retry ID. @param undo True restores shown snapshot; false keeps deletion. @returns Covered call IDs. @throws Changed evidence, concurrent action or sandbox/IO failure. */
13208
+ async function missingDecide(ctx, session, cwd, bucket, token, operationId, undo) {
13209
+ const item = frozen.get(token);
13210
+ if (!item || item.expires < Date.now() || item.cwd !== cwd || item.session.id !== session.id) throw new Error("删除预览已过期,请刷新后重试");
13211
+ if (!operationId || operationId.length > 128) throw new Error("缺少有效操作 ID");
13212
+ const target = await resolveTargetIn(ctx, cwd, item.view.path);
13213
+ const key = JSON.stringify([cwd, fileKey(target)]);
13214
+ if (locks.has(key)) throw new Error("该文件正在处理,请稍后重试");
13215
+ locks.add(key);
13216
+ item.session = session;
13217
+ try {
13218
+ return await commitMissing(ctx, item, bucket, target, operationId, undo);
13219
+ } finally {
13220
+ locks.delete(key);
13221
+ }
13222
+ }
13223
+ /** @private @author ddj 2026年10月28号 @param ctx Host. @param item Frozen preview. @param bucket Live records. @param target Resolved target. @param operationId Retry identity. @param undo Restore action. @returns Archived pending call IDs; disk success survives bookkeeping failure in the same Host lifetime. */
13224
+ async function commitMissing(ctx, item, bucket, target, operationId, undo) {
13225
+ const previous = item.operation;
13226
+ if (previous && (previous.id !== operationId || previous.undo !== undo)) throw new Error("已有不同操作,请刷新后重试");
13227
+ if (previous?.done) return item.view.callIds;
13228
+ if (hasOfficial(item.cwd, target) || hasOfficial(item.cwd, item.view.path)) throw new Error("该文件已有官方审查记录,请刷新");
13229
+ if (revisionOf(await fileRecords(ctx, item.cwd, bucket, target)) !== item.revision) throw new Error("历史记录已变化,请刷新删除预览");
13230
+ item.operation ??= {
13231
+ id: operationId,
13232
+ undo,
13233
+ restored: false,
13234
+ done: false
13235
+ };
13236
+ await applyDisk(ctx, item, target, undo);
13237
+ const current = await fileRecords(ctx, item.cwd, bucket, target);
13238
+ if (revisionOf(current) !== item.revision) throw new Error("历史记录已变化,未结清记录,请刷新");
13239
+ const pending = current.filter((record) => item.view.callIds.includes(record.callId));
13240
+ const decisions = pending.map((record) => record.decisions);
13241
+ const decision = undo ? "rejected" : "accepted";
13242
+ for (const record of pending) record.decisions = {
13243
+ call: decision,
13244
+ perHunk: record.hunks.map(() => decision)
13245
+ };
13246
+ try {
13247
+ await archiveRecords(ctx, item.session, item.cwd, bucket, pending, undo ? "已恢复删除文件快照" : "已保留文件删除");
13248
+ } catch (error) {
13249
+ pending.forEach((record, index) => {
13250
+ record.decisions = decisions[index];
13251
+ });
13252
+ throw error;
13253
+ }
13254
+ item.operation.done = true;
13255
+ return item.view.callIds;
13256
+ }
13257
+ //#endregion
13117
13258
  //#region src/rpc.ts
13118
13259
  /** Process-local cursor signer; reads must stay on the Host filesystem capability. */
13119
13260
  const readTextPage = createTextPages();
@@ -13202,8 +13343,13 @@ async function readTargetOf(ctx, sc, path) {
13202
13343
  const target = await resolveTargetIn(ctx, sc.cwd, path);
13203
13344
  debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(path ?? "") + " resolved=" + fs.processPath(target), "debug");
13204
13345
  const info = await fs.stat(target);
13205
- if (!info || info.type !== "file") return {
13346
+ if (!info) return {
13206
13347
  err: "文件不存在",
13348
+ resolvedPath: fs.processPath(target),
13349
+ code: "file-missing"
13350
+ };
13351
+ if (info.type !== "file") return {
13352
+ err: "目标不是文件",
13207
13353
  resolvedPath: fs.processPath(target)
13208
13354
  };
13209
13355
  if ((info.size ?? 0) > 33554432) return {
@@ -13216,6 +13362,10 @@ async function readTargetOf(ctx, sc, path) {
13216
13362
  info
13217
13363
  };
13218
13364
  } catch (error) {
13365
+ if (["ENOENT", "FS_NOT_FOUND"].includes(String(error.code))) return {
13366
+ err: "文件不存在",
13367
+ code: "file-missing"
13368
+ };
13219
13369
  return { err: "读取失败:" + String(error) };
13220
13370
  }
13221
13371
  }
@@ -13316,6 +13466,23 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
13316
13466
  });
13317
13467
  continue;
13318
13468
  }
13469
+ try {
13470
+ if (await fileAbsent(ctx, cwd, record.path)) {
13471
+ results.push({
13472
+ callId: item.callId,
13473
+ ok: false,
13474
+ error: "文件已删除,请刷新后按整文件删除预览执行 Keep / Undo"
13475
+ });
13476
+ continue;
13477
+ }
13478
+ } catch (error) {
13479
+ results.push({
13480
+ callId: item.callId,
13481
+ ok: false,
13482
+ error: "无法确认文件状态:" + String(error)
13483
+ });
13484
+ continue;
13485
+ }
13319
13486
  const alreadyDone = (item.scope === "hunk" ? record.decisions.perHunk[item.hunkIndex ?? -1] : record.decisions.call) === item.decision;
13320
13487
  if (alreadyDone) {
13321
13488
  if (recordResolved(record) && !record.archived) resolved.push(record);
@@ -13735,6 +13902,48 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
13735
13902
  record: item.record
13736
13903
  };
13737
13904
  },
13905
+ "edrv.missingReview": async (args) => {
13906
+ const sc = await requireSession(ctx, args.sessionId);
13907
+ if ("err" in sc) return {
13908
+ ok: false,
13909
+ error: sc.err
13910
+ };
13911
+ try {
13912
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
13913
+ if (!await fileAbsent(ctx, sc.cwd, args.path)) return {
13914
+ ok: true,
13915
+ review: null
13916
+ };
13917
+ return {
13918
+ ok: true,
13919
+ review: await missingReview(ctx, sc.session, sc.cwd, bucket, args.path)
13920
+ };
13921
+ } catch (error) {
13922
+ return {
13923
+ ok: false,
13924
+ error: String(error)
13925
+ };
13926
+ }
13927
+ },
13928
+ "edrv.missingDecide": async (args) => {
13929
+ const sc = await requireSession(ctx, args.sessionId);
13930
+ if ("err" in sc) return {
13931
+ ok: false,
13932
+ error: sc.err
13933
+ };
13934
+ try {
13935
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
13936
+ return {
13937
+ ok: true,
13938
+ callIds: await missingDecide(ctx, sc.session, sc.cwd, bucket, args.token, args.operationId, args.undo === true)
13939
+ };
13940
+ } catch (error) {
13941
+ return {
13942
+ ok: false,
13943
+ error: String(error)
13944
+ };
13945
+ }
13946
+ },
13738
13947
  "edrv.decideBatch": async (args) => {
13739
13948
  const sc = await requireSession(ctx, args.sessionId);
13740
13949
  if ("err" in sc) return {