Merge branch 'master' into feat/produced-files-folder

This commit is contained in:
Ziya
2026-08-11 13:24:43 +08:00
committed by GitHub
68 changed files with 348 additions and 212 deletions
@@ -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-06-21-mandatory-app-attribution-headers.md
2026-06-21-mandatory-app-attribution-headers.md: 12482ca80d19e5cd1e62b8860865db6cccea61ab
2026-06-21-mandatory-app-attribution-headers.zh.md: 43f356887948b88d34ba41d46ed1cd2f1c89a1e2
2026-06-21-mandatory-app-attribution-headers.md: 39050a53ec76e8c5a6cac4d8e31fa15b992c406e
2026-06-21-mandatory-app-attribution-headers.zh.md: bfd6aa2540022f68cf9f69ccc2c12b3bc0978960
@@ -32,7 +32,7 @@ The provider-neutral identity is owned by `dsh-llm` (`packages/llm/llm/src/attri
- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-Agent Note wire value and the repo/org identity)
- version: read from the owning package's manifest via `createRequire`, never a hand-copied constant
- app URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` - the planned public home, which must exist before release
- app URL: `https://github.com/deepseek-ai/deepseek-harness` - the repository home
The default is mandatory and non-empty. White-label deployments pass their own `AppIdentity` to `attributionHeaders(identity)` - the override hook is the function parameter, with no deployment config plumbing until a consumer needs it - and omission falls back to the harness default rather than suppressing attribution. There is no per-request API for the model, user prompt, session id, cwd, user email, API key owner, or local machine identity to influence these fields.
@@ -77,8 +77,6 @@ The landed contract:
**Providers see that traffic comes from the harness.** That is the point, but it means deployments that previously blended into generic SDK traffic become identifiable. Mitigation: send only static public product data and let forks/white-label deployments pass their own `AppIdentity`.
**The app URL points at a repository that does not exist yet.** `deepseek-ai/deepseek-harness-sdk` is the planned public home; until it is created the URL is a dangling promise that blocks release.
**Header support differs by client library.** The hand-rolled adapter sets headers directly; the pi-ai-backed adapter depends on pi-ai continuing to honor `StreamOptions.headers` (merged last over provider defaults). The wire-level mock-server tests are the guard: if a pi-ai upgrade stops delivering the header, the suite goes red. This is useful pressure on the abstraction: a provider adapter that cannot set mandatory headers cannot fully implement the harness LLM contract.
**OpenRouter rankings do not benefit yet.** `User-Agent` is the correct baseline for provider-neutral HTTP identity, but it will not create OpenRouter app pages or rankings because OpenRouter requires `HTTP-Referer` for that product feature. That is deliberate: public app marketplace participation is a separate product decision, not a prerequisite for mandatory request attribution.
@@ -32,7 +32,7 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
- `User-Agent` 的产品 token`deepseek-harness`(与 Agent Note 之前的线路值及仓库/组织身份保持连续性)
- 版本:通过 `createRequire` 从所属包的 manifest(元数据清单)读取,绝不手动复制常量
- 应用 URL`https://github.com/deepseek-ai/deepseek-harness-sdk`——计划中的公开主页,且必须在发布前实际存在
- 应用 URL`https://github.com/deepseek-ai/deepseek-harness`——仓库主页
默认值是强制的且非空。白标部署通过向 `attributionHeaders(identity)` 传入自己的 `AppIdentity` 来覆盖——覆盖钩子就是函数参数,在有消费方需要之前不做部署配置管道——省略时回退到 harness 默认值而非抑制归属。没有逐请求 API 允许模型、用户提示词、会话 id、cwd、用户邮箱、API key 所有者或本地机器身份影响这些字段。
@@ -77,8 +77,6 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
**提供方看到流量来自 harness。** 这正是目的,但意味着此前混在通用 SDK 流量中的部署变得可识别。缓解措施:仅发送静态公开产品数据,并允许 fork/白标部署传入自己的 `AppIdentity`
**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在它创建之前,该 URL 是一个阻塞发布的悬空承诺。
**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;基于 pi-ai 的适配器依赖 pi-ai 继续尊重 `StreamOptions.headers`(最后合并覆盖提供方默认值)。线路级 mock 服务器测试是守卫:如果 pi-ai 升级后不再投递该头部,套件会变红。这对抽象施加了有益的压力:一个无法设置强制头部的提供方适配器不能完整实现 harness 的 LLM 约定。
**OpenRouter 排名尚未受益。** `User-Agent` 是提供方无关的 HTTP 身份的正确基线,但它不会创建 OpenRouter 应用页面或排名,因为 OpenRouter 要求 `HTTP-Referer` 来实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。
@@ -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-07-25-web-client-session-scope-and-provide-channel.md
2026-07-25-web-client-session-scope-and-provide-channel.md: ff411e1b387dcd83b04f8c02d5dde3c70a815932
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 593e194654bb3552ad3cac4534847fba11cf3694
2026-07-25-web-client-session-scope-and-provide-channel.md: f371b93ccf6cb3ba10cbdb73baa670cbf889f393
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 78440131c4e09c9a458009fbe5a2a34a707a481c
@@ -93,7 +93,7 @@ Slot scope is the closed set `root | session-maybe | session`:
- `session-maybe` follows the current session with ADOPTION identity (the only behavior — there is no hold-identity-forever mode): an incarnation born session-less keeps its React instance across the arrival of the FIRST session (the blank shell adopts it — no remount, the DOM survives), and from then on behaves exactly like a strict session entry — switching to a different session remounts, and dropping back to no-session remounts into a fresh blank incarnation that will adopt again. Component-local per-session state therefore clears by construction; state that must survive a switch belongs in session-bound sources (machine, store, hooks). With no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates by subscribing to the runtime's atomic `currentProvide` projection — selection moves and provider-roster changes publish through the same source, so a roster change under a stable current id republishes the mounted bundle instead of stranding entries on an obsolete hook/prop schema — while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session; the per-entry adoption bookkeeping (incarnation-counter key) lives in the renderer's `SessionMaybeEntry`.
- `session` guarantees that `sessionId`, every hook source, and every prop exist; each strict entry's error boundary is keyed by `sessionId`, so switching sessions recreates that entry and its session store.
`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session it renders inert (machine faces absent, `disabled` owner prop), and the same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
- The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
- Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).
@@ -132,5 +132,5 @@ Slot scope is the closed set `root | session-maybe | session`:
- Plugins gain session context isomorphic to the host's: per-session state hangs on the actx and mounts/tears down in one piece with the scope fiber, making leaks structurally impossible; two-session isolation is structurally guaranteed by the scope filter.
- The client object layer converges to a wire mirror: session identity, lifecycle, and capability adjudication all defer to the host entity — the input system (the next layer) always faces a session with a real Agent, and providers like slash/skill uniformly address by sessionId directly.
- Blank-session governance takes zero dedicated mechanisms: state rides one derived bit, visibility rides the unified list projection (only the current blank shows, as `New Session`), reclamation rides lazy persistence's existing contract (evaporation on restart), and the ordinary ceiling rides same-Workspace reuse.
- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests; fully disabled input while no workspace is picked is an experience cost the product surface accepts (the price of the single state axis).
- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests. The single state axis still withholds machine faces until a Session exists; the resident card routes activation to the Workspace picker during that interval ([decision](../feature/2026-08-07-workspace-picker-composer-entry.md)).
- Known gaps: approval/question recovery across prune (TODO); model selection returns in live-mutation shape (the host `selectModel` trio is ready-made, its client consumer not yet built).
@@ -93,7 +93,7 @@ slot scope 是闭集 `root | session-maybe | session`
- `session-maybe` 以**收养(adoption)身份语义**跟随 current session(唯一行为——不存在「永久保持实例」模式):空态出生的化身在**第一个** session 到来时保持 React 实例(空壳收养它——不重挂,DOM 存活);此后行为与严格 session entry 完全一致——切到不同 session 重挂,跌回无 session 也重挂为崭新的空态化身(之后再次收养)。因此组件本地的 per-session 状态**由构造保证**随切换清零;需要活过切换的状态必须住 session 绑定的源(machine、store、hooks)。无 session 时 `sessionId``useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅 runtime 的原子 `currentProvide` 投影驱动这条更新——选择移动和提供方名册变化经同一 source 发布,current id 不变时的名册变化也会重发已挂载 bundle,而不是把 entry 困在过期的钩子/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无 session 时仍保留完整钩子/prop 形状;逐 entry 的收养记账(化身计数 key)住在 renderer 的 `SessionMaybeEntry`
- `session` 保证 `sessionId`、所有钩子 source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key,切换 session 会重建该 entry 及其 session store。
`conversation``session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumbtabaction`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时以惰性态渲染(machine face 缺席、`disabled` owner prop),session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
`conversation``session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumbtabaction`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine face 和消息操作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
- 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
- Concurrent 纪律:渲染平面只从 hooks 格读(uSES 一致性保证);props 格回调只在事件 handler 空间用;描述符解析 render-safe(幂等缓存、废弃渲染残留由 prune 收尸)。
@@ -132,5 +132,5 @@ slot scope 是闭集 `root | session-maybe | session`
- 插件获得与 host 同构的会话上下文:逐会话状态挂 actx、随 scope fiber 一次拆装,泄漏结构性不可能;双会话隔离由 scope filter 结构性保证。
- client 对象层收敛为 wire 镜像:会话身份、生命周期、能力判别全部以 host 实体为准——输入体系(下一层)面对的永远是「有真 Agent 的会话」,slash/skill 等提供方一律以 sessionId 直接寻址。
- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约定(重启蒸发),常规上限靠同 Workspace 复用。
- 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住;「未选 workspace」期间输入全禁是产品面接受的体验代价(单一状态轴换来的)。
- 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住。单一状态轴仍会在 Session 存在前隐藏 machine face;这段时间内,常驻卡片会把激活操作转到 Workspace picker[决策](../feature/2026-08-07-workspace-picker-composer-entry.md))。
- 已知欠账:approval/question 跨 prune 恢复(TODO);模型选择以 live-mutation 形状回归(host `selectModel` 三件套现成,其 client 消费方尚未构建)。
@@ -0,0 +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/feature/2026-08-07-workspace-picker-composer-entry.md
2026-08-07-workspace-picker-composer-entry.md: dc9c26c291de6e7614ace3c787030c0032a9740d
2026-08-07-workspace-picker-composer-entry.zh.md: 585c59b12392823a7388ab7a635a864bd4108929
@@ -0,0 +1,29 @@
# Agent Note: The no-Workspace composer opens the existing picker
Status: implemented
English | [中文](2026-08-07-workspace-picker-composer-entry.zh.md)
## Problem
The [session-scope decision](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.md) keeps one resident composer before a Workspace exists, but its textarea was disabled and only the smaller Workspace chip could open the picker. The largest and most familiar starting affordance therefore rejected the user's first click even though a recovery action was available on the same surface.
## Decision
While no Workspace owns the new Session, the whole composer card activates the existing `conversation.hero.workspace` picker by pointer click — the card owns the click handler and its disabled controls let pointer events fall through, so the full capsule is one target — and the read-only resident textarea does the same by Enter or Space. `aria-haspopup="menu"` and `aria-expanded` describe the shared picker menu while it is mounted. On a fresh installation with no Workspace rows, the picker immediately hands off to the directory dialog and clears its expanded state; that dialog exposes its own accessibility semantics. A dashed l4 stroke (an SVG dash ring, since native `dashed` has a fixed pattern) with a business-blue hover marks the card as the pick affordance. The card contains `pointerdown`, so the open picker's outside-close cannot race the click's reopen — that close-then-open flickered the chip's expansion echo. Message submission, command, permission, model, and other Session-scoped controls remain locked until Workspace selection creates or reconnects a real Session.
Workspace selection retains the existing owner and flow. `ConversationRoot` opens the picker, `WorkspacePicker` lists or creates the Workspace, and the same textarea DOM node becomes the editable composer after the Session arrives.
## Alternatives considered
**Keep the textarea disabled and emphasize the Workspace chip.** This preserves the old control boundary but leaves the dominant composer surface inert during the first action.
**Place a transparent button over the textarea.** A button has direct trigger semantics, but it creates a second focusable element over the resident textarea and complicates the DOM-identity transition that preserves focus, IME, and draft behavior.
**Accept a draft before Workspace selection.** This would require a client-owned draft Session or another pre-Session state axis. The feature only needs a discoverable path into the existing picker.
## Consequences
The first composer click now continues the required setup flow, and keyboard users can activate the same path. The textarea accurately reports read-only state until a Session exists, while adjacent controls remain disabled. The UI introduces no new Workspace state, transport, or directory-selection flow.
Component coverage pins pointer and keyboard activation, the card-wide click target, the contained `pointerdown`, locked adjacent controls, picker expansion, and the same-node transition to an editable textarea. The assembled Web helper begins fresh Workspace setup through the textarea, so replayed browser scenarios exercise the shipped path.
@@ -0,0 +1,29 @@
# Agent Note: 未选择 Workspace 时从编辑器打开现有选择器
Status: implemented
[English](2026-08-07-workspace-picker-composer-entry.md) | 中文
## 问题
[Session scope 决策](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.md)会在 Workspace 存在前保留同一个常驻编辑器,但 textarea 处于禁用状态,只有较小的 Workspace chip 能打开选择器。用户首次点击最显眼、也最熟悉的输入区域时,界面不会响应,尽管同一界面已有继续操作的入口。
## 决策
新会话尚未归属任何 Workspace 时,整张输入卡片都可通过鼠标点击激活现有的 `conversation.hero.workspace` 选择器——点击处理器归卡片所有,其禁用控件放行指针事件,因此整个胶囊是同一个目标;只读的常驻 textarea 也可经 Enter 或 Space 激活。`aria-haspopup="menu"``aria-expanded` 在共享选择器菜单挂载时描述其展开状态。全新安装没有 Workspace 行时,选择器会立即转交目录对话框并清除自身的展开状态;该对话框使用自己的可访问性语义。虚线 l4 描边(SVG dash ring,因为原生 `dashed` 的间距不可调)配合 hover 时的 business 蓝,把卡片标记为选择入口。卡片会拦下 `pointerdown`,使已打开选择器的外点关闭无法与点击的重新打开竞态——先关后开会让 chip 的展开回显闪动。消息提交、命令、权限、模型及其他 Session 作用域控件会保持锁定,直到用户选择 Workspace 并创建或重新连接真实 Session。
Workspace 选择继续使用现有 owner 和流程。`ConversationRoot` 打开选择器,`WorkspacePicker` 列出或创建 WorkspaceSession 到达后,同一个 textarea DOM 节点变为可编辑状态。
## 考虑过的替代方案
**保持 textarea 禁用并突出 Workspace chip。** 这样能保留原有控件边界,但首次操作时最主要的编辑器区域仍然没有响应。
**在 textarea 上方放置透明按钮。** 按钮具备直接的触发器语义,但它会在常驻 textarea 上方增加第二个可聚焦元素,并使保留焦点、输入法和草稿行为的 DOM identity 过渡更复杂。
**在选择 Workspace 前接收草稿。** 这需要由 client 拥有的草稿 Session 或另一条 Session 前状态轴。此功能只需要提供一个更容易发现的现有选择器入口。
## 后果
用户首次点击编辑器即可继续必要的设置流程,键盘用户也能激活同一路径。textarea 会如实报告只读状态,直到 Session 存在;相邻控件仍处于禁用状态。界面没有引入新的 Workspace 状态、传输或目录选择流程。
组件测试会固定鼠标和键盘激活、覆盖整卡的点击目标、被拦下的 `pointerdown`、相邻控件锁定、选择器展开,以及同一节点变为可编辑 textarea 的过渡。组装后的 Web helper 会通过 textarea 开始全新 Workspace 设置,因此重放浏览器场景会覆盖实际交付路径。
@@ -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/process/2026-07-13-documentation-site-projection.md
2026-07-13-documentation-site-projection.md: d9af915754fa6a1df51a27d18d412597472aaa73
2026-07-13-documentation-site-projection.zh.md: 7d7b4752b8f27d55aae8426a7dc001ce4340e661
2026-07-13-documentation-site-projection.md: 309dbd96c5f1ca87d137cdc3839acfc5c5aa22f2
2026-07-13-documentation-site-projection.zh.md: b01965fb1b46ab618b5b8bcb748bb5a8940d6e69
@@ -18,9 +18,9 @@ Canonical Markdown remains in the repository tier that owns it. Product-facing g
Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching.
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a source link under the public `deepseek-ai/deepseek-harness-sdk` home; a repository image is copied into the generated tree and referenced from there ([why](2026-08-06-doc-site-carries-its-images.md)). Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a source link under the `deepseek-ai/deepseek-harness` repository home; a repository image is copied into the generated tree and referenced from there ([why](2026-08-06-doc-site-carries-its-images.md)). Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
`verify-public-repository-links` rejects internal repository remotes from tracked files. Public source links use the public home, while work tracking stays in repository metadata and source carries a TODO only when the local boundary matters to maintainers.
`verify-public-repository-links` rejects references to the unavailable legacy repository from tracked files. Source and edit links use the current repository home.
`website/AGENTS.md` is the only maintained Markdown file in the website subtree. The projector test enumerates tracked and unignored files and rejects any other website Markdown, so site-specific locale, route, API, or generated source copies cannot bypass the publication manifest.
@@ -18,9 +18,9 @@ Status: implemented
各 locale 的首页投影只保留权威 YAML frontmatter。面向仓库的正文可以保留其 H1 和双语源文件链接,而 VitePress 首页主题负责渲染 hero 与功能区,网站导航负责切换 locale。
投影器解析 Markdown 链接,但不会重新序列化文档。指向另一个已发布源文件的链接会变成站内相对路由;指向未发布仓库文件的链接会变成公开 `deepseek-ai/deepseek-harness-sdk` 主页下的源文件链接;仓库图片会被拷贝进生成树并从那里引用([原因](2026-08-06-doc-site-carries-its-images.md))。相对目标不存在时,投影会失败。单元测试会锁定这些转换行为,`docs:check` 则运行投影器测试和 VitePress 生产构建,并将二者纳入 `doc-sync` 和并行文档门禁。
投影器解析 Markdown 链接,但不会重新序列化文档。指向另一个已发布源文件的链接会变成站内相对路由;指向未发布仓库文件的链接会变成 `deepseek-ai/deepseek-harness` 仓库主页下的源文件链接;仓库图片会被拷贝进生成树并从那里引用([原因](2026-08-06-doc-site-carries-its-images.md))。相对目标不存在时,投影会失败。单元测试会锁定这些转换行为,`docs:check` 则运行投影器测试和 VitePress 生产构建,并将二者纳入 `doc-sync` 和并行文档门禁。
`verify-public-repository-links` 会拒绝已跟踪文件中的内部仓库远程链接。公开源文件链接使用公开主页,而工作跟踪留在仓库元数据中;只有本地边界对维护者有意义时,源文件才保留 TODO
`verify-public-repository-links` 会拒绝已跟踪文件中指向不可用旧仓库的引用。源文件链接和编辑链接使用当前仓库主页
`website/AGENTS.md` 是网站子树中唯一维护的 Markdown 文件。投影器测试会枚举所有已跟踪文件和未被忽略的未跟踪文件,并拒绝网站中的任何其他 Markdown,因此网站专用的 locale、路由、API 或生成源文件副本无法绕过发布 manifest。
+4 -4
View File
@@ -85,14 +85,14 @@ describe('dsh badge assembled snapshot', () => {
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
\`\`\`
If attribution should not be linked, use:
@@ -124,14 +124,14 @@ describe('dsh badge assembled snapshot', () => {
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
\`\`\`
If attribution should not be linked, use:
+4 -4
View File
@@ -57,9 +57,9 @@ export function probeFreePort(): Promise<number> {
/**
* Drive the hero's workspace picker through the composed directory dialog
* until the live composer unlocks. A fresh world has no Workspace, so the boot
* lands in the locked view state (startup auto-selection has nothing to
* lands in the Workspace-trigger view state (startup auto-selection has nothing to
* select); every scenario that types into the composer must connect one
* first. With nothing to list, the chip gesture raises the dialog directly —
* first. With nothing to list, activating the textarea raises the dialog directly —
* adding a workspace is the picker's only entry. The directory is staged here
* and adopted through the path editor, which is idempotent across the repeated
* connects a scenario may make; creating a folder from inside the dialog (the
@@ -73,7 +73,7 @@ export function probeFreePort(): Promise<number> {
*/
export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise<void> {
mkdirSync(join(root, name), { recursive: true })
await page.getByRole('button', { name: 'Choose workspace' }).click()
await page.getByRole('textbox', { name: 'Choose workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Edit path' }).click()
@@ -97,7 +97,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
*/
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise<void> {
mkdirSync(join(root, name), { recursive: true })
await page.getByRole('button', { name: '选择工作区' }).click()
await page.getByRole('textbox', { name: '选择工作区' }).click()
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '编辑路径' }).click()
@@ -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/cordis-tutorial/01-first-plugin.md
01-first-plugin.md: 448034b54107d5620673052ad388feecc22fe1e1
01-first-plugin.zh.md: a1838be61f8099831d59afa266fe40ce6bd165bf
01-first-plugin.md: 79df2f42df1f34a7ef32cc81607e1b926a1854ae
01-first-plugin.zh.md: 87cd7a96843d69d68fb95662e59bbfb9ebfc2a37
+1 -1
View File
@@ -92,4 +92,4 @@ One caveat worth knowing early: a config entry whose module cannot be **resolved
Next: [Lifecycle and effects](02-lifecycle-and-effects.md) — what happens when a plugin unloads.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+1 -1
View File
@@ -92,4 +92,4 @@ export function apply(ctx: Context) {
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -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/cordis-tutorial/02-lifecycle-and-effects.md
02-lifecycle-and-effects.md: 3e88c6f1e1fb1bc825fb74434520993c855010c3
02-lifecycle-and-effects.zh.md: 3cceecfb8334f2ed2ec9942fa876a0e51e1b315f
02-lifecycle-and-effects.md: 8e75708eb0cba1aceb7fa3dae5ae334995eb113f
02-lifecycle-and-effects.zh.md: bc4489e702ab58b5d1efd8a72fc693ee48af2624
@@ -95,4 +95,4 @@ One ordering caveat: disposers start in reverse registration order, but multiple
Next: [Services](03-services.md) — how plugins share capabilities.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -95,4 +95,4 @@ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
下一章:[服务](03-services.md):插件如何共享功能。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+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/cordis-tutorial/03-services.md
03-services.md: 3f2273ba4061912640e409d7b4deb4cd1b45684f
03-services.zh.md: 657271aba6c0b2e824c79fa822e29c8a6dcf4275
03-services.md: 32007284be99ef46b4621089c9b3a80317e77189
03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd
+1 -1
View File
@@ -95,4 +95,4 @@ Service names live in one flat namespace per application. Prefix or namespace yo
Next: [Events](04-events.md) — communication without a shared service.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+1 -1
View File
@@ -95,4 +95,4 @@ export function apply(ctx: Context) {
下一章:[事件](04-events.md):无需共享服务即可通信。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+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/cordis-tutorial/04-events.md
04-events.md: 0302adf7c81b802b406f5f6737539ccf3eb970f9
04-events.zh.md: ae41e757c06a46ef70c250c94a124546769e0112
04-events.md: db911b9a06d7304d73030a2020de3d465fde2f9c
04-events.zh.md: e0357b2c04785d52a54f839d1b8ff3ecccb027ed
+1 -1
View File
@@ -141,4 +141,4 @@ The harness uses waterfalls for decisions that cooperating plugins may wrap or a
Next: [Configuration](05-config.md) — plugin options from `cordis.yml`.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+1 -1
View File
@@ -141,4 +141,4 @@ harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`ag
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+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/cordis-tutorial/05-config.md
05-config.md: ad73a732242e4015b2976e6fb193ff464e148dfa
05-config.zh.md: edb3c4113288dfa80e899fb2e5eb21a67d929ca6
05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4
05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33
+1 -1
View File
@@ -81,4 +81,4 @@ The loader used in this repo supports a `!!js` tag for config values that must b
Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+1 -1
View File
@@ -81,4 +81,4 @@ ValidationError: invalid config:
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -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/cordis-tutorial/06-composition-and-hmr.md
06-composition-and-hmr.md: 490e3de3a98dd823190deefd47e1b6f2f8ba71b5
06-composition-and-hmr.zh.md: 4aefb5ecd036929590ab373a2883d90779997b4b
06-composition-and-hmr.md: 87ea26014657ae8c8199e1ebb486556c827d96ca
06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7
@@ -110,4 +110,4 @@ needs-timer is PENDING — a required service is missing
Next: [Into the harness](07-into-the-harness.md) — the same patterns against real harness services.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -110,4 +110,4 @@ needs-timer is PENDING — a required service is missing
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -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/cordis-tutorial/07-into-the-harness.md
07-into-the-harness.md: 41a30f032ac02d8e9e3b17ac8d9cd13e9973e36a
07-into-the-harness.zh.md: 159cede00d453796f6a2cacae184229d71e17f87
07-into-the-harness.md: 2d3c23f9f7f7fc6bd6cabd4e7e68ebfc46e20665
07-into-the-harness.zh.md: 45dc0ee6f07ab3b0275499cd4ab0c436eebdddef
+1 -1
View File
@@ -104,4 +104,4 @@ Where to go next:
- The generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) — everything you can inject and listen to, each on its owning page.
- [Architecture](../architecture.md) — the system map these plugins live in.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
@@ -104,4 +104,4 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
- [子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块:可以注入和监听的所有内容,各在其所属页面上。
- [架构](../architecture.md):这些插件所处的系统地图。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+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/cordis-tutorial/index.md
index.md: cf61fec07acd2022591cd858ba4146b6a4ae1a3d
index.zh.md: 775bf2fce2138b9edd91f5ee46b93b8c4eb4559e
index.md: fb700344e6d07d3864655009d2edac15ee9eede8
index.zh.md: a68e931d81e745164d8f9a5dc7ec9aec4cd0e590
+2 -2
View File
@@ -13,7 +13,7 @@ If you want the condensed concept reference instead of a walkthrough, read the [
You need a clone of this repository with dependencies installed — the [quick start](../user/guide/quickstart.md) covers prerequisites. No API key is needed for this tutorial; every example runs keylessly.
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -55,4 +55,4 @@ The examples use three TypeScript features beyond ordinary modern JavaScript:
Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema<Config>` to say which object fields a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+2 -2
View File
@@ -13,7 +13,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -55,4 +55,4 @@ node --import tsx ../../vendor/cordis/bin.js
第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema<Config>` 这类泛型表示 schema 校验哪些对象字段。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
+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/llm-streaming.md
llm-streaming.md: 8ae4e8b376b4c4221e6179eb719fcf162f3031e4
llm-streaming.zh.md: 7d244ab882521a90217873fc3cdee12cd5232db8
llm-streaming.md: 17f984166906914c49b2c330bdf57b3cecdbd013
llm-streaming.zh.md: 6519710dad8a174418bcc97f1bc4b36296ab969e
+1 -1
View File
@@ -234,7 +234,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
/** Public home URL of the app, used as the `User-Agent` comment. */
/** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
+1 -1
View File
@@ -238,7 +238,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
/** Public home URL of the app, used as the `User-Agent` comment. */
/** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
+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/python-sdk.md
python-sdk.md: c48bc95c9334cfd16a925d12726c20b2f968c753
python-sdk.zh.md: dc31c391a180a742c7dc10807f6ed2ef8d11927d
python-sdk.md: 0713de6f96c110624d5f5a8f2ccf56dd4ce01096
python-sdk.zh.md: f8fa877b407a43c643fda405e1059fb487182761
+1 -1
View File
@@ -30,7 +30,7 @@ python -m pip install deepseek-harness-sdk
A source build additionally requires Git, Node.js ^22.19 or >= 24, Corepack-enabled pnpm 11, and `uv`. The following commands build the runtime for the current supported host platform, build both wheels, and install them into the active virtual environment:
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git deepseek-harness
git clone https://github.com/deepseek-ai/deepseek-harness.git deepseek-harness
cd deepseek-harness
python -m pip install uv==0.11.23
corepack enable
+1 -1
View File
@@ -30,7 +30,7 @@ python -m pip install deepseek-harness-sdk
从源码构建还需要 Git、Node.js ^22.19 或 >= 24、通过 Corepack 启用的 pnpm 11,以及 `uv`。以下命令为当前受支持的宿主平台构建运行时和两个 wheel 包,并将它们安装进当前虚拟环境:
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git deepseek-harness
git clone https://github.com/deepseek-ai/deepseek-harness.git deepseek-harness
cd deepseek-harness
python -m pip install uv==0.11.23
corepack enable
+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/quickstart.md
quickstart.md: 635c0dffd6119f95c1951a441691f87f04396b4a
quickstart.zh.md: d0921382bcb0844fa0260c51546622c582867e68
quickstart.md: e93e5a430f0cb345728581cd6fa3175ffd20b7d1
quickstart.zh.md: 69cde830bb802ef19cc1204685395b957a0e02e3
+1 -1
View File
@@ -19,7 +19,7 @@ pnpm -v
## Step 1: install and configure the API key
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
+1 -1
View File
@@ -19,7 +19,7 @@ pnpm -v
## 第一步:安装并配置 API 密钥
```sh
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
+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 examples/mcp-memory/README.md
README.md: 58da672030eaf2ddf70ee92d506de300efcd9650
README.zh.md: 0a2f109f9458ec7e1aba50e7fc9b6fd0fca15dbd
README.md: 7e7de76f4123481b78898b8d62228e4821f3ebc9
README.zh.md: 3473af862011725844ede95dc9460dd91534b484
-12
View File
@@ -30,18 +30,6 @@ dsh web --patch "$PWD/examples/mcp-memory/memorix.cordis.yml"
Replace the filename with `mcp-reference-memory.cordis.yml` or `engram.cordis.yml`. The path may point to a copied file anywhere on disk. No memory server is present in the shipped composition, so omitting `--patch` keeps all three disabled.
Without a repository checkout, download the selected overlay directly:
```sh
mkdir -p "${DSH_HOME:-$HOME/.dsh}"
curl --fail --location \
--output "${DSH_HOME:-$HOME/.dsh}/memory.cordis.yml" \
https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/examples/mcp-memory/memorix.cordis.yml
dsh web --patch "${DSH_HOME:-$HOME/.dsh}/memory.cordis.yml"
```
Replace `memorix.cordis.yml` in the URL with either of the other filenames to select it. Review a downloaded overlay before running it: Cordis configuration can contain executable `!!js` expressions.
To keep the selection across runs, merge the chosen file's single `insert` patch into a user patch layer — `$DSH_HOME/profiles/<name>/cordis.patch.yml` for one profile, or `$DSH_HOME/cordis.patch.yml` for every profile on the machine. Do not copy over an existing file: it may already contain unrelated user patches.
## Provider setup
-12
View File
@@ -30,18 +30,6 @@ dsh web --patch "$PWD/examples/mcp-memory/memorix.cordis.yml"
请将文件名替换为 `mcp-reference-memory.cordis.yml``engram.cordis.yml`。该路径可以指向磁盘任意位置的一份复制文件。交付组合不包含任何记忆服务器,因此不传 `--patch` 就会让这三项全部保持关闭。
如果本地没有仓库 checkout,可直接下载所选 overlay
```sh
mkdir -p "${DSH_HOME:-$HOME/.dsh}"
curl --fail --location \
--output "${DSH_HOME:-$HOME/.dsh}/memory.cordis.yml" \
https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/examples/mcp-memory/memorix.cordis.yml
dsh web --patch "${DSH_HOME:-$HOME/.dsh}/memory.cordis.yml"
```
若要选择另外任一配置,请将 URL 中的 `memorix.cordis.yml` 替换为对应文件名。运行下载的 overlay 前,请先审阅其内容:Cordis 配置可以包含可执行的 `!!js` 表达式。
如果要跨次运行保留所选配置,请将对应文件中的单个 `insert` patch 合并到用户 patch 层:只对一个 profile 生效则写入 `$DSH_HOME/profiles/<name>/cordis.patch.yml`,对本机所有 profile 生效则写入 `$DSH_HOME/cordis.patch.yml`。不要覆盖已有文件,其中可能已经包含无关的用户 patch。
## 提供方设置
@@ -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/client/ui-conversation/README.md
README.md: 7c4855a75abb982ff55b903808d6a65c42cbc91c
README.zh.md: c3a5d7beb2e90289f4340fc251fd3527370e3e23
README.md: 605bba15d704c0c6e9f28abb3cddeb68bdd7e0d8
README.zh.md: e6a2dd0b545b66ab01b213b5ebc937e22af8ac1a
+2 -2
View File
@@ -6,7 +6,7 @@ Conversation domain: skeleton (header/tabs/composer/empty state), chat view (gro
Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. Automatic compaction uses the context-compacted title. Every completed marker with a loaded `compact/summary` event shows the replaced-item and estimated-token counts and discloses the summary on click. Manual `/compact` starts as a running `compact` row; on successful settlement its explicit summary-event reference folds that command into the checkpoint row under the same React key. A completed checkpoint keeps the context-compaction icon at rest and replaces it with the collapsed or expanded disclosure only on hover or keyboard focus. Input rejection, no compactable history, cancellation, and failure retain the generic command row and its handler-authored text. Pairing never depends on adjacency because durable context may be injected while compaction is running. The framed checkpoint payload is model-facing and never renders; when the cited `compact/summary` event is outside the loaded window, the checkpoint remains visible but non-expandable.
The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
The resident conversation shell survives no-session and session transitions. Without a current session it locks message actions and presents the whole dashed composer card as a trigger for the root-scoped `conversation.hero.workspace` Workspace picker; the textarea remains read-only and keyboard-accessible. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
Another plugin can make one session's composer inert through `ctx.conversation.blocks`: it sets a block carrying its own localized reason, and the bar renders the same disabled textarea with that reason as the placeholder — the no-workspace posture, reused. The push direction is the constraint, not a preference: the plugins that know a session cannot send (ui-model, when no adapter serves its route) already depend on this package, so this package cannot read them. The model seat is the one control a block leaves live — every block this contract has is cleared by choosing a model, so locking it too would leave the composer asking for the only thing it prevents. A block is an affordance only; the Host refuses a prompt it cannot route regardless of what any client disables. The no-workspace state wins when both hold, because picking a workspace is the earlier prerequisite.
@@ -36,7 +36,7 @@ Keyboard message submission resolves delivery from the addressed session's runni
Per-session UI state for selection and the active view lives in the declared chat store (`stores.ts` `createChatStore`); the InputHub owns the composer state machine and mirrors its draft into that store for persistence. Apply passes one store handle to the strict session subtree, chat view, and details registrations, so each session shares one instance and the framework owns its lifecycle. Components are pure: the framework standard kit supplies `useSession`/`sessionId`, global `useSessions`/`useWorkspaces`, and the input machine's `useInput`/`inputActions`; store faces and inject factories supply the remaining state and callbacks.
The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `SlashController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-slash's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar renders inert (machine faces absent, `disabled` owner prop) instead of swapping in a parallel disabled tree, so the textarea DOM survives the workspace pick; the strict-session control seats simply stay empty until a session exists.
The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `SlashController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-slash's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar keeps message actions inert (machine faces absent, `disabled` owner prop), while the whole dashed card opens the existing Workspace picker by pointer and the read-only textarea opens it through Enter or Space. Disabled controls release pointer events to the card, and the card contains `pointerdown` so the open picker's outside-close cannot race a reopen. The bar never swaps in a parallel tree, so the textarea DOM survives Workspace selection; strict-session control seats stay empty until a session exists.
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.
+2 -2
View File
@@ -6,7 +6,7 @@
压缩(compaction)在检查点自身的消息流位置渲染为一行折叠标记,不替换其上方的 transcript(文本记录)。自动压缩使用「上下文已压缩」标题。每个已加载对应 `compact/summary` 事件的完成标记都会显示被替换条目数量和估算 token 数量,并可点击展开摘要。手动 `/compact` 开始时显示为运行中的 `compact` 行;成功结算后,其显式摘要事件引用会在保持同一 React key 的前提下把该命令折叠进检查点行。完成的检查点静止时保留上下文压缩图标,仅在悬停或键盘聚焦时将其替换为收起/展开指示图标。输入被拒绝、没有可压缩历史、取消和失败时仍使用通用命令行及处理器撰写的文本。配对绝不依赖相邻关系,因为压缩运行期间可能注入持久上下文。面向模型的带框检查点载荷绝不渲染;被引用的 `compact/summary` 事件位于已加载窗口之外时,检查点仍然可见但不可展开。
常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。根组件始终拥有同一个滚动容器与 Hero/编辑器子树;首个会话到达时,彼此独立的严格会话页头和主体 outlet 只填入各自区域,因此 Workspace 选择器、滚动主体、编辑器 seat 与 textarea 都保留原有 React 和 DOM identity。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。该滚动容器无条件预留自己的滚动条槽,选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md))。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。
常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会锁定消息操作,并让整张虚线编辑器卡片成为根作用域 `conversation.hero.workspace` Workspace picker 的入口;textarea 保持只读且支持键盘操作。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。根组件始终拥有同一个滚动容器与 Hero/编辑器子树;首个会话到达时,彼此独立的严格会话页头和主体 outlet 只填入各自区域,因此 Workspace picker、滚动主体、编辑器 seat 与 textarea 都保留原有 React 和 DOM identity。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。该滚动容器无条件预留自己的滚动条槽,选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md))。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。
别的插件可以经 `ctx.conversation.blocks` 让某个会话的编辑器变为惰性:它设置一个携带自己本地化理由的 block,输入栏就渲染同一个禁用的 textarea,并把该理由作为 placeholder——复用无 Workspace 时的那套姿态。推送方向是约束而非偏好:知道某会话发不出消息的插件(ui-model,在没有适配器服务其路由时)本就依赖本包,因此本包读不到它们。模型 seat 是 block 唯一保留可用的控件——这份约定里的每个 block 都靠选模型来解除,把它一起锁上会让编辑器索要它自己拦下的那件事。block 只是提示性设计;无论客户端禁用了什么,宿主都会拒绝一个它路由不了的 prompt。两者同时成立时以无 Workspace 姿态为准,因为选 Workspace 是更靠前的前提。
@@ -36,7 +36,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu
逐 Session UI 状态中的选择与活跃视图位于已声明的聊天 store(`stores.ts` `createChatStore`)中;InputHub 拥有输入区状态机,并将草稿镜像到该 store 以便持久化。apply 将同一个 store handle 传给严格限定于会话的子树、聊天视图和详情注册,因此每个会话内共享一个实例,框架拥有其生命周期。组件保持纯粹:框架标准工具包提供 `useSession``sessionId`、全局 `useSessions``useWorkspaces`,以及输入状态机的 `useInput``inputActions`store 表层与 inject factory 提供其余状态和回调。
输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat,并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态;ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher,而非附件入口:它要求当前会话的 `SlashController` 基于 textarea 当前 selection,只打开 `/` trigger 的 `command` source,同时 ui-slash 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时,InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值;owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent(智能体)仍能收到回答;没有待处理交互时,活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 以不可交互状态渲染machine face 均缺席、`disabled` owner prop),而不是换入一棵平行的 disabled 树,因此选择 workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。
输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat,并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态;ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher,而非附件入口:它要求当前会话的 `SlashController` 基于 textarea 当前 selection,只打开 `/` trigger 的 `command` source,同时 ui-slash 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时,InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值;owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent(智能体)仍能收到回答;没有待处理交互时,活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 会让消息操作保持不可交互machine face 均缺席、`disabled` owner prop),整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。禁用控件会把指针事件交给卡片,卡片也会拦下 `pointerdown`,避免已打开 picker 的外点关闭与重新打开发生竞态。它不会换入一棵平行树,因此选择 Workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。
聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`:计费输入为未缓存输入、缓存读取与缓存写入之和;缓存命中率以缓存读取除以该总量。可见节点只提供轮次与步骤计数,以及 LLM(大语言模型)和工具的墙钟时间:这些是关于「屏幕上有什么」的窗口作用域事实,而非账目;压缩(compaction)使已加载窗口不再包含 assistant 节点时,持久 token 与上下文分组仍保持可见。同一次窗口折算还会把每个有完整记录的步骤的 TTFT(首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字,而不是让它们失真。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率渲染为 composer 尾部的 ContextMeter:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。
@@ -122,8 +122,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* takeover election hides rather than unmounts it and the textarea DOM
* survives). Session-maybe: the bar stays mounted across the
* no-session/session transition — the no-workspace hero renders the SAME
* textarea DOM disabled instead of a parallel inert tree — with the
* machine hooks absent until a session is current. InputBar registers
* textarea DOM as a read-only Workspace-picker trigger instead of a
* parallel inert tree — with the machine hooks absent until a session is
* current. InputBar registers
* here from this package's apply; its machine state arrives through the
* standard provide channel (useInput + inputActions), the keyboard
* command face through its own inject.
@@ -380,11 +381,14 @@ export interface ComposerBarOwnerProps {
*/
blocked?: { readonly reason: string }
/**
* Inert no-workspace state: the bar renders its normal DOM fully disabled
* (textarea, add, send) so the workspace pick transitions in place instead
* of swapping component trees.
* Inert no-workspace state: the bar locks message actions while preserving
* its normal DOM so the Workspace pick transitions in place.
*/
disabled?: boolean
/** Whether the shared Workspace picker menu is expanded, regardless of which trigger opened it. */
workspacePickerOpen?: boolean
/** Open the existing Workspace picker from the inert textarea. */
onRequestWorkspace?: () => void
placeholder?: string
/** Optional content rendered above the textarea. */
accessory?: ReactNode
@@ -123,7 +123,7 @@ export function ConversationRoot({
</div>
)
// The placeholder chip ("Choose workspace") and the inert input travel
// The placeholder chip ("Choose workspace") and the Workspace-trigger input travel
// together: no workspace picked yet (cold start, no session at all), or a
// blank session whose workspace vanished (deleted from the sidebar). The
// bar is ONE session-maybe slot rendered unconditionally — inert is a prop,
@@ -136,7 +136,12 @@ export function ConversationRoot({
const inputBar = renderSlot('conversation.composer.bar', {
variant: hero ? 'hero' : 'composer',
...(inert
? { disabled: true, placeholder: t('placeholder.workspace') }
? {
disabled: true,
placeholder: t('placeholder.workspace'),
workspacePickerOpen: pickerOpen,
onRequestWorkspace: () => { setPickerOpen(true) },
}
: blocked
// `blocked`, not `disabled`: the bar refuses input either way, but a
// block keeps the model seat live because choosing a model is how the
@@ -102,6 +102,40 @@
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
/* No-workspace trigger state: dashed l4 stroke marks the card as a pick-a-
workspace affordance rather than a live composer; hover answers in the
business blue to invite the click. Native `dashed` has a fixed browser
pattern, so the stroke is an ::after overlay: theme-token background masked
by an SVG dash ring (stroke-width 2 centered on the box edge = 1px visible
inside), which keeps the 22px radius and both themes. */
.cardWorkspaceTrigger {
border-color: transparent;
cursor: pointer;
}
.cardWorkspaceTrigger::after {
content: '';
position: absolute;
inset: -1px;
border-radius: 22px;
background: var(--dsw-alias-border-l4);
transition: background-color 100ms ease;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E");
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E");
pointer-events: none;
}
/* Disabled toolbar controls neither receive nor swallow clicks in the trigger
state: pointer events fall through to the card's own click handler, making
the full capsule one pick target. */
.cardWorkspaceTrigger :disabled {
pointer-events: none;
}
.cardWorkspaceTrigger:hover::after {
background: var(--dsw-alias-state-business-primary);
}
.dragActive {
border-color: var(--dsw-alias-state-business-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent), var(--dsw-shadow-lv2);
@@ -313,6 +347,10 @@
cursor: not-allowed;
}
.input[aria-haspopup='menu'] {
cursor: pointer;
}
.mirror {
visibility: hidden;
pointer-events: none;
@@ -39,8 +39,9 @@ export function InputBar({
useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages,
resolveSubmitMode, toggleCommandMenu, stop, command, t,
renderSlot, useNotices, useLexicon, useMenuLauncher,
useProjection, sessionId, variant, disabled: inert = false, blocked, placeholder,
accessory, overlay, leftItems, rightItems, footer,
useProjection, sessionId, variant, disabled: inert = false, blocked,
workspacePickerOpen = false, onRequestWorkspace,
placeholder, accessory, overlay, leftItems, rightItems, footer,
}: InputBarProps) {
const input = useInput(s => s)
const notice = useNotices(s => s)
@@ -109,6 +110,12 @@ export function InputBar({
// be disabled do lock it — there is no session to choose a model for.
const modelSeatLocked = removed || inert || !live
const machineBusy = input?.phase === 'adjudicating' || input?.phase === 'submitting'
// The no-workspace textarea remains the resident DOM node but acts as the
// existing picker trigger. Message controls stay locked until a Session
// exists; the trigger itself is read-only rather than disabled so pointer
// and keyboard users can reach the recovery action.
const workspaceTrigger = inert && !removed && onRequestWorkspace !== undefined
const textareaDisabled = removed || (locked && !workspaceTrigger)
const canSteerQueue = !locked && !machineBusy && !commandMenuOpen && empty && running && subagent === null
&& input.queue.some(row => row.placement === 'queued')
@@ -233,8 +240,15 @@ export function InputBar({
}, [])
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
// Absent machine (no session): the textarea is disabled so events cannot
// fire; the guard narrows the faces for the paths below.
if (workspaceTrigger) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onRequestWorkspace()
}
return
}
// Absent machine without a Workspace recovery action stays disabled; the
// guard narrows the faces for the paths below.
if (keyboard === undefined || inputActions === undefined) return
// Shift+Enter is the native newline UNCONDITIONALLY — decided before the
// IME guard so a composition-closing Shift+Enter still breaks the line.
@@ -298,7 +312,7 @@ export function InputBar({
}
const onChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
if (keyboard === undefined) return // absent machine: disabled textarea, no events
if (keyboard === undefined || locked) return // disabled/read-only states cannot edit the draft
if (machineBusy) return // submitting is the read-only span; adjudicating holds the pending lock
const next = e.target.value
keyboard.setDraft(next)
@@ -324,7 +338,7 @@ export function InputBar({
/* oxlint-enable typescript/no-unnecessary-condition */
const onCopyOrCut = (e: React.ClipboardEvent<HTMLTextAreaElement>, cut: boolean): void => {
if (input === undefined || keyboard === undefined) return // absent machine: disabled textarea, no events
if (input === undefined || keyboard === undefined) return // absent machine: no draft can be copied or cut
const el = e.currentTarget
const { start, end } = selectionOf(el)
if (start === end) return
@@ -349,7 +363,7 @@ export function InputBar({
}
const onPaste = (e: React.ClipboardEvent<HTMLTextAreaElement>): void => {
if (keyboard === undefined) return // absent machine: disabled textarea, no events
if (keyboard === undefined) return // absent machine: no draft can accept a paste
if (machineBusy || locked) return
const files = Array.from(e.clipboardData.items)
.filter(item => item.kind === 'file')
@@ -539,10 +553,17 @@ export function InputBar({
{notice.text}
</div>
)}
{/* Trigger clicks land on the card, not the textarea: the toolbar row's
disabled controls swallow clicks otherwise (the CSS state disarms
their pointer events), so the WHOLE capsule is the pick target.
pointerdown stops here so the Menu's outside-close cannot race the
click's reopen (close-then-open flickers the chip's open echo). */}
{dropError !== null && <div className={css.error} role="alert">{dropError}</div>}
<div
className={clsx(css.card, dragActive && css.dragActive)}
className={clsx(css.card, workspaceTrigger && css.cardWorkspaceTrigger, dragActive && css.dragActive)}
data-composer-card
onClick={workspaceTrigger ? onRequestWorkspace : undefined}
onPointerDown={workspaceTrigger ? (e) => { e.stopPropagation() } : undefined}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
@@ -590,8 +611,11 @@ export function InputBar({
ref={inputRef}
className={css.input}
value={draft}
disabled={locked}
readOnly={machineBusy}
disabled={textareaDisabled}
readOnly={machineBusy || workspaceTrigger}
aria-label={workspaceTrigger ? t('hero.chooseWorkspace') : undefined}
aria-haspopup={workspaceTrigger ? 'menu' : undefined}
aria-expanded={workspaceTrigger ? workspacePickerOpen : undefined}
data-phase={input?.phase ?? 'inert'}
placeholder={placeholder ?? (parentOffline
? t('placeholder.parentOffline')
@@ -82,10 +82,20 @@ describe('resident composer', () => {
runtime.slots.installLocale(locale)
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
await runtime.mount({ inject: [...inject], apply })
runtime.slots.register({ name: 'conversation.hero.workspace' }, WorkspaceProbe)
const view = runtime.renderRoot()
const textarea = view.container.querySelector('textarea')
expect(textarea).not.toBeNull()
expect(textarea!.disabled).toBe(true)
expect(textarea!.disabled).toBe(false)
expect(textarea!.readOnly).toBe(true)
expect(textarea!.getAttribute('aria-haspopup')).toBe('menu')
expect(view.getByTestId('workspace-probe').textContent).toBe('false:0')
fireEvent.click(textarea!)
expect(view.getByTestId('workspace-probe').textContent).toBe('true:0')
expect(textarea!.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(view.getByRole('button', { name: '选择工作区' }))
fireEvent.keyDown(textarea!, { key: 'Enter' })
expect(view.getByTestId('workspace-probe').textContent).toBe('true:0')
expect(view.getByRole('button', { name: '选择工作区' })).toBeTruthy()
await runtime.dispose()
})
@@ -111,7 +121,8 @@ describe('resident composer', () => {
const textarea = view.container.querySelector('textarea')!
const workspaceChip = view.getByRole('button', { name: '选择工作区' })
const workspaceProbe = view.getByTestId('workspace-probe')
expect(textarea.disabled).toBe(true)
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(true)
fireEvent.click(workspaceChip)
fireEvent.click(workspaceProbe)
@@ -131,6 +142,7 @@ describe('resident composer', () => {
expect(view.getByTestId('workspace-probe')).toBe(workspaceProbe)
expect(workspaceProbe.textContent).toBe('true:1')
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(false)
await runtime.dispose()
})
@@ -60,6 +60,9 @@ interface BenchOptions {
running?: boolean
subagent?: Exclude<ConversationSnapshot['subagent'], null>
disabled?: boolean
inert?: boolean
workspacePickerOpen?: boolean
onRequestWorkspace?: () => void
promptError?: ConversationSnapshot['promptError']
/** Authoritative queue rows served to the machine overlay (empty = none). */
queue?: ConversationSnapshot['queue']
@@ -168,6 +171,9 @@ function bench(over?: BenchOptions) {
t: over?.t ?? makeTranslate(zh, commonZh),
renderSlot,
variant: over?.variant ?? 'composer',
...(over?.inert === true ? { disabled: true } : {}),
...(over?.workspacePickerOpen !== undefined ? { workspacePickerOpen: over.workspacePickerOpen } : {}),
...(over?.onRequestWorkspace !== undefined ? { onRequestWorkspace: over.onRequestWorkspace } : {}),
...(over?.placeholder !== undefined ? { placeholder: over.placeholder } : {}),
...(over?.accessory !== undefined ? { accessory: over.accessory } : {}),
...(over?.overlay !== undefined ? { overlay: over.overlay } : {}),
@@ -785,6 +791,40 @@ describe('running and lock semantics', () => {
expect(custom.textarea.placeholder).toBe('Custom placeholder')
})
it('the inert textarea opens the Workspace picker by pointer or keyboard', () => {
const onRequestWorkspace = vi.fn()
const { view, textarea } = bench({
inert: true,
workspacePickerOpen: false,
onRequestWorkspace,
placeholder: '选择一个工作区开始',
})
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(true)
expect(textarea.getAttribute('aria-haspopup')).toBe('menu')
expect(textarea.getAttribute('aria-expanded')).toBe('false')
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
fireEvent.click(textarea)
fireEvent.keyDown(textarea, { key: 'Enter' })
fireEvent.keyDown(textarea, { key: ' ' })
expect(onRequestWorkspace).toHaveBeenCalledTimes(3)
// The WHOLE capsule is the pick target, and its pointerdown never reaches
// the document — the open picker's outside-close must not race the reopen.
const card = view.container.querySelector('[data-composer-card]') as HTMLElement
fireEvent.click(card)
expect(onRequestWorkspace).toHaveBeenCalledTimes(4)
const onDocumentPointerDown = vi.fn()
document.addEventListener('pointerdown', onDocumentPointerDown)
try {
fireEvent.pointerDown(card)
} finally {
document.removeEventListener('pointerdown', onDocumentPointerDown)
}
expect(onDocumentPointerDown).not.toHaveBeenCalled()
})
it('the plan projection swaps the placeholder while its effective target is plan mode', () => {
const active = bench({ plan: { active: true, pending: false } })
expect(active.textarea.placeholder).toBe('描述你的任务以生成计划')
@@ -296,8 +296,12 @@ describe('ConversationRoot resident composer', () => {
composerBlock: { reason: 'select a model first' },
})
const box = b.view.getByRole('textbox') as HTMLTextAreaElement
expect(box.disabled).toBe(true)
expect(box.disabled).toBe(false)
expect(box.readOnly).toBe(true)
expect(box.getAttribute('aria-haspopup')).toBe('menu')
expect(box.placeholder).not.toBe('select a model first')
const modelSeat = b.seatOwners.filter(call => call.key === 'conversation.input.model').at(-1)?.owner
expect(modelSeat).toEqual({ locked: true })
})
it('keeps composer text in the machine, mirrors to the chat store, and submits through the sink', () => {
+2 -3
View File
@@ -27,7 +27,7 @@ export interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
/** Public home URL of the app, used as the `User-Agent` comment. */
/** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
@@ -40,8 +40,7 @@ export interface AppIdentity {
export const APP_IDENTITY: AppIdentity = {
product: 'deepseek-harness',
version,
// TODO(public-home): Ensure this public source repository exists before release.
url: 'https://github.com/deepseek-ai/deepseek-harness-sdk',
url: 'https://github.com/deepseek-ai/deepseek-harness',
}
/**
+2 -2
View File
@@ -21,7 +21,7 @@ describe('APP_IDENTITY', () => {
expect(APP_IDENTITY).toEqual({
product: 'deepseek-harness',
version: manifest.version,
url: 'https://github.com/deepseek-ai/deepseek-harness-sdk',
url: 'https://github.com/deepseek-ai/deepseek-harness',
})
})
})
@@ -29,7 +29,7 @@ describe('APP_IDENTITY', () => {
describe('userAgent', () => {
it('renders product/version with the +url comment', () => {
expect(userAgent()).toBe(
`deepseek-harness/${manifest.version} (+https://github.com/deepseek-ai/deepseek-harness-sdk)`,
`deepseek-harness/${manifest.version} (+https://github.com/deepseek-ai/deepseek-harness)`,
)
})
@@ -6,14 +6,14 @@ Add the official “powered by dsh” badge without recreating or restyling it.
- Local PNG: [`dsh-badge.png`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: `https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white`
- Project URL: `https://github.com/deepseek-ai/deepseek-harness-sdk`
- Project URL: `https://github.com/deepseek-ai/deepseek-harness`
## Markdown
Use this linked badge in Markdown:
```markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
```
If attribution should not be linked, use:
+3 -3
View File
@@ -104,7 +104,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[B](./reference/b.md#part) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/packages/tool.ts#L2) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
+ '[web](https://example.com)\n',
)
})
@@ -130,7 +130,7 @@ describe('rewriteMarkdown', () => {
pages,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/abc123/packages/logo.svg)\n')
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/abc123/packages/logo.svg)\n')
})
it('hands an image to the placer and uses the URL it returns', () => {
@@ -209,7 +209,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[title](./reference/b.md "b.md") '
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/docs/x(y).md)\n',
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/docs/x(y).md)\n',
)
})
+2 -2
View File
@@ -15,7 +15,7 @@ import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness-sdk'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness'
const root = resolve(import.meta.dirname, '..')
const generatedRoot = resolve(root, 'website/.generated')
@@ -209,7 +209,7 @@ function githubTarget(
image: boolean,
): string {
const path = repoPath(absPath, repoRoot)
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/${repositoryRef}/${path}${suffix}`
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/${repositoryRef}/${path}${suffix}`
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
const lineSuffix = line === undefined ? suffix : `#L${line}`
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
+26 -41
View File
@@ -1,60 +1,45 @@
import { describe, expect, it } from 'vitest'
import { findInternalRepositoryReferences } from './verify-public-repository-links.ts'
import { findUnavailableRepositoryReferences } from './verify-public-repository-links.ts'
describe('public repository link policy', () => {
it('rejects encoded and case-varied internal identities without blocking public repositories', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const encodedRepository = internalRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = internalRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = internalRepository.replace('/', '\\/')
const unicodeEscapedRepository = internalRepository.replace('/', String.raw`\u002f`)
describe('repository link policy', () => {
it('rejects encoded and case-varied references to the unavailable repository', () => {
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableName}`
const encodedRepository = unavailableRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = unavailableRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = unavailableRepository.replace('/', '\\/')
const unicodeEscapedRepository = unavailableRepository.replace('/', String.raw`\u002f`)
const source = [
'https://github.com/deepseek-ai/deepseek-harness-sdk',
`https://github.com/${internalOwner}/cordis`,
`https://github.com/${internalRepository.toUpperCase()}/issues/1`,
'https://github.com/deepseek-ai/deepseek-harness',
`https://github.com/${unavailableRepository.toUpperCase()}/issues/1`,
`https://github.com/${encodedRepository}/issues/2`,
`https://github.com/${htmlEncodedRepository}/issues/3`,
`"https:\\/\\/github.com\\/${jsonEscapedRepository}\\/issues\\/4"`,
`"https:\\/\\/github.com\\/${unicodeEscapedRepository}\\/issues\\/5"`,
`${internalOwner.toUpperCase()}#6`,
`https://github.com/${unavailableOwner}/cordis`,
`https://github.com/example/${unavailableName}`,
].join('\n')
expect(findInternalRepositoryReferences('subject.md', source)).toEqual([
expect(findUnavailableRepositoryReferences('subject.md', source)).toEqual([
{ file: 'subject.md', line: 2 },
{ file: 'subject.md', line: 3 },
{ file: 'subject.md', line: 4 },
{ file: 'subject.md', line: 5 },
{ file: 'subject.md', line: 6 },
{ file: 'subject.md', line: 7 },
{ file: 'subject.md', line: 8 },
])
})
it('allows only the exact audited trusted-publishing repository declarations', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const repositoryUrl = `git+https://github.com/${internalRepository}.git`
const manifestLine = ` "url": "${repositoryUrl}",`
const constraintLine = `const repositoryUrl = '${repositoryUrl}'`
const allowedDeclarations = [
['native/landlock-run/packages/entry/package.json', manifestLine],
['native/landlock-run/packages/linux-arm64/package.json', manifestLine],
['native/landlock-run/packages/linux-x64/package.json', manifestLine],
['scripts/check-workspace-constraints.ts', constraintLine],
] as const
it('preserves frozen archived Agent Notes', () => {
const unavailableRepository = ['deepseek-ai', 'deepseek-harness-sdk'].join('/')
for (const [file, source] of allowedDeclarations) {
expect(findInternalRepositoryReferences(file, source)).toEqual([])
}
const wrongFile = 'native/landlock-run/package.json'
expect(findInternalRepositoryReferences(wrongFile, manifestLine)).toEqual([{ file: wrongFile, line: 1 }])
const manifestFile = 'native/landlock-run/packages/entry/package.json'
const wrongField = ` "homepage": "${repositoryUrl}",`
expect(findInternalRepositoryReferences(manifestFile, wrongField)).toEqual([{ file: manifestFile, line: 1 }])
const encodedLine = manifestLine.replace('github.com/', 'github.com\\/')
expect(findInternalRepositoryReferences(manifestFile, encodedLine)).toEqual([{ file: manifestFile, line: 1 }])
expect(findUnavailableRepositoryReferences(
'.agents/notes/archived/process/historical-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([])
expect(findUnavailableRepositoryReferences(
'.agents/notes/implemented/process/active-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([{ file: '.agents/notes/implemented/process/active-record.md', line: 1 }])
})
})
+19 -30
View File
@@ -1,4 +1,4 @@
/** Reject tracked files that expose the internal repository identity outside audited publishing declarations. */
/** Reject tracked files that reference an unavailable legacy repository. */
import { execFileSync } from 'node:child_process'
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
@@ -6,22 +6,13 @@ import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '..')
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const internalIssueShorthand = `${internalOwner}#`
const trustedPublishingRepositoryUrl = `git+https://github.com/${internalRepository}.git`
/** Exact declarations that intentionally expose the source repository for trusted publishing. */
const allowedInternalRepositoryLineByFile: Readonly<Record<string, string>> = {
'native/landlock-run/packages/entry/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-arm64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-x64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'scripts/check-workspace-constraints.ts': `const repositoryUrl = '${trustedPublishingRepositoryUrl}'`,
}
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableRepositoryName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableRepositoryName}`
const archivedAgentNotePrefix = '.agents/notes/archived/'
const namedReferenceCharacters: Readonly<Record<string, string>> = {
hyphen: '-',
num: '#',
sol: '/',
}
@@ -40,8 +31,8 @@ function canonicalReferenceText(source: string): string {
.toLowerCase()
}
/** One tracked reference to the internal repository. */
export interface InternalRepositoryReference {
/** One tracked reference to the unavailable repository. */
export interface UnavailableRepositoryReference {
/** Repository-relative file path. */
file: string
/** One-based source line. */
@@ -49,20 +40,18 @@ export interface InternalRepositoryReference {
}
/**
* Locate unaudited internal-repository references in one text file.
* Locate unavailable-repository references in one active text file.
* @param file - Repository-relative path used in diagnostics.
* @param source - Text to inspect.
* @returns every matching source line.
* @returns every matching source line, excluding frozen archived Agent Notes.
*/
export function findInternalRepositoryReferences(file: string, source: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
export function findUnavailableRepositoryReferences(file: string, source: string): UnavailableRepositoryReference[] {
if (file.startsWith(archivedAgentNotePrefix)) return []
const references: UnavailableRepositoryReference[] = []
for (const [index, line] of source.split('\n').entries()) {
const canonicalLine = canonicalReferenceText(line)
const isAllowedPublishingDeclaration = line.trim() === allowedInternalRepositoryLineByFile[file]
if (!isAllowedPublishingDeclaration
&& (canonicalLine.includes(internalRepository) || canonicalLine.includes(internalIssueShorthand))) {
references.push({ file, line: index + 1 })
}
if (canonicalLine.includes(unavailableRepository)) references.push({ file, line: index + 1 })
}
return references
}
@@ -73,8 +62,8 @@ function trackedFiles(repoRoot: string): string[] {
.filter(file => file !== '')
}
function scanRepository(repoRoot: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
function scanRepository(repoRoot: string): UnavailableRepositoryReference[] {
const references: UnavailableRepositoryReference[] = []
for (const file of trackedFiles(repoRoot)) {
const path = resolve(repoRoot, file)
if (!existsSync(path)) continue
@@ -82,7 +71,7 @@ function scanRepository(repoRoot: string): InternalRepositoryReference[] {
if (!stat.isFile() && !stat.isSymbolicLink()) continue
const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
if (source.includes('\0')) continue
references.push(...findInternalRepositoryReferences(file, source))
references.push(...findUnavailableRepositoryReferences(file, source))
}
return references
}
@@ -92,9 +81,9 @@ const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(re
if (isMain) {
const references = scanRepository(root)
if (references.length === 0) {
console.log('verify-public-repository-links: tracked files expose no unexpected internal repository identity.')
console.log('verify-public-repository-links: tracked files reference no unavailable repository.')
} else {
console.error('verify-public-repository-links: unexpected internal repository references found:')
console.error('verify-public-repository-links: unavailable repository references found:')
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
process.exitCode = 1
}
+3 -3
View File
@@ -94,14 +94,14 @@ const sharedTheme: Pick<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLin
},
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/deepseek-ai/deepseek-harness-sdk' },
{ icon: 'github', link: 'https://github.com/deepseek-ai/deepseek-harness' },
],
editLink: {
pattern: ({ frontmatter }: PageData) => {
const data: unknown = frontmatter
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
return `https://github.com/deepseek-ai/deepseek-harness-sdk/edit/master/${editSource}`
return `https://github.com/deepseek-ai/deepseek-harness/edit/master/${editSource}`
},
text: '在 GitHub 上编辑此页',
},
@@ -161,7 +161,7 @@ export default withMermaid({
const data: unknown = frontmatter
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
return `https://github.com/deepseek-ai/deepseek-harness-sdk/edit/master/${editSource}`
return `https://github.com/deepseek-ai/deepseek-harness/edit/master/${editSource}`
},
text: 'Edit this page on GitHub',
},