@deepseek-ai/dsh-api-workspace-files 0.1.5-alpha.1 → 0.1.5-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +21 -19
- package/README.zh.md +21 -19
- package/lib/client.js +34 -82
- package/lib/index.js +138 -62
- package/lib/typert.host.js +160 -525
- package/lib/typert.remote-client.d.ts +14 -17
- package/lib/typert.remote-client.js +125 -40
- package/lib/types/changes.d.ts +2 -2
- package/lib/types/changes.js +2 -2
- package/lib/types/client/change-feed.d.ts +6 -18
- package/lib/types/client/change-feed.js +6 -38
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/index.js +4 -8
- package/lib/types/client/provider.d.ts +9 -20
- package/lib/types/client/provider.js +13 -33
- package/lib/types/client/types.d.ts +7 -26
- package/lib/types/index.d.ts +65 -50
- package/lib/types/index.js +108 -61
- package/lib/types/types.d.ts +4 -4
- package/package.json +12 -14
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/api/workspace-files/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: f0f9cb1532fa65954415e10a5e248b775cdff83a
|
|
6
|
+
README.zh.md: c33d0632fde318c330da4102211b69ad2be048f7
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Workspace file service for the web GUI:
|
|
2
|
+
description: "Workspace file service for the web GUI: bounded file reads through the composed filesystem, plus directory listing and instrumented filesystem observation inside the Session workspace root."
|
|
3
3
|
kind: "package-reference"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
Use this package to
|
|
12
|
+
Use this package to preview files readable through a Session's filesystem from the web client. It reads UTF-8 text by page, reads bounded byte windows or complete files, resolves related files from a base file's directory, and reports file metadata. File reads may target paths outside the workspace; directory listing and instrumented filesystem observations remain workspace-scoped. The service exposes no mutation operation.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -25,19 +25,21 @@ Use this package to browse and inspect files within a Session's workspace from t
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## Use this package
|
|
27
27
|
|
|
28
|
-
Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway; the bundle does so right after the Session Controller. Every method takes the Session identity on the wire, so a Client calls `remote.workspaceFiles.read(
|
|
28
|
+
Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, the Session store, and the Typert Gateway; the bundle does so right after the Session Controller. Every method takes the Session identity on the wire, so a Client calls `remote.workspaceFiles.read(sessionId, path, range, signal)`, `stat(sessionId, path, signal)`, `readBytes(sessionId, path, range, signal)`, `list(sessionId, path, signal)`, or `changes(sessionId, signal)` and never names a root itself. The Host reads a live Session header or uses persistence `stat` for a cold Session; it does not activate an Agent, read the event body, or borrow a parent Session's root. Session persistence is optional for live reads, but without it a cold Session cannot resolve and the Gateway returns `gateway/lookup-not-found`.
|
|
29
29
|
|
|
30
30
|
| Method | Returns | Purpose |
|
|
31
31
|
|---|---|---|
|
|
32
32
|
| `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | Identity, version, and size of one regular file, without content |
|
|
33
33
|
| `read(path, { offset?, limit? })` | `WorkspaceFileText` = stat + `{ offset, text, lines, eof }` | One window of lines from a UTF-8 text file; `lines` counts them, so one empty line and a page past the end read differently |
|
|
34
34
|
| `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | One window of raw bytes from any regular file, base64-encoded |
|
|
35
|
+
| `readAll(path)` | `WorkspaceFileBytes` with `offset: 0`, `eof: true` | Complete raw bytes under `maxFileBytes`; oversized files fail instead of being truncated |
|
|
36
|
+
| `readRelated(path, relativePath)` | `WorkspaceFileBytes` | Complete bytes of a file resolved from the base file's directory on the Host |
|
|
35
37
|
| `list(path)` | `WorkspaceDirectoryListing { path, entries, truncated }` | Direct children of one directory |
|
|
36
|
-
| `changes()` | stream of `WorkspaceFileWatchFrame` | Subscription readiness, then
|
|
38
|
+
| `changes()` | stream of `WorkspaceFileWatchFrame` | Subscription readiness, then filesystem observations inside the workspace root |
|
|
37
39
|
|
|
38
40
|
### Addressing and paths
|
|
39
41
|
|
|
40
|
-
`read`, `
|
|
42
|
+
`read`, `readBytes`, `readAll`, `readRelated`, and `stat` accept an absolute path or one relative to the selected Session's workspace root. The composed filesystem decides whether the path is readable; the service does not impose workspace containment on file reads. `readRelated` resolves a relative filesystem path from the base file's directory, including when either file is outside the workspace. These methods report the file's absolute path in the filesystem's execution world. `list` remains workspace-scoped and reports the listed directory relative to that root. `changes` likewise reports only instrumented filesystem observations inside the workspace root.
|
|
41
43
|
|
|
42
44
|
### Pages
|
|
43
45
|
|
|
@@ -47,9 +49,9 @@ Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway;
|
|
|
47
49
|
|
|
48
50
|
`read` pages by lines and never by bytes; a byte window is `readBytes`. `range.offset` is the 0-based first byte and defaults to 0; `range.length` is the largest number of bytes in the window and defaults to `maxBytes`, which it may not exceed — a longer window fails with `too-large` instead of arriving shortened, and an offset or length that is not an integer in range is a `gateway/bad-request`. The window comes back as base64 `data`, shorter than `length` at the end of the file and empty at or past it; `eof` is true when the window includes the file's last byte. Nothing is decoded and nothing is refused as binary, so an image or a NUL-laden file reads where `read` fails with `not-text`. The same `version` and `bytes` ride along as on a page.
|
|
49
51
|
|
|
50
|
-
###
|
|
52
|
+
### File-read and directory checks
|
|
51
53
|
|
|
52
|
-
Every
|
|
54
|
+
Every operation first uses `lstat` to reject a missing path, a final symlink, or the wrong file kind. File operations then resolve and read through the composed filesystem without an additional workspace-containment check. `list` alone requires the resolved directory to remain inside the workspace root. The configured page, window, complete-file, and listing caps still apply. Text pages additionally reject invalid UTF-8 and NUL bytes; byte reads do not decode content. An empty path is a `gateway/bad-request`.
|
|
53
55
|
|
|
54
56
|
### The change feed
|
|
55
57
|
|
|
@@ -60,6 +62,7 @@ Every read, stat, and listing passes four gates in this order. First, `lstat` in
|
|
|
60
62
|
| Field | Default | Meaning |
|
|
61
63
|
|---|---|---|
|
|
62
64
|
| `maxBytes` | `2097152` (2 MiB) | Inclusive byte cap on one page's text and on one byte window; a larger page or window fails |
|
|
65
|
+
| `maxFileBytes` | `33554432` (32 MiB) | Inclusive complete-file cap for `readAll` and `readRelated`; larger files fail with `too-large` |
|
|
63
66
|
| `maxLines` | `5000` | Default and largest page size in lines; a larger `limit` is refused |
|
|
64
67
|
| `maxEntries` | `2000` | Cap on returned directory entries; the rest is dropped and reported cut |
|
|
65
68
|
|
|
@@ -67,15 +70,15 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
67
70
|
|
|
68
71
|
### Failures
|
|
69
72
|
|
|
70
|
-
Each failure is one `RemoteError` code with typed details, declared in [`src/types.ts`](src/types.ts): `workspace-file/not-found`, `workspace-file/outside-workspace
|
|
73
|
+
Each failure is one `RemoteError` code with typed details, declared in [`src/types.ts`](src/types.ts): `workspace-file/not-found`, `workspace-file/outside-workspace` (directory listing only), `workspace-file/too-large` (with `limit`, the applicable page, window, or complete-file cap), `workspace-file/not-text`, `workspace-file/not-regular-file` (`kind`: `directory`, `symlink`, or `other`), and `workspace-file/not-directory` (`kind`: `file`, `symlink`, or `other`). Callers branch on the code, never on message text.
|
|
71
74
|
|
|
72
75
|
### Client file resources
|
|
73
76
|
|
|
74
|
-
The browser export registers the `file` provider into `ctx.resources` and requires `resources`, `remote`, `remote.workspaceFiles
|
|
77
|
+
The browser export registers the `file` provider into `ctx.resources` and requires `resources`, `remote`, and `remote.workspaceFiles`. The bundle's single `workspace-files` row supplies both faces; the Client has no separate configuration. A component reads `WorkspaceFileStat { absolutePath, version, bytes? }` metadata through `useResource<'file'>(address)` and fetches content separately through Remote reads. Any UI, including Global components, shares the observation for the same complete address.
|
|
75
78
|
|
|
76
|
-
A `session/<sessionId>/<path>`
|
|
79
|
+
A `session/<sessionId>/<path>` address carries the authorizing Session and a relative or absolute path; leading slashes are preserved, as in `dsh-resource://file/session/s//etc/hosts`. The Host receives the path unchanged and owns resolution and access checks; the Client needs no Session `cwd`. `absolute/<path>` remains parseable but has no authorizing Session and fails with `workspace-file/unknown-workspace`, without borrowing current or Tab Session. Unsupported addresses fail with `workspace-file/unsupported-address`. [Workspace-path](../../util/workspace-path/README.md) owns the grammar; the generic Resource layer knows only the address and `signal`.
|
|
77
80
|
|
|
78
|
-
The provider waits for the Host's `ready` frame before its first `stat`, queues changes during the read, then binds the follower to `stat.absolutePath`. Both queued and live changes match that Host-returned path. A new write version
|
|
81
|
+
The provider waits for the Host's `ready` frame before its first `stat`, queues changes during the read, then binds the follower to `stat.absolutePath`. Both queued and live changes match that Host-returned path. A new write version updates metadata while retaining the last byte size; duplicate versions are ignored. An absent notice re-stats the file. A failed stat keeps the address followed; a later write can recover it, and any Session write can trigger a retry before the first successful path binding. Frames are `RemoteResult` values, and programming exceptions remain uncaught.
|
|
79
82
|
|
|
80
83
|
One supervised `changes` stream serves every followed file in a Session. Followers match absolute paths with backslashes normalized to slashes. Carrier loss reconnects through the Gateway supervisor; a Host-ended or terminally failed feed ends its followers and leaves their last metadata readable until reopened. The last follower leaving disposes the stream, a successor waits for that disposal, and plugin teardown awaits all pending closes. The provider declares `ResourceProtocolMap.file`; the text preview declares its Sidebar line-navigation parameters.
|
|
81
84
|
|
|
@@ -89,13 +92,13 @@ One supervised `changes` stream serves every followed file in a Session. Followe
|
|
|
89
92
|
|
|
90
93
|
### Design concept
|
|
91
94
|
|
|
92
|
-
Reads through `ctx.fs`
|
|
95
|
+
Reads through `ctx.fs` use the backend's read authority; the sandboxing backend fences writes and edits, not reads. A Typert lookup derives `WorkspaceFileScope` from a live Session header or the persistence service's header-only `stat`, so cold subagent Sessions need neither Agent activation nor event-body reads. The service adds regular-file checks and bounded transfer, while workspace containment belongs only to directory listing and change observation. A page is cut from `streamText`, which decodes and rejects non-UTF-8 chunk by chunk: the cutter counts lines before the window without keeping them, admits each in-window segment against the byte cap before buffering it, and returns at the first character past the window. One `stat` before the stream names the version and size the page reports.
|
|
93
96
|
|
|
94
97
|
### Source map
|
|
95
98
|
|
|
96
99
|
| File | Role |
|
|
97
100
|
|---|---|
|
|
98
|
-
| [`src/index.ts`](src/index.ts) | `WorkspaceFiles`: the `workspaceFiles` service and Remote namespace, `Config`, the gates, the page cutter, `read`, `readBytes`, `stat`, `list` |
|
|
101
|
+
| [`src/index.ts`](src/index.ts) | `WorkspaceFiles`: the `workspaceFiles` service and Remote namespace, `Config`, the gates, the page cutter, `read`, `readBytes`, `readAll`, `readRelated`, `stat`, `list` |
|
|
99
102
|
| [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`: `fs/observed` subscription and one queue per open `changes` generation |
|
|
100
103
|
| [`src/types.ts`](src/types.ts) | Wire types and the `RemoteErrorDetailsMap` codes, published as `./types` for Client packages |
|
|
101
104
|
| [`src/client/index.ts`](src/client/index.ts), [`provider.ts`](src/client/provider.ts), [`change-feed.ts`](src/client/change-feed.ts) | Browser plugin, file metadata, and per-Session change feed |
|
|
@@ -116,7 +119,7 @@ Typert generates the Host and Client Remote artifacts exposed by `./typert` and
|
|
|
116
119
|
- [Remote assembly](../../api/remotes/README.md) — how Client packages reach the `workspaceFiles` namespace.
|
|
117
120
|
- [Client resources](../../client/resources/README.md) — the resource model, `useResource`, pins, and provider lifetime.
|
|
118
121
|
- [Workspace path helpers](../../util/workspace-path/README.md) — `fileAddressFor` and `parseFileAddress`, the `dsh-resource://file/…` address grammar both ends share.
|
|
119
|
-
- [Sidebar text preview](../../client/ui-sidebar-
|
|
122
|
+
- [Sidebar text preview](../../client/ui-sidebar-documentpreview/README.md) — the tab type that follows a file through the `file` provider and reads its pages.
|
|
120
123
|
|
|
121
124
|
-----
|
|
122
125
|
|
|
@@ -133,15 +136,14 @@ None; this package neither assembles nor sends a provider request.
|
|
|
133
136
|
|
|
134
137
|
<a id="known-limitations-and-deferred-work"></a>
|
|
135
138
|
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
139
|
+
- **Instrumented operations only** — `changes` relays `fs/observed` emissions; a file changed by a subprocess, a shell command, or the user's editor produces no frame.
|
|
140
|
+
- **Directory scope only** — `list` and `changes` stay inside the Session workspace even though file preview reads may use any path readable by the filesystem backend.
|
|
138
141
|
- **No total line count** — a page reports `eof`, not how many lines follow; a consumer that needs the total pages to the end or estimates from `bytes`.
|
|
139
142
|
- **One giant line has no page** — a single line above `maxBytes` fails `too-large` at every window that includes it, because pages are cut by lines, not bytes.
|
|
140
|
-
- **
|
|
143
|
+
- **Reads are not transactional** — result metadata comes from stat before content is read; a concurrent write can make the reported version and returned contents differ.
|
|
141
144
|
- **Unbounded generation queue** — a `changes` generation buffers every contained observation until its consumer pulls; a stalled consumer grows Host memory for the life of the stream.
|
|
142
145
|
- **`maxEntries` bounds the answer, not the listing** — `list` asks `ctx.fs.listDir` for every child and cuts the array afterwards, so a directory far above the cap still costs the Host the whole listing (on `fs-local`, one stat per child); bounding that work needs a limit on the filesystem seam's `listDir`.
|
|
143
|
-
- **Dead feeds retain metadata** — after the Host ends `changes` or the stream fails terminally, open values retain their last state until reopened
|
|
144
|
-
- **Reload is shared by path** — a reload re-stats every follower of that absolute path in the Session and clears their `changed` flags, including readers that did not reload their content. Per-record reload delivery remains deferred.
|
|
146
|
+
- **Dead feeds retain metadata** — after the Host ends `changes` or the stream fails terminally, open values retain their last state until reopened.
|
|
145
147
|
|
|
146
148
|
<a id="dev-note"></a>
|
|
147
149
|
### Dev Note
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "面向 Web GUI
|
|
2
|
+
description: "面向 Web GUI 的工作区文件服务:通过组合文件系统进行有界文件读取,并在 Session 工作区根内列举目录和观察已埋点的文件系统操作。"
|
|
3
3
|
kind: "package-reference"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
使用本包可从 Web Client
|
|
12
|
+
使用本包可从 Web Client 预览 Session 文件系统允许读取的文件。它按页读取 UTF-8 文本、按有界窗口或完整文件读取原始字节、从基文件目录解析关联文件,并报告文件元数据。文件读取可以指向工作区外路径;目录列举与已埋点的文件系统观察仍限定于工作区。本服务不提供修改操作。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -25,19 +25,21 @@ kind: "package-reference"
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## 使用本包
|
|
27
27
|
|
|
28
|
-
把本包与 `dsh-fs`、`dsh-sandbox-policy
|
|
28
|
+
把本包与 `dsh-fs`、`dsh-sandbox-policy`、Session store 和 Typert Gateway 一起挂载;bundle 把它紧随 Session Controller 之后挂载。每个方法都在线路上携带 Session 身份,Client 调用 `remote.workspaceFiles.read(sessionId, path, range, signal)`、`stat(sessionId, path, signal)`、`readBytes(sessionId, path, range, signal)`、`list(sessionId, path, signal)` 或 `changes(sessionId, signal)`,从不自己指定根。Host 读取 live Session header,cold Session 则使用持久层 `stat`;它不会激活 Agent、读取事件正文或借用父 Session 的根。live 读取不要求挂载 Session persistence;未挂载时 cold Session 无法解析,Gateway 返回 `gateway/lookup-not-found`。
|
|
29
29
|
|
|
30
30
|
| 方法 | 返回 | 用途 |
|
|
31
31
|
|---|---|---|
|
|
32
32
|
| `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | 一个普通文件的身份、版本与大小,不含内容 |
|
|
33
33
|
| `read(path, { offset?, limit? })` | `WorkspaceFileText` = stat + `{ offset, text, lines, eof }` | UTF-8 文本文件的一个行窗口;`lines` 计行数,使单个空行与越过文件末尾的页可区分 |
|
|
34
34
|
| `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | 任意普通文件的一个原始字节窗口,base64 编码 |
|
|
35
|
+
| `readAll(path)` | `WorkspaceFileBytes`,其中 `offset: 0`、`eof: true` | `maxFileBytes` 内的完整原始字节;超大文件失败,不截断 |
|
|
36
|
+
| `readRelated(path, relativePath)` | `WorkspaceFileBytes` | Host 从基文件目录解析出的文件的完整字节 |
|
|
35
37
|
| `list(path)` | `WorkspaceDirectoryListing { path, entries, truncated }` | 一个目录的直接子项 |
|
|
36
|
-
| `changes()` | `WorkspaceFileWatchFrame` 流 |
|
|
38
|
+
| `changes()` | `WorkspaceFileWatchFrame` 流 | 订阅就绪确认,随后为工作区根内的文件系统观察 |
|
|
37
39
|
|
|
38
40
|
### 寻址与路径
|
|
39
41
|
|
|
40
|
-
`read`、`
|
|
42
|
+
`read`、`readBytes`、`readAll`、`readRelated` 与 `stat` 接受绝对路径或相对于所选 Session 工作区根的路径。组合文件系统决定路径是否可读;本服务不额外要求文件读取限定于工作区。`readRelated` 从基文件所在目录解析相对文件系统路径,基文件或目标文件位于工作区外时同样适用。这些方法以文件系统执行环境中的绝对路径报告文件。`list` 仍限定于工作区,并以相对于该根的路径报告被列举目录。`changes` 同样只报告工作区根内已埋点的文件系统观察。
|
|
41
43
|
|
|
42
44
|
### 分页
|
|
43
45
|
|
|
@@ -47,9 +49,9 @@ kind: "package-reference"
|
|
|
47
49
|
|
|
48
50
|
`read` 按行分页,绝不按字节;字节窗口走 `readBytes`。`range.offset` 是 0 起算的首字节,缺省为 0;`range.length` 是窗口最多的字节数,缺省为 `maxBytes` 且不得超过它——更长的窗口以 `too-large` 失败而不是被截短,不是整数或越界的 offset / length 则是 `gateway/bad-request`。窗口以 base64 的 `data` 返回,到文件末尾时短于 `length`,位于或越过末尾时为空;窗口含文件最后一个字节时 `eof` 为 true。不做任何解码,也不按二进制拒绝,因此图片或含 NUL 的文件在 `read` 以 `not-text` 失败之处仍可读出。与页一样附带同一 `version` 与 `bytes`。
|
|
49
51
|
|
|
50
|
-
###
|
|
52
|
+
### 文件读取与目录检查
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
每项操作都先通过 `lstat` 拒绝不存在的路径、末端符号链接或错误的文件类型。文件操作随后通过组合文件系统解析和读取,不做额外的工作区包含检查。只有 `list` 要求解析后的目录仍位于工作区内。配置的分页、窗口、完整文件和目录列举上限仍然适用。文本页还拒绝无效 UTF-8 与 NUL 字节;字节读取不解码内容。空路径是 `gateway/bad-request`。
|
|
53
55
|
|
|
54
56
|
### 变更流
|
|
55
57
|
|
|
@@ -60,6 +62,7 @@ kind: "package-reference"
|
|
|
60
62
|
| 字段 | 默认值 | 含义 |
|
|
61
63
|
|---|---|---|
|
|
62
64
|
| `maxBytes` | `2097152`(2 MiB) | 单页文本与单个字节窗口的字节上限(含);更大的页或窗口失败 |
|
|
65
|
+
| `maxFileBytes` | `33554432`(32 MiB) | `readAll` 和 `readRelated` 的完整文件字节上限(含);更大文件以 `too-large` 失败 |
|
|
63
66
|
| `maxLines` | `5000` | 页大小的缺省值与上限(行);更大的 `limit` 被拒绝 |
|
|
64
67
|
| `maxEntries` | `2000` | 返回目录条目数上限;其余丢弃并报告截断 |
|
|
65
68
|
|
|
@@ -67,15 +70,15 @@ kind: "package-reference"
|
|
|
67
70
|
|
|
68
71
|
### 失败
|
|
69
72
|
|
|
70
|
-
每种失败都是一个带类型化 details 的 `RemoteError` 代码,声明于 [`src/types.ts`](src/types.ts):`workspace-file/not-found`、`workspace-file/outside-workspace
|
|
73
|
+
每种失败都是一个带类型化 details 的 `RemoteError` 代码,声明于 [`src/types.ts`](src/types.ts):`workspace-file/not-found`、`workspace-file/outside-workspace`(仅目录列举)、`workspace-file/too-large`(带 `limit`,即适用的页、窗口或完整文件上限)、`workspace-file/not-text`、`workspace-file/not-regular-file`(`kind` 为 `directory`、`symlink` 或 `other`)以及 `workspace-file/not-directory`(`kind` 为 `file`、`symlink` 或 `other`)。调用方按代码分支,绝不按消息文本。
|
|
71
74
|
|
|
72
75
|
### Client 文件资源
|
|
73
76
|
|
|
74
|
-
浏览器导出向 `ctx.resources` 注册 `file`
|
|
77
|
+
浏览器导出向 `ctx.resources` 注册 `file` 提供方,要求 `resources`、`remote` 和 `remote.workspaceFiles` 在场。bundle 中单个 `workspace-files` 条目供应两面;Client 没有单独配置。组件通过 `useResource<'file'>(address)` 读取 `WorkspaceFileStat { absolutePath, version, bytes? }` 元数据,内容另经 Remote 读取。任何 UI(包括 Global)访问同一完整地址都共享观察。
|
|
75
78
|
|
|
76
|
-
`session/<sessionId>/<path>`
|
|
79
|
+
`session/<sessionId>/<path>` 地址携带授权 Session,以及相对或绝对路径;前导斜杠保留,例如 `dsh-resource://file/session/s//etc/hosts`。Host 原样接收路径,负责解析与权限检查;Client 不需要 Session `cwd`。`absolute/<path>` 仍可解析,但没有授权 Session,以 `workspace-file/unknown-workspace` 失败,不借用当前或 Tab Session。不支持的地址以 `workspace-file/unsupported-address` 失败。语法由 [workspace-path](../../util/workspace-path/README.zh.md) 定义;Resource 泛型层只认地址和 `signal`。
|
|
77
80
|
|
|
78
|
-
提供者等到 Host 的 `ready` 帧后才发首次 `stat`,读取期间将变更排队,随后将跟随者绑定到 `stat.absolutePath`。排队与实时变更都按该 Host
|
|
81
|
+
提供者等到 Host 的 `ready` 帧后才发首次 `stat`,读取期间将变更排队,随后将跟随者绑定到 `stat.absolutePath`。排队与实时变更都按该 Host 返回路径匹配。新的写入版本更新元数据并保留最近的字节大小;重复版本被忽略。消失通知会重新 stat 文件。stat 失败后仍跟随地址,后续写入可使其恢复;首次成功绑定路径前,Session 内任何写入都可触发重试。帧是 `RemoteResult` 值,编程异常不被捕获。
|
|
79
82
|
|
|
80
83
|
每个 Session 的所有被跟随文件共用一条受监督的 `changes` 流。跟随者按反斜杠归一为斜杠的绝对路径匹配。载体掉线由 Gateway 监督器重连;Host 结束或终态失败的流会结束其跟随者,最后的元数据仍可读取,直到重新打开。最后一个跟随者离开时释放流,后继流等待该释放完成,插件拆除等待所有在途关闭。提供者声明 `ResourceProtocolMap.file`;文本预览声明其 Sidebar 行号导航参数。
|
|
81
84
|
|
|
@@ -89,13 +92,13 @@ kind: "package-reference"
|
|
|
89
92
|
|
|
90
93
|
### 设计概念
|
|
91
94
|
|
|
92
|
-
经 `ctx.fs`
|
|
95
|
+
经 `ctx.fs` 的读取使用后端的读取权限;沙箱后端限制写与编辑,而不限制读取。Typert lookup 从 live Session header 或持久层的 header-only `stat` 导出 `WorkspaceFileScope`,所以 cold subagent Session 不需要激活 Agent 或读取事件正文。本服务增加普通文件检查与有界传输,工作区包含要求只属于目录列举与变更观察。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回。流之前的一次 `stat` 给出页所报告的版本与大小。
|
|
93
96
|
|
|
94
97
|
### 源码地图
|
|
95
98
|
|
|
96
99
|
| 文件 | 职责 |
|
|
97
100
|
|---|---|
|
|
98
|
-
| [`src/index.ts`](src/index.ts) | `WorkspaceFiles`:`workspaceFiles` 服务与 Remote 命名空间、`Config`、四道关、切页器、`read`、`readBytes`、`stat`、`list` |
|
|
101
|
+
| [`src/index.ts`](src/index.ts) | `WorkspaceFiles`:`workspaceFiles` 服务与 Remote 命名空间、`Config`、四道关、切页器、`read`、`readBytes`、`readAll`、`readRelated`、`stat`、`list` |
|
|
99
102
|
| [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`:`fs/observed` 订阅与每个打开的 `changes` generation 各一条队列 |
|
|
100
103
|
| [`src/types.ts`](src/types.ts) | 线路类型与 `RemoteErrorDetailsMap` 错误码,以 `./types` 发布给 Client 包 |
|
|
101
104
|
| [`src/client/index.ts`](src/client/index.ts)、[`provider.ts`](src/client/provider.ts)、[`change-feed.ts`](src/client/change-feed.ts) | 浏览器插件、文件元数据与每 Session 变更流 |
|
|
@@ -116,7 +119,7 @@ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物
|
|
|
116
119
|
- [Remote 装配](../../api/remotes/README.zh.md)——Client 包如何触达 `workspaceFiles` 命名空间。
|
|
117
120
|
- [Client 资源](../../client/resources/README.zh.md)——资源模型、`useResource`、pin 与提供者生命周期。
|
|
118
121
|
- [工作区路径辅助](../../util/workspace-path/README.zh.md)——`fileAddressFor` 与 `parseFileAddress`,两端共享的 `dsh-resource://file/…` 地址语法。
|
|
119
|
-
- [Sidebar 文本预览](../../client/ui-sidebar-
|
|
122
|
+
- [Sidebar 文本预览](../../client/ui-sidebar-documentpreview/README.zh.md)——经 `file` 提供者跟随文件并读取其页的 tab 类型。
|
|
120
123
|
|
|
121
124
|
-----
|
|
122
125
|
|
|
@@ -133,15 +136,14 @@ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物
|
|
|
133
136
|
|
|
134
137
|
<a id="known-limitations-and-deferred-work"></a>
|
|
135
138
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
139
|
+
- **仅覆盖已埋点操作**——`changes` 转发 `fs/observed` 的发射;子进程、shell 命令或用户编辑器改动的文件不产生任何帧。
|
|
140
|
+
- **仅目录受限**——尽管文件预览可以读取文件系统后端允许的任意路径,`list` 与 `changes` 仍限定在 Session 工作区内。
|
|
138
141
|
- **没有总行数**——页只报告 `eof`,不报告后面还有多少行;需要总数的消费方要翻到末尾或按 `bytes` 估算。
|
|
139
142
|
- **超长单行没有页**——超过 `maxBytes` 的单行在包含它的每个窗口都以 `too-large` 失败,因为页按行而非按字节切。
|
|
140
|
-
-
|
|
143
|
+
- **读取不具备事务性**——结果元数据来自内容读取之前的 stat;并发写入可能使报告版本与返回内容不一致。
|
|
141
144
|
- **generation 队列无界**——一个 `changes` generation 会缓冲每一条被包含的观察直到消费方 pull;停滞的消费方会在流的生命期内持续增长 Host 内存。
|
|
142
145
|
- **`maxEntries` 限制的是答案,不是列举**——`list` 让 `ctx.fs.listDir` 列出全部子项后再截断数组,远超上限的目录仍让 Host 付出整个列举的代价(`fs-local` 上每个子项一次 stat);要限制这份工作,需要文件系统 seam 的 `listDir` 支持上限。
|
|
143
|
-
- **失效流保留元数据**——Host 结束 `changes`
|
|
144
|
-
- **刷新按路径共享**——同一会话中,一次刷新会重新 stat 此绝对路径的全部跟随者并清除其 `changed` 标记,包括没有重读内容的其它读者。按记录投递刷新仍是延期工作。
|
|
146
|
+
- **失效流保留元数据**——Host 结束 `changes` 或流终态失败后,已打开的值保持最后已知状态,直到重新打开。
|
|
145
147
|
|
|
146
148
|
<a id="dev-note"></a>
|
|
147
149
|
### 开发备注
|
package/lib/client.js
CHANGED
|
@@ -16,7 +16,6 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
}
|
|
17
17
|
/** Notices of one follower, delivered in order and pulled by its consumer. */
|
|
18
18
|
var Follower = class {
|
|
19
|
-
address;
|
|
20
19
|
leave;
|
|
21
20
|
pending = [];
|
|
22
21
|
started = Promise.withResolvers();
|
|
@@ -29,17 +28,11 @@ window.__ModuleLoader__.load({
|
|
|
29
28
|
*/
|
|
30
29
|
ready = this.started.promise;
|
|
31
30
|
/**
|
|
32
|
-
* @param address - resource address used for reload lookup.
|
|
33
31
|
* @param leave - unregisters this follower and its abort listener.
|
|
34
32
|
*/
|
|
35
|
-
constructor(
|
|
36
|
-
this.address = address;
|
|
33
|
+
constructor(leave) {
|
|
37
34
|
this.leave = leave;
|
|
38
35
|
}
|
|
39
|
-
/** The normalized Host path, absent until a successful stat. */
|
|
40
|
-
get key() {
|
|
41
|
-
return this.hostKey;
|
|
42
|
-
}
|
|
43
36
|
/**
|
|
44
37
|
* Select the Host path for queued and future changes.
|
|
45
38
|
* @param absolutePath - the successful stat's absolute path.
|
|
@@ -54,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
54
47
|
/**
|
|
55
48
|
* Queue one notice.
|
|
56
49
|
* @param notice - what the consumer receives next.
|
|
57
|
-
* @param key - normalized Host path for
|
|
50
|
+
* @param key - normalized Host path for the change.
|
|
58
51
|
*/
|
|
59
52
|
push(notice, key) {
|
|
60
53
|
this.pending.push({
|
|
@@ -79,7 +72,7 @@ window.__ModuleLoader__.load({
|
|
|
79
72
|
while (true) {
|
|
80
73
|
const next = this.pending.shift();
|
|
81
74
|
if (next !== void 0) {
|
|
82
|
-
if (
|
|
75
|
+
if (this.hostKey === void 0 || next.key === this.hostKey) yield next.notice;
|
|
83
76
|
continue;
|
|
84
77
|
}
|
|
85
78
|
if (this.ended) return;
|
|
@@ -134,15 +127,6 @@ window.__ModuleLoader__.load({
|
|
|
134
127
|
this.followers.delete(follower);
|
|
135
128
|
if (this.followers.size === 0) this.close();
|
|
136
129
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Reload an address and every follower bound to the same Host path.
|
|
139
|
-
* @param address - the resource address requesting a reload.
|
|
140
|
-
*/
|
|
141
|
-
requestRestat(address) {
|
|
142
|
-
const keys = /* @__PURE__ */ new Set();
|
|
143
|
-
for (const follower of this.followers) if (follower.address === address && follower.key !== void 0) keys.add(follower.key);
|
|
144
|
-
for (const follower of this.followers) if (follower.address === address || follower.key !== void 0 && keys.has(follower.key)) follower.push({ kind: "restat" });
|
|
145
|
-
}
|
|
146
130
|
async pump() {
|
|
147
131
|
try {
|
|
148
132
|
for await (const item of this.stream) {
|
|
@@ -217,7 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
217
201
|
this.remote = remote;
|
|
218
202
|
}
|
|
219
203
|
/**
|
|
220
|
-
* Follow one resource
|
|
204
|
+
* Follow one resource in one session before its Host path is known.
|
|
221
205
|
*
|
|
222
206
|
* The follower is registered on call, not on first pull. Changes delivered
|
|
223
207
|
* to this Client are queued while stat is pending. The first follower starts
|
|
@@ -229,18 +213,17 @@ window.__ModuleLoader__.load({
|
|
|
229
213
|
* any session write can trigger a retry; after binding, only matching queued
|
|
230
214
|
* and live changes pass.
|
|
231
215
|
* @param sessionId - the session whose workspace holds the file.
|
|
232
|
-
* @param address - the resource address, used only for reload lookup.
|
|
233
216
|
* @param signal - ends the follow.
|
|
234
217
|
* @returns a single-consumer subscription with Host-path binding and explicit disposal.
|
|
235
218
|
*/
|
|
236
|
-
follow(sessionId,
|
|
219
|
+
follow(sessionId, signal) {
|
|
237
220
|
const feed = signal.aborted ? void 0 : this.feedOf(sessionId);
|
|
238
221
|
const leave = () => {
|
|
239
222
|
signal.removeEventListener("abort", leave);
|
|
240
223
|
follower.end();
|
|
241
224
|
feed?.remove(follower);
|
|
242
225
|
};
|
|
243
|
-
const follower = new Follower(
|
|
226
|
+
const follower = new Follower(leave);
|
|
244
227
|
if (feed === void 0) follower.end();
|
|
245
228
|
else {
|
|
246
229
|
feed.add(follower);
|
|
@@ -249,14 +232,6 @@ window.__ModuleLoader__.load({
|
|
|
249
232
|
return follower;
|
|
250
233
|
}
|
|
251
234
|
/**
|
|
252
|
-
* Ask an address and its same-session Host-path peers to `stat` again.
|
|
253
|
-
* @param sessionId - the session whose workspace holds the file.
|
|
254
|
-
* @param address - the resource address requesting a reload.
|
|
255
|
-
*/
|
|
256
|
-
requestRestat(sessionId, address) {
|
|
257
|
-
this.sessions.get(sessionId)?.requestRestat(address);
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
235
|
* Wait for every stream that is still closing, so an owner tearing down
|
|
261
236
|
* leaves no Host stream behind.
|
|
262
237
|
* @returns resolves once no stream of this feed is closing.
|
|
@@ -308,20 +283,29 @@ window.__ModuleLoader__.load({
|
|
|
308
283
|
};
|
|
309
284
|
//#endregion
|
|
310
285
|
//#region ../../util/workspace-path/lib/index.js
|
|
286
|
+
/**
|
|
287
|
+
* The `dsh-resource://file/…` address grammar: how a file is named across the
|
|
288
|
+
* Sidebar and the resource model, built and parsed without touching a
|
|
289
|
+
* filesystem.
|
|
290
|
+
* @module
|
|
291
|
+
*/
|
|
292
|
+
/** The scheme and type every file address opens with. */
|
|
293
|
+
const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
|
|
311
294
|
/** Whether a decoded first path segment is a Windows drive (`C:`). */
|
|
312
295
|
function isDriveSegment(segment) {
|
|
313
296
|
return segment !== void 0 && /^[A-Za-z]:$/.test(segment);
|
|
314
297
|
}
|
|
315
298
|
/**
|
|
316
|
-
* Read a file address back into its parts
|
|
299
|
+
* Read a file address back into its parts without resolving `.` or `..`.
|
|
300
|
+
* Query and fragment suffixes are ignored; encoded path segments are decoded.
|
|
317
301
|
* @param address - a candidate address.
|
|
318
302
|
* @returns the parts, or `undefined` when the string is not a `dsh-resource://file/` URI in a known scope with a path, or a segment is not validly encoded.
|
|
319
303
|
*/
|
|
320
304
|
function parseFileAddress(address) {
|
|
321
305
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const [
|
|
306
|
+
if (!address.startsWith(FILE_ADDRESS_PREFIX)) return void 0;
|
|
307
|
+
const end = address.search(/[?#]/);
|
|
308
|
+
const [scope, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
|
|
325
309
|
if (scope === "session") {
|
|
326
310
|
const [id, ...segments] = rest;
|
|
327
311
|
if (id === void 0 || id === "" || segments.length === 0) return void 0;
|
|
@@ -352,23 +336,22 @@ window.__ModuleLoader__.load({
|
|
|
352
336
|
//#endregion
|
|
353
337
|
//#region lib/types/client/provider.js
|
|
354
338
|
/**
|
|
355
|
-
* Build the `file` provider over one Remote face
|
|
339
|
+
* Build the `file` provider over one Remote face and one change feed.
|
|
356
340
|
* @param remote - the Remote face carrying `workspaceFiles.stat`.
|
|
357
341
|
* @param changes - the per-session change fan-out.
|
|
358
|
-
* @param sessions - the current Session, read for absolute addresses on every open and reload.
|
|
359
342
|
* @returns the provider to register into `ctx.resources`.
|
|
360
343
|
*/
|
|
361
|
-
function createFileResourceProvider(remote, changes
|
|
344
|
+
function createFileResourceProvider(remote, changes) {
|
|
362
345
|
return {
|
|
363
346
|
protocol: "file",
|
|
364
347
|
async *open(address, { signal }) {
|
|
365
|
-
const resolved = resolve(address
|
|
348
|
+
const resolved = resolve(address);
|
|
366
349
|
if (!resolved.ok) {
|
|
367
350
|
yield resolved;
|
|
368
351
|
return;
|
|
369
352
|
}
|
|
370
353
|
const { sessionId, path } = resolved.value;
|
|
371
|
-
const notices = changes.follow(sessionId,
|
|
354
|
+
const notices = changes.follow(sessionId, signal);
|
|
372
355
|
const stat = () => remote.workspaceFiles.stat(sessionId, path, signal);
|
|
373
356
|
const aborted = () => signal.aborted;
|
|
374
357
|
let current;
|
|
@@ -378,7 +361,7 @@ window.__ModuleLoader__.load({
|
|
|
378
361
|
if (aborted()) return;
|
|
379
362
|
if (first.ok) {
|
|
380
363
|
notices.bind(first.value.absolutePath);
|
|
381
|
-
current =
|
|
364
|
+
current = first.value;
|
|
382
365
|
yield {
|
|
383
366
|
ok: true,
|
|
384
367
|
value: current
|
|
@@ -391,8 +374,7 @@ window.__ModuleLoader__.load({
|
|
|
391
374
|
if (notice.version === current.version) continue;
|
|
392
375
|
current = {
|
|
393
376
|
...current,
|
|
394
|
-
version: notice.version
|
|
395
|
-
changed: true
|
|
377
|
+
version: notice.version
|
|
396
378
|
};
|
|
397
379
|
yield {
|
|
398
380
|
ok: true,
|
|
@@ -408,7 +390,7 @@ window.__ModuleLoader__.load({
|
|
|
408
390
|
continue;
|
|
409
391
|
}
|
|
410
392
|
notices.bind(again.value.absolutePath);
|
|
411
|
-
current =
|
|
393
|
+
current = again.value;
|
|
412
394
|
yield {
|
|
413
395
|
ok: true,
|
|
414
396
|
value: current
|
|
@@ -417,20 +399,15 @@ window.__ModuleLoader__.load({
|
|
|
417
399
|
} finally {
|
|
418
400
|
notices.dispose();
|
|
419
401
|
}
|
|
420
|
-
},
|
|
421
|
-
reload(address) {
|
|
422
|
-
const resolved = resolve(address, sessions);
|
|
423
|
-
if (resolved.ok) changes.requestRestat(resolved.value.sessionId, address);
|
|
424
402
|
}
|
|
425
403
|
};
|
|
426
404
|
}
|
|
427
405
|
/**
|
|
428
406
|
* Resolve one address to the Host call it stands for, or to the failure frame it earns.
|
|
429
407
|
* @param address - the full address, scheme included.
|
|
430
|
-
* @param sessions - the Client's Session list.
|
|
431
408
|
* @returns the Host file, or the `unsupported-address` / `unknown-workspace` failure.
|
|
432
409
|
*/
|
|
433
|
-
function resolve(address
|
|
410
|
+
function resolve(address) {
|
|
434
411
|
const parsed = parseFileAddress(address);
|
|
435
412
|
if (parsed === void 0) return {
|
|
436
413
|
ok: false,
|
|
@@ -443,18 +420,10 @@ window.__ModuleLoader__.load({
|
|
|
443
420
|
path: parsed.path
|
|
444
421
|
}
|
|
445
422
|
};
|
|
446
|
-
|
|
447
|
-
if (sessionId === void 0) return {
|
|
423
|
+
return {
|
|
448
424
|
ok: false,
|
|
449
425
|
error: unknownWorkspace(address)
|
|
450
426
|
};
|
|
451
|
-
return {
|
|
452
|
-
ok: true,
|
|
453
|
-
value: {
|
|
454
|
-
sessionId,
|
|
455
|
-
path: parsed.path
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
427
|
}
|
|
459
428
|
/**
|
|
460
429
|
* The failure frame's error for an address this provider does not serve.
|
|
@@ -465,45 +434,28 @@ window.__ModuleLoader__.load({
|
|
|
465
434
|
return new RemoteError("workspace-file/unsupported-address", `${address} is not a dsh-resource://file/session/<sessionId>/<path> or dsh-resource://file/absolute/<path> address`, { address });
|
|
466
435
|
}
|
|
467
436
|
/**
|
|
468
|
-
* The failure frame's error for an absolute address with no
|
|
437
|
+
* The failure frame's error for an absolute address with no Session.
|
|
469
438
|
* @param address - the offending address.
|
|
470
439
|
* @returns the typed error.
|
|
471
440
|
*/
|
|
472
441
|
function unknownWorkspace(address) {
|
|
473
|
-
return new RemoteError("workspace-file/unknown-workspace", `${address} requires a
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* The resource value one `stat` result amounts to.
|
|
477
|
-
* @param stat - what the Host reported.
|
|
478
|
-
* @param changed - whether the consumer's content may be stale: `true` after a
|
|
479
|
-
* Host notice prompted the stat, `false` for the opening stat and a reload's.
|
|
480
|
-
* @returns the metadata frame value.
|
|
481
|
-
*/
|
|
482
|
-
function metadataOf(stat, changed) {
|
|
483
|
-
return {
|
|
484
|
-
absolutePath: stat.absolutePath,
|
|
485
|
-
version: stat.version,
|
|
486
|
-
changed,
|
|
487
|
-
...stat.bytes === void 0 ? {} : { bytes: stat.bytes }
|
|
488
|
-
};
|
|
442
|
+
return new RemoteError("workspace-file/unknown-workspace", `${address} requires a dsh-resource://file/session/<sessionId>/<path> address`, { address });
|
|
489
443
|
}
|
|
490
444
|
//#endregion
|
|
491
445
|
//#region lib/types/client/index.js
|
|
492
|
-
/** Required browser services: the resource model, the Remote carrier and its namespace
|
|
446
|
+
/** Required browser services: the resource model, the Remote carrier and its namespace. */
|
|
493
447
|
const inject = [
|
|
494
448
|
"resources",
|
|
495
449
|
"remote",
|
|
496
|
-
"remote.workspaceFiles"
|
|
497
|
-
"sessions"
|
|
450
|
+
"remote.workspaceFiles"
|
|
498
451
|
];
|
|
499
452
|
/**
|
|
500
453
|
* Client plugin body: register the `file` provider for this plugin's lifetime.
|
|
501
|
-
* @param ctx - client root context carrying `resources
|
|
454
|
+
* @param ctx - client root context carrying `resources` and the Remote face.
|
|
502
455
|
*/
|
|
503
456
|
function apply(ctx) {
|
|
504
|
-
const sessions = { current: () => ctx.sessions.list.getSnapshot().current };
|
|
505
457
|
const changes = new ChangeFeed(ctx.remote);
|
|
506
|
-
const provider = createFileResourceProvider(ctx.remote, changes
|
|
458
|
+
const provider = createFileResourceProvider(ctx.remote, changes);
|
|
507
459
|
ctx.effect(() => {
|
|
508
460
|
const release = ctx.resources.register(provider);
|
|
509
461
|
return async () => {
|