Merge origin/master into xtr/trajectory-json-string-wrapping

Resolve the markdown-preview revert and README proofreading against this
PR's trajectory inspection work:

- CodeBlock keeps `showHeader` (the PTC code inspector supplies its own
  toolbar) on top of master's `contentRef` scrollport wrapper.
- ui-primitives README records both `contentRef` and `showHeader`.
- ui-trajectory README keeps master's proofread wording alongside this
  PR's PTC inspection and timing paragraphs.
- Re-record the README.i18n.yaml pairing hashes for the three merged pairs.
This commit is contained in:
_Kerman committed 2026-09-10 11:19:39 +08:00
commit b511cd6c2d
1140 files changed
+3778 -5650

No files matched your search

@@ -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 .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md
2026-09-05-sidebar-tab-types-and-navigation.md: a79292eba25828287ce43ad16b7eb917dcdddcb4
2026-09-05-sidebar-tab-types-and-navigation.zh.md: bf661948036257366714617b192525ac35b0905f
2026-09-05-sidebar-tab-types-and-navigation.md: 24332c844d13c41e4f966f39aa7d2fe79fd8553f
2026-09-05-sidebar-tab-types-and-navigation.zh.md: 3aeb38e2ec9c997b9aaf040ae0cb575fa08fa235
@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
`priority` is one of three literal bands, spelled as strings so that a type from another package needs no runtime import: `extension` is the band of a type from outside the product and the highest, so a type that declares nothing outranks every viewer shipped here; `builtin` is the ordinary band for shipped types; `fallback` is the plain-content position that anything more specific should beat, which VS Code's text editor holds implicitly and our text preview holds explicitly. `candidates(address)` returns every type whose globs match and whose `canOpen` does not veto, ranked by band, then by the length of the longest pattern that matched, then by registration order. `claim(address, kind?)` takes the best candidate, or the named kind's type in force when the caller overrides (its globs are not consulted; naming the type is the decision), and throws for an address nothing will open — a wiring mistake, not a user error. `get(kind)` returns the type in force; `entries()` and `guide()` list the types and their guide boxes in force; `subscribe` observes changes.
`title(address)` and `guide[].title()` are thunks read on every use, so a language change needs no re-registration. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. [Guide start page and stat pill refinements](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the current description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
### Bodies and titles: keyed Slot seats under the definition's `id`
@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
`priority` 是三个字面量档位之一,写成字符串,好让别的包的类型不需要任何运行时引入:`extension` 是来自产品之外的类型的档位也是最高档,所以什么都不声明的类型压过这里随包交付的每个查看器;`builtin` 是随包类型的常规档;`fallback` 是任何更具体的东西都应压过的纯内容位置,VS Code 的文本编辑器隐含地占据它,我们的文本预览明确地占据它。`candidates(address)` 返回 glob 命中且 `canOpen` 未否决的每个类型,按档位、再按命中的最长 pattern 长度、再按注册顺序排序。`claim(address, kind?)` 取最佳候选,或在调用方指定时取该 kind 生效的类型(不查它的 glob;点名即决定),对无人愿开的地址抛错——这是接线错误,不是用户错误。`get(kind)` 返回生效类型;`entries()` 与 `guide()` 列出生效类型及其引导入口;`subscribe` 观察变化。
`title(address)` 与 `guide[].title()` 是每次使用时重读的 thunk,语言切换无需重新注册。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[引导起始页与统计 pill 的细化](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责当前的描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
### 正文与标题:按定义 `id` keyed 的 Slot 坑位
@@ -1,25 +0,0 @@
# Agent Note: Content-sized diagram previews
Status: implemented
English | [中文](2026-09-09-content-sized-diagram-previews.zh.md)
## Problem
Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser.
## Decision
Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer.
This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document.
## Alternatives considered
**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport.
**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content.
## Consequences
Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements.
@@ -1,25 +0,0 @@
# Agent Note: 按内容定高的图表预览
Status: implemented
[English](2026-09-09-content-sized-diagram-previews.md) | 中文
## Problem
较矮的 Graphviz 与 SVG 图片显示在 400px iframe 中时,会留下大块空白。浏览器已经知道图片的渲染尺寸,但一张 59px 高的 Graphviz 图表仍占据 400px 的预览区域。
## Decision
Mermaid、Graphviz 与 SVG 共享 `SourcePreview` 的不可执行图片画布。浏览器根据图片决定高度,在可用宽度不足时按比例缩小,并添加 16px 内边距。源码切换保留图片,复制读取原始代码。主题变化重新生成 Graphviz 配色,无需单独的尺寸观察器。
这替代了[静态 Markdown fence 预览](../feature/2026-09-09-markdown-static-previews.zh.md)中的图表 iframe 选择;该记录继续维护 HTML 隔离、渲染生命周期与许可证义务。HTML 保留不透明来源、禁用脚本的 400px iframe。SVG 图片模式禁用脚本、链接交互与外部资源加载,无需把源码控制的标记插入应用文档。
## Alternatives considered
**把 iframe 高度设为 auto。** iframe 不会根据内部文档决定外部高度,仍然会保留与内容无关的视口。
**添加 iframe 测量脚本或同源访问。** 图表已经具备图片尺寸,不需要额外执行权限、来源权限或尺寸消息协议。
## Consequences
较矮图表只占据渲染高度与画布内边距。浏览器几何回归用例在浅色和深色下比较图片与容器高度,再验证窄视口中的等比例缩放。浏览器安全检查包含 SVG 脚本、事件处理器、嵌套 HTML 与外部图片,并保留复制和源码切换覆盖。HTML 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。
@@ -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 .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md
2026-09-02-in-history-system-prompt-replacement.md: b34a0d2b3591c1b62aba16d79963940be787f373
2026-09-02-in-history-system-prompt-replacement.zh.md: 296932a977ae852c4ef32de1c23b2c5d9e4a1bbf
2026-09-02-in-history-system-prompt-replacement.md: 8e5fa65ec4649537889cefc75459d2a4958165e5
2026-09-02-in-history-system-prompt-replacement.zh.md: 73003b04de82f45154f9c673654cc492eb62a4fd
@@ -18,7 +18,7 @@ For a model route that declares the capability, the loop appends a new `system/m
### Capability
`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. No default catalog entry declares it; a deployment enables it through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour.
`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. The sole built-in entry in `dsh-llm-deepseek`, `deepseek-flash`, declares it alongside text and image input. This exact catalog entry records the model capability; names and protocol families do not imply support for other models. A deployment can replace the catalog through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour.
The loop records the mode in the session: `RequestContext.systemPromptUpdate` joins provider, model, and capacity as a `request/context` field, logged whenever any of them differs from the latest snapshot. Admission reads `PreparedLlmCall.systemPromptUpdate` from the actual call prepared after `agent/request`; the preceding snapshot is not an admission input. First requests, resumed sessions, route changes, and same-route capability changes therefore use the capability of the bound adapter that will serve the call.
@@ -18,7 +18,7 @@ Status: implemented
### 能力
`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。没有默认目录条目声明它;部署方通过 `cordis.yml` 的 `models` 列表启用,所有 `dsh-llm-pi-ai` 路由保持替换行为。
`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。`dsh-llm-deepseek` 仅内置 `deepseek-flash` 条目,在该条目上声明它,同时声明文本和图片输入。该精确目录条目记录模型能力;名称和协议类别不能推导其他模型是否支持。部署方可以通过 `cordis.yml` 的 `models` 列表替换目录,所有 `dsh-llm-pi-ai` 路由保持替换行为。
循环把该模式记录进会话:`RequestContext.systemPromptUpdate` 与 provider、model、容量并列成为 `request/context` 的字段,其中任一项与最新快照不同时就记录一次。准入读取 `agent/request` 之后实际准备调用的 `PreparedLlmCall.systemPromptUpdate`;先前快照不是准入输入。因此首次请求、恢复的会话、路由变更以及同一路由的能力变更,都使用将服务该调用的绑定适配器的能力。
@@ -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 .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md
2026-09-05-sidebar-text-preview-and-file-tree.md: e25e1c55f5434f2b3b1b2703f886cf3c1e9fe80c
2026-09-05-sidebar-text-preview-and-file-tree.zh.md: f313d30fa68f9d8afba84a470e5e80902b13d1e3
2026-09-05-sidebar-text-preview-and-file-tree.md: 0142b325004a1fc018e944ed908d600e75f0ab0d
2026-09-05-sidebar-text-preview-and-file-tree.zh.md: d40ca8c790c9a7208a2243422c0973fc293a5915
@@ -20,7 +20,7 @@ Three tab types ship with the Sidebar: the **guide** (`ui-sidebar-right`), the *
The guide is what a pane shows before it holds content. Its registration is `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }` with no `patterns`: a guide views nothing, so it is opened by kind through `openTab` and recorded under the page address `sidebar://guide`, which is the registry's bookkeeping and never composed by a caller. The tab's title is `开始` / `Start`, captured into the layout record when the pane is seeded, so a later language change relabels the type and not tabs already open.
The body is a centred column — a lead line (`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`), one line of copy (`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`), and a grid of entry boxes at most 480px wide, each box at least 160px, filling as many columns as fit. The boxes are projected from every registered type's `guide[]` in `order`, through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. A box shows the contributing type's glyph, title, and description, and picking it calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened.
The body projects every registered type's `guide[]` in `order` through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the compass, optional descriptions, fallback glyph, and current capsule layout. Picking a capsule calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened.
The body is also the replacement seam. It renders the `sidebar.right.tab.guide` chain with the shipped guide as the chain's fallback, so a product that registers its own entry takes the whole body, and with no entry, or every entry declining, the shipped guide draws. Because the shipped guide is the fallback and not a chain entry, there is always exactly one body and it cannot be outvoted by accident.
@@ -42,13 +42,13 @@ Navigation is a `line`. The `read` tool row passes its 1-based `offset` as `open
A changed file is announced, not applied. The body compares the loaded version and the observation captured at read start with later `WorkspaceFileStat.version`; a difference shows the change bar. Reload rereads only this tab through the Preview face and does not mutate shared resource metadata or another tab. A resource failure takes the same bar's place above any content already loaded.
The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls and renderer behavior. The preview takes the pane body's full height, and its document body is the scroller beneath the fixed header and change bar.
The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls, renderer behavior, and scrolling. The preview takes the pane body's full height.
A failed read keeps content already shown and adds a localized failure with a retry action. The Preview names actionable file failures and falls back to the carrier message for other codes; `outside-workspace` belongs to directory listing and is not a Preview-specific failure.
### The file tree
`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does.
`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `FolderSheetGlyph` adapts the shared coloured folder sheet to the guide's requested glyph size; the [guide refinement](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns that presentation choice. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does.
The root is the session's working directory as the Host reports it in the session list (`useSessions().byId[sessionId].cwd`), labelled by `workspaceTitleOf` from `dsh-util-workspace-path` — the final non-empty path segment — with the root string itself as the label when the path is separator-only. A session without a working directory shows one line (`noWorkspace`) and issues no request. There is no root chooser and no way to browse upward: the Host's `list` refuses paths outside the Session's workspace root, so the one directory the client can list is the one it shows.
@@ -20,7 +20,7 @@ Sidebar 随包交付三个 tab 类型:**引导页**(`ui-sidebar-right`)、
引导页是 pane 承载内容之前显示的东西。它的注册定义是 `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }`,没有 `patterns`:引导页不查看任何东西,所以经 `openTab` 按 kind 打开,并记在页地址 `sidebar://guide` 之下——那是注册表自己的记账,调用方从不拼它。tab 标题是 `开始` / `Start`,在 pane 播种时捕获进布局记录,于是之后切换语言只重标类型,不改已开着的 tab。
体是一根居中的列——一句引导语(`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`)、一行文案(`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`),以及一组最宽 480px 的入口框栅格,每框至少 160px,能放几列放几列。入口框按 `order` 从每个已注册类型的 `guide[]` 投影而来,经注册表可观察的 `guide()` 列表,因此后注册的类型不用引导页知道就能出现。一个框显示贡献类型的图标、标题与说明;点选它调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。
正文按 `order` 从每个已注册类型的 `guide[]` 投影入口,并通过注册表可观察的 `guide()` 列表更新,因此后注册的类型无需引导页感知即可出现。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责罗盘、可选描述、兜底图标和当前胶囊布局。点选胶囊会调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。
体同时也是替换接缝。它渲染 `sidebar.right.tab.guide` 链,并以随包交付的引导页作为链的 fallback,于是注册了自己入口的产品接管整个体,而没有入口、或每个入口都拒绝时,随包交付的引导页照常绘制。因为随包交付的引导页是 fallback 而不是链上的一个入口,所以永远恰有一个体,也不可能被意外投掉。
@@ -42,13 +42,13 @@ store 是 Slot 标准件:每会话一个独占实例,按 tab id 分桶,持
文件变了只提示,不应用。正文把已加载版本及读取开始时捕获的观察版本与后续 `WorkspaceFileStat.version` 比较;不同则显示变更提示。重新载入只通过 Preview face 重读当前 tab,不修改共享资源元数据或其他 tab。资源失败占用同一个提示位置,已加载内容仍保留在下方。
正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件与渲染器行为。预览占满 pane 正文的全部高度,其文档正文是固定头部与变更提示条下方的滚动区域。
正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件、渲染器行为与滚动方式。预览占满 pane 正文的全部高度。
读取失败时保留已显示的内容,并增加本地化失败说明与重试操作。Preview 为可处理的文件错误提供专用文案,其他代码使用载体消息兜底;`outside-workspace` 属于目录列举,不是 Preview 专用失败。
### 文件树
`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。
`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`FolderSheetGlyph` 把共享的彩色文件夹页适配到引导页请求的图标尺寸;[引导页细化记录](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责这项展示选择。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。
根是 Host 在会话列表里上报的会话工作目录(`useSessions().byId[sessionId].cwd`),标签由 `dsh-util-workspace-path` 的 `workspaceTitleOf` 给出——路径最后一个非空段——路径只有分隔符时用根串本身作标签。没有工作目录的会话只显示一行(`noWorkspace`),不发请求。没有根选择器,也不能往上浏览:Host 的 `list` 拒绝会话工作区根之外的路径,所以客户端能列的那一个目录就是它显示的目录。
@@ -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 .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md
2026-09-07-composer-session-stats-pills.md: 818c3c156f72816eb6540d505782be8e03dc6aa0
2026-09-07-composer-session-stats-pills.zh.md: 8a74cddd3176026d799fb5b4223a51371ee8e253
2026-09-07-composer-session-stats-pills.md: 541e5a363d78c4333b1b8e3927928a19754822fa
2026-09-07-composer-session-stats-pills.zh.md: b6ef6da3c37a46d9e07e7ebdf77439e26896fddc
@@ -12,7 +12,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
`StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed.
- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually.
- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the zero cache-write omission.
- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy stays on the composer's ContextMeter ring, where it already lived beside `StatsLine` — the strip never carried it.
- **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing.
- **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent.
@@ -21,7 +21,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
- **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row, with a hover tooltip restating the full line when it truncated. Lost on crowding and reach: exact token counts appeared nowhere (the line and its tooltip both carried compact totals only), and one row gave time and billing figures no visual grouping.
- **Three resident groups with one shared dialog.** An intermediate iteration kept counts, time, and tokens as three inline groups. Two pills won because the time/usage split matches the two underlying projections one-to-one and each pill's icon telegraphs its dialog.
- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (all buckets always present vs optional per-turn fields plus model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline.
- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (required session input, cache-read, and output rows plus a non-zero cache-write row, versus optional per-turn fields and model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline.
## Consequences
@@ -12,7 +12,7 @@ Status: implemented
`StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。
- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。
- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责缓存写入为零时省略该行的规则。
- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍在输入框旁的 ContextMeter 圆环上,`StatsLine` 时代它就在那里——统计条从未承载过它。
- **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。
- **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例。
@@ -21,7 +21,7 @@ Status: implemented
- **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,悬停提示只在截断时复述整行。败在拥挤与可达性:精确 token 计数无处可看(行内和提示里都只有紧凑总量),单行也无法给时间与计费数字分组。
- **三个常驻分组共享一个弹层。** 中间迭代曾把计数、时间、token 保持为三个内联分组。双 pill 胜出是因为时间/用量的切分与两个底层投影一一对应,且每个 pill 的图标直接预告其弹层内容。
- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但契约不同(此处所有分桶恒在;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。
- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但约定不同(会话输入、缓存读取与输出行始终存在,缓存写入行仅在非零时出现;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。
## 后果
@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md
2026-09-09-markdown-static-previews.md: f3a0363e3d7c4cb19ca67dfc5011e936f7c024cf
2026-09-09-markdown-static-previews.zh.md: 24f87cd73504c1fbd274e1420a3692b3ca55ed5e
@@ -1,43 +0,0 @@
# Agent Note: Static Markdown fence previews
Status: implemented
English | [中文](2026-09-09-markdown-static-previews.zh.md)
## Problem
Readers need to see Mermaid and DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies without interpreting source or copying it to another renderer. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations.
## Decision
The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview` and the parsed fence language. The [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `SourcePreview`, which accepts a renderer, source, and labels without Session, file, or Cordis dependencies. Mermaid and Graphviz each supply a `.ts` renderer to that same component. Consumers without preview labels and streaming messages retain code.
`CodeBlock.preview` owns default visualization, source switching, and verbatim copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep actions visible below the diagram. Switching to source hides the mounted preview, preserving completed rendering and pending work. Both views use the same focused toggle button, and copying always reads the source prop.
`SourcePreview` owns loading, failure, and cancellation of stale result publication. Source replacement and unmounting cancel publication; cancellation before runtime loading completes skips layout. Failures show a localized error and the original source, and replacing invalid source with valid input recovers the preview.
Mermaid loads on demand. A shared queue serializes theme initialization with diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the application palette, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas follows the code-block background. Mounted previews observe document theme attributes and regenerate only when resolved colors change; obsolete renders cannot publish. Graphviz default colors follow the same palette, while authored DOT, SVG, and HTML colors remain intact.
HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and displayed with Graphviz output as inert images in a [content-sized canvas](../bug-fix/2026-09-09-content-sized-diagram-previews.md), so SVG scripts and links never become active. HTML frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts.
Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy.
## Alternatives considered
**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row.
**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. Message settlement provides complete source.
**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md).
**Execute HTML scripts in a frame.** Static chat examples do not need script execution. An empty sandbox plus sanitization and CSP gives the preview fewer capabilities and avoids introducing a frame messaging protocol.
**Insert rendered markup into Chat.** The requested preview needs diagram display and source access; HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior.
**Treat Viz.js as MIT-only or use a remote Graphviz service.** The compiled Graphviz license still applies locally; a remote renderer would send conversation content off-device. The bundled renderer retains source availability and legal notices without network rendering.
## Consequences
The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Inline HTML styles work, while interactive scripts, external assets, and links do not. Mermaid and Graphviz add lazy browser assets and run layout on the browser thread. Cancellation cannot preempt active layout; Mermaid finishes and releases its measurement DOM even when its result cannot be published. The previews have no editing, export, zoom controls, or interactive diagram links.
Component tests cover source copying, default preview, delayed completion, stale success and failure, unmounting, fallback, cancellation, runtime loading failure, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies Chinese Mermaid flowcharts, sequence diagrams, malformed source, configuration overrides, opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, English/Chinese UI snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review.
@@ -1,43 +0,0 @@
# Agent Note: 静态 Markdown fence 预览
Status: implemented
[English](2026-09-09-markdown-static-previews.md) | 中文
## Problem
读者需要直接在 Assistant 回复中查看 Mermaid 和 DOT 图表、SVG 图像和 HTML 示例,而不必解读源码或复制到其他渲染器。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。
## Decision
共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 和解析后的 fence 语言启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。[UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `SourcePreview`,它接收渲染器、源码和文案,不依赖 Session、文件或 Cordis。Mermaid 与 Graphviz 各自向同一组件提供 `.ts` 渲染器。未提供预览文案的调用方与流式消息保留代码显示。
`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。预览省略语言横幅,在悬停或键盘聚焦时显示紧凑的图标操作;具有任何触控输入的设备始终在图表下方显示操作。切换到源码会隐藏已挂载的预览,保留完成的渲染和待完成的工作。两种视图使用同一个保持焦点的切换按钮,复制始终读取源码属性。
`SourcePreview` 负责加载、失败和取消过期结果发布。替换源码和卸载组件会取消结果发布;在运行时加载完成前取消会跳过布局。失败时显示本地化错误与原始源码,用有效输入替换无效源码后可以恢复预览。
Mermaid 按需加载。共享队列将主题初始化与图表渲染一起串行执行,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、应用配色和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,画布使用代码块背景。已挂载的预览观察文档主题属性,仅在解析后的配色变化时重新生成;过期渲染不能发布结果。Graphviz 的默认颜色采用同一配色,DOT、SVG 和 HTML 中明确指定的颜色保持原样。
HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后,与 Graphviz 输出一同作为不可执行图片显示在[按内容定高的画布](../bug-fix/2026-09-09-content-sized-diagram-previews.zh.md)中,因此 SVG 脚本与链接不会激活。HTML iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。
Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。
## Alternatives considered
**在图表上方保留代码横幅。** 语言标签和常驻文字控件会分散对图表的注意力。浮层操作保留源码访问,不占用标题行。
**渲染每个流式片段。** 未完成的图表通常无效,反复布局会与文本流式输出争用资源。消息定稿可提供完整源码。
**在 Chat 内部渲染,或添加通用预览注册表。** 使用普通属性的共享基础组件即可满足复用,无需另加注册表或功能插件依赖。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。
**在 iframe 内执行 HTML 脚本。** 静态聊天示例不需要脚本执行。空 sandbox、清理与 CSP 降低预览权限,也避免引入 iframe 消息协议。
**把渲染标记插入 Chat。** 所需预览只需显示图表和访问源码;HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。
**将 Viz.js 视为仅使用 MIT,或使用远程 Graphviz 服务。** 本地编译的 Graphviz 仍受其许可证约束;远程渲染会把对话内容发送到设备之外。内置渲染器保留源码可获取性与法律声明,无需网络渲染。
## Consequences
该功能改变呈现,不改变持久化消息、provider 请求、工具或 Host API。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Mermaid 和 Graphviz 增加按需加载的浏览器资源,并在浏览器线程上执行布局。取消无法抢占进行中的布局;即使结果无法发布,Mermaid 仍会完成并释放测量 DOM。预览不提供编辑、导出、缩放控件或交互式图表链接。
组件测试覆盖源码复制、默认预览、延迟完成、过期成功与失败、卸载、回退、取消、运行时加载失败与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证中文 Mermaid 流程图、时序图、无效源码、配置覆盖、不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、中英文 UI 快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。
@@ -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 .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md
2026-09-09-sidebar-and-preview-interaction-polish.md: df0e768aa1aea913344c356d84c2a7d3386ccc6b
2026-09-09-sidebar-and-preview-interaction-polish.zh.md: d3f8b00fd86ec12d867d48a917cd4a65cab18ba9
2026-09-09-sidebar-and-preview-interaction-polish.md: eba8bd3a2314c7eaf18cd9982aa4e599665dbc62
2026-09-09-sidebar-and-preview-interaction-polish.zh.md: 93d88d41e99f50621c80d69d615e39852b715c89
@@ -18,7 +18,7 @@ Five small interaction defects around the right Sidebar and the document preview
**Menus close on focus entering an iframe.** [Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) adds a window `blur` listener gated on `document.activeElement instanceof HTMLIFrameElement` — the focus move is the only signal a pointerdown inside a cross-origin iframe leaves, and the gate keeps app or tab switches from closing the list.
**The code preview pins its banner and drops the card fill.** With wrap off, [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) sizes the renderer `max-content` so the sticky banner has the full scroll width to ride, and pins the banner `sticky; left: 0; width: 100cqw` against the document scroller (`container-type: inline-size` on the preview body). The shared CodeBlock's fill is routed through a new `--dsl-code-block-background` variable (default unchanged, so chat keeps its gray card) and the shared banner carries an inert `data-code-block-banner` hook; the preview sets the variable to `transparent` so code sits on the pane's own background.
**The code preview separates its banner from scrolling source and drops the card fill.** The shared CodeBlock wraps its rendered source in a stable `data-code-block-content` node that defaults to `display: contents`, so existing consumers keep their layout. [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) materializes that node as a full-height inner scrollport below the banner; the code renderer reports it through a callback ref, so the document owner follows Slot replacement for position restoration, paging, and line navigation. The shared CodeBlock's fill is routed through `--dsl-code-block-background` (default unchanged, so chat keeps its gray card), while the preview sets it to `transparent` so code sits on the pane's own background.
## Alternatives considered
@@ -28,8 +28,8 @@ Five small interaction defects around the right Sidebar and the document preview
**A bare window-blur close for menus.** Closes the list on every app or tab switch; the `activeElement` gate scopes the close to the one case the document cannot see.
**An inner code scroller for the horizontal axis.** Restoring `overflow-x: auto` on the `pre` keeps the banner still, but puts the horizontal scrollbar at the bottom of the whole block — unreachable in a long file — and both axes deliberately live in the document owner's scroller.
**Keep code in the shared document scroller.** A child banner cannot cover its parent's native scrollbar. Giving the stable source wrapper both scroll axes keeps the scrollbar at the visible viewport edge below the adjacent banner, rather than at the end of a long code block.
## Consequences
`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable and `data-code-block-banner` attribute are the code block's owner-styling seam; no shared stylesheet rule targets the attribute. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules.
`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable keeps chat's default gray card while the preview uses the pane background, and `data-code-block-content` lets an owner materialize a dedicated source viewport without changing other CodeBlock layouts. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; document-preview specs cover inner scrolling and line navigation; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules.
@@ -18,7 +18,7 @@ Status: implemented
**焦点进入 iframe 时关闭菜单。**[Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) 增加 window `blur` 监听,以 `document.activeElement instanceof HTMLIFrameElement` 为门:焦点移动是跨源 iframe 内 pointerdown 留下的唯一信号,这道门也让应用或标签页切换不会误关列表。
**代码预览钉住复制条并去掉卡片填充。**关闭折行时,[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 把渲染器设为 `max-content`,让吸附的复制条拥有完整滚动宽度可骑行,并以 `sticky; left: 0; width: 100cqw` 把它钉在文档滚动区上(预览正文设 `container-type: inline-size`)。共享 CodeBlock 的填充改经新变量 `--dsl-code-block-background`(默认值不变,会话保持灰色卡片),共享复制条带上惰性的 `data-code-block-banner` 钩子;预览把变量设为 `transparent`,代码于是坐在分栏自身的背景上。
**代码预览把复制条与滚动源码分开,并去掉卡片填充。**共享 CodeBlock 用稳定的 `data-code-block-content` 节点包裹渲染后的源码;该节点默认使用 `display: contents`,因此既有消费者保持原布局。[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 将该节点实体化为复制条下方占满剩余高度的内部滚动区;代码渲染器通过 callback ref 报告该节点,使文档 owner 在 Slot 替换后仍能用当前节点恢复位置、分页和跳转代码行。共享 CodeBlock 的填充通过 `--dsl-code-block-background` 设置(默认值不变,会话保持灰色卡片),预览将它设为 `transparent`,代码因此直接使用分栏背景。
## Alternatives considered
@@ -28,8 +28,8 @@ Status: implemented
**菜单用裸的 window blur 关闭。**每次应用或标签页切换都会关掉列表;`activeElement` 门把关闭收窄到父文档看不见的那一种情形。
**代码横轴用内层滚动。**在 `pre` 上恢复 `overflow-x: auto` 能让复制条不动,但横向滚动条会落在整个代码块底部——长文件里够不着——而且两个轴本就有意放在文档 owner 的滚动区里。
**让代码继续使用共享文档滚动区。**子级复制条无法覆盖父级的原生滚动条。让稳定的源码包装节点同时承载两个滚动轴,滚动条会停在复制条下方的可见视口边缘,而不是长代码块的末端。
## Consequences
`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量与 `data-code-block-banner` 属性是代码块的 owner 定制接缝;共享样式表没有任何规则指向该属性。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。
`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量保留 Chat 默认的灰色卡片,Preview 则使用分栏背景;`data-code-block-content` 允许 owner 实体化专用源码视口,而不改变其它 CodeBlock 布局。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;文档预览规格覆盖内部滚动与行跳转;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md
2026-09-09-content-sized-diagram-previews.md: ceb29238eec09f2dde312835dbf376855998a507
2026-09-09-content-sized-diagram-previews.zh.md: 2689602bfe3469f53e532367877f7e49d52d3d5d
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md
2026-09-10-guide-start-page-and-stat-pill-refinements.md: 987e46fd597b11046843a943c60187fdb1a769d0
2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md: 590f18a2963507a8918a0aca30997aac23c6e477
@@ -0,0 +1,29 @@
# Agent Note: Guide start page and stat pill refinements
Status: implemented
English | [中文](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)
## Problem
The right Sidebar's guide tab was a bare list of entry capsules: no visual anchor above them, a capsule could only say its title, and an entry whose type registered no glyph rendered with no icon at all, so a mixed list read as broken rather than sparse. Separately, the session token-usage dialog under the composer printed a `Cache write 0 tok` row for sessions that never wrote cache.
## Decision
**The guide is a compass over self-describing capsules.** This refines the guide contracts in [Right Sidebar tab types and navigation](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md) and [Sidebar text preview and file tree](2026-09-05-sidebar-text-preview-and-file-tree.md). [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) draws a muted 56px compass hero over the entry capsules and no heading, as a browser start page shows its doors without a caption. [`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) gains an optional thunked `description` — read fresh on every render like `title`, so language changes need no re-registration. A capsule shows its description under the title only while the guide lists at most `MAX_DESCRIBED_ENTRIES` (4) entries; a longer list drops every description to stay light, so a type must stand on its title. The glyph rides the capsule's height: 22px beside a bare title, 26px beside two lines.
**Icon-less entries fall back to a shipped cube placeholder.** The fallback is decided at the render site (`entry.icon ?? CubeGlyph`), not at registration, so every contributor — builtin or extension — gets it uniformly and a chain replacement of the body replaces the rule with it. `CubeGlyph` lives beside `CompassGlyph` in [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx): an isometric box in 1.1px straight strokes with rounded joins on `currentColor`, drawn on `--dsw-alias-label-tertiary` — one step quieter than a registered glyph's ink — to mark the slot as unclaimed. The files type registers a description and the shared folder glyph in [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx).
**The session usage dialog drops the zero cache-write row.** This refines [Composer session stats](2026-09-07-composer-session-stats-pills.md). [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) renders the `Cache write` row only when `cacheWriteTokens !== 0`, as the per-turn panel already drops its absent optional fields; the always-present buckets (input, cache read, output) keep their rows.
## Alternatives considered
**Register the cube in `ui-primitives`.** Its `icons/index.tsx` is the imported figma `ic_ds_*` set, and the cube has a single consumer; `CompassGlyph` set the precedent of package-local guide glyphs.
**Default the icon at registration time.** A `?? default` inside the registry would hide the fallback from the body and make "registered no glyph" undetectable, losing the quieter placeholder ink; explicit render-site fallback keeps registrations honest.
**Show `Cache write 0`.** A session on a provider that never writes cache would carry the row forever; zero here means "not a thing", not a measurement.
## Consequences
`description` is new pre-stable registry API; every consumer was updated (the files entry registers one). The 4-entry threshold is a shipped constant of the guide body, not configuration. Guide-body specs cover the placeholder (size and ink), the description threshold on both sides, and the registered-glyph path; chat-stats specs cover the dropped and present cache-write row. The `ui-sidebar-right` and `ui-sidebar-files` READMEs restate the guide rules.
@@ -0,0 +1,29 @@
# Agent Note: 引导起始页与统计 pill 的细化
Status: implemented
[English](2026-09-10-guide-start-page-and-stat-pill-refinements.md) | 中文
## Problem
右侧边栏的引导 tab 只是一列光秃秃的入口胶囊:上方没有视觉锚点,胶囊只能显示标题,而类型没注册图标的入口干脆不画图标,混合列表看起来像坏了而不是稀疏。另外,输入框下方的会话 token 用量弹窗会给从未写入缓存的会话打印一行 `Cache write 0 tok`。
## Decision
**引导页是罗盘压着能自我说明的胶囊。** 本决议细化[右侧 Sidebar tab 类型与导航](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md)和 [Sidebar 文本预览与文件树](2026-09-05-sidebar-text-preview-and-file-tree.zh.md)中的引导页约定。[GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) 在入口胶囊上方画一枚弱化的 56px 罗盘,不加标题,如同浏览器起始页不给它的入口配说明文字。[`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) 新增可选的 thunk 化 `description`——与 `title` 一样每次渲染重新读取,语言切换无需重新注册。仅当引导页列出的入口不超过 `MAX_DESCRIBED_ENTRIES`(4)个时,胶囊才在标题下显示描述;更长的列表去掉所有描述以保持轻盈,所以类型必须靠标题立得住。图标随胶囊高度变化:单行标题旁 22px,两行旁 26px。
**没有图标的入口回退到内置的立方体占位符。** 回退在渲染点决定(`entry.icon ?? CubeGlyph`)而不在注册时,因此每个贡献者——内置或扩展——得到统一的占位符,链式替换 body 时规则随之整体替换。`CubeGlyph` 与 `CompassGlyph` 一起放在 [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx):一只等距视角的盒子,1.1px 直线描边、圆角拼接、走 `currentColor`,用 `--dsw-alias-label-tertiary` 着色——比注册图标的墨色浅一档——标记这个槽位无人认领。files 类型在 [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx) 注册了描述和共享的文件夹图标。
**会话用量弹窗去掉为零的缓存写入行。** 本决议细化[输入框下的会话统计](2026-09-07-composer-session-stats-pills.zh.md)。[StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) 仅当 `cacheWriteTokens !== 0` 时渲染 `Cache write` 行,与 per-turn 面板去掉缺失可选字段的做法一致;始终存在的桶(输入、缓存读取、输出)保留各自的行。
## Alternatives considered
**把立方体注册进 `ui-primitives`。** 其 `icons/index.tsx` 是导入的 figma `ic_ds_*` 集合,而立方体只有一个消费者;`CompassGlyph` 已开了引导图标包内自持的先例。
**在注册时默认图标。** 注册表内部的 `?? default` 会对 body 隐藏回退,让"没注册图标"无法辨认,浅色占位墨色随之丢失;显式的渲染点回退让注册保持诚实。
**显示 `Cache write 0`。** 用不写缓存的 provider 的会话会永远挂着这一行;这里的零意味着"没有这回事",不是一次测量。
## Consequences
`description` 是新的 pre-stable 注册表 API;所有消费者已同步更新(files 入口注册了一个)。4 个入口的阈值是引导 body 的内置常量,不是配置。guide-body 用例覆盖占位符(尺寸与墨色)、描述阈值两侧和已注册图标路径;chat-stats 用例覆盖缓存写入行的去除与保留。`ui-sidebar-right` 与 `ui-sidebar-files` 的 README 重述了引导页规则。
+1 -5
View File
@@ -55,7 +55,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
| [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
| [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT |
| [`@viz-js/viz`](https://github.com/mdaines/viz-js) | MIT |
| [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT |
| [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT |
| [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
@@ -67,7 +66,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`commander`](https://github.com/tj/commander.js) | MIT |
| [`compression`](https://github.com/expressjs/compression) | MIT |
| [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause |
| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) |
| [`e2b`](https://github.com/e2b-dev/e2b) | MIT |
| [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT |
| [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
@@ -81,7 +79,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT |
| [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT |
| [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT |
| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
| [`micromark-core-commonmark`](https://github.com/micromark/micromark/tree/main/packages/micromark-core-commonmark) | MIT |
| [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT |
| [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT |
@@ -115,8 +112,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`zod`](https://github.com/colinhacks/zod) | MIT |
| [`zustand`](https://github.com/pmndrs/zustand) | MIT |
The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside `@viz-js/viz` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as `preview-third-party-notices.txt`.
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
- `@yao-pkg/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
@@ -194,6 +189,7 @@ External packages **directly declared** for development, tests, types, or toolin
| [`jsdom`](https://github.com/jsdom/jsdom) | MIT |
| [`lefthook`](https://github.com/evilmartians/lefthook) | MIT |
| [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 |
| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
| [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT |
| [`oxlint`](https://github.com/oxc-project/oxc) | MIT |
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/README.md
README.md: 2e74ef68cf0b8487083a2e2af0f5175c78ec5212
README.zh.md: 85a75f4c025e1bfe1461b9ffdaca533d308318ba
README.zh.md: 553f28f3af78b18658701449678de80b086dbb15
+8 -8
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP(Agent Client Protocol)都是 profile,而不是独立的公开可执行命令。Python 运行时 wheel 包中也包含同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
## 入口模式
@@ -10,14 +10,14 @@
|---|---|
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
| `dsh --profile <name> --from-default-profile <template>` | 从随附模板创建新的自定义 profile,然后启动它。 |
| `dsh --profile acp` | 通过 ACP stdio 为自动化 client 提供服务,直至断开连接。 |
| `dsh --profile acp` | 通过 ACP stdio 为自动化客户端提供服务,直至断开连接。 |
| `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 |
| `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK client 提供服务,直至关闭或断开连接。 |
| `dsh --profile sdk-minimal` | 以独立极简 agent 配置树为 SDK client 提供服务。 |
| `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK 客户端提供服务,直至关闭或断开连接。 |
| `dsh --profile sdk-minimal` | 以独立极简 agent(智能体)配置树为 SDK 客户端提供服务。 |
| `dsh web` | `--profile web` 的别名。 |
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化。使用 `--from-default-profile` 可以基于这些模板之一,在尚未使用的非内置名称处创建其他 profile;通过 `dsh plugin` 则可以初始化一个以 base 为基础的 profile。`desktop` 名称保留给 Electron 持有的 profile,因此 CLI 会拒绝针对它的启动、配置 dump 和插件管理请求。
运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化。使用 `--from-default-profile` 可以基于这些模板之一,在尚未使用的非内置名称处创建其他 profile;通过 `dsh plugin` 则可以初始化一个以 base 为基础的 profile。`desktop` 名称保留给 Electron 持有的 profile,因此 CLI(命令行界面)会拒绝针对它的启动、配置 dump 和插件管理请求。
## 应用参数
@@ -45,11 +45,11 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以
使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI(命令行界面)行为参考](reference/README.zh.md)为准。
层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI 行为参考](reference/README.zh.md)为准。
## 可选 Overlay
## 可选覆盖层
`config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务与运行时 Cordis 工具的可选 overlay。它们绝不属于默认 profile;安装与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。
`config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务器与运行时 Cordis 工具的可选覆盖层。它们绝不属于默认 profile;设置与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。
## 开发
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
+2 -2
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 apps/cli/reference/README.md
README.md: 99ef8310cd1316e9ee4ade55fcac00766ae1e821
README.zh.md: aa446d539a5b666e7745f459b5a014b22e72f90e
README.md: 5a92052eb24c8af432c2ede371a2ff2bc84f57c0
README.zh.md: a9268ad4920a7d5a72a4b0ead9b2672d22e10805
+3 -3
View File
@@ -10,7 +10,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. Every launch also links packages carried only by selected external bundles through a dsh-owned directory into the current profile's `node_modules`; existing pnpm entries win, and each profile owns its links independently.
The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loudly with a hint to run `dsh plugin --profile <name> add <package>`.
`dsh --profile <name> --from-default-profile <template>` initializes a new custom target from one of those five shipped templates before boot. The target name cannot be a shipped profile name, and its complete profile directory must not exist. The launcher claims that directory exclusively, so residual files and another concurrent creator are rejected without modification. It copies the template's current bundle list and `patchReload` value into a new manifest with empty dependencies and an empty user patch. It does not read the local profile named by `<template>`, copy its dependencies or patch, or persist an inheritance field; later template-list changes do not rewrite the new profile. The in-box bundles named by that copied list still resolve from the current dsh installation. A successful initialization adds no launcher output.
@@ -37,7 +37,7 @@ The shipped apps own these command lines:
| `headless` | the task text, as the positional argument |
| `sdk` | no options; stdio carries the JSON-RPC protocol |
| `sdk-minimal` | no options; stdio carries the same JSON-RPC protocol |
| `acp` | no options; stdio carries Agent Client Protocol |
| `acp` | no options; stdio carries ACP (Agent Client Protocol) |
A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It streams non-empty provider reasoning deltas to stderr under a `dsh: reasoning:` heading, prints only the final text on stdout, and exits 0 for `completed`, else 1; a successful response with no reasoning leaves stderr empty. An invocation with no task is a usage error from that app. The shipped headless profile mounts no browser Connection, HTTP server, Web runtime, or browser client, and opens no listening port.
@@ -100,7 +100,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, opt-in DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
Feedback is recorded in the Session log without starting model work. Enable the [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) to send complete unaccepted log suffixes with subsequent DeepSeek requests, including configured gateways. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
Feedback is recorded in the Session log without starting model work. Enable the [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) to send complete unaccepted log suffixes with subsequent DeepSeek requests, including requests sent through configured gateways. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
+10 -10
View File
@@ -10,13 +10,13 @@
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。每次启动还会把仅由所选外部 bundle 携带的包经 dsh 自有目录链接到当前 profile 的 `node_modules`;已有 pnpm 条目优先,且每个 profile 独立拥有自己的链接。
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。每次启动还会把仅由所选外部组合包携带的包经 dsh 自有目录链接到当前 profile 的 `node_modules`;已有 pnpm 条目优先,且每个 profile 独立拥有自己的链接。
`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
`dsh --profile <name> --from-default-profile <template>` 会在启动前,从上述五个随附模板之一初始化新的自定义目标。目标名称不能是随附 profile 名称,并且完整的目标 profile 目录必须不存在。launcher 会以独占方式领取该目录,因此残留文件和另一个并发创建者都会在不作修改的情况下被拒绝。它把模板当前的 bundle 列表和 `patchReload` 值复制进一份依赖为空、用户 patch 为空的新 manifest。它不会读取 `<template>` 指定的本地同名 profile,不会复制其依赖或 patch,也不会持久化继承字段;模板列表之后的变化不会改写新 profile。复制列表中指名的内置 bundle 仍从当前 dsh 安装目录解析。初始化成功不会增加 launcher 输出。
`dsh --profile <name> --from-default-profile <template>` 会在启动前,从上述五个随附模板之一初始化新的自定义目标。目标名称不能是随附 profile 名称,并且完整的目标 profile 目录必须不存在。launcher 会以独占方式领取该目录,因此残留文件和另一个并发创建者都会在不作修改的情况下被拒绝。它把模板当前的组合包列表和 `patchReload` 值复制进一份依赖为空、用户 patch 为空的新 manifest。它不会读取 `<template>` 指定的本地同名 profile,不会复制其依赖或 patch,也不会持久化继承字段;模板列表之后的变化不会改写新 profile。复制列表中指名的内置组合包仍从当前 dsh 安装目录解析。初始化成功不会增加 launcher 输出。
profile 已经存在时,`--from-default-profile` 会被拒绝,且不会修改或启动它;去掉该选项即可使用它。残留的目标目录同样会被原样保留,此时必须改用另一个 profile 名称。未知模板或随附目标名称会在创建目标之前失败;未知模板的诊断会列出有效模板。初始化在 bundle 解析和应用启动之前提交,因此后续失败仍会把新 profile 留在磁盘上,重试时需要去掉创建选项。`--dump-config` 和 `--dump-default-config` 接受该选项:它们初始化目标并打印所请求的配置树,但不启动应用。
profile 已经存在时,`--from-default-profile` 会被拒绝,且不会修改或启动它;去掉该选项即可使用它。残留的目标目录同样会被原样保留,此时必须改用另一个 profile 名称。未知模板或随附目标名称会在创建目标之前失败;未知模板的诊断会列出有效模板。初始化在组合包解析和应用启动之前提交,因此后续失败仍会把新 profile 留在磁盘上,重试时需要去掉创建选项。`--dump-config` 和 `--dump-default-config` 接受该选项:它们初始化目标并打印所请求的配置树,但不启动应用。
```sh
dsh --profile rescue --from-default-profile web
@@ -39,9 +39,9 @@ dsh --profile rescue
| `headless` | 任务文本,作为位置参数 |
| `sdk` | 无选项;stdio 携带 JSON-RPC 协议 |
| `sdk-minimal` | 无选项;stdio 携带相同的 JSON-RPC 协议 |
| `acp` | 无选项;stdio 携带 Agent Client Protocol |
| `acp` | 无选项;stdio 携带 ACP(Agent Client Protocol) |
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 `dsh: reasoning:` 标题下将非空的提供方推理分片流式写入 stderr,只在 stdout 打印最终文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出;没有推理内容的成功响应会保持 stderr 为空。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载浏览器 Connection、HTTP 服务器、Web 运行时或浏览器客户端,也不会打开监听端口。
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 `dsh: reasoning:` 标题下将非空的提供方推理(reasoning)增量流式写入 stderr,只在 stdout 打印最终文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出;没有推理内容的成功响应会保持 stderr 为空。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载浏览器 Connection、HTTP 服务器、Web 运行时或浏览器客户端,也不会打开监听端口。
可在不启动的情况下检查组合出的配置树:
@@ -56,7 +56,7 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
Codex 与 Claude Code subagent provider 是两个彼此独立的可选 Bundle。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
Codex 与 Claude Code subagent 提供方是两个彼此独立的可选组合包。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
```sh
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
@@ -66,7 +66,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
```
pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装 Bundle 只注册自己的休眠 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.zh.md)与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.zh.md)负责可执行文件、身份验证、载荷与失败细节;[base Bundle 参考](../../../packages/bundle/base/README.zh.md)负责默认依赖闭包。
pnpm 操作成功后会改变磁盘上的 Profile manifest 与组合包列表;正在运行的 Profile 会保留本次启动时的组合包集合。添加、移除或更新组合包后须重启该 Profile。这个启动边界只适用于组合包成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装组合包只注册自己的休眠 Host 提供方;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.zh.md) 与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.zh.md) 负责可执行文件、身份验证、载荷与失败细节;[base 组合包参考](../../../packages/bundle/base/README.zh.md) 负责默认依赖闭包。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
@@ -92,7 +92,7 @@ dsh web --help
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose 所有实时监视器。
基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。
@@ -102,9 +102,9 @@ dsh web --help
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,需主动开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
反馈记录在会话日志中,不会启动模型工作。开启 [DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)后,后续 DeepSeek 请求会发送尚未确认的完整日志后缀,包括发往配置网关的请求。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整权威日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
反馈记录在会话日志中,不会启动模型工作。开启 [DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)后,后续 DeepSeek 请求会发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent 沙箱之外的受信任可执行代码。
<a id="source-execution"></a>
## 源码执行
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop-host",
"description": "Private upstream-Node host process for the Electron desktop application",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"private": true,
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/desktop/README.md
README.md: cf350a9d5e9577f5cf52c4371b32f16e278133f9
README.zh.md: df48eb5903e21cae9ff041626c28d41d12e5c632
README.zh.md: aa870c2a74f3b74d3f6de921c4bc5ef04fbfad3f
+21 -21
View File
@@ -9,32 +9,32 @@
| 决策 | 原因 | 直接结果 |
|---|---|---|
| 发布身份 | 桌面壳 API、Web 客户端、后端与插件依赖图作为一个组合完成验证;独立版本会产生未经验证的组合,并让更新可用性含糊不清。 | Electron 与 `@deepseek-ai/dsh` 始终使用同一精确版本。即使桌面壳代码不变,升级 dsh 也必须发布新 Desktop 版本。 |
| 运行时 | Electron 的 Node.js 带有 Electron 补丁、fuse、ABI 与生命周期约束,而系统运行时和用户包管理器状态不可控。 | dsh 通过内置的上游 Node.js 运行,所有包操作都使用内置 pnpm。Electron 的 Node.js、系统 Node.js、系统 pnpm 与用户的包管理器配置都不进入执行路径。 |
| 包来源 | 必须能在发布到 npm 之前从同一次源码构建打包精确的 dsh,并支持离线安装;插件则需要保留为用户选择的普通 npm 包。 | 已签名应用携带本地打包的第一方 dsh 包与离线 seed store。桌面插件仍是从固定 Desktop registry 解析的普通 npm 依赖。 |
| Seed 传输 | Apple 公证会检查归档内的代码;把 pnpm store 的每个文件分别放入应用,还会让应用签名记录数万个缓存条目,而单个压缩归档会放大小幅包变更。 | macOS 打包先签署每个 Mach-O CAS 对象、重写其 pnpm 哈希并再次证明离线安装,再把 store 文件分配到 16 个确定性的未压缩 tar 分片。外层安装包负责压缩,差分更新可以复用未变化的分片。 |
| 状态归属 | 共享可执行依赖图会让 CLI 与 Desktop 相互改变 dsh、Cordis、插件或原生模块版本,而两个桌面进程还可能争用同一个 profile。 | Electron 在访问任何 profile 前获取进程生命周期单实例锁,并独占 `$DSH_HOME/profiles/desktop` 及其包管理器状态。CLI 与 Desktop 共享 `$DSH_HOME` 下受支持的产品数据,但绝不共享可执行包、插件激活、锁文件或 `node_modules`。 |
| 运行时 | Electron 的 Node.js 带有 Electron 补丁、fuse、ABI 与生命周期约束,而系统运行时和包管理器状态不可控。 | dsh 通过内置的上游 Node.js 运行,所有包操作都使用内置 pnpm。Electron 的 Node.js、系统 Node.js、系统 pnpm 与用户的包管理器配置都不进入执行路径。 |
| 包来源 | 必须能在发布到 npm 之前从同一次源码构建打包精确的 dsh,并支持离线安装;插件则需要保留为用户选择的普通 npm 包。 | 已签名应用携带本地打包的第一方 dsh 包与离线 seed 存储。桌面插件仍是从固定 Desktop 注册表解析的普通 npm 依赖。 |
| Seed 传输 | Apple 公证会检查归档内的代码;把 pnpm 存储的每个文件分别放入应用,还会让应用签名记录数万个缓存条目,而单个压缩归档会放大小幅包变更。 | macOS 打包先签署每个 Mach-O CAS 对象、重写其 pnpm 哈希并再次证明离线安装,再把存储文件分配到 16 个确定性的未压缩 tar 分片。外层安装包负责压缩,差分更新可以复用未变化的分片。 |
| 状态归属 | 共享可执行依赖图会让 CLI(命令行界面)与 Desktop 相互改变 dsh、Cordis、插件或原生模块版本,而两个桌面进程还可能争用同一个 profile。 | Electron 在访问任何 profile 前获取进程生命周期单实例锁,并独占 `$DSH_HOME/profiles/desktop` 及其包管理器状态。CLI 与 Desktop 共享 `$DSH_HOME` 下受支持的产品数据,但绝不共享可执行包、插件激活、锁文件或 `node_modules`。 |
| 通信 | 监听 Web 服务会引入端口归属、认证、CORS 与暴露风险;Electron 与上游 Node.js 之间也需要明确的跨进程协议。 | 应用不打开 Web 端口。`dsh-app://` 承载 Web 资源和 Fetch 流量;分帧字节管道以背压传输有界请求与响应分块,Node IPC 只承载子进程生命周期控制。 |
| 激活 | 依赖解析、生命周期脚本、原生模块与插件启动都可能失败,目录替换期间进程也可能中断。 | 发布与插件变更先安装到 staging,并启动完整后端执行健康检查;只有成功后才替换活跃 profile,中断替换由事务日志和一个 rollback profile 恢复。 |
| 更新 | 桌面壳与 dsh 独立更新会重新产生版本分裂,而桌面壳未变化的数据块不应强制完整传输。 | Electron 壳、匹配的 dsh seed、Node.js 与 pnpm 组成一个已签名更新单元。平台更新产物可以复用未变化的数据块,但运行时版本选择绝不脱离 Desktop 发布。 |
[Electron 打包与更新 Agent Note](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md)记录了这些决策背后的理由、替代方案、安全约束和发布验证要求。
[Electron 打包与更新 Agent Note](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md) 记录了这些决策背后的理由、替代方案、安全约束和发布验证要求。
## 安装归属
Electron 拥有保留 profile `$DSH_HOME/profiles/desktop`。其 manifest 通过 `dsh.profile.bundles` 列出内置与已安装插件 bundle,`node_modules` 则同时包含精确版本的 `@deepseek-ai/dsh`、与之匹配的私有 `@deepseek-ai/dsh-desktop-host` 和所有桌面插件。把 Electron 专用进程入口与 overlay 放入私有应用包,可以避免 Desktop 实现成为公共 CLI 包的一部分。CLI 不能启动或修改该 profile。Electron 始终调用自身内置的 Node.js 与 pnpm,并把 store 固定在 `$DSH_HOME/desktop/pnpm/store`;它绝不使用系统 pnpm 或调用方的 npm/pnpm 配置。
Electron 拥有保留 profile `$DSH_HOME/profiles/desktop`。其 manifest(元数据清单)通过 `dsh.profile.bundles` 列出内置与已安装插件 bundle,`node_modules` 则同时包含精确版本的 `@deepseek-ai/dsh`、与之匹配的私有 `@deepseek-ai/dsh-desktop-host` 和所有桌面插件。把 Electron 专用进程入口与 overlay 放入私有应用包,可以避免 Desktop 实现成为公共 CLI 包的一部分。CLI 不能启动或修改该 profile。Electron 始终调用自身内置的 Node.js 与 pnpm,并把存储固定在 `$DSH_HOME/desktop/pnpm/store`;它绝不使用系统 pnpm 或调用方的 npm/pnpm 配置。
dsh 主渲染进程只获得桌面协议标记。独立插件窗口获得结构化的列出、安装、移除、更新和更新检查操作;两个渲染进程都拿不到文件系统、原始 Electron IPC、shell 或任意 pnpm 参数。
Electron 根据应用 locale 选择类型化的中英文字典,并以英文作为 fallback。菜单、原生对话框与插件管理渲染进程使用同一 locale 数据;仓库的 Client UI i18n gate 会检查这些桌面源文件。
Electron 根据应用 locale 选择类型化的英文或中文桌面壳文案,并以英文作为 fallback。菜单、原生对话框与插件管理渲染进程使用同一 locale 数据;仓库的 Client UI i18n gate 会检查这些桌面源文件。
### Seed 安装
安装包内的 seed 是安装工具包,不是可以直接运行的 `node_modules` 目录。打包过程会生成锁文件,在禁用生命周期脚本的情况下在线物化生产依赖图,删除 `node_modules` 以及所有临时 pnpm cache、config 和 state 目录,然后只使用最终 store 完成一次完整离线安装,并验证私有 Desktop Host 的入口与 overlay 均存在。macOS 构建随后从 pnpm 内容寻址 store staging 每个 Mach-O 对象,最多并发四个 Developer ID 签名进程,并且只在所有签名成功后才更新受影响的 SHA-512 索引记录。再一次离线安装会在分片前证明重写后的 store;准备过程随后解包最终归档,并验证每个内嵌签名。签名 seed 保留发布身份、本地第一方 tarball 及其描述文件、项目元数据、锁文件、完整性清单,以及在用户机器上重复该安装所需的 pnpm store 内容。
安装包内的 seed 是安装工具包,不是可以直接运行的 `node_modules` 目录。打包过程会生成锁文件,在禁用生命周期脚本的情况下在线物化生产依赖图,删除 `node_modules` 以及所有临时 pnpm cache、配置和 state 目录,然后只使用最终存储完成一次完整离线安装,并验证私有 Desktop Host 的入口与 overlay 均存在。macOS 构建随后暂存 pnpm 内容寻址存储中的每个 Mach-O 对象,最多并发签署四个独立副本,并且只在所有签名成功后才更新受影响的 SHA-512 索引记录。再一次离线安装会在分片前证明重写后的存储;准备过程随后解包最终归档,并验证每个内嵌签名。签名 seed 保留发布身份、本地第一方 tarball 及其描述文件、项目元数据、锁文件、完整性清单,以及在用户机器上重复该安装所需的 pnpm 存储内容。
| Seed 内容 | 可写目标或用途 |
|---|---|
| `integrity.json` 与 `desktop-packages.json` | 在修改包状态前验证清单记录的每个 seed 文件、本地 tarball 哈希以及绑定的 dsh 与 Desktop Host 版本。 |
| `store-archives.json` 与 `store-archives/*.tar` | 验证确定性的未压缩分片,把它们解包到唯一的 Desktop staging 目录,替换匹配的不可变 store 文件,并以事务方式把 pnpm 的版本化 SQLite 包索引合并进 `$DSH_HOME/desktop/pnpm/store`,且不移除已经为 Desktop 插件下载的包。 |
| `store-archives.json` 与 `store-archives/*.tar` | 验证确定性的未压缩分片,把它们解包到唯一的 Desktop staging 目录,替换匹配的不可变存储文件,并以事务方式把 pnpm 的版本化 SQLite 包索引合并进 `$DSH_HOME/desktop/pnpm/store`,且不移除已经为 Desktop 插件下载的包。 |
| 项目元数据与 `desktop-packages/` | 复制到唯一的 `$DSH_HOME/desktop/staging/<transaction-id>/profile` 项目。 |
| 锁文件与本地包映射 | 驱动内置 pnpm 完成安装,且不会从 npm 解析已打包的核心包名。 |
@@ -42,12 +42,12 @@ Electron 根据应用 locale 选择类型化的中英文字典,并以英文作
1. 恢复中断的激活事务日志,验证完整 seed 清单与本地包集,并要求 seed 版本等于 Electron 应用版本。
2. 如果活跃 profile 已包含该发布及匹配的 dsh 与 Desktop Host 版本,则验证其中的本地包集并直接复用,不重新安装。
3. 否则验证每个归档条目,把全部 store 分片解包到 Desktop 拥有的临时 staging 目录,将包文件与 SQLite 包索引记录合并进私有 store,再创建 staging profile,并通过内置 Node.js 与 pnpm 执行 `pnpm install --offline --frozen-lockfile --trust-lockfile`。Seed 记录替换匹配的索引键,插件专属记录继续保留。
3. 否则验证每个归档条目,把全部存储分片解包到 Desktop 拥有的临时 staging 目录,将包文件与 SQLite 包索引记录合并进私有存储,再创建 staging profile,并通过内置 Node.js 与 pnpm 执行 `pnpm install --offline --frozen-lockfile --trust-lockfile`。Seed 记录替换匹配的索引键,插件专属记录继续保留。
4. Electron 升级时,从旧活跃 profile 读取每个插件的名称和精确版本,再通过现有 Desktop pnpm 状态以 `--offline` 把这些版本加入 staging。首次安装不执行插件恢复。
5. 停止活跃后端,启动并停止完整的 staging 后端执行健康检查,再在激活前重新启动活跃后端。这种串行方式避免两个桌面后端共享 `$DSH_HOME`;安装错误或插件不兼容会删除 staging,并保持活跃 profile 不变。
6. 在每次目录移动前先持久化下一个激活阶段,把活跃 profile 移到 `$DSH_HOME/desktop/rollback/profile`,再把 staging 移到 `$DSH_HOME/profiles/desktop`。恢复过程同时检查日志与真实的 profile、rollback 和 staging 目录,因此在任一个写入与移动间隙中断后仍会恢复或保留一个完整 profile。
6. 在每次目录移动前先持久化下一个激活阶段,把活跃 profile 移到 `$DSH_HOME/desktop/rollback/profile`,再把 staging 移到 `$DSH_HOME/profiles/desktop`。恢复过程同时检查日志与真实的 profile、rollback 和 staging 目录,因此在写入后、移动前的任一间隙中断后仍会恢复或保留一个完整 profile。
GUI 插件修改会在把 registry 包安装到共享 Desktop pnpm store 后,使用相同的 staging、健康检查、激活与 rollback 路径。
GUI 插件修改会在把注册表包安装到共享 Desktop pnpm 存储后,使用相同的 staging、健康检查、激活与 rollback 路径。
进程生命周期 Electron 锁是桌面端的主要 owner。事务锁用于纵深防御:准备本地状态时记录 Electron,在 pnpm worker 仍可能写入时记录该 worker,worker 退出后再把 owner 交还 Electron。后续进程不会把仍然存活的孤儿 worker 误判为陈旧事务。
@@ -67,11 +67,11 @@ pnpm run dev:desktop
pnpm run start:desktop
```
Workspace 开发使用调用命令的 Node.js 运行当前 CLI 与私有 Desktop Host 包,并禁用桌面包修改;只有该模式明确链接的一次性 profile 可以从自身目录外解析 bundle。需要验证内置 Node.js、内置 pnpm、发布 seed、插件安装、staging 和 rollback 时,应运行未封装安装器的应用目录。
Workspace 开发使用调用命令的 Node.js 运行当前 CLI 与私有 Desktop Host 包,并禁用桌面包修改;只有该模式明确链接的一次性 profile 可以从自身目录外解析 bundle。需要验证内置 Node.js、内置 pnpm、发布 seed、插件安装、staging 和 rollback 时,应运行未打包应用。
## 打包
正常打包只需执行一条完整命令。该命令会先准备发布资源,再生成宿主平台的安装包与更新元数据。所有目标都要求通过 `DSH_DESKTOP_APP_ID` 提供反向域名形式的应用 ID。macOS 目标还要求通过 `DSH_DESKTOP_MACOS_SIGNING_IDENTITY` 提供 electron-builder 证书限定名,通过 `DSH_DESKTOP_MACOS_TEAM_ID` 提供对应的 10 字符 Apple Team ID,并提供一套完整的 notarytool 凭据。App Store Connect API Key 方式使用以下变量:
正常打包只需执行一条完整命令。该命令会先准备发布资源,再生成宿主平台的安装包与更新元数据。所有目标都要求通过 `DSH_DESKTOP_APP_ID` 提供反向域名形式的应用 ID。macOS 目标还要求通过 `DSH_DESKTOP_MACOS_SIGNING_IDENTITY` 提供 electron-builder 证书限定名,通过 `DSH_DESKTOP_MACOS_TEAM_ID` 提供对应的 10 字符 Apple Team ID,并提供一套完整的 notarytool 凭据方案。App Store Connect API Key 方式使用以下变量:
```sh
export DSH_DESKTOP_APP_ID='<reverse-DNS application ID>'
@@ -96,7 +96,7 @@ pnpm run package:desktop:mac:x64
pnpm run package:desktop:win:x64
```
macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 或带 Rosetta 的 Apple Silicon 上运行。Windows x64 命令要求 Windows x64。Desktop 尚不支持 Linux 发布目标。
macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 或带 Rosetta 的 Apple Silicon 上运行。Windows x64 命令要求 Windows x64。Linux 不是受支持的 Desktop 发布目标。
每个目标都在 `apps/desktop/.desktop-build/targets/<target>/` 下持有自己的打包输入、已准备运行时、包集合、seed、pnpm 准备状态、未打包应用、更新元数据和最终产物。Node.js 归档缓存继续由 `.desktop-build/downloads` 共享,因为每个归档文件名都包含版本、平台和架构,并且在解包前经过验证。目标构建绝不读取其他目标的可变准备状态。
@@ -123,9 +123,9 @@ export DOWNLOAD_TEST_COS_SECRET_KEY='<test COS SecretKey>'
pnpm run upload:mac:arm64
```
生产发布需在打包前设置 `DSH_DESKTOP_AUTO_UPDATE_ENV=production`,再在执行 `upload:mac:arm64`、`upload:mac:x64` 或 `upload:win:x64` 前提供 `DOWNLOAD_PROD_COS_BUCKET` 与生产凭据对。打包不要求 COS bucket 或凭据。它会明确禁止 electron-builder 发布,从其子进程中删除全部四个 COS 凭据字段,并且只有在 electron-builder 以及全部签名或公证 hook 成功后才写入目标完成记录。上传会先要求该记录与所选环境、目标、公开 URL 和当前 dsh 版本一致,再要求根 dsh 版本、Desktop 版本、频道元数据版本、产物名称、大小与 SHA-512 全部一致,之后才读取所选 COS 凭据对。它只上传该目标不可变且带版本的产物,最后以 `no-cache` 上传根据版本得出的频道元数据,并且不会删除历史对象。稳定版本使用 `latest-mac.yml` 或 `latest.yml`;`alpha` 等预发布版本则使用 `alpha-mac.yml` 或 `alpha.yml`,与 electron-builder 生成的文件名一致。
生产发布需在打包前设置 `DSH_DESKTOP_AUTO_UPDATE_ENV=production`,再在执行 `upload:mac:arm64`、`upload:mac:x64` 或 `upload:win:x64` 前提供 `DOWNLOAD_PROD_COS_BUCKET` 与生产凭据对。打包不要求 COS bucket 或凭据。它会明确禁止 electron-builder 发布,从其子进程中删除全部四个 COS 凭据字段,并且只有在 electron-builder 以及全部签名或公证钩子成功后才写入目标完成记录。上传会先要求该记录与所选环境、目标、公开 URL 和当前 dsh 版本一致,再要求根 dsh 版本、Desktop 版本、频道元数据版本、产物名称、大小与 SHA-512 全部一致,之后才读取所选 COS 凭据对。它只上传该目标不可变且带版本的产物,最后以 `no-cache` 上传根据版本得出的频道元数据,并且不会删除历史对象。稳定版本使用 `latest-mac.yml` 或 `latest.yml`;`alpha` 等预发布版本则使用 `alpha-mac.yml` 或 `alpha.yml`,与 electron-builder 生成的文件名一致。
macOS 配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 打包要求已配置的身份及其私钥可用。Seed 准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。Electron-builder 会在封装前公证应用并钉票,然后签署 DMG。DMG 的 artifact-completion hook 随后会公证它并钉票,再要求其身份、票据与 Gatekeeper 验证全部通过;只有 hook 成功,electron-builder 才能发布该文件。私钥可以来自登录钥匙串或 electron-builder 的标准 `CSC_LINK` 输入;环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
macOS 配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 打包要求已配置的身份及其私钥可用。Seed 准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。Electron-builder 会在封装前公证应用并钉票,然后签署 DMG。DMG 的 artifact-completion 钩子随后会公证它并钉票,再要求其身份、票据与 Gatekeeper 验证全部通过;只有钩子成功,electron-builder 才能发布该文件。私钥可以来自登录钥匙串或 electron-builder 的标准 `CSC_LINK` 输入;环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
### Windows EV 签名
@@ -139,7 +139,7 @@ $env:DSH_DESKTOP_WINDOWS_TOKEN_PIN = '<SafeNet Token Password>'
pnpm run package:desktop:win:x64
```
打包前插入并解锁 Token。electron-builder hook 把每个产物交给采用 CRLF 的 `scripts/windows-sign.cmd`;该 CMD 只调用一次已配置的 SignTool,并指定 `/f`、SafeNet `/kc "[{{PIN}}]=容器"`、`/csp "eToken Base Cryptographic Provider"`、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。hook 不会改用 electron-builder 内置的 SignTool,也不会重试失败的签名请求。SignTool、证书、容器、PIN、Token 或签名不可用时,Windows 打包会失败,不会生成未签名产物。
打包前插入并解锁 Token。electron-builder 钩子把每个产物交给采用 CRLF 的 `scripts/windows-sign.cmd`;该 CMD 只调用一次已配置的 SignTool,并指定 `/f`、SafeNet `/kc "[{{PIN}}]=容器"`、`/csp "eToken Base Cryptographic Provider"`、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。钩子不会改用 electron-builder 内置的 SignTool,也不会重试失败的签名请求。SignTool、证书、容器、PIN、Token 或签名不可用时,Windows 打包会失败,不会生成未签名产物。
PIN 不能包含 `]`、引号或换行,因为这些字符用于分隔 SafeNet `/kc` 值或对应的 CMD 参数。CMD 会禁用延迟展开,因此包含 `!` 的 PIN 可以原样到达 SafeNet。打包流程不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 seed 准备子进程;它只向 electron-builder 提供四个配置输入,在其他字段已经清理的环境中只向签名 CMD 提供经过校验的签名字段,在 SignTool 启动前清除这些字段,并遮盖 SignTool 诊断。SafeNet 仍要求 PIN 出现在 SignTool 进程命令行中。只能在连接了物理 Token 的受控 self-hosted Windows runner 上把它注入为临时 secret;绝不能提交该值、把它写进 `.env`,或持久保存为 Windows 用户或系统环境变量。
@@ -158,9 +158,9 @@ pnpm run prepare:desktop
这条诊断命令是另一种停止位置,并非两条命令构建流程的前半段。之后执行 `package:desktop*` 时仍会重新完成正式构建与准备,避免使用陈旧的 dsh 包、运行时文件或 seed 内容。
每条打包命令都会先执行仓库的正式构建,打包 dsh 与 vendored 包族,在本地打包私有 Desktop Host 包,并打包 Landlock 入口,然后再准备发布资源。`prepare:packages` 选择分别以 `@deepseek-ai/dsh` 和 `@deepseek-ai/dsh-desktop-host` 为根的第一方生产依赖闭包之并集,验证私有 Host tarball 同时包含 `lib/index.js` 与 `config/desktop.cordis.patch.yml`,把选中的 tarball 复制到 seed 输入,并记录其大小与 SHA-512 完整性。Host 包不会发布到 npm;它的 `files` manifest 只包含该运行入口与 overlay。公共包 tarball 仍是由各包发布 manifest 控制的正式 `pnpm pack` 输出,因此 Desktop 不增加第二套过滤规则,会保留 `lib/types` 等已发布声明,也不会独立删除或增加 source map。Registry 包同样在 pnpm 内容寻址 store 中保留其发布的包字节。dsh 发布版本更新会同步更新两个私有 Desktop manifest、仓库根与可发布 workspace;打包还会要求根 dsh 包、Desktop Host 包与 Electron 包使用同一版本。构建 Desktop 应用前不要求 dsh 或私有 Host 已发布到 npm。`prepare:runtime` 从 Node.js 官方发行服务下载 Node.js 24.17.0,在解压前验证其 SHA-256 条目,并在兼容的构建宿主上执行准备完成的目标二进制文件以验证其报告版本。它复制桌面包声明的 pnpm 版本,并把两个运行时版本记录进发布 seed。`prepare:seed` 运行该目标 Node.js 与内置 pnpm,因此按平台和 CPU 过滤的可选依赖会使 pnpm store 与 seed 成为目标专用内容。它生成本地核心包映射、禁用全局 virtual store、从 npm 物化外部生产依赖并禁用生命周期脚本、删除 `node_modules` 以及所有临时 pnpm cache、config 和 state,证明完整依赖图可以离线安装并包含私有 Host 的入口与 overlay,在适用时执行 macOS 重写,再通过一次离线安装证明重写后的 store,删除临时 pnpm 项目注册,然后把松散 store 替换为 16 个确定性的未压缩 tar 分片。它会解包这些最终分片,并在生成清单前验证每个内嵌 macOS 签名。后续 GUI 插件操作保留本地核心包映射,同时从固定的 Desktop npm registry 解析插件包及其外部依赖。`electron-builder` 把各目标的平台产物写到 `apps/desktop/.desktop-build/targets/<target>/artifacts`;后续版本会保留不同名称的不可变安装包与 blockmap,但会替换该目标的未打包应用、诊断文件、完成记录与频道元数据。
每条打包命令都会先执行仓库的正式构建,打包 dsh 与 vendored 包族,在本地打包私有 Desktop Host 包,并打包 Landlock 入口,然后再准备发布资源。`prepare:packages` 选择分别以 `@deepseek-ai/dsh` 和 `@deepseek-ai/dsh-desktop-host` 为根的第一方生产依赖闭包之并集,验证私有 Host tarball 同时包含 `lib/index.js` 与 `config/desktop.cordis.patch.yml`,把选中的 tarball 复制到 seed 输入,并记录其大小与 SHA-512 完整性。Host 包不会发布到 npm;它的 `files` manifest 只包含该运行入口与 overlay。公共包 tarball 仍是由各包发布 manifest 控制的正式 `pnpm pack` 输出,因此 Desktop 不增加第二套过滤规则,会保留 `lib/types` 等已发布声明,也不会独立删除或增加 source map。注册表包同样在 pnpm 内容寻址存储中保留其发布的包字节。dsh 发布版本更新会同步更新两个私有 Desktop manifest、仓库根与可发布 workspace;打包还会要求根 dsh 包、Desktop Host 包与 Electron 包使用同一版本。构建 Desktop 应用前不要求 dsh 或私有 Host 已发布到 npm。`prepare:runtime` 从 Node.js 官方发行服务下载 Node.js 24.17.0,在解压前验证其 SHA-256 条目,并在兼容的构建宿主上执行准备完成的目标二进制文件以验证其报告版本。它复制桌面包声明的 pnpm 版本,并把两个运行时版本记录进发布 seed。`prepare:seed` 运行该目标 Node.js 与内置 pnpm,因此按平台和 CPU 过滤的可选依赖会使 pnpm 存储与 seed 成为目标专用内容。它生成本地核心包映射、禁用全局 virtual 存储、从 npm 物化外部生产依赖并禁用生命周期脚本、删除 `node_modules` 以及所有临时 pnpm cache、配置和 state,证明完整依赖图可以离线安装并包含私有 Host 的入口与 overlay,在适用时执行 macOS 重写,再通过一次离线安装证明重写后的存储,删除临时 pnpm 项目注册,然后把松散存储替换为 16 个确定性的未压缩 tar 分片。它会解包这些最终分片,并在生成清单前验证每个内嵌 macOS 签名。后续 GUI 插件操作保留本地核心包映射,同时从固定的 Desktop npm 注册表解析插件包及其外部依赖。`electron-builder` 把各目标的平台产物写到 `apps/desktop/.desktop-build/targets/<target>/artifacts`;后续版本会保留不同名称的不可变安装包与 blockmap,但会替换该目标的未打包应用、诊断文件、完成记录与频道元数据。
未压缩产物包含四块相互独立的体积:Electron、离线 seed store 分片与本地 dsh tarball、上游 Node.js 与 pnpm 运行时,以及很小的桌面壳应用。分片不压缩,使外层 DMG、ZIP 或 NSIS 压缩器与差分更新器可以处理稳定的数据区间。文件系统占用不等于安装包下载大小,因此必须分别测量。打包应用首次启动时还会先把 seed store 解包到 `$DSH_HOME/desktop/pnpm/store`,再安装可写 profile,因此发布验证必须同时测量应用与 Harness home 的磁盘占用。
未打包产物包含四块相互独立的体积:Electron、离线 seed 存储分片与本地 dsh tarball、上游 Node.js 与 pnpm 运行时,以及很小的桌面壳应用。分片不压缩,使外层 DMG、ZIP 或 NSIS 压缩器与差分更新器可以处理稳定的数据区间。文件系统占用不等于安装包下载大小,因此必须分别测量。打包应用首次启动时还会先把 seed 存储解包到 `$DSH_HOME/desktop/pnpm/store`,再安装可写 profile,因此发布验证必须同时测量应用与 Harness home 的磁盘占用。
## 更新
@@ -174,7 +174,7 @@ Electron-builder 始终为 `DSH_DESKTOP_AUTO_UPDATE_ENV` 选择的部署生成 g
## 已知限制
- Desktop 禁用 Web 的“在本地应用中打开”操作,因为其 Host 插件依赖 HTTP 路由,而 Desktop 不提供 `webServer`。
- Desktop 禁用 Web 的「在本地应用中打开…」操作,因为其 Host 插件依赖 HTTP 路由,而 Desktop 不提供 `webServer`。
- 发布签名、公证、更新托管和跨上一版本的已安装产物验证需要生产发布环境。
- 依赖包含 lifecycle script 的桌面插件,只有其包名进入桌面项目经过评审的 `allowBuilds` 策略后才能安装。
- 桌面壳与 CLI dsh 共享 `$DSH_HOME` 下的会话、设置、凭据、工作区和存储,但可执行包、插件激活、锁文件与包管理器状态彼此隔离。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop",
"description": "Electron desktop shell for an isolated pnpm-installed dsh runtime",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"private": true,
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
+2 -2
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 apps/web/tests/README.md
README.md: a3503e66d780e62562348b02830517381d21df92
README.zh.md: a9039bbd0444a0786cd419c1e6aba0c3504e26d3
README.md: 883d93d746afa6bac0b67f2c2d6bc6c03b31553a
README.zh.md: 7a232329b890a669240272aada3a5bafff9efd99
+5 -29
View File
@@ -2,11 +2,7 @@
English | [中文](README.zh.md)
These tests boot the real web composition in-process and drive it with a real
Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and
the deliberate composition divergences from `dsh web` — are documented in
[`scaffold.ts`](scaffold.ts) and the
[browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
These tests boot the real web composition in-process and drive it with a real Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and the deliberate composition divergences from `dsh web` — are documented in [`scaffold.ts`](scaffold.ts) and the [browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
## Completion observations
@@ -14,34 +10,14 @@ State-sensitive cases use Workspace, admission, attachment, and model-stream bar
## These are Host-face tests
They type-check in the root `tsconfig.host.json`, not in the Client aggregate,
because they read Host services directly: `ctx.connection`, the Host
`SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does
not make a file part of the Client program — the two faces merge cordis
`Context` under the same keys with different services, so one program cannot see
both. Moving these files into the Client aggregate makes every Host-service
access fail to compile.
They type-check in the root `tsconfig.host.json`, not in the Client aggregate, because they read Host services directly: `ctx.connection`, the Host `SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not make a file part of the Client program — the two faces merge Cordis `Context` under the same keys with different services, so one program cannot see both. Moving these files into the Client aggregate makes every Host-service access fail to compile.
## Do not import `@deepseek-ai/dsh-client-*` here
Importing a Client package — a value or a type — pulls its whole TypeScript
project, and every project it references, into the **Host build graph**. That has
bitten this lane once already: four Client consumer packages reference
`api/remotes`' Client face, which cannot compile until Host tsdown has generated
`@deepseek-ai/dsh-goal/remote`, so the Host build phase ended up waiting on an
artifact it produces itself.
Importing a Client package — a value or a type — pulls its whole TypeScript project, and every project it references, into the **Host build graph**. That has bitten this lane once already: four Client consumer packages reference `api/remotes`' Client face, which cannot compile until Host tsdown has generated `@deepseek-ai/dsh-goal/remote`, so the Host build phase ended up waiting on an artifact it produces itself.
When a scenario needs a Client-owned constant or pure function, mirror it here
instead, next to the commented-out import that names the source module. A drift
then surfaces as a missed selector or a stale mirrored value — a loud failure,
never a silent pass. `scaffold.ts` follows this rule for the welcome-notice
namespace, acknowledgement field, version, and asserted Chinese copy.
When a scenario needs a Client-owned constant or pure function, mirror it here instead, next to the commented-out import that names the source module. A drift then surfaces as a missed selector or a stale mirrored value — a loud failure, never a silent pass. `scaffold.ts` follows this rule for the welcome-notice namespace, acknowledgement field, version, and asserted Chinese copy.
One kind of Client import stands. `assembled-boot.ts` drives the shell itself, so
it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest
type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what
that harness is for, and both packages are already in the Host graph. The chat
scenarios mirror `conversationContextKey` in `support.ts` instead of importing
its Client owner.
One kind of Client import stands. `assembled-boot.ts` drives the shell itself, so it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what that harness is for, and both packages are already in the Host graph. The chat scenarios mirror `conversationContextKey` in `support.ts` instead of importing its Client owner.
Nothing mechanically enforces this rule; keep it in review.
+5 -21
View File
@@ -2,10 +2,7 @@
[English](README.md) | 中文
这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane
的运行机制——模式、fixture、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在
[`scaffold.ts`](scaffold.ts) 和
[浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)中。
这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane 的运行机制——模式、fixture(测试前置数据)、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在 [`scaffold.ts`](scaffold.ts) 和 [浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md) 中。
## 完成状态观察
@@ -13,27 +10,14 @@
## 这些是 Host 面的测试
它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取
Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动
浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 cordis
`Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处
Host 服务访问都无法编译。
它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取 Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 Cordis `Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处 Host 服务访问都无法编译。
## 不要在此 import `@deepseek-ai/dsh-client-*`
import 一个 Client 包——无论值还是类型——都会把它整个 TypeScript 工程、以及它引用的每个工程
拉进 **Host 构建图**。这已经坑过本 lane 一次:四个 Client 消费方包引用了 `api/remotes` 的
Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 之后才能编译,
于是 Host 构建阶段变成在等一个由它自己产出的产物。
import 一个 Client 包——无论值还是类型——都会把它整个 TypeScript 工程、以及它引用的每个工程拉进 **Host 构建图**。这已经坑过本 lane 一次:四个 Client 消费方包引用了 `api/remotes` 的 Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 之后才能编译,于是 Host 构建阶段变成在等一个由它自己产出的产物。
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
通过。`scaffold.ts` 按此规则镜像欢迎声明的 namespace、确认字段、版本和被断言的中文文案。
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 import 点明源模块。这样漂移会表现为选择器未命中或镜像值陈旧——是响亮的失败,绝不会是静默通过。`scaffold.ts` 按此规则镜像 welcome-notice 的 namespace、确认字段、版本和被断言的中文文案。
有一类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从
`@deepseek-ai/dsh-client-modules/client` import boot manifest 类型:启动真实 shell 正是该
harness 的用途,且这两个包本来就在 Host 图中。chat 场景则在 `support.ts` 中镜像
`conversationContextKey`,而不 import 其 Client owner。
有一类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 `@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 `@deepseek-ai/dsh-client-modules/client` import boot manifest(元数据清单)类型:启动真实 shell 正是该 harness 的用途,且这两个包本来就在 Host 图中。chat 场景则在 `support.ts` 中镜像 `conversationContextKey`,而不 import 其 Client owner。
没有任何机制强制这条规则;靠 review 守住它。
+20 -9
View File
@@ -240,6 +240,18 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
const iframe = preview.locator('[data-html-preview]')
await iframe.waitFor({ timeout: 15_000 })
expect(await iframe.getAttribute('sandbox')).toBe('allow-scripts')
expect(await iframe.evaluate((node) => {
const host = node.closest('[data-textpreview-body]')
if (!(host instanceof HTMLElement)) throw new Error('HTML preview body is unavailable')
const outer = host.getBoundingClientRect()
const frame = node.getBoundingClientRect()
return {
top: Math.round(frame.top - outer.top),
right: Math.round(outer.right - frame.right),
bottom: Math.round(outer.bottom - frame.bottom),
left: Math.round(frame.left - outer.left),
}
})).toEqual({ top: 0, right: 0, bottom: 0, left: 0 })
const html = page.frameLocator('[data-html-preview]')
await html.getByRole('heading', { name: 'HTML smoke', exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => html.locator('#result').innerText()).toBe('INLINE_OK')
@@ -379,6 +391,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
const highlightedLines = preview.locator('.shiki .line')
await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(PAGE_LINES)
const codeBlock = preview.locator('.md-code-block')
const codeScrollport = preview.locator('[data-code-block-content]')
expect(await codeBlock.getAttribute('data-line-numbers')).toBe('true')
await expect.poll(() => highlightedLines.first().evaluate(node => getComputedStyle(node, '::before').content))
.not.toMatch(/^(?:none|normal)$/u)
@@ -397,33 +410,31 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
const prefix = await highlightedLines.allTextContents()
expect(prefix).toEqual(codeLines.slice(0, PAGE_LINES))
await expect.poll(() => preview.locator('[data-textpreview-more]').isEnabled()).toBe(true)
await scrollForNextPage(body)
await scrollForNextPage(codeScrollport)
await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(codeLines.length)
const completed = await highlightedLines.allTextContents()
expect(completed).toEqual(codeLines)
await expect.poll(() => preview.locator('[data-textpreview-more]').count()).toBe(0)
const scrollTop = await body.evaluate((node) => {
const scrollTop = await codeScrollport.evaluate((node) => {
const target = Math.floor((node.scrollHeight - node.clientHeight) / 2)
if (target <= 0) throw new Error('code fixture does not overflow the document body')
node.scrollTop = target
return target
})
await expect.poll(() => body.evaluate((node) => {
const banner = node.querySelector('.md-code-block')?.firstElementChild
await expect.poll(() => codeScrollport.evaluate((node) => {
const codeBlock = node.parentElement
const banner = codeBlock?.firstElementChild
const firstLine = node.querySelector('.shiki .line')
if (!(banner instanceof HTMLElement) || firstLine === null) throw new Error('missing rendered code banner or source line')
const bounds = node.getBoundingClientRect()
const clipTop = bounds.top + node.clientTop
const bannerBounds = banner.getBoundingClientRect()
const hit = document.elementFromPoint(bounds.left + node.clientLeft + node.clientWidth / 2, clipTop + 1)
return {
scrollTop: node.scrollTop,
position: getComputedStyle(banner).position,
topGap: bannerBounds.top - clipTop,
scrollportBelowBanner: Math.abs(bannerBounds.bottom - bounds.top) < 1,
firstLineAbove: firstLine.getBoundingClientRect().top < clipTop,
topCoveredByBanner: hit !== null && banner.contains(hit),
}
})).toEqual({ scrollTop, position: 'sticky', topGap: 0, firstLineAbove: true, topCoveredByBanner: true })
})).toEqual({ scrollTop, scrollportBelowBanner: true, firstLineAbove: true })
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin })
await page.evaluate(() => navigator.clipboard.writeText(''))
await codeBlock.getByRole('button', { name: 'Copy', exact: true }).click()
@@ -1,24 +0,0 @@
- heading "Mermaid previews" [level=1]
- button "Source"
- button "Copy"
- tooltip "Copy"
- img "Mermaid diagram"
- button "Source"
- button "Copy"
- img "Mermaid diagram"
- button "Source"
- button "Copy"
- status: Unable to render this diagram. The source is shown below.
- code: flowchart LR A[unfinished
- button "Source"
- button "Copy"
- img "Mermaid diagram"
- button "Source"
- button "Copy"
- img "Graphviz diagram"
- button "Source"
- button "Copy"
- img "SVG preview"
- button "Source"
- button "Copy"
- iframe
@@ -1,23 +0,0 @@
- heading "Mermaid previews" [level=1]
- button "源码"
- button "复制"
- img "Mermaid 图表"
- button "源码"
- button "复制"
- img "Mermaid 图表"
- button "源码"
- button "复制"
- status: 无法渲染此图表,源码如下。
- code: flowchart LR A[unfinished
- button "源码"
- button "复制"
- img "Mermaid 图表"
- button "源码"
- button "复制"
- img "Graphviz 图表"
- button "源码"
- button "复制"
- img "SVG 预览"
- button "源码"
- button "复制"
- iframe
@@ -0,0 +1,86 @@
- dialog "设置":
- navigation:
- text: 设置
- button "通用设置":
- img
- text: 通用设置
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
- text: 关闭
- heading "模型" [level=2]
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
- list:
- listitem:
- text: DeepSeek
- img "API 密钥已配置"
- button "编辑 DeepSeek (deepseek-official)": 编辑
- text: DeepSeek deepseek-official API 密钥
- textbox "API 密钥":
- /placeholder: 已配置——输入新值可替换
- group:
- text: 自定义设置 API 地址
- textbox "API 地址":
- /placeholder: https://api.deepseek.com
- region "模型目录":
- text: 模型目录 正在使用适配器默认模型
- textbox "模型 ID 1":
- /placeholder: 模型 ID
- text: deepseek-flash
- textbox "显示名称 1":
- /placeholder: 显示名称
- text: DeepSeek-V41-Flash
- button "容量 1":
- img
- button "删除模型 1":
- img
- textbox "模型 ID 2":
- /placeholder: 模型 ID
- text: deepseek-v4-flash
- textbox "显示名称 2":
- /placeholder: 显示名称
- text: DeepSeek-V4-Flash
- button "容量 2":
- img
- button "删除模型 2":
- img
- textbox "模型 ID 3":
- /placeholder: 模型 ID
- text: deepseek-v4-pro
- textbox "显示名称 3":
- /placeholder: 显示名称
- text: DeepSeek-V4-Pro
- button "容量 3":
- img
- button "删除模型 3":
- img
- textbox "模型 ID 4":
- /placeholder: 模型 ID
- text: deepseek-v4-flash-vision-exp
- textbox "显示名称 4":
- /placeholder: 显示名称
- text: DeepSeek-V4-Flash-Vision-Exp
- button "容量 4":
- img
- button "删除模型 4":
- img
- button "添加模型":
- img
- text: 添加模型
- button "取消"
- button "保存"
- button "添加提供方":
- img
- text: 添加提供方
- button "添加自定义提供方":
- img
- text: 添加自定义提供方
@@ -36,40 +36,20 @@
- button "恢复默认模型"
- textbox "模型 ID 1":
- /placeholder: 模型 ID
- text: deepseek-v4-pro
- text: private-preview
- textbox "显示名称 1":
- /placeholder: 显示名称
- text: DeepSeek-V4-Pro
- button "容量 1":
- text: Private Preview
- button "容量 1" [expanded]:
- img
- button "删除模型 1":
- img
- textbox "模型 ID 2":
- /placeholder: 模型 ID
- text: deepseek-v4-flash-vision-exp
- textbox "显示名称 2":
- /placeholder: 显示名称
- text: DeepSeek-V4-Flash-Vision-Exp
- button "容量 2":
- img
- button "删除模型 2":
- img
- textbox "模型 ID 3":
- /placeholder: 模型 ID
- text: private-preview
- textbox "显示名称 3":
- /placeholder: 显示名称
- text: Private Preview
- button "容量 3" [expanded]:
- img
- button "删除模型 3":
- img
- text: 上下文窗口
- textbox "上下文窗口 3":
- textbox "上下文窗口 1":
- /placeholder: 1M
- text: "131072"
- text: 最大输出 token 数
- textbox "最大输出 token 数 3":
- textbox "最大输出 token 数 1":
- /placeholder: 256K
- text: 64K
- button "添加模型":
-320
View File
@@ -1,320 +0,0 @@
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-title'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
seedSession, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/markdown-mermaid', import.meta.url))
const MODE = webSnapshotMode()
const SEED_ID = 'markdown-mermaid-web-e2e'
const DIAGRAM_MESSAGE = '[class*="markdown"]:has(.md-code-block)'
const FLOW = 'flowchart LR\n A[输入] --> B[共享渲染器] --> C[图形预览]'
const SEQUENCE = 'sequenceDiagram\n participant U as User\n participant R as Renderer\n U->>R: Mermaid source\n R-->>U: Diagram'
const INVALID = 'flowchart LR\n A[unfinished'
const UNTRUSTED = [
'%%{init: {"securityLevel":"loose","htmlLabels":true,"themeCSS":"body {display:none!important}"}}%%',
'flowchart LR',
' A["<img src=x onerror=alert(1)>"] --> B[Safe]',
' click B "javascript:alert(1)"',
].join('\n')
const DOT = 'digraph { rankdir=LR; Input -> Preview }'
const SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="460" height="100" onload="parent.document.body.dataset.previewEscaped='yes'">
<rect width="460" height="100" fill="lightblue"/><text x="20" y="55">SVG preview</text>
<script>parent.document.body.dataset.previewEscaped='yes';alert('unsafe SVG')</script>
<image href="https://preview.invalid/svg-image" width="1" height="1"/>
<foreignObject width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"><script>alert('unsafe HTML')</script></div></foreignObject>
</svg>`
const HTML = `<style>h2 { color: green }</style><h2>Static HTML</h2>
<script>parent.document.body.dataset.previewEscaped = 'yes'; alert('unsafe')</script>
<meta http-equiv="refresh" content="0;url=https://preview.invalid/refresh">
<a href="https://preview.invalid/navigate">Disabled navigation</a>
<img src="https://preview.invalid/image" onerror="alert('unsafe')">
<iframe src="https://preview.invalid/frame"></iframe>`
function fixture(): string {
const session = Session.create(SessionId('markdown-mermaid-source'))
session.append('turn/start', { turn: 1 })
const user = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Preview these Mermaid diagrams.' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('session/title', { title: 'Mermaid previews', messageSeqs: [user.seq], source: { kind: 'fallback' } })
session.append('step/start', { turn: 1, step: 1 })
session.append('assistant/message', {
stream: [], turn: 1, step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: [
'# Mermaid previews',
...[FLOW, SEQUENCE, INVALID, UNTRUSTED].map(code => `\`\`\`mermaid\n${code}\n\`\`\``),
...[['dot', DOT], ['svg', SVG], ['html', HTML]].map(([lang, code]) => `\`\`\`${lang}\n${code}\n\`\`\``),
].join('\n\n') }],
source: { kind: 'model', provider: 'fixture', model: 'fixture' },
}),
}, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const noon = new Date().setHours(12, 0, 0, 0)
return [
JSON.stringify({
type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: 0,
cwd: '{{cwd}}', isSeeded: false, delegationDepth: 0,
}),
...session.snapshotEvents().map(event => JSON.stringify({ ...event, time: noon + event.seq * 1000 })),
'',
].join('\n')
}
async function openConversation(page: Page, scaffold: WebScaffold): Promise<void> {
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.getByRole('treeitem').first().click({ timeout: 30_000 })
await page.getByRole('treeitem').nth(1).click()
await page.getByRole('heading', { name: 'Mermaid previews' }).waitFor()
}
describe('web e2e: Mermaid chat previews', () => {
let scaffold: WebScaffold
let browser: Browser
beforeAll(async () => {
scaffold = await launchWebScaffold({})
await seedSession(scaffold, fixture(), SEED_ID)
browser = await chromium.launch()
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it.skipIf(MODE === 'record').each(['light', 'dark'] as const)(
'tightly wraps short diagram previews in %s mode', async (scheme) => {
const page = await newEnglishPage(browser)
try {
await page.emulateMedia({ colorScheme: scheme })
await openConversation(page, scaffold)
await expect.poll(() => page.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe(scheme)
const mermaid = page.getByRole('img', { name: 'Mermaid diagram' }).first()
await mermaid.evaluate(async (node: HTMLImageElement) => { await node.decode() })
const mermaidHeight = await mermaid.evaluate(node => node.getBoundingClientRect().height)
const canvasHeight = await mermaid.evaluate(node => node.parentElement!.getBoundingClientRect().height)
// Compact diagrams need at most the Mermaid canvas's 16px padding on each side.
expect(canvasHeight - mermaidHeight).toBeLessThanOrEqual(32)
for (const title of ['Graphviz diagram', 'SVG preview']) {
const image = page.getByRole('img', { name: title, exact: true })
await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
const imageHeight = await image.evaluate(node => node.getBoundingClientRect().height)
const containerHeight = await image.evaluate(node => node.parentElement!.getBoundingClientRect().height)
expect(imageHeight).toBeGreaterThan(0)
expect(containerHeight).toBeGreaterThanOrEqual(imageHeight)
expect.soft(containerHeight - imageHeight,
`${scheme} ${title}: ${containerHeight}px container around ${imageHeight}px content`,
).toBeLessThanOrEqual(32)
}
await page.setViewportSize({ width: 360, height: 800 })
const svg = page.getByRole('img', { name: 'SVG preview', exact: true })
await expect.poll(() => svg.evaluate((node) => {
const { width, height } = node.getBoundingClientRect()
return width > 0 && height > 0 && width < 460
})).toBe(true)
const compact = await svg.evaluate((node: HTMLImageElement) => ({
width: node.getBoundingClientRect().width,
height: node.getBoundingClientRect().height,
canvasHeight: node.parentElement!.getBoundingClientRect().height,
ratio: node.naturalWidth / node.naturalHeight,
}))
expect(compact.width / compact.height).toBeCloseTo(compact.ratio, 1)
expect(compact.canvasHeight - compact.height).toBeLessThanOrEqual(32)
} finally {
await page.close()
}
},
)
it.skipIf(MODE === 'record')('updates existing diagrams when the document switches between light and dark', async () => {
const page = await newEnglishPage(browser)
try {
await openConversation(page, scaffold)
const image = page.getByRole('img', { name: 'Mermaid diagram' }).first()
await image.waitFor()
let previous = await image.getAttribute('src')
for (const scheme of ['dark', 'light'] as const) {
await page.evaluate((value) => {
document.documentElement.style.colorScheme = value
document.body.toggleAttribute('data-ds-dark-theme', value === 'dark')
}, scheme)
await expect.poll(() => image.getAttribute('src')).not.toBe(previous)
await expect.poll(() => image.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
const colors = await image.evaluate((node) => {
const reference = document.createElement('span')
reference.style.backgroundColor = 'var(--dsw-alias-markdown-code-block)'
document.body.append(reference)
try {
return {
canvas: getComputedStyle(node.parentElement!).backgroundColor,
expected: getComputedStyle(reference).backgroundColor,
}
} finally {
reference.remove()
}
})
expect(colors.canvas).toBe(colors.expected)
await expect.poll(() => page.getByTitle('HTML preview', { exact: true }).getAttribute('srcdoc'))
.toContain(`color-scheme:${scheme}`)
previous = await image.getAttribute('src')
}
} finally {
await page.close()
}
})
it.skipIf(MODE === 'record')('renders diagrams, switches to source, copies source, and contains malformed content', async () => {
const page = await newEnglishPage(browser)
onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-mermaid'))
const tripwire = watchConsole(page)
const dialogs: string[] = []
page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() })
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
await openConversation(page, scaffold)
const images = page.getByRole('img', { name: 'Mermaid diagram' })
await expect.poll(() => images.count(), { timeout: 20_000 }).toBe(3)
await expect.poll(() => images.evaluateAll(nodes => nodes.every(node => (node as HTMLImageElement).naturalWidth > 0))).toBe(true)
expect(await images.evaluateAll(nodes => nodes.every(node =>
node.getBoundingClientRect().width <= (node as HTMLImageElement).naturalWidth))).toBe(true)
expect(await page.getByText('Unable to render this diagram. The source is shown below.', { exact: true }).count()).toBe(1)
expect(await page.locator('pre code').allTextContents()).toContain(INVALID)
expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器')
const first = page.locator('.md-code-block').first()
const diagram = await first.getByRole('img').elementHandle()
const controls = first.locator('[class*="bannerWrap"]')
expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0)
await page.mouse.move(0, 0)
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('0')
await first.hover()
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
await first.getByRole('button', { name: 'Copy', exact: true }).click()
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW)
await page.mouse.move(0, 0)
await first.getByRole('button', { name: 'Source', exact: true }).focus()
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
await page.keyboard.press('Enter')
expect(await first.getByRole('button', { name: 'Preview', exact: true })
.evaluate(node => node === document.activeElement)).toBe(true)
expect(await first.locator('pre code').textContent()).toBe(FLOW)
expect(await diagram!.evaluate(node => node.isConnected)).toBe(true)
expect(await diagram!.isVisible()).toBe(false)
await first.getByRole('button', { name: 'Preview', exact: true }).click()
expect(await diagram!.isVisible()).toBe(true)
await first.getByRole('button', { name: 'Copy', exact: true }).waitFor()
expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none')
expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0)
expect(dialogs).toEqual([])
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await page.getByRole('heading', { name: 'Mermaid previews' }).click()
await page.mouse.move(0, 0)
const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd)
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE)
await page.close()
}, 60_000)
it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => {
const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true })
await openConversation(page, scaffold)
await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3)
expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(7)
const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]')
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
expect(await page.getByRole('button', { name: '源码', exact: true }).first()
.evaluate(node => node.getBoundingClientRect().width)).toBe(44)
expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1)
const firstImage = page.getByRole('img', { name: 'Mermaid 图表' }).first()
await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
expect(await controls.evaluate(node => node.getBoundingClientRect().top))
.toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom))
const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd)
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE)
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md'])
await page.close()
}, 60_000)
it.skipIf(MODE === 'record')('previews inert DOT and SVG images and isolates static HTML', async () => {
const page = await newEnglishPage(browser)
const requests: string[] = []
const dialogs: string[] = []
// Routing observes requests eligible for transport; Chromium also emits 'request' for CSP-blocked URLs.
await page.route('https://preview.invalid/**', async (route) => {
requests.push(route.request().url())
await route.abort()
})
page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() })
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
await openConversation(page, scaffold)
for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]] as const) {
const preview = title === 'HTML preview'
? page.getByTitle(title, { exact: true })
: page.getByRole('img', { name: title, exact: true, includeHidden: true })
await preview.waitFor()
if (title === 'HTML preview') {
expect(await preview.getAttribute('sandbox')).toBe('')
const body = preview.contentFrame()
await body.getByRole('heading', { name: 'Static HTML' }).waitFor()
expect(await body.getByRole('heading').evaluate(node => getComputedStyle(node).color)).toBe('rgb(0, 128, 0)')
await body.getByText('Disabled navigation').click()
expect(await preview.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull()
} else {
await expect.poll(() => preview.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
}
const block = page.locator('.md-code-block').filter({ has: preview })
await block.hover()
await block.getByRole('button', { name: 'Copy', exact: true }).click()
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(code)
await block.getByRole('button', { name: 'Source', exact: true }).click()
expect(await block.locator('pre code').textContent()).toBe(code)
await block.getByRole('button', { name: 'Preview', exact: true }).click()
expect(await preview.isVisible()).toBe(true)
}
expect(await page.locator('body').getAttribute('data-preview-escaped')).toBeNull()
expect(dialogs).toEqual([])
expect(requests).toEqual([])
const notices = await page.request.get(new URL('/preview-third-party-notices.txt', scaffold.authenticatedUrl).href)
expect(notices.ok()).toBe(true)
expect(await notices.text()).toContain('Eclipse Public License - v 2.0')
await page.close()
})
it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => {
const hybridBrowser = await chromium.launch({
args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'],
})
try {
const page = await newEnglishPage(hybridBrowser)
expect(await page.evaluate(() =>
matchMedia('(hover: hover) and (pointer: fine) and (any-pointer: coarse)').matches)).toBe(true)
await openConversation(page, scaffold)
const first = page.locator('.md-code-block').first()
const diagram = first.getByRole('img', { name: 'Mermaid diagram' })
await expect.poll(() => diagram.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
await page.mouse.move(0, 0)
const controls = first.locator('[class*="bannerWrap"]')
expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
expect(await controls.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('auto')
expect(await first.getByRole('button', { name: 'Source', exact: true })
.evaluate(node => node.getBoundingClientRect().width)).toBe(44)
expect(await controls.evaluate(node => node.getBoundingClientRect().top))
.toBeGreaterThanOrEqual(await diagram.evaluate(node => node.getBoundingClientRect().bottom))
await first.getByRole('button', { name: 'Source', exact: true }).click()
expect(await first.locator('pre code').textContent()).toBe(FLOW)
} finally {
await hybridBrowser.close()
}
}, 60_000)
})
@@ -21,6 +21,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-deepseek-confi
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
const DEFAULT_MODELS_EXPECTED = join(SNAPSHOT_DIR, 'default-models.expected.md')
const MODE = webSnapshotMode()
describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup', () => {
@@ -202,15 +203,39 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await deepSeek.waitFor({ timeout: 10_000 })
await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
await settings.getByText('自定义设置').click()
await settings.getByRole('button', { name: /删除模型/ }).first().click()
expect(await settings.getByLabel('模型 ID 1').inputValue()).toBe('deepseek-flash')
expect(await settings.getByLabel('显示名称 1').inputValue()).toBe('DeepSeek-V41-Flash')
expect(await settings.getByLabel('模型 ID 2').inputValue()).toBe('deepseek-v4-flash')
expect(await settings.getByLabel('模型 ID 3').inputValue()).toBe('deepseek-v4-pro')
expect(await settings.getByLabel('模型 ID 4').inputValue()).toBe('deepseek-v4-flash-vision-exp')
expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(4)
const defaultModels = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(DEFAULT_MODELS_EXPECTED, defaultModels, MODE)
await settings.getByLabel('显示名称 1').fill('Configured Flash')
await settings.getByRole('button', { name: '保存', exact: true }).click()
await settings.getByLabel('模型 ID 1').waitFor({ state: 'detached', timeout: 15_000 })
const savedDefaults = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
expect(savedDefaults).toContain('id: deepseek-flash')
expect(savedDefaults).toContain('inputModalities:')
expect(savedDefaults).toContain('- text')
expect(savedDefaults).toContain('- image')
expect(savedDefaults).toContain('systemPromptUpdate: in-history')
await expect(scaffold.ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')).resolves.toMatchObject({
name: 'Configured Flash', inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history',
})
await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
await settings.getByText('自定义设置').click()
for (let index = 0; index < 4; index++) {
await settings.getByRole('button', { name: /删除模型/ }).first().click()
}
await settings.getByRole('button', { name: '添加模型' }).click()
const customModelId = settings.getByLabel('模型 ID 3')
const customModelId = settings.getByLabel('模型 ID 1')
await customModelId.fill('private-preview')
await settings.getByLabel('显示名称 3').fill('Private Preview')
await settings.getByLabel('显示名称 1').fill('Private Preview')
// Capacities live behind the row's own disclosure, as in the pi-ai form.
await settings.getByRole('button', { name: '容量 3' }).click()
await settings.getByLabel('上下文窗口 3').fill('131072')
await settings.getByLabel('最大输出 token 数 3').fill('64K')
await settings.getByRole('button', { name: '容量 1' }).click()
await settings.getByLabel('上下文窗口 1').fill('131072')
await settings.getByLabel('最大输出 token 数 1').fill('64K')
await expect.poll(
() => settings.getByLabel('API 密钥', { exact: true }).getAttribute('placeholder'),
@@ -222,15 +247,11 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await customModelId.waitFor({ state: 'detached', timeout: 15_000 })
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
expect(document).toContain('id: deepseek-v4-pro')
expect(document).toContain('id: deepseek-v4-flash-vision-exp')
expect(document).toContain('inputModalities:')
expect(document).toContain('- image')
expect(document).toContain('id: private-preview')
expect(document).toContain('name: Private Preview')
expect(document).toContain('contextWindow: 131072')
expect(document).toContain('maxTokens: 64000')
expect(document).not.toMatch(/^\s*- id: deepseek-v4-flash$/m)
expect(document).not.toContain('id: deepseek-flash')
await page.keyboard.press('Escape')
// A connected Workspace is what puts a live composer — and its model
@@ -241,8 +262,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await modelTrigger.waitFor({ timeout: 10_000 })
await modelTrigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
expect(await page.getByText('deepseek-v4-flash', { exact: true }).count()).toBe(0)
await page.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash-Vision-Exp' }).waitFor({ timeout: 10_000 })
expect(await page.getByText('Configured Flash', { exact: true }).count()).toBe(0)
await page.getByRole('menuitemradio', { name: 'Private Preview' }).waitFor({ timeout: 10_000 })
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
@@ -251,7 +271,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(
SNAPSHOT_DIR,
['welcome.expected.md', 'missing.expected.md', 'models.expected.md'],
['welcome.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
)
})
})
+4
View File
@@ -515,6 +515,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
const patches: PatchOptions[] = [
...basePatches,
...surfacePatches,
// Keyless scenarios retain the recorded default; explicit scenario overlays win.
...mode === 'record' || options.deepSeekMissingCredential === true
? []
: [{ id: 'agent-default-model', config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } }],
...extraOverlayPatches,
// The roster's shipped presets are the plugin's own, bundled inside
// `dsh-agent-presets` and prepended by it. Pin only the machine-local
+1 -1
View File
@@ -1061,7 +1061,7 @@ describe('web e2e: shipped right Sidebar', () => {
expect(await width(column)).toBeGreaterThan(300)
await expect.poll(async () => await tabTitles(column)).toEqual(['文件', '开始'])
await expect.poll(async () => await guide.locator('[data-sidebar-right-guide-entry="files"]').innerText())
.toBe('工作区文件')
.toBe('工作区文件\n浏览会话工作区的文件')
await shot(zhPage, '05-guide-copy-zh')
expect(zhTripwire.pageErrors).toEqual([])
-1
View File
@@ -79,7 +79,6 @@
"tests/math-rendering.e2e.ts",
"tests/markdown-cjk-strong.e2e.ts",
"tests/markdown-inline-code-links.e2e.ts",
"tests/markdown-mermaid.e2e.ts",
"tests/clickable-links-gallery.e2e.ts",
"tests/queue-actions.e2e.ts",
"tests/queue-image.e2e.ts",
+1 -15
View File
@@ -27,20 +27,6 @@ function clientDocumentTitle(): Plugin {
}
}
/** Preserve preview licenses and Graphviz source availability in every browser distribution. */
function previewNotices(): Plugin {
return {
name: 'dsh-preview-notices',
async generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'preview-third-party-notices.txt',
source: await readFile(src('../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8'),
})
},
}
}
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
function rejectStandaloneServe(): Plugin {
return {
@@ -161,7 +147,7 @@ export default defineConfig({
// Relative asset URLs: preview.html mounts the same output under any base
// directory, and the served index resolves identically from the site root.
base: './',
plugins: [previewNotices(), rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()],
plugins: [rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()],
build: {
// The worker bootstrap holds its page at top-level await; Vite's default
// `modules` target (es2020-era) rejects that syntax.
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write benchmarks/active-stream-reconnect/README.md
README.md: e75a41eba3952bb4db343c2018e217e6f5e88f96
README.zh.md: acf0260f53855f9f4e643b2e72c67aa7a8afd6bb
README.zh.md: 3d831208144e3810d353fb64fc5ae4adae766bdd
@@ -2,6 +2,6 @@
[English](README.md) | 中文
[reconnect.bench.client.ts](reconnect.bench.client.ts) 测量重连携带未完成的 100,000 个 reasoning delta 前缀时,生产 Client 的折叠成本。编译后的私有适配器调用 `ClientAssistantStream.replace()`,不增加产品导出。三个全新纯 Node worker 在计时前合成紧凑 baseline;替换时间与强制 GC 后的保留 heap 分别执行中位数预算检查。下一个稠密序号的实时 frame 仍须被接受。标准托管 CI 使用 50 ms 替换预期及共享的 1.25× 余量(向上取整为 63 ms);保留 heap 预算仍为 30 MiB。记录样本和合成回归对照使用与 worker 判定相同的时间断言。
[reconnect.bench.client.ts](reconnect.bench.client.ts) 测量重连携带一个包含 100,000 个 delta、尚未完成的 reasoning 前缀时,生产 Client 的折叠成本。编译后的私有适配器调用 `ClientAssistantStream.replace()`,不增加产品导出。三个全新的纯 Node worker 在计时前合成紧凑基线;替换时间与强制 GC 后的保留堆分别执行中位数预算检查。下一个稠密序号的实时 frame 仍须被接受。标准托管 CI 使用 50 ms 替换预期及共享的 1.25× 余量(上限为 63 ms);保留 heap 预算仍为 30 MiB。实测样本和合成回归对照使用与 worker 判定相同的时间断言。
通过 `pnpm run build:bench` 构建,再在 `vitest.bench.config.ts` 中选择 `benchmarks/active-stream-reconnect`。该聚焦 Node workload 既不构建也不测量浏览器渲染。[前端性能预算](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)记录校准与排除项。
通过 `pnpm run build:bench` 构建,再在 `vitest.bench.config.ts` 中选择 `benchmarks/active-stream-reconnect`。这项针对 Node 的工作负载既不构建也不测量浏览器渲染。[前端性能预算](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)记录校准与排除项。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write benchmarks/agent-continuation/README.md
README.md: 489939499af8d98922df2cbbdd6be793bf6e3796
README.zh.md: e99760cfd1aba0ca41e78243f333fe18446474fe
README.zh.md: e51fac2d0df005de9d609cb634778f30b537ae41
+6 -6
View File
@@ -2,15 +2,15 @@
[English](README.md) | 中文
## Summary
## 概述
在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
在不使用网络服务或所记录的用户数据的情况下,测量长历史请求处理、冷启动的工具密集型续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
## Table of Contents
## 目录
- [运行](#run)
- [测量](#measurements)
- [Dev Note](#dev-note)
- [开发备注](#dev-note)
<a id="run"></a>
@@ -24,8 +24,8 @@
## 测量
[workload.ts](workload.ts)拥有合成维度。其当前代历史在首个 step 的用户输入之前保留空 system 头节点,因此续聊提示会替换该头节点而不移动历史消息。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md)拥有计时终点、校准证据、内存解释和排除项。模型适配器不执行服务商序列化或网络调用;集成用例通过真实工具执行管线运行合成工具体,SDK profile 变体则执行真实文件读取。
[workload.ts](workload.ts) 负责定义合成维度。其当前代历史在首个步骤的用户输入之前保留空 system 头节点,因此续聊提示词会替换该头节点而不移动历史消息。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md) 负责说明计时终点、校准证据、内存解释和排除项。模型适配器不执行提供方序列化或网络调用;集成用例通过真实工具执行流水线运行合成工具体,SDK profile 变体则执行真实文件读取。
## Dev Note
## 开发备注
无。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write benchmarks/long-session-browser/README.md
README.md: 009a7a61d9a1a0d4b198ae017def5d3126b713a2
README.zh.md: 2dd0c52b0f1cc04aac8a78d847401067201afa13
README.zh.md: df1da8fd76c2f4fa926b6348fa631530fb98181a
+4 -4
View File
@@ -1,8 +1,8 @@
# 长 Session 浏览器基准
# 长会话浏览器基准
[English](README.md) | 中文
[long-session.bench.ts](long-session.bench.ts) 中必需的 Chromium 工作流测量打开合成的 240 轮 Session、加载所有更早的分页、访问 Trajectory,以及在有节奏的流式回复期间输入下一条草稿。随产品维护的 Web scaffold 拥有隔离的主目录、持久化、重放适配器和回环监听器;Chromium 加载构建后的 Web 产物,而非替代开发服务器。
[long-session.bench.ts](long-session.bench.ts) 中必需的 Chromium 工作流测量打开包含 240 个轮次的合成会话、加载所有较早页面、访问 Trajectory,以及在有节奏的流式回复期间输入下一条草稿。随产品维护的 Web scaffold 拥有隔离的主目录、持久化、回放适配器和回环监听器;Chromium 加载构建后的 Web 产物,而非替代开发服务器。
## 运行
@@ -10,8 +10,8 @@
## 测量
三个全新浏览器进程与 scaffold 环境产生原始样本及中位数判定。打开和分页在预期对话状态出现且经过两次动画帧后结束;这包含一次渲染机会,而非硬件显示时间戳。分页报告每一页,并对各样本最慢分页时间的中位数执行预算检查。流式报告首段可见回复、真实草稿键入、完整回复壁钟时间和 Chromium 主线程任务时间。Enter 从已聚焦的输入框提交;草稿键入保留该焦点,不执行鼠标点击。回复标记查找与输入事件文本证据仅读取最新 Assistant step,避免重复扫描全部历史文本与无障碍属性。输入观察器在提交前安装,首个标记可见后立即开始草稿键入,不额外等待输入前动画帧。诊断报告首段可见等待后的标记状态与焦点,以及首个输入事件的浏览器时钟时间戳与焦点。诊断不会暂停重放;首段观察或输入延迟仍可能导致重叠失败。实际首个输入事件必须观察到未完成的回复;完成测量在 Host 结算后等待新 turn-tail 渲染。测量后,在 DONE 之后发送的真实按键必须无法通过同一个重叠断言。打开、最慢更早分页和首次 Trajectory 使用标准托管预期 900/700/500 ms。共享的 1.25× 余量分别产生 1125/875/625 ms 上限;流式终点的额外开销预算不变。强制 GC 后的 heap 与 DOM 数量仅供诊断,不作为泄漏预算。
三个全新浏览器进程与 scaffold 环境产生原始样本及中位数判定。打开和分页在预期 transcript(文本记录)状态出现且经过两次动画帧后结束;这包含一次渲染机会,而非硬件显示时间戳。分页报告每一页,并对各样本最慢分页时间的中位数执行预算检查。流式报告首次可见回复、受信任的草稿键入、完整回复壁钟时间和 Chromium 主线程任务时间。Enter 从已聚焦的输入框提交;草稿键入保留该焦点,不执行鼠标点击。回复标记查找与输入事件文本观察器仅读取最新 Assistant 步骤,避免重复进行全量历史文本扫描和无障碍扫描。输入事件文本观察器在提交前安装,首个标记可见后立即开始草稿键入,不额外等待输入前动画帧。诊断报告等待首次可见回复后的标记状态与焦点,以及首个输入事件的浏览器时钟时间戳与焦点。诊断不会暂停回放;首次观察或输入延迟仍可能导致重叠失败。实际首个输入事件必须观察到未完成的回复;完整回复的测量会在 Host 结算后等待新 turn-tail 渲染完成。测量后,在 DONE 之后发送的受信任按键必须无法通过同一个重叠断言。打开、最慢的较早页面和首次 Trajectory 使用标准托管预期 900/700/500 ms。共享的 1.25× 余量分别产生 1125/875/625 ms 上限;流式终点的额外开销预算不变。强制 GC 后的 heap 与 DOM 数量仅供诊断,不作为泄漏预算。
fixture(测试前置数据)在首条用户消息前保留空 system 头节点,每条用户消息都位于其 step 内。它包含混合语言提示、正文、推理、20 个代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑 stream,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish chunk 构建。其内容不来自模型、工具、外部网络、录制 Session 或私有 Harness 主目录。流式回复以 16 ms 重放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
fixture(测试前置数据)在首条用户消息前保留空 system 头节点,每条用户消息都位于其步骤内。它包含混合语言提示词、正文、推理(reasoning)、20 个围栏代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑流,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish 分片构建。其内容不来自模型、工具、外部网络、录制会话或私有 Harness 主目录。流式回复以 16 ms 回放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
[决策记录](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)拥有校准、排除项与替代方案。更大规模的[手动诊断](../../apps/web/tests/complex-history.perf.ts)保持独立。
+2 -2
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 docs/config-catalog.md
config-catalog.md: 9adf44b995601196b602e9219e53c52208039051
config-catalog.zh.md: e2f6c6babc18bfa689f6830c81231012b4d9de18
config-catalog.md: 79878fc8daaa5ba3e64d4ffb27d279ff172b6749
config-catalog.zh.md: f254241618d280acaf738cc011ba1f7c5a2b6b21
+2 -2
View File
@@ -1037,7 +1037,7 @@ export interface Config {
maxTokens?: number
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
defaultContextWindow?: number
/** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
/** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
models?: DeepSeekCatalogModel[]
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
streamIdleTimeoutMs?: number
@@ -1094,7 +1094,7 @@ export interface DeepSeekCatalogModel {
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
Source: [`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts)
<a id="deepseek-aidsh-llm-pi-ai"></a>
+2 -2
View File
@@ -1039,7 +1039,7 @@ export interface Config {
maxTokens?: number
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
defaultContextWindow?: number
/** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
/** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
models?: DeepSeekCatalogModel[]
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
streamIdleTimeoutMs?: number
@@ -1096,7 +1096,7 @@ export interface DeepSeekCatalogModel {
依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
来源:[`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
来源:[`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts)
<a id="deepseek-aidsh-llm-pi-ai"></a>
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/i18n/README.md
README.md: 2ff8fc62f21d58a4d31b8aadd80c7a0c14556e6d
README.zh.md: 73da4445bc35e779a990d4cd4aef605cc9078a06
README.zh.md: 2e0c183f6f9374c08d698fbd7109eb4e59f87c10
+1 -1
View File
@@ -33,7 +33,7 @@
面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest(元数据清单)中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。
`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。
`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 `missing` 与 `out-of-sync` 行指出普通检查会拒绝的违规。
`pnpm run verify-translation-pairing <pair...>` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/README.md
README.md: 9afbdda338ed797279b719eaf577be45a84349e2
README.zh.md: ee3c89830a76bc1cddb83e0a50507f3e4439800d
README.zh.md: 78f799d99221d8458ad1639dffbdda69b6783ef6
+13 -13
View File
@@ -6,21 +6,21 @@
| 页面 | 负责内容 |
|---|---|
| [core.md](core.zh.md) | `packages/core` 如何控制 agent loop(智能体循环):逐包的循环说明、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄的投递/取消/拦截约定,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) |
| [core.md](core.zh.md) | `packages/core` 如何控制 agent loop(智能体循环):逐包的循环说明、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄的投递/取消/拦截约定,以及全仓通用类型模式(`…Map → derived-union`、品牌化 id) |
| [llm-streaming.md](llm-streaming.zh.md) | `packages/llm` 的对话类型——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` wire protocol 和适配器约定(adapter contract)、`BlockAssembler`,以及 `LlmAdapter` 提供方约定 |
| [token-meter.md](token-meter.zh.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
| [scope.md](scope.zh.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
| [typert.md](typert.zh.md) | 远程调用描述符、lookup/Context 声明、Typert 注册表,以及 Host Gateway/Client API 边界 |
| [goal.md](goal.zh.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 |
| [schedule.md](schedule.zh.md) | 仅限 Session 内的提醒记录、持久转换、活动视图与普通对话交付 |
| [todo.md](todo.zh.md) | todo 包的整列表条目类型、持久事件所有权、投影和开放轮次不变量 |
| [schedule.md](schedule.zh.md) | 仅限会话内的提醒记录、持久转换、活动视图与普通对话交付 |
| [todo.md](todo.zh.md) | todo 包的整列表条目类型、持久事件所有权、投影和未结束轮次不变式 |
| [commands.md](commands.zh.md) | 人类命令注册表服务:定义、适配器发现、直接调用、结果与解析视图 |
| [session.md](session.zh.md) | 完整的 `SessionEventMap` 变体目录、`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 |
| [persistence.md](persistence.zh.md) | 持久性 seam:`SessionPersistence`、JSONL provider、`session/flush`、崩溃恢复、`SessionHeader` |
| [persistence.md](persistence.zh.md) | 持久性 seam:`SessionPersistence`、JSONL 提供方、`session/flush`、崩溃恢复、`SessionHeader` |
| [settings.md](settings.zh.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
| [credentials.md](credentials.zh.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、提供方来源层 |
| [session-query.md](session-query.zh.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
| [feedback.md](feedback.zh.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 契约 |
| [feedback.md](feedback.zh.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 约定 |
| [session-title.md](session-title.zh.md) | 持久标题快照、被引用的来源消息 seq 与异步提供方约定 |
| [session-reference.md](session-reference.zh.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
| [system-prompt.md](system-prompt.zh.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
@@ -33,7 +33,7 @@
| [terminal.md](terminal.zh.md) | 持久化终端 ID、后端/会话约定、发送就绪状态、有界读取与 owner 可见快照 |
| [sandbox.md](sandbox.zh.md) | 每会话策略解析与进程约束 seam:文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 |
| [code-runtime.md](code-runtime.zh.md) | 代码执行 seam:`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 |
| [extensions.md](extensions.zh.md) | 带版本的动态 Cordis Plugin 与 Package、Host/Client 激活、审批、运行时检查和生命周期撤销 |
| [extensions.md](extensions.zh.md) | 带版本的动态 Cordis 插件与包、Host/Client 激活、审批、运行时检查和生命周期清理 |
| [filesystem.md](filesystem.zh.md) | 文件系统 seam:`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` |
| [lsp.md](lsp.zh.md) | LSP 导航 seam:`LspQueryRequest`/`Result`、`LspProvider`/`Service`、四种操作、`LspError` |
| [skills.md](skills.zh.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 |
@@ -48,16 +48,16 @@
| [plan.md](plan.zh.md) | 计划模式:仅记日志的 `plan/mode` 状态、待定选择的冲刷、`PlanModeConfig`、`exit_plan_mode` 审阅流程 |
| [invariants.md](invariants.zh.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件约定 |
| [web-server.md](web-server.zh.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 渲染挂接点 |
| [webhook.md](webhook.zh.md) | 通过身份验证的提供方交付、任意程序化规则,以及 fire-and-forget 的 Workspace Session 创建 |
| [webhook.md](webhook.zh.md) | 通过身份验证的提供方交付、任意程序化规则,以及发起 Workspace 会话创建后不等待结果 |
| [storage.md](storage.zh.md) | 存储子系统:后端约定(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
| [workspace.md](workspace.zh.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
| [web-client.md](web-client.zh.md) | 浏览器架构:启动、Remote 通信、配对的 Client model、UI adapter、Conversation 组装、Slots 与重连语义 |
| [client-modules.md](client-modules.zh.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
| [slots.md](slots.zh.md) | 类型化 Web UI 组合:声明所有权、cardinality 与 scope、框架与功能注入、props 推导及当前层级 |
| [client-resources.md](client-resources.zh.md) | 客户端资源模型:`dsh-resource://<type>/…` 地址、协议提供方与 `ResourceProtocolMap`、`useResource` 全局 hook 及其状态、钉住与释放 |
| [web-client.md](web-client.zh.md) | 浏览器架构:启动、Remote 通信、配对的 Client model、UI 适配器、Conversation 组装、slot 与重连语义 |
| [client-modules.md](client-modules.zh.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 协议格式组合、bundle 路由与 index 挂接点 |
| [slots.md](slots.zh.md) | 类型化 Web UI 组合:声明所有权、cardinality 与 scope、框架与功能注入、props 推导及已交付的层级结构 |
| [client-resources.md](client-resources.zh.md) | 客户端资源模型:`dsh-resource://<type>/…` 地址、协议提供方与 `ResourceProtocolMap`、`useResource` 全局钩子及其状态、钉住与释放 |
| [sidebar-right.md](sidebar-right.zh.md) | 右侧 Sidebar:资源地址与导航地址、tab 类型注册与路由、`ctx.sidebarRight` 导航服务、pane-tab slot 与 owner props、资源模型及 Workspace Files 服务 |
| [conversation.md](conversation.zh.md) | target-neutral Session event 组装:Context identity、Location data、replay 路径、view builder 与 target 自有 render node |
| [conversation.md](conversation.zh.md) | 目标无关的会话事件组装:上下文标识、位置数据、回放路径、视图构建器与目标自有的渲染节点 |
| [session-projection.md](session-projection.zh.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
| [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall |
| [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall(瀑布式事件) |
> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.zh.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis API** 小节。
+2 -2
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 docs/subsystems/sidebar-right.md
sidebar-right.md: 807edc5873c97a256cb66923e776b0573af3186f
sidebar-right.zh.md: a56ebe819e8fbc63f5e6aa7697eb08e03b8d1c46
sidebar-right.md: 3b6e5eb08fc2c0a0bac74369bde4a48b2ee0fdb6
sidebar-right.zh.md: 680e2ae16f8f8aeeea57b11172be8c8c9e359122
+2 -2
View File
@@ -43,7 +43,7 @@ Tab identity is the pair `(kind, address)`: the registry's claim uses the addres
| `priority` | One of three literal bands: `extension` (the default and the highest: a type from outside the product outranks every shipped viewer), `builtin` (types shipped with the product), `fallback` (plain-content viewers anything more specific should beat). |
| `canOpen(address)` | Optional synchronous veto of a glob match; it runs on every routing decision. |
| `title(address)` | The chip's text, captured into the layout record when the tab opens and never rewritten. |
| `guide` | Optional entry boxes for the guide page: `{ order, title(), description(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. |
| `guide` | Optional entry boxes for the guide page: `{ order, title(), description?(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. |
Routing is a ranked claim. `candidates(address)` ranks the types whose patterns match and whose `canOpen` does not veto: by band, then by the length of the longest matched pattern, then by registration order. `claim(address, kind?)` picks the first candidate, or the named `kind` outright — its globs are skipped, its `canOpen` still applies — and returns `{ kind, contentId: address, title }`. An address no type claims throws: it is a wiring mistake, not a user error.
@@ -130,7 +130,7 @@ The Host `ctx.workspaceFiles` service and generated `workspaceFiles` Remote name
## Shipped types
- **`guide`** — `builtin`, opened as `openTab('guide')`. A centred title, one line, and one entry box per `guide` entry the registered types contributed, in `order`; picking a box opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)).
- **`guide`** — `builtin`, opened as `openTab('guide')`. A muted compass sits above one capsule per contributed `guide` entry, in `order`; short lists show registered descriptions, and every missing icon uses the shipped placeholder. Picking a capsule opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)).
- **`text`** — `fallback`, `dsh-resource://file/**`, claiming Session addresses only. Document Preview observes metadata through `useResource<'file'>`, loads content through Remote callbacks, and owns renderer selection, the toolbar, per-tab refresh, scroll, and source navigation; unknown extensions render as plain text ([README](../../packages/client/ui-sidebar-documentpreview/README.md)).
- **`files`** — `builtin`, opened as `openTab('files')`. The workspace directory tree, listed lazily through `list`, opening a file with `tab.actions.openResource(fileAddressFor(sessionId, root, path))` into its own pane ([README](../../packages/client/ui-sidebar-files/README.md)).
+2 -2
View File
@@ -43,7 +43,7 @@ tab 身份是 `(kind, address)` 二元组:注册表的认领把地址原文用
| `priority` | 三档字面量之一:`extension`(缺省且最高:产品之外的类型压过所有内置查看器)、`builtin`(随产品发布的类型)、`fallback`(任何更具体的类型都应压过的纯内容查看器)。 |
| `canOpen(address)` | 可选的同步否决,对 glob 命中生效;每次路由决策都会调用。 |
| `title(address)` | chip 文本,在 tab 打开时捕获进布局记录,之后不再改写。 |
| `guide` | 可选的引导页入口框:`{ order, title(), description(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 |
| `guide` | 可选的引导页入口框:`{ order, title(), description?(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 |
路由是一次排序认领。`candidates(address)` 对模式命中且未被 `canOpen` 否决的类型排序:先按档,再按最长命中模式的长度,最后按注册顺序。`claim(address, kind?)` 取第一个候选,或直接用点名的 `kind`——跳过它的 glob,但 `canOpen` 仍生效——返回 `{ kind, contentId: address, title }`。没有任何类型认领的地址会抛错:这是接线错误,不是用户错误。
@@ -130,7 +130,7 @@ Host 的 `ctx.workspaceFiles` 服务与生成的 `workspaceFiles` Remote 命名
## 内置类型
- **`guide`**——`builtin`,以 `openTab('guide')` 打开。居中标题、一行说明,以及已注册类型贡献的每个 `guide` 入口一框、按 `order` 排列;点一框即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。
- **`guide`**——`builtin`,以 `openTab('guide')` 打开。一枚弱化的罗盘位于各类型按 `order` 贡献的入口胶囊上方;入口较少时显示已注册的描述,未提供图标的入口统一使用内置占位符。点选胶囊即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。
- **`text`**——`fallback`,`dsh-resource://file/**`,只认领 Session 地址。Document Preview 通过 `useResource<'file'>` 观察元数据,经 Remote 回调加载内容,并拥有渲染器选择、工具栏、逐 tab 刷新、滚动与源码定位;未知扩展名按纯文本渲染([README](../../packages/client/ui-sidebar-documentpreview/README.zh.md))。
- **`files`**——`builtin`,以 `openTab('files')` 打开。工作区目录树,经 `list` 懒加载,用 `tab.actions.openResource(fileAddressFor(sessionId, root, path))` 在自己所在 pane 打开文件([README](../../packages/client/ui-sidebar-files/README.zh.md))。
+2 -2
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 docs/user/guide/providers.md
providers.md: 0db3280760c487c8469399bc9668b0685213e375
providers.zh.md: 3af9096f5a77ee40afe2ee788aa22a0758dd0e86
providers.md: 90d3bb2c96a80185fbc5e3e70f1ff577f6923acc
providers.zh.md: 2937d676253ee45dad48655398736d36fc37dff2
+1 -1
View File
@@ -186,5 +186,5 @@ Every switch, its accepted values, and the protocols that take it are listed und
- **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `settings.yaml`.
- **`off` does not stop a DeepSeek model from thinking** — An empty `off` sends no reasoning field at all, and an endpoint that thinks by default keeps thinking. Set `compat.thinkingFormat: deepseek` on the model or the route.
- **A compat switch is refused as having no value** — A key written with nothing after the colon. Give it a value, or remove the key to keep the installed catalog's.
- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select `deepseek-v4-flash-vision-exp`, the model that declares images.
- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select an image-capable entry from the configured catalog (`deepseek-flash` by default) and confirm that your gateway serves that model with image input.
- **The provider rejects a request carrying an image** — The model declares images its endpoint does not actually serve. Remove `image` from whichever list granted it — the model's `input`, or the route's `defaultInput` — then start a new session: the attached image stays in the session log, so the same request repeats until the session moves off it.
+1 -1
View File
@@ -186,5 +186,5 @@ llm-pi-ai:
- **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `settings.yaml` 中给该模型加上 `reasoningEfforts`。
- **`off` 无法让 DeepSeek 模型停止思考**:留空的 `off` 不发送任何推理字段,默认思考的端点就继续思考。请在模型或路由上设置 `compat.thinkingFormat: deepseek`。
- **某个 compat 开关因没有值而被拒绝**:冒号后什么都没写。给它一个值,或删掉该键以沿用已安装 catalog 的值。
- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请选择声明了图片能力的模型 `deepseek-v4-flash-vision-exp`。
- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请从配置的目录中选择支持图片的条目(默认为 `deepseek-flash`),并确认网关提供该模型且支持图片输入。
- **提供方拒绝了带图片的请求**:该模型声明了其端点实际并不提供的图片能力。请从授予它图片能力的那个列表中移除 `image`——可能是模型的 `input`,也可能是路由的 `defaultInput`——然后开启新会话:附加的图片会留在会话日志里,因此在会话离开它之前,同一个请求会不断重复。
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/system/README.md
README.md: 5a01754cb96fabbdfd2dbf4f068f19addd7a2347
README.zh.md: dabd00cecd05ded13a50d2efd000985afed713d4
README.zh.md: 27d0ca70e28e68e46cb121b8e6b63c56ba5c99d3
+5 -5
View File
@@ -1,5 +1,5 @@
---
description: "为 Linux 进程隔离与 POSIX Session 写锁提供预编译系统原语。"
description: "为 Linux 进程隔离与 POSIX 会话写锁提供预编译系统原语。"
kind: "package-library"
---
# @deepseek-ai/node-addon-system
@@ -8,7 +8,7 @@ kind: "package-library"
## Summary
使用 Linux `landlock-run` 可执行文件限制子进程,或通过 `./flock` 入口获取 POSIX 写锁。平台包包含预编译二进制;用户安装时不会构建原生代码。Landlock 策略与 Session 生命周期仍由调用方负责。
使用 Linux `landlock-run` 可执行文件限制子进程,或通过 `./flock` 入口获取 POSIX 写锁。平台包包含预编译二进制;消费方安装时不会构建原生代码。Landlock 策略与会话生命周期仍由调用方负责。
## Table of Contents
@@ -22,9 +22,9 @@ kind: "package-library"
[flock 行为约定](docs/flock-contract.md) 将描述符、进程和咨询式锁语义对应到独立原生测试。
`@deepseek-ai/node-addon-system/flock` 导出 `tryLockExclusive(fd): Promise<void>`。在调用完成前保持描述符打开。获取操作使用非阻塞独占 flock;竞争以 `EAGAIN` 或 `EWOULDBLOCK` 拒绝,关闭该打开文件描述的最后一个描述符即释放锁。参见[入口 README](packages/entry/README.zh.md)。
`@deepseek-ai/node-addon-system/flock` 导出 `tryLockExclusive(fd): Promise<void>`。在调用完成前保持描述符打开。获取操作使用非阻塞独占 flock;发生竞争时,返回的 Promise 会以 `EAGAIN` 或 `EWOULDBLOCK` 拒绝,关闭该打开文件描述的最后一个描述符即释放锁。参见[入口 README](packages/entry/README.zh.md)。
导入任一入口都不会加载 addon。Landlock 可执行文件缺失时探测为不可用;flock 绑定缺失时拒绝获取。两条路径都不会编译或静默授予不受支持的行为。
导入任一入口都不会加载 addon。Landlock 可执行文件缺失时探测为不可用;flock 绑定缺失时拒绝获取。两条路径都不会进行编译,也不会静默允许不受支持的行为。
## Support
@@ -32,7 +32,7 @@ Linux x64/arm64 包包含静态 Landlock 可执行文件,以及分别用于 gl
## Development
在本目录运行 `pnpm build:ts` 构建入口、`pnpm build:native` 构建当前宿主声明的原生产物、`pnpm build:test-oracle` 构建独立的 flock 系统调用 fixture。随后用 `pnpm test` 验证入口、锁、打包及可用的内核行为。Linux 完整构建需要 musl-gcc;macOS 使用 cc。根目录 `pnpm run build:native-system` 只构建源码测试所需的当前宿主 addon。
在本目录运行 `pnpm build:ts` 构建入口、`pnpm build:native` 构建当前宿主声明的原生产物、`pnpm build:test-oracle` 构建独立的 flock 系统调用 fixture(测试前置数据)。随后用 `pnpm test` 验证入口、锁、打包及可用的内核行为。Linux 完整构建需要 musl-gcc;macOS 使用 cc。根目录 `pnpm run build:native-system` 只构建源码测试所需的当前宿主 addon。
[架构](docs/architecture.md)、[打包](docs/packaging.md)和[发布流程](docs/release.md)分别负责实现与发布细节。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/system/packages/darwin-x64/README.md
README.md: 1740788eef943373591a895baeb7207635972a46
README.zh.md: 644165743486a29e48bee7af33429ebf4f5d8fe2
README.zh.md: 88053b85150c94048a4ffc32b506d365ff13c15f
@@ -6,4 +6,4 @@ kind: "package-library"
[English](README.md) | 中文
此平台包提供 `bin/system.node`,供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS x64 上构建它,并负责验证安装后的产物。
此平台包提供 `bin/system.node`,这是一个供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS x64 上构建它,并负责验证安装后的产物。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/system/packages/entry/README.md
README.md: 8f6a4d341c8f9e4c104e5706e08d9a7d45c065e1
README.zh.md: 152257916df0cf40511b9743d389ae753ab6c85c
README.zh.md: e401fc2ec71b1afb664d8e73bf3e9372054957a7
+2 -2
View File
@@ -8,8 +8,8 @@ kind: "package-library"
`./landlock-run` 入口导出 Landlock 启动器路径、强制执行探测、授权参数和协议常量。独立的 `./flock` 入口导出 `tryLockExclusive(fd): Promise<void>`;导入任一入口都不会加载 `system.node`。包不提供根导出。
锁操作异步尝试 `LOCK_EX | LOCK_NB`。在完成前保持调用方拥有的描述符打开;竞争以 `EAGAIN`/`EWOULDBLOCK` 拒绝,其他系统调用失败也会拒绝,错误携带 code、正 errno 和 `syscall: 'flock'`。原生调用准备阶段的错误也会拒绝同一个 promise。关闭该打开文件描述的最后一个描述符即释放锁。绑定不打开、复制、关闭或显式解锁描述符。
锁操作异步尝试 `LOCK_EX | LOCK_NB`。在完成前保持调用方拥有的描述符打开;竞争以 `EAGAIN`/`EWOULDBLOCK` 拒绝,其他系统调用失败也会拒绝,错误携带 code、值为正数的 errno 和 `syscall: 'flock'`。原生调用准备阶段的错误也会拒绝同一个 promise。关闭指向该打开文件描述的最后一个描述符即释放锁。绑定不打开、复制、关闭或显式解锁描述符。
可选操作系统/CPU 平台包携带二进制。Linux 包含 `bin/landlock-run` 和分别用于两种 libc 的 `bin/glibc/system.node` / `bin/musl/system.node`;macOS 包含 `bin/system.node`。flock 绑定缺失或无法加载时拒绝获取,不在安装时编译。Landlock 仍是遵循既有失败关闭协议的独立可执行文件;不支持的内核或平台探测为不可用。
可选操作系统/CPU 平台包携带二进制。Linux 包含 `bin/landlock-run` 和分别用于两种 libc 的 `bin/glibc/system.node` / `bin/musl/system.node`;macOS 包含 `bin/system.node`。flock 绑定缺失或无法加载时,锁获取请求会被拒绝,不在安装时编译。Landlock 仍是遵循既有失败关闭协议的独立可执行文件;不支持的内核或平台探测结果为不可用。
两个 C 源文件随包分发以供审计。参见工作区[架构](../../docs/architecture.md)、[支持矩阵](../../docs/support-matrix.md)和 [CLI 约定](../../docs/cli-contract.md)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/system/packages/linux-arm64/README.md
README.md: b1e97481a3f59b5b7cf85e6565cd3d078e42cd7f
README.zh.md: 79f3b75e16570c4dff8d7249bcd75481f11cc85c
README.zh.md: 68efb95c60f41b2b95d1e963d9b4f0c8c130a80c
@@ -6,6 +6,6 @@ kind: "package-library"
[English](README.md) | 中文
此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口会选择与正在运行的 Node 进程所用 libc 匹配的 addon;Landlock 可执行文件在两种 libc 系统上共用。
包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。
包中没有 JavaScript 或安装编译脚本。平台 prepack 检查产物完整性、ELF 架构、Node-API 导出和启动器可执行权限;已安装产物演练核对字节并执行原生行为。参见工作区的[支持矩阵](../../docs/support-matrix.md)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/system/packages/linux-x64/README.md
README.md: caebb5d8a888a69a952fc89afb3edfb8ece489fc
README.zh.md: 6ad1dcf6fe26007f76cdeb98936a94262ece4d7c
README.zh.md: bf5edf59222a5675dbaa63d02fcac115bec7b58d
@@ -6,6 +6,6 @@ kind: "package-library"
[English](README.md) | 中文
此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。
包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;已安装产物演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-root",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"license": "MIT",
"private": true,
"type": "module",
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/README.md
README.md: 9c298d3f2426bab92a6d602495d836ef8ee9fb44
README.zh.md: 309a0e678968927b79d527991447847d0fa45f83
README.zh.md: db8977a279713ac7a2f86144323a54125743a46a
+16 -16
View File
@@ -9,7 +9,7 @@ kind: "package-group"
## 概述
harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会话与 agent 循环、面向模型的工具、shell 与文件系统执行、Web 访问、subagent 等等。把本页当作顶层地图使用:先找到拥有某能力的组,再打开其 README 查看包列表。每个包都以 `@deepseek-ai/dsh-*` 为作用域、只属于一个组;每个组的 README 都是该能力系列的权威包映射。
harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会话与 agent loop(智能体循环)、面向模型的工具、shell 与文件系统执行、Web 访问、subagent 等等。把本页当作顶层地图使用:先找到拥有某能力的组,再打开其 README 查看包列表。每个包都以 `@deepseek-ai/dsh-*` 为作用域、只属于一个组;每个组的 README 都是该能力系列的权威包映射。
## 目录
@@ -35,24 +35,24 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
| [`schedule/`](schedule/README.zh.md) | 仅限会话内的定时后续操作 |
| [`feedback/`](feedback/README.zh.md) | 人类反馈的采集与命令 |
| [`identity/`](identity/README.zh.md) | 共享匿名身份 |
| [`llm/`](llm/README.zh.md) | LLM 能力系列:抽象服务 + 提供方适配器 |
| [`llm/`](llm/README.zh.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 |
| [`e2b/`](e2b/README.zh.md) | E2B 远程运行时提供方 |
| [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 |
| [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 |
| [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
| [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + PTC mode Consumer |
| [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 |
| [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap、Landlock、Seatbelt 后端 |
| [`fs/`](fs/README.zh.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、发现工具 |
| [`lsp/`](lsp/README.zh.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 |
| [`skill/`](skill/README.zh.md) | skill 能力系列:提供方注册表、本地提供方、面向模型的目录/loader |
| [`compaction/`](compaction/README.zh.md) | 压缩能力系列:Service Definition + 基础提供方 + 命令 Consumer |
| [`skill/`](skill/README.zh.md) | skill(技能)能力系列:提供方注册表、本地提供方、面向模型的目录/loader |
| [`compaction/`](compaction/README.zh.md) | 压缩(compaction)能力系列:Service Definition + 基础提供方 + 命令 Consumer |
| [`context/`](context/README.zh.md) | 模型可见请求上下文:workspace 指令、时间上下文、引用 |
| [`subagent/`](subagent/README.zh.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 |
| [`jobs/`](jobs/README.zh.md) | 通用后台任务运行时和面向模型的作业控制工具 |
| [`experimental/`](experimental/README.zh.md) | 私有原型与内部专用插件 |
| [`workflow/`](workflow/README.zh.md) | 工作流 seam、worker 线程引擎、面向模型的 `workflow`/`ralph` 工具 |
| [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace Session |
| [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 |
| [`workflow/`](workflow/README.zh.md) | 工作流 seam、worker 线程引擎、面向模型的 `workflow`/`ralph` 工具 |
| [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace 会话 |
| [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 |
| [`attachment/`](attachment/README.zh.md) | 持久附件标识、校验、本地内容寻址存储 |
| [`spill/`](spill/README.zh.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 |
| [`todo/`](todo/README.zh.md) | 面向模型的 `todo_write` 工具 |
@@ -60,30 +60,30 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
| [`preset/`](preset/README.zh.md) | 由 preset `cordis.yml` 按会话组装 agent |
| [`guard/`](guard/README.zh.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 |
| [`bundle/`](bundle/README.zh.md) | 可安装的 `dsh --profile` 补丁层 |
| [`extensions/`](extensions/README.zh.md) | agent 运行时自修改:实时插件/服务检查与模型所写挂载/卸载 |
| [`hooks/`](hooks/README.zh.md) | 钩子桥接 + 共享的 Claude Code / Codex 线协议库 |
| [`extensions/`](extensions/README.zh.md) | agent 运行时自修改:实时插件/服务检查与模型所写挂载/卸载 |
| [`hooks/`](hooks/README.zh.md) | 钩子桥接 + 共享的 Claude Code/Codex 线协议库 |
| [`session/`](session/README.zh.md) | 持久会话数据平面:持久化 seam + 后端、投影 seam、基于日志的标题、会话上报 |
| [`session-query/`](session-query/README.zh.md) | 会话检索系列:逻辑语料库、有界读取、血缘、语义过滤、SQLite 全文搜索 |
| [`settings/`](settings/README.zh.md) | 用户设置 seam + 基于文件的提供方 |
| [`credentials/`](credentials/README.zh.md) | 凭据引用/记录 seam + 环境变量优先于 `.env` 的提供方 + 询问人类的授权 flow |
| [`credentials/`](credentials/README.zh.md) | 凭据引用与凭据记录 seam + 环境变量优先于 `.env` 的提供方 + 需要向人询问的授权流程 |
| [`storage/`](storage/README.zh.md) | 非会话存储中枢 + 后端 + 领域形式 |
| [`workspace/`](workspace/README.zh.md) | Workspace 实体 |
| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议与 TypeScript 客户端/服务器 |
| [`acp/`](acp/README.zh.md) | 仅面向自动化的 Agent Client Protocol 服务器 |
| [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 |
| [`acp/`](acp/README.zh.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 |
| [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 |
| [`boot/`](boot/README.zh.md) | 共享的 app bin 启动粘合层 |
| [`host/`](host/README.zh.md) | web GUI 宿主半侧:API 网关 + HTTP 路由服务器 |
| [`client/`](client/README.zh.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 |
| [`test-support/`](test-support/README.zh.md) | 支持基础设施(testkit、不变式、回放、Loader 冒烟测试) |
| [`runtime-diagnostics/`](runtime-diagnostics/README.zh.md) | 运行时诊断:按包归属的运行时不变式检查与报告 |
| [`util/`](util/README.zh.md) | 组间共享的低层零依赖工具(`Branded<B>`、home/路径辅助函数、超时、留存) |
| [`util/`](util/README.zh.md) | 组间共享的低层零依赖工具(`Branded<B>`、home/路径辅助函数、超时、留存) |
-----
<a id="release-expectations"></a>
## 发布预期
大多数组是产品——稳定 API。例外:`e2b/` 是 POC,`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
大多数组属于产品组,提供稳定 API。例外:`e2b/` 是 POC,`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
-----
@@ -92,7 +92,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
依赖图由工具生成:[docs/module-graph.md](../docs/module-graph.zh.md)(`pnpm run gen-module-graph`,CI 中有新鲜度门禁)。
**扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。组合包可以依赖主干插件。能力在需要独立演进时分离 Service Definition / Service Provider / Consumer 角色;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)。
**扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。组合包可以依赖主干插件。能力在需要独立演进时分离 Service Definition/Service Provider/Consumer 角色;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)。
-----
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/acp/README.md
README.md: 710a77335eec233c6fdef36091742fb61e95f30a
README.zh.md: eb2e6847aeb5926d276e2c77d7a0482f55217287
README.zh.md: a40fb43ef64a92a1467faf2cfeb9fd884e8890a1
+4 -4
View File
@@ -1,15 +1,15 @@
---
description: "ACP(Agent Client Protocol)包组:通过 JSON-RPC stdio 将全新 harness agent 暴露给程序化客户端的仅自动化服务器。"
description: "ACP(Agent Client Protocol)包组:通过 JSON-RPC stdio 将全新 harness agent(智能体)暴露给程序化客户端的仅面向自动化的服务器。"
kind: "package-group"
---
# acp/ — Agent Client Protocol 自动化
# acp/:Agent Client Protocol 自动化
[English](README.md) | 中文
## 概述
acp 组提供一个包:一台服务器,让程序与自动化可以通过标准 Agent Client Protocol 运行持久 DeepSeek Harness agent。客户端可以创建、列出、恢复与关闭会话,挂载标准 MCP 服务器,选择模型选项,发送文本与图片提示词,接收语义更新,响应权限提示并取消工作——无需人类参与。从另一个 harness 启动这种服务器的配套客户端位于 `subagent/subagent-acp`。本页是组的映射;包 README 负责各自的包级约定。
acp 组提供一个包:一个服务器,让程序与自动化流程可以通过标准 Agent Client Protocol 运行持久 DeepSeek Harness agent。客户端可以创建、列出、恢复与关闭会话,挂载标准 MCP 服务器,选择模型选项,发送文本与图片提示词,接收语义更新,响应权限提示并取消工作——无需人类参与。从另一个 harness 启动这种服务器的配套客户端位于 `subagent/subagent-acp`。本页概述该包组;各包的具体约定由其 README 规定。
## 目录
@@ -24,7 +24,7 @@ acp 组提供一个包:一台服务器,让程序与自动化可以通过标
| 包 | 职责 |
|---|---|
| [`acp/`](acp/README.zh.md) | 让程序通过 ACP 管理持久 agent、挂载 MCP 服务器、选择模型选项、发送或取消工作并接收语义更新 |
| [`acp/`](acp/README.zh.md) | 让程序通过 ACP 管理持久 agent、挂载 MCP 服务器、选择模型选项、发送提示词、取消工作并接收语义更新 |
-----
+2 -2
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/acp/acp/README.md
README.md: 635e0e6993a65f403a5cc89f2c9d147b48308a04
README.zh.md: 65d7d3b2f9f66701c801c910048a501f565b0a46
README.md: a30396733026108fdeaff67713fbe7aefb8c5101
README.zh.md: 7f7f2f43ac6fa3059a6d5400e6c2c8d7227197d9
+3 -3
View File
@@ -1,5 +1,5 @@
---
description: "Automation-only Agent Client Protocol server for programmatic clients and maintainers driving DeepSeek Harness agents over JSON-RPC stdio."
description: "Automation-only ACP (Agent Client Protocol) server for programmatic clients and maintainers driving DeepSeek Harness agents over JSON-RPC stdio."
kind: "package-reference"
---
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
`dsh-acp` lets trusted programs automate persistent DeepSeek Harness agents through the standard [Agent Client Protocol](https://agentclientprotocol.com): create or resume sessions, select a model and reasoning effort, attach MCP servers, submit or cancel work, receive semantic updates, and close sessions independently. Choose it for out-of-process subagents, test runners, and scripted controllers; it intentionally omits DSH-specific presentation data and interactive UI features. Persistence supports listing, resuming, and closing sessions across process restarts, but deletion, forks, transcript replay, and additional directories are unsupported. Run `pnpm dsh --profile acp` to start the server; use `dsh-subagent-acp` as the repository client.
`dsh-acp` lets trusted programs automate persistent DeepSeek Harness agents through the standard [ACP](https://agentclientprotocol.com): create or resume sessions, select a model and reasoning effort, attach MCP servers, submit or cancel work, receive semantic updates, and close sessions independently. Choose it for out-of-process subagents, test runners, and scripted controllers; it intentionally omits DSH-specific presentation data and interactive UI features. Persistence supports listing, resuming, and closing sessions across process restarts, but deletion, forks, transcript replay, and additional directories are unsupported. Run `pnpm dsh --profile acp` to start the server; use `dsh-subagent-acp` as the repository client.
## Table of Contents
@@ -73,7 +73,7 @@ One connection can run several sessions at once, each independent. The calls a c
| `session/update` | Committed assistant messages and thoughts, generic tool lifecycle, configuration changes, and context usage, serialized per session. |
| `session/request_permission` | A permission prompt with one-shot allow/reject choices; your client can answer automatically. |
Session configuration offers opaque provider/model choices from the live LLM service catalog and a `reasoning_effort` selector when the exact model declares one. A prompt snapshots that selection before asynchronous image admission and pins it across every model step in that turn; a concurrent option change applies to the next turn. ACP clients are trusted controllers: stdio MCP entries authorize their absolute commands and environment, HTTP entries authorize their absolute HTTP(S) URLs and headers, and any initial connection or discovery failure rolls back the unpublished Agent. Unsupported surfaces are omitted or reject: `session/load`, deletion, fork, additional directories, SSE or ACP-transport MCP, modes, commands, plans, terminals, client filesystem operations, and elicitation.
Session configuration offers opaque provider/model choices from the live LLM service catalog and a `reasoning_effort` selector when the exact model declares one. A prompt snapshots that selection before asynchronous image admission and pins it across every model step in that turn; a concurrent option change applies to the next turn. ACP clients are trusted controllers: stdio MCP entries authorize their absolute commands and environment, HTTP entries authorize their absolute HTTP(S) URLs and headers, and any initial connection or discovery failure rolls back the unpublished Agent. Unsupported surfaces are omitted or rejected: `session/load`, deletion, fork, additional directories, SSE or ACP-transport MCP, modes, commands, plans, terminals, client filesystem operations, and elicitation.
-----
+11 -11
View File
@@ -1,5 +1,5 @@
---
description: "面向程序化客户端与维护者的仅自动化 Agent Client Protocol 服务器,用于通过 JSON-RPC stdio 驱动 DeepSeek Harness agent。"
description: "面向程序化客户端与维护者的仅自动化 ACP(Agent Client Protocol)服务器,用于通过 JSON-RPC stdio 驱动 DeepSeek Harness agent(智能体)。"
kind: "package-reference"
---
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-acp` 让受信程序通过标准 [Agent Client Protocol(ACP)](https://agentclientprotocol.com) 自动操作持久 DeepSeek Harness agent:创建或恢复会话、选择模型与推理强度、挂载 MCP 服务器、提交或取消工作、接收语义更新,并独立关闭会话。进程外 subagent、测试运行器与脚本化控制器适合选择它;它刻意不提供 DSH 专用呈现数据与交互式 UI 功能。持久化支持跨进程重启列出、恢复与关闭会话,但不支持删除、fork、转录回放与附加目录。运行 `pnpm dsh --profile acp` 可启动服务器;仓库客户端使用 `dsh-subagent-acp`。
`dsh-acp` 让受信程序通过标准 [ACP](https://agentclientprotocol.com) 自动操作持久 DeepSeek Harness agent:创建或恢复会话、选择模型与推理强度、挂载 MCP 服务器、提交或取消工作、接收语义更新,并独立关闭会话。进程外 subagent、测试运行器与脚本化控制器适合选择它;它刻意不提供 DSH 专用呈现数据与交互式 UI 功能。持久化支持跨进程重启列出、恢复与关闭会话,但不支持删除、fork、transcript(文本记录)回放与附加目录。运行 `pnpm dsh --profile acp` 可启动服务器;仓库客户端使用 `dsh-subagent-acp`。
## 目录
@@ -64,8 +64,8 @@ kind: "package-reference"
| `initialize` | 稳定 ACP v1,以及 `session/list`、`session/resume`、`session/close` 与 Streamable HTTP MCP 支持;图片提示词只在持久附件存储和配置的确切路由支持时公布。 |
| `authenticate` | 立即成功;服务器不需要身份验证。 |
| `session/new` | 全新持久 agent;其绝对工作区与 stdio 或 HTTP MCP 服务器会在发布前通过校验,并返回完整配置选项状态。 |
| `session/list` | 按确定的新到旧顺序分页返回已持久、可恢复的根会话;可选绝对 `cwd` 筛选会尽可能使用物理目录标识。 |
| `session/resume` | 恢复一个已持久且非活跃的会话;组合前校验其规范工作区,并恢复日志但不回放旧更新。 |
| `session/list` | 按确定的新到旧顺序分页返回已持久化、可恢复的根会话;可选绝对 `cwd` 筛选会尽可能使用物理目录标识。 |
| `session/resume` | 恢复一个已持久化且非活跃的会话;组合前校验其规范工作区,并恢复日志但不回放旧更新。 |
| `session/close` | 停稳式取消、更新 drain、后代释放、持久化 flush,并且只释放指定 Agent 作用域。 |
| `session/set_config_option` | 串行更新公布的 `model` 或 `reasoning_effort`,并返回完整结果状态。 |
| `session/prompt` | 有序文本、资源链接与受支持图片,每个会话一次一个提示词;Agent 空闲且有序更新交付后才结算。 |
@@ -73,7 +73,7 @@ kind: "package-reference"
| `session/update` | 已提交 assistant 消息与 thought、通用工具生命周期、配置变化与上下文用量,按会话串行交付。 |
| `session/request_permission` | 带一次性允许/拒绝选项的权限提示;你的客户端可以自动回答。 |
会话配置从实时 LLM 服务目录提供不透明的提供方/模型选项,并在确切模型声明推理选项时提供 `reasoning_effort`。提示词会在异步图片准入前快照该选择,并在该轮的每个模型步骤中固定它;并发选项变更从下一轮开始生效。ACP 客户端是受信控制器:stdio MCP 条目授权其绝对命令与环境,HTTP 条目授权其绝对 HTTP(S) URL 与 header;初始连接或发现失败会回滚尚未发布的 Agent。不支持的界面会被省略或拒绝:`session/load`、删除、fork、附加目录、SSE 或 ACP 传输 MCP、mode、命令、计划、终端、客户端文件系统操作与 elicitation。
会话配置从实时 LLM(大语言模型)服务目录提供不透明的提供方/模型选项,并在确切模型声明推理选项时提供 `reasoning_effort`。提示词会在异步图片准入前快照该选择,并在该轮次的每个模型步骤中固定它;并发选项变更从下一轮次开始生效。ACP 客户端是受信控制器:stdio MCP 条目授权其绝对命令与环境,HTTP 条目授权其绝对 HTTP(S) URL 与 header;初始连接或发现失败会回滚尚未发布的 Agent。不支持的界面会被省略或拒绝:`session/load`、删除、fork、附加目录、SSE(Server-Sent Events)或 ACP 传输 MCP、mode、命令、计划、终端、客户端文件系统操作与 elicitation。
-----
@@ -102,15 +102,15 @@ kind: "package-reference"
| [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、`AgentSideConnection` 接线、按会话记录、准入与结算、清理 |
| [`src/content.ts`](src/content.ts) | 协议内容准入与投影:图片校验、路由重查、提示词重建、assistant 块转换 |
| [`src/codec.ts`](src/codec.ts) | 轮次结束到 ACP `stopReason` 的纯映射 |
| — | 不发布运行时不变式伴生入口;本传输不拥有持久包内事件流。 |
| — | 不发布运行时不变式伴生入口;本传输不拥有持久包内事件流;协议与生命周期测试覆盖其映射。 |
### 准入与提示词结算
每个会话只允许一个正在处理的提示词。准入先校验整个提示词批次、快照所选路由、重新检查 Agent 是否为同一对象与图片能力、持久化图片附件,然后才把用户消息入队——赢得准入的取消绝不会入队迟到的轮次。入队后,会话模块把该快照与 inbox 消息关联到认领时刻,并在提示词变量与该轮的每个模型步骤中固定相同的提供方、模型与推理强度。按会话更新会串行交付;已提交图片会重新读取并验证完整性,因此图片缺失或损坏会让关联提示词失败,而不是发出占位符。结算优先级依次为显式取消、已提交输出失败、区间内 Agent 失败、关联轮次结束。
每个会话只允许一个正在处理的提示词。准入先校验整个提示词批次、快照所选路由、重新检查 Agent 是否为同一对象与图片能力、持久化图片附件,然后才把用户消息入队——先于准入完成的取消绝不会让迟到的轮次入队。入队后,会话模块会将该快照与 inbox 消息关联起来,直至该消息被认领,并在提示词变量与该轮次的每个模型步骤中固定相同的提供方、模型与推理强度。按会话更新会串行交付;已提交图片会重新读取并验证完整性,因此图片缺失或损坏会让关联提示词失败,而不是发出占位符。结算优先级依次为显式取消、已提交输出失败、区间内 Agent 失败、关联轮次结束。
### 清理与连接归属
每个会话模块拥有其 Agent 句柄、MCP 挂载、未来与轮次固定的模型选择、提示词槽位、更新链和记忆化关闭操作。显式关闭、客户端断开与 Cordis 释放使用同一停稳式清理流程:停止新工作、取消提示词准入与 Agent 活动、drain 已提交更新、按子优先顺序释放可继续后代、flush 持久化并释放所拥有的 Agent 作用域。会话关闭后,持久状态仍可供列出与恢复;共享上下文的其他会话或前端不受影响。
每个会话模块拥有其 Agent 句柄、MCP 挂载、未来与轮次固定的模型选择、提示词槽位、更新链和记忆化关闭操作。显式关闭、客户端断开与 Cordis 释放使用同一停稳式清理流程:停止新工作、取消提示词准入与 Agent 活动、drain 已提交更新、按子优先顺序释放可继续后代、flush 持久化并释放所拥有的 Agent 作用域。会话关闭后,已持久化的状态仍可供列出与恢复;共享上下文的其他会话或前端不受影响。
</details>
@@ -139,11 +139,11 @@ kind: "package-reference"
#### Token 影响
提示词内容、工具调用/结果和持久图片引用会保留在该会话中直到 compaction。并发会话保留独立上下文。
提示词内容、工具调用/结果和持久图片引用会保留在该会话中直到上下文压缩(context compaction)。并发会话保留独立上下文。
#### KV Cache 影响
仅追加;新用户消息位于可复用请求前缀之后,不会使先前缓存条目失效。
只要所选路由与组装后的前缀保持不变,就仅追加。模型变更会使下一个 ACP 轮次改用新路由。
### 权限决策
@@ -169,7 +169,7 @@ kind: "package-reference"
- **仅一个主 workspace**——附加目录仍不支持。
- **仅光栅提示词图片**——PNG、JPEG、WebP 与 GIF 要求持久附件存储及确切的图片能力路由。
- **仅 MCP 工具**——MCP resource 与 prompt 没有 DSH 消费方。
- **没有转录回放或交互式扩展**——会话删除、fork、`session/load`、mode、命令、计划、终端、客户端文件系统操作与 elicitation 仍不属于此自动化界面。
- **没有 transcript 回放或交互式扩展**——会话删除、fork、`session/load`、mode、命令、计划、终端、客户端文件系统操作与 elicitation 仍不属于此自动化界面。
<a id="dev-note"></a>
### 开发备注
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/api/README.md
README.md: 5bc878fa53b9ad0c3795c13e9442274b98820190
README.zh.md: 814113f405b3d5c40f74b529bff57d0aa2a9c77f
README.zh.md: 8aa364850e2378c7361807ae1065cfdd6eeaa9cc
+5 -5
View File
@@ -22,18 +22,18 @@ kind: "package-group"
<a id="packages"></a>
## 包
下面这些包共同提供 Remote 层;穷尽式约定以各包 README 为准。
下面这些包共同提供 Remote 层;完整约定以各包 README 为准。
| 包 | 职责 | ctx key |
|---|---|---|
| [`remotes/`](remotes/README.zh.md) | 决定 Client 可以消费哪些 Host 能力与事件。 | — |
| [`gateway/`](gateway/README.zh.md) | 承载带类型的单次调用、多路复用 stream 与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
| [`session-controller/`](session-controller/README.zh.md) | 拥有 Session 命令、历史 stream、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
| [`gateway/`](gateway/README.zh.md) | 承载类型化一元调用、多路复用流与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
| [`session-controller/`](session-controller/README.zh.md) | 拥有会话命令、历史记录流、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
| [`settings-controller/`](settings-controller/README.zh.md) | 拥有 settings 域各 seam 之上的配置界面读写。 | `ctx.settingsController`、`ctx.credentialsController` / `ctx.remote.settings`、`ctx.remote.credentials` |
| [`workspace-controller/`](workspace-controller/README.zh.md) | 拥有 Workspace 变更与完整 Client Workspace 投影。 | `ctx.workspaceController` / `ctx.remote.workspace` |
| [`workspace-files/`](workspace-files/README.zh.md) | 拥有有界的工作区文件访问——`stat`、分页 `read`、`list` 与已埋点操作的 `changes` 流——以及其上的 Client `file` 资源提供者。 | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` |
| [`workspace-files/`](workspace-files/README.zh.md) | 拥有有界的工作区文件访问——`stat`、分页 `read`、`list` 与已埋点操作的 `changes` 流——以及其上的 Client `file` 资源提供方。 | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` |
Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各 controller 包分别拥有 Session、配置界面与 Workspace 行为。流式下载等不适合 Remote 调用的响应由功能包注册精确的 Connection Fetch 路由。
Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各控制器包分别拥有 Session、配置界面与 Workspace 行为。流式下载等不适合 Remote 调用的响应由功能包注册精确的 Connection Fetch 路由。
-----
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/api/gateway/README.md
README.md: a234189cfff09bae894de14dada607a357b398fd
README.zh.md: 067adbdbe06db8fb4caec323db0361d13f6309a7
README.zh.md: 7a6e66724724efd83173835095a409c527faccef
+12 -12
View File
@@ -1,5 +1,5 @@
---
description: "带类型的 Client 到 Host 调用与 stream:分派、校验、取消、重连与转发的 Host 事件。"
description: "带类型的 Client 到 Host 调用与流:分派、校验、取消、重连与转发的 Host 事件。"
kind: "package-reference"
---
@@ -26,15 +26,15 @@ kind: "package-reference"
每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-typert-protocol`](../../typert/protocol/README.zh.md) 的 `TypertRemoteService`,并用 `@Remote` 或 `@RemoteScope` 标记方法;已有其他基类时仍可改用 `bindTypertRemote()`。
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的 resolver:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context adapter 解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的解析器:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context 适配器解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 是 `RemoteError` 的子类,其 `gateway/*` 码命名了分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。因策略而拒绝的 resolver——冷恢复失败或 ownership fence——抛出自己的 `RemoteError`,它选定的码原样到达调用方。
Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 是 `RemoteError` 的子类,其 `gateway/*` 码命名了分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。因策略而拒绝的解析器——冷恢复失败或 ownership fence——抛出自己的 `RemoteError`,它选定的码原样到达调用方。
支持取消的 Remote 方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。signal 是 descriptor 元数据,而不是 wire 参数:Connection 将它提供给 Gateway,Gateway 则在已解码的业务参数之后注入它。SRC 识别这个保留的末位参数名,严格生成还要求它具有全局 `AbortSignal` 类型。
流式 Remote 使用 `@Remote({ mode: 'stream' })` 并返回 `Iterable` 或 `AsyncIterable`。`ctx.typertGateway.stream()` 执行与一元调用相同的 endpoint、参数、lookup 和取消校验,再用生成的 result codec 校验每个产出项。Client 插件激活时打开 Gateway 自有的 `/api/remote.mux` WebSocket,并让它在空闲时保持连接。Connection 拥有重试调度;每次 retry 前,它要求 mux 取消候选或活动 socket,并且只做一次全新的物理连接尝试。Host 按配置的 `websocketHeartbeatIntervalMs` 间隔(默认 2 秒)发送 Ping 控制帧,浏览器在 WebSocket 协议层自动回复 Pong,使空闲网络中间层持续看到流量,而不新增 Remote stream frame。若 socket 尚未回复上一次 Ping,Host 会在下一间隔终止它。可独立取消的逻辑流共享这条连接;进程内 Connection 载体直接提供等价的流,不打开该 WebSocket。
流式 Remote 使用 `@Remote({ mode: 'stream' })` 并返回 `Iterable` 或 `AsyncIterable`。`ctx.typertGateway.stream()` 执行与一元调用相同的 endpoint、参数、lookup 和取消校验,再用生成的 result codec 校验每个产出项。Client 插件激活时打开 Gateway 自有的 `/api/remote.mux` WebSocket,并让它在空闲时保持连接。Connection 拥有重试调度;每次 retry 前,它要求 mux 取消候选或活动 socket,并且只做一次全新的物理连接尝试。Host 按配置的 `websocketHeartbeatIntervalMs` 间隔(默认 2 秒)发送 Ping 控制帧,浏览器在 WebSocket 协议层自动回复 Pong,使空闲网络中间层持续看到流量,而不新增 Remote 流帧。若 socket 尚未回复上一次 Ping,Host 会在下一间隔终止它。可独立取消的逻辑流共享这条连接;进程内 Connection 载体直接提供等价的流,不打开该 WebSocket。
Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的 stream。事件名单、参数校验、每 Client 队列及 opening `{ type: 'ready', clientId, host: { home } }` frame 中的 Host home 由 API Remotes 拥有。source factory 在返回 iterable 前同步挂好增量 listener,因此 Client 只在增量投递就绪后发布 generation 并开始 baseline 读取。
Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的流。事件名单、参数校验、每个 Client 的队列及 opening `{ type: 'ready', clientId, host: { home } }` frame 中的 Host home 由 API Remotes 拥有。source factory 在返回 iterable 前同步挂好增量 listener,因此 Client 只在增量投递就绪后发布 generation 并开始 baseline 读取。
<a id="client-service-clientremote-ctx-key-remote"></a>
## Client 服务:`ClientRemote`(ctx key:`remote`)
@@ -43,13 +43,13 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
每次一元调用都会校验位置参数,构造与描述符完全匹配的具名 `args`,再通过 `ctx.connection.rpc.call('/api', endpoint, ...)` 发送。生成的流方法返回 `AsyncIterable`,并在进程内 Connection 载体可用时通过它打开逻辑流,否则通过共享的 Gateway WebSocket 打开。生成的支持取消的方法接受最后一个可选 `AbortSignal`;Client 会在调用载体前将它与贡献项的挂载生命周期合并。一元结果和每个流项都经过校验后才会交给应用代码。撤回贡献项会同时移除其描述符和方法、中止正在进行的调用与流,并使外部仍持有的方法句柄在调用时返回拒绝。
每次一元调用都解析为 `RemoteResult<T>`——`{ ok: true, value }` 或 `{ ok: false, error }`——且绝不因载体问题 reject:本面把断线载体折入错误分支,调用方 signal 中止时答以 `gateway/cancelled`,因此没有消费方需要包一层来兜载体失败。只有装配故障仍会 reject:参数个数不符、方法未挂载、贡献已撤下、缺少 Context adapter。`error` 是活的 `RemoteError` 实例,所以 `throw result.error` 保持 throw 语义;而 `isRemoteFailure(value)` 是消费方唯一需要的谓词——它认下的捕获值带着 Host 码,它拒绝的一律是本地故障,调用方应当让其崩掉。
每次一元调用都解析为 `RemoteResult<T>`——`{ ok: true, value }` 或 `{ ok: false, error }`——且绝不因载体问题 reject:本面把断线载体折入错误分支,调用方 signal 中止时答以 `gateway/cancelled`,因此没有消费方需要包一层来兜载体失败。只有装配故障仍会 reject:参数个数不符、方法未挂载、贡献已撤下、缺少 Context 适配器。`error` 是活的 `RemoteError` 实例,所以 `throw result.error` 保持 throw 语义;而 `isRemoteFailure(value)` 是消费方唯一需要的谓词——它认下的捕获值带着 Host 码,它拒绝的一律是本地故障,调用方应当让其崩掉。
`ctx.remote.$host` 以普通值读取固定的 Host 事实:`home`(首个 ready 帧之前为 undefined)与 `isLoopback`。它不是 store——没有订阅、没有代次计数——所以需要响应重连的消费方去监听 `connection/reset`,而不是轮询它。
`ctx.remote.$host` 以普通值读取固定的 Host 事实:`home`(首个 ready 帧之前为 undefined)与 `isLoopback`。它不是存储——没有订阅、没有代次计数——所以需要响应重连的消费方去监听 `connection/reset`,而不是轮询它。
`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。一切终态失败离开本面时都是 `RemoteError`,包括重试耗尽和在 opening value 之前就结束的代次,因此流消费方与一元调用方用同一种方式判别。`RemoteStreamCarrierError` 命名的是可重试的物理丢失,它只作为 `carrierFailed` 回调参数到达领域,绝不作为终态结果。`RemoteSnapshotStream` 在此之上规定每代由一个 opening snapshot 和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。领域还可以携带无 cursor 的通知:通知绝不推进或修复持久 cursor,在缺口修复期间收到的通知只会在 replacement page 提交后发布。若更新代次取代该修复,旧代次 held notification 会与其 page 一同丢弃。dispose 任一种 stream 都会取消其请求,并在活动 iterator 完全停止后完成。
`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。一切终态失败离开本面时都是 `RemoteError`,包括重试耗尽和在 opening value 之前就结束的代次,因此流消费方与一元调用方用同一种方式判别。`RemoteStreamCarrierError` 命名的是可重试的物理丢失,它只作为 `carrierFailed` 回调参数到达领域,绝不作为终态结果。`RemoteSnapshotStream` 在此之上规定每代由一个初始快照和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。领域还可以携带无 cursor 的通知:通知绝不推进或修复持久 cursor,在缺口修复期间收到的通知只会在 replacement page 提交后发布。若更新代次取代该修复,旧代次 held notification 会与其 page 一同丢弃。对任一种流执行 dispose(资源释放)时,系统会取消该流的请求,并在活动 iterator 完全停止后完成资源释放。
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属调用方 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,因此即使当前无 `$on` 订阅,它也会在 Connection 循环启动时打开。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;opening `ready` 项建立 Connection generation 并提供 Host 信息。物理 carrier 失败、Remote stream error、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 按持续且间隔封顶的带抖动指数退避重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall 允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属调用方 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,无论当前是否存在 `$on` listener。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;opening `ready` 项建立 Connection generation 并提供 Host 信息。物理 carrier 失败、Remote 流故障、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 按持续且间隔封顶的带抖动指数退避重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall(瀑布式事件)允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
`ctx.remote` 不暴露 Connection 生命周期控制。只有职责包含恢复的消费方才直接读取 `ctx.connection.state` 并调用 `ctx.connection.reconnect()`;普通 Remote 消费方仍只使用生成的 namespace 与 `$stream()`。
@@ -71,8 +71,8 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
- Connection 适配器对分发故障与未归类异常答以 `gateway/internal`,且不附带详细信息;拥有方或 Gateway 自己抛出的 `RemoteError` 带着自有码、message 与 details 过线。其 `cause` 链与 `TypertGatewayError` 子类身份只对同进程调用方留存。
- SRC 模式仅支持名称唯一的标识符参数,不支持解构、默认值或剩余参数。它只校验值能否安全表示为 JSON,不校验生成的业务类型,也绝不会推断可选字段。
- Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events`;单向通知不会重放,仍处于 pending 的 scoped waterfall 则沿用同一个 event id 重放。
- lookup resolver 按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events` 流;单向通知不会重放,仍处于 pending 的 scoped waterfall 则沿用同一个 event id 重放。
- lookup 解析器按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
- 被转发的事件到达 `$on` 时不做业务载荷投影或脱敏。普通通知在重连后不重放;Agent-scoped waterfall 只投影选择 Client Context 所需的顶层 Agent 身份,并自行携带 pending 生命周期。
- `websocketHeartbeatIntervalMs` 同时是 Ping 周期和 Pong 截止时间。对端未在下一周期前回复时,Host 会终止连接;如果部署的事件循环或网络可能停顿超过该间隔,必须调大此配置。
@@ -87,4 +87,4 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
</details>
**运行时不变式:** 不发布伴生入口。Host 调用会重新读取权威的 Cordis 与 Typert 状态,Client 方法、描述与 `$on` 订阅则在同一个 effect 中完成变更。
**运行时不变式:** 不发布伴生入口。Host 调用会重新读取权威的 Cordis 与 Typert 状态,Client 方法、描述符与 `$on` 订阅的变更则统一归属同一个 effect。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
+2 -2
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/api/remotes/README.md
README.md: 52ef6223f4d7770224df1b1c5d783962c55f73f6
README.zh.md: eab5d2c6642a7899fac60d8c666ee55057eaea05
README.md: 88b7b0fd023049be1169122f0f613aa740ccbc37
README.zh.md: 9bfdc0a05920363ac523657e71ca02f728d7ee03
+2 -2
View File
@@ -29,7 +29,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
The Client assembly mounts Commands, credentials, settings, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the api-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md).
This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the API-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md).
This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
@@ -70,7 +70,7 @@ No direct effect; mounted Host capabilities own any model-visible behavior they
- The capability set is fixed by explicit build-time value imports; the Client does not discover the Host's active Services or Remote definitions at runtime.
- Additional capabilities require an explicit `/remote` value import and mount in this assembly.
- Ordinary forwarded events are not replayed; state that requires reliable recovery needs an owner-provided query, cursor, or opening baseline.
- Only scoped waterfalls that are still pending are replayed after reconnection; ordinary one-way notifications remain isolated best-effort deliveries and are not replayed. State that requires reliable recovery needs an owner-provided query, cursor, or opening baseline.
<a id="dev-note"></a>
+9 -9
View File
@@ -25,24 +25,24 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 agent(智能体)与会话身份策略,包括供其他 namespace 使用的 Typert lookup 解析器。本包只选择并挂载生成的会话 contribution,不复制激活策略。
Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与会话引用、只读 Host 插件清单、消息反馈、会话控制器和 Workspace 控制器 contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪的 namespace 服务、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域的浏览器安全载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 api 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。
本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域对 Client 安全的载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 API 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。
本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和唯一的 Host Cordis event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution,以及每个 Client 各自独立的 Host 事件源;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
-----
<a id="forwarded-host-events"></a>
## 转发的 Host 事件
`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`,即本应用不改名转发给消费端的 Host Cordis 事件名单;每个条目还会选择普通发送或 Agent-scoped waterfall 投递。该名单同时就是 `ctx.remote.$on` 的合法键集,只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一项:类型投影、消费端键面与 Host 转发循环全部由它派生。
`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`,即本应用不改名转发给消费端的 Host Cordis 事件名单;每个条目还会选择普通发送或 agent-scoped waterfall(瀑布式事件)投递。该名单同时就是 `ctx.remote.$on` 的合法键集,只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一项:类型投影、消费端键面与 Host 转发循环全部由它派生。
监听器签名不在此处重写。名单内每条事件的 Cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口,本包两个 face 都把那些声明纳入编译面。Host face 还会把每个条目断言给 `TypertForwardableEventEntry`:`emit` 条目必须是已声明的单向事件,`waterfall` 条目则必须是已声明的 Agent-scoped waterfall,且其最后一个参数是返回相同结果类型的 `next()` 回调。
监听器签名不在此处重写。名单内每条事件的 Cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 导出,本包两个 face 都把那些声明纳入编译面。Host face 还会把每个条目断言给 `TypertForwardableEventEntry`:`emit` 条目必须是已声明的单向事件,`waterfall` 条目则必须是已声明的 agent-scoped waterfall,且其最后一个参数是返回相同结果类型的 `next()` 回调。
Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,并在普通事件入队前拒绝非 JSON 参数。对于 waterfall,它只投影顶层 Agent 身份与 JSON 请求字段;Client 结果也必须能无损表示为 JSON,而 `next()` 会委托给后续 Host listener。每个作用域 waterfall 请求都必须以 `request.agent` 直接携带路由所用的 Agent;Host 会在转发前拒绝缺失或不匹配的身份。该 source 在 `ctx.typertGateway.registerRemoteEvents()` 暴露 Gateway 内部的 `$events` logical stream 前同步挂好所有 listener,因此首个 `ready` 项既能证明增量投递已就绪,也会携带供 Client 显示路径的 Host home。撤回注册会中止活动 stream。
Host entry 为每条 Client 流独立注册一组 allowlist listener 和一个队列,并在普通事件入队前拒绝非 JSON 参数。对于 waterfall,它只投影顶层 agent 身份与 JSON 请求字段;Client 结果也必须能无损表示为 JSON,而 `next()` 会委托给后续 Host listener。每个作用域 waterfall 请求都必须以 `request.agent` 直接携带路由所用的 agent;Host 会在转发前拒绝缺失或不匹配的身份。该 source 在 `ctx.typertGateway.registerRemoteEvents()` 暴露 Gateway 内部的 `$events` 逻辑流前同步挂好所有 listener,因此首个 `ready` 项既能证明增量投递已就绪,也会携带供 Client 显示路径的 Host home。撤回注册会中止活动流。
<a id="build-boundary"></a>
## 构建边界
@@ -70,7 +70,7 @@ Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,
- 能力集合由构建时显式导入的值固定确定;Client 不会在运行时发现 Host 中已启用的服务或 Remote 定义。
- 若要增加能力,必须显式导入相应的 `/remote` 值并在此组合中挂载。
- 只有仍在等待的作用域 waterfall 会在重连后重放;单向通知仍是相互隔离的 best-effort 投递。
- 只有仍在等待的作用域 waterfall 会在重连后回放;单向通知仍是相互隔离的 best-effort 投递,不会回放。需要可靠恢复的状态必须由拥有方提供查询、游标或初始基线。
<a id="dev-note"></a>
@@ -83,4 +83,4 @@ Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,
</details>
**运行时不变式:** 不发布伴生入口。被观察的关系由 Typert 以及 Agent、Session 注册表负责。
**运行时不变式:** 不发布伴生入口。被观察的关系由 Typert、agent 注册表和会话注册表负责。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly for application-selected Host capabilities",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
README.md: 24cfbba626ddec510452e3b6c8bc4333c03876bd
README.zh.md: c90357c60eab9427f5656b48c4ea99252d87e2bf
README.zh.md: 883fa2343693e6512638fa437a3f8bd0db09310c
+10 -10
View File
@@ -8,7 +8,7 @@ kind: "package-reference"
## 概述
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills` 和 `fileReferences` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、工作区路径打开、用户可调用 skill 发现和面向 Agent 的文件引用。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills` 和 `fileReferences` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、工作区路径打开、用户可调用 skill(技能)发现和 Agent(智能体)范围的文件引用。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。
## 目录
@@ -24,21 +24,21 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
历史页与 follow opening snapshot 为每个持久 Session event 携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑 stream 内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;controller 不解析 Tool definition、不运行 presenter,也不附加 UI 数据。
历史页与 follow opening 快照为每个持久 Session 事件携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑流内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;控制器不解析工具定义、不运行展示转换器,也不附加 UI 数据。
Client journal 在发布 follow snapshot、live entry 或历史页之前验证精确的 V3 event envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 来源、request header 可选字段的省略规则以及 tool error 一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
Client journal 在发布 follow 快照、live entry 或历史页之前验证精确的 V3 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 来源、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。queue 变更与取消要求 live 状态;模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。prompt 会在解析 Agent 或追加 Session event 前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。 经过认证的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时打开默认应用。
每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。queue 变更与取消要求 live 状态;模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时打开默认应用。
Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条 message,而 `loadThrough(seq)`——轮次跳转加载器——按 200 条 message 一页循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web adapter 显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且 Turn 与 Step 匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的 event seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。每次 inbox 变更时,Host 会先发布 projection frame,再从同一份已校验的折叠后值派生 queue replacement,因此监听器注册顺序不会产生陈旧的 queue frame。Client Agent context 提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条消息,而 `loadThrough(seq)`——轮次跳转加载器——按每页 200 条消息循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作持久事件。每次 inbox 变更时,Host 会先发布 projection frame,再从同一份已校验的折叠后值派生 queue replacement,因此监听器注册顺序不会产生陈旧的 queue frame。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与 prompt 之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并在序列化期间保留该位置。prompt 的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,queue occurrence 也把它投影为 `SessionQueuedItem.rpcId`。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休,带标识的 prompt 失败或被放弃时立即退休,销毁时按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从 durable event 重建会话。
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并在序列化期间保留该位置。提示词的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,queue occurrence 也把它投影为 `SessionQueuedItem.rpcId`。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休,带标识的提示词失败或被放弃时立即退休,销毁时按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从持久事件重建会话。
<a id="session-media-references"></a>
## 会话媒体引用
当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 20 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与 sandbox CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。
当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 20 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。
-----
@@ -56,11 +56,11 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用
<a id="model-experience"></a>
## 模型体验
无,因为被调用的 Agent 命令拥有任何模型可见效果。
无;任何模型可见效果都由被调用的 Agent 命令负责。
#### KV Cache 影响
无直接影响;模型请求仍由 Agent 和 LLM 包拥有。
无直接影响;模型请求仍由 Agent 和 LLM(大语言模型)包拥有。
## 已知限制与延期工作
@@ -69,7 +69,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用
- 图片字节上限不校验解码后的尺寸或像素数。
- Control baseline 表示进程本地状态,因此 Host 重启后无法重建 jobs。
- follow 恢复失败会对调用方可见,而不会无限重试。
- 浏览器原始字节上传使用一次不带断点续传偏移的流式 HTTP 请求;重试会从第一个字节重新传输整个文件。
- 浏览器原始字节上传使用一次不带断点续传偏移的流式 HTTP 请求;重试会从第零字节重新传输整个文件。
- 文件引用补全使用共享 Agent lookup,因此可能恢复冷 Session;`skills/list` 目录是不激活 Agent 的 skill 元数据读取路径。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-session-controller",
"description": "Session Remote commands, cold reads, and live control transport",
"version": "0.1.5-alpha.1",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/api/settings-controller/README.md
README.md: 761e4c43751675eec141a2efcf85caf52cc91590
README.zh.md: 541c9deb3dbc35914e4e95abdda2af5cdce2bbd0
README.zh.md: c5a59f3eebfd1e6702c2abcd2407bc2ef4e725bd
Loaded 100 of 1140 files, more files were not shown because too many files have changed in this diff. Show more