fix(windows): canonicalize native watch paths

This commit is contained in:
Tianyi Cui
2026-08-08 19:29:43 +08:00
parent a6c129a3c6
commit 702e2d024a
21 changed files with 157 additions and 36 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/settings/settings-local/README.md
README.md: d1f3d755f9073acdf6fcfc5d1de883d74cc023c4
README.zh.md: 03c7ef7c35312d9e1e4a86b0df71412875af7d6c
README.md: a0bc94630f78aa7c101e3eb0795a7d07e585e347
README.zh.md: 2d2d0828f2eadde9867ab91fe8fbd4c61dea4c11

View File

@@ -24,6 +24,7 @@ Defaulting is one explicit `resolveSpec(config)` step; an unsupported extension
- **YAML edits are leaf-level diffs.** A write sets only the values that changed and deletes only the keys that were removed, so comments, anchors, and formatting survive on every untouched node and on the key of every changed pair; a changed array (or other non-map value) replaces wholesale, taking comments inside it along. JSON re-serializes without comments.
- **Reloads and writes share one operation chain.** Watcher refreshes and persists from every namespace queue run one at a time in queue order; each render sees the text the previous operation committed.
- **The watcher's ready signal reconciles once.** The initial load races the watcher's own setup, so a change written in between never fires an event; the reconcile at ready closes that startup gap.
- **The native watcher receives a canonical path.** Before Chokidar opens the target, the provider realpaths its deepest existing ancestor and restores any missing suffix. File access and user-facing diagnostics retain the configured path, while Windows cannot mix an 8.3 alias with long-form event paths inside libuv.
- **Dispose quiesces in every watch mode.** Teardown marks the provider closed, closes the watcher when present, then waits out every queued or in-flight document operation, so nothing publishes after disposal.
- **Self-write suppression by content.** The provider caches the last good text; a watcher event whose content equals the cache (its own write included) is a no-op.
- **Host configuration adapters receive the resolved path.** `ctx.settings.documentPath` is the absolute `resolveSpec()` filename, including a custom YAML/JSON path; `prepareDocument()` preserves an existing file or exclusively creates an absent empty file with owner-only permissions before the Host opens it. The browser receives only an availability flag, never reconstructs `$DSH_HOME`, and never submits a filesystem target.

View File

@@ -24,6 +24,7 @@
- **YAML 编辑是叶子级 diff。** 写入只设置发生变化的值、只删除被移除的键,因此注释、锚点与排版在每个未触碰的节点上以及每个被改键值对的键上都得以保留;被改的数组(或其他非 map 值整体替换其中的注释随之一同被换掉。JSON 重新序列化,无注释。
- **重载与写入共享一条操作链。** watcher 刷新与来自各 namespace 队列的 persist 按队列顺序逐个执行;每次渲染都基于上一次操作提交后的文本。
- **watcher 的 ready 信号做一次对账。** 初始加载与 watcher 自身的建立存在竞态因此其间写入的变更绝不会触发事件ready 时的对账补上这个启动缺口。
- **原生 watcher 接收规范化路径。** 在 Chokidar 打开目标之前,提供方会对层级最深的现有祖先路径执行 realpath 解析,再拼回缺失的后缀。文件访问和面向用户的诊断仍使用配置路径,从而避免 Windows 在 libuv 内部混用 8.3 别名与长格式事件路径。
- **Dispose 在每种 watch 模式下都保证静止。** 卸载先把提供方标记为已关闭,在 watcher 存在时将其关闭,再等待所有已排队或进行中的文档操作完成,之后不再有任何发布。
- **按内容抑制自写。** provider 缓存最后可用文本watcher 事件内容与缓存相同(含自己的写入)即为 no-op。
- **Host 配置适配器会收到解析后的路径。** `ctx.settings.documentPath``resolveSpec()` 得出的绝对文件名,包括自定义 YAML/JSON 路径;`prepareDocument()` 会保留现有文件,或在 Host 打开文档前,以仅属主可访问的权限独占创建缺失的空文件。浏览器只收到可用性标志,绝不重建 `$DSH_HOME`,也绝不提交文件系统目标。

View File

@@ -14,7 +14,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, extname, join, resolve } from 'node:path'
import { Document, parseDocument } from 'yaml'
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { canonicalizeWatchPath, resolveDshHome } from '@deepseek-ai/dsh-paths'
import { Settings, deepEqualJson, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
/** Plugin config: file location and hot-reload behavior. */
@@ -235,7 +235,7 @@ export class SettingsLocal extends Settings {
// silently ignored or overwritten.
yield* super[Service.init]()
const watcher = this.spec.watch
? chokidarWatch(this.spec.filename, {
? chokidarWatch(await canonicalizeWatchPath(this.spec.filename), {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: this.spec.debounceMs,