mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge remote-tracking branch 'origin/master' into perf/desktop-packaging-integration
# Conflicts: # docs/development.i18n.yaml # scripts/build.spec.ts
This commit is contained in:
commit
46ce2b61e0
3120 files changed
+63711
-6317
No files matched your search
@@ -9,8 +9,8 @@
|
||||
en: c8b932b02a4e1c80
|
||||
zh: 96c466be1a521f3f
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/decision:
|
||||
en: b57eb0f268033ff1
|
||||
zh: 4143382a50181414
|
||||
en: 2e08d801099cbe18
|
||||
zh: 8c5ab162ada9cc41
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/package-topology:
|
||||
en: 5ea3ddfae82df181
|
||||
zh: 2d05d95e51081696
|
||||
|
||||
@@ -32,7 +32,7 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov
|
||||
- A tool is never unregistered merely because its selected provider is missing, misconfigured, missing credentials, ambiguous, or temporarily unavailable.
|
||||
- The provider is resolved at execution time, and a structured `WebError` is returned when the selected capability cannot run.
|
||||
|
||||
This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
|
||||
This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. `dsh-tool-web` narrows this per model request: it [withholds `web_search` while the selected provider reports that the request's search cannot authenticate](2026-09-27-web-search-offered-by-provider-readiness.md). If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
|
||||
|
||||
The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
|
||||
- 工具绝不会仅仅因为其选定的提供方缺失、配置错误、缺少凭证、存在歧义或暂时不可用就被注销。
|
||||
- 提供方在执行时解析,当选定的能力无法运行时返回结构化的 `WebError`。
|
||||
|
||||
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
|
||||
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。发行版的 `dsh-tool-web` 按模型请求收窄了这一点:选中的提供方报告该请求的搜索无法鉴权时,它会[撤下 `web_search`](2026-09-27-web-search-offered-by-provider-readiness.zh.md)。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
|
||||
|
||||
该 seam 刻意不暴露任何观察面——没有注册表变更事件,也没有聚合的能力状态查询。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
en: b78785f2a603eba8
|
||||
zh: b8c268228da7e2e7
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/consequences:
|
||||
en: 93c32b795b71a1cc
|
||||
zh: d42b5c563341c898
|
||||
en: 60d7bf28fa5a8fd1
|
||||
zh: a17b222e7aca01e2
|
||||
@@ -30,7 +30,7 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool
|
||||
|
||||
## Consequences
|
||||
|
||||
Tool lists and logical update events are capability-independent; only outgoing declarations and messages vary. Calls to removed retained tools fail through the existing `UNKNOWN_TOOL` path. Changed definitions and incomplete auxiliary prefixes sacrifice prefix reuse for coherent declarations; unchanged restoration keeps the cached prefix on `in-history` routes only while the request series continues. Routes supporting tool updates and in-history system prompts preserve earlier messages when tools are added alongside a changed prompt or after an earlier prompt update. Routes without tool-update support still consolidate on tool changes; explicit series starts and surface replacements still consolidate on every route. No persisted fields or Session format revision are added.
|
||||
Tool lists and logical update events are capability-independent; only outgoing declarations and messages vary. Calls to removed retained tools fail through the existing `UNKNOWN_TOOL` path. Changed definitions and incomplete auxiliary prefixes sacrifice prefix reuse for coherent declarations; unchanged restoration keeps the cached prefix on `in-history` routes only while the request series continues. Routes supporting tool updates and in-history system prompts preserve earlier messages when tools are added alongside a changed prompt or after an earlier prompt update. Routes without tool-update support still start a new series on tool changes; explicit series starts and surface replacements consolidate the prompt only when the effective text differs, and otherwise leave the surviving prompt node in place. No persisted fields or Session format revision are added.
|
||||
|
||||
The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
|
||||
|
||||
## Consequences
|
||||
|
||||
工具列表和逻辑更新事件不依赖能力;只有发出的声明和消息不同。调用已移除但保留声明的工具时,通过现有 `UNKNOWN_TOOL` 路径失败。定义变化和不完整辅助前缀为保证声明一致而放弃前缀复用;仅当请求序列继续时,未变化的恢复才在 `in-history` 路由上保留缓存前缀。同时支持工具更新和历史内系统提示词的路由,在工具添加与提示词变化同时发生或晚于此前提示词更新时,保留此前消息。不支持工具更新的路由仍在工具变化时归并提示词;所有路由仍在显式新序列和 surface 替换时归并。不增加持久字段或 Session 格式版本。
|
||||
工具列表和逻辑更新事件不依赖能力;只有发出的声明和消息不同。调用已移除但保留声明的工具时,通过现有 `UNKNOWN_TOOL` 路径失败。定义变化和不完整辅助前缀为保证声明一致而放弃前缀复用;仅当请求序列继续时,未变化的恢复才在 `in-history` 路由上保留缓存前缀。同时支持工具更新和历史内系统提示词的路由,在工具添加与提示词变化同时发生或晚于此前提示词更新时,保留此前消息。不支持工具更新的路由仍在工具变化时开启新序列;显式新序列和 surface 替换只在有效文本不同时归并提示词,否则保留存活的提示词节点。不增加持久字段或 Session 格式版本。
|
||||
|
||||
手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。
|
||||
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 2eeb916eb65c66e7
|
||||
zh: 7bf5c0abb8bb61e4
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/decision:
|
||||
en: 42e800c4cd3b6ebe
|
||||
zh: 06fabe5a9681fa27
|
||||
en: 0125e33ba966c8ac
|
||||
zh: bf6af389336999af
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/alternatives-considered:
|
||||
en: 7c7880fdb0cc95f6
|
||||
zh: 2b8a539192626789
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ The account route's catalog was deployment configuration: a build advertised the
|
||||
|
||||
- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.md) loads the listing lazily for each credential, waits for it once up to `modelsTimeoutMs`, refreshes it in the background after `modelsMaxAgeMs`, keeps the last good listing when a load fails or returns no model, and serves the bundled catalog before any listing loads. The cache lives in memory and is keyed by endpoint and token fingerprint, so per-account rollouts stay separate.
|
||||
- A listed model is available when its `client_requirements["deepseek-harness"].min_version` is at most the current DeepSeek Harness version in semantic-version order. Without that declaration, only ids the built-in catalog knows are available; the others stay hidden, because a client cannot tell which future version supports them. A request for a refused listed model fails before network I/O with `CLIENT_UPGRADE_REQUIRED` or `MODEL_UNSUPPORTED`.
|
||||
- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge such as tool-update mode and image budgets; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it.
|
||||
- The shared transport gains only two provider hooks, `connect` and `admit`; listing, gating, and merging stay in the account package.
|
||||
- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge the listing does not declare, such as image budgets and a protocol mode the listing omits or sets to `null`; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it.
|
||||
- The shared transport gains only two provider hooks, `connect` and `admit`. Listing, gating, and merging live in the shared transport, which the [official route](2026-10-22-deepseek-official-model-listing.md) also uses.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ Status: implemented
|
||||
|
||||
- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.zh.md) 按凭据懒加载列表,首次访问最多等待 `modelsTimeoutMs`,超过 `modelsMaxAgeMs` 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。
|
||||
- 列出的模型在其 `client_requirements["deepseek-harness"].min_version` 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 `CLIENT_UPGRADE_REQUIRED` 或 `MODEL_UNSUPPORTED` 失败。
|
||||
- 列表中的能力逐字段覆盖内置能力;内置条目保留工具更新模式、图片预算等客户端知识;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。
|
||||
- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子;列表、门控与合并都留在账号包中。
|
||||
- 列表中的能力逐字段覆盖内置能力;内置条目保留列表没有声明的客户端知识,例如图片预算,以及列表没有给出或设为 `null` 的协议模式;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。
|
||||
- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子。列表、门控与合并位于共享传输包中,[官方路由](2026-10-22-deepseek-official-model-listing.zh.md)也使用它们。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
en: abd0263d14861926
|
||||
zh: e1736cd08e87f795
|
||||
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision:
|
||||
en: 52b45f1ac62e65ab
|
||||
zh: 7f83cd4fb8dd09a6
|
||||
en: fdd39b7e8fa89d70
|
||||
zh: 4a0dfc81509e5485
|
||||
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered:
|
||||
en: 1737484c099fab52
|
||||
zh: 303b144c52d167bd
|
||||
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences:
|
||||
en: 6ec74e7ac065bd5e
|
||||
zh: 04d029059dea732b
|
||||
en: 79d8d66071453d1f
|
||||
zh: f940f0b6d74148d6
|
||||
@@ -20,6 +20,8 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d
|
||||
|
||||
`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen, no coercion hook is overridden, and a span is bounded by the exactly representable millisecond range, which is what makes the split exact. The published surface is `fromMilliseconds` and a whole-part `decompose()`, the two entries the migrated consumer calls.
|
||||
|
||||
The last copy of the canonical instant profile moves too. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` validated a stored schedule instant with its own regular expression and round trip; the shared `fourDigitYearEpoch` owns that same profile, so the migration now asks it and keeps its own error type and message. Because that migration reads released logs, its accepted set is a compatibility promise: `packages/session/session-format-v0-to-v1/tests/validation.spec.ts` accepts the four-digit-year bounds and refuses each spelling a regular expression alone would admit (`0000`, `2026-02-30`, a four-digit fraction, a numeric offset, surrounding whitespace).
|
||||
|
||||
The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why `packages/README.md` and the `util/` line in `AGENTS.md` keep the phrase while `packages/util/README.md` and this package's own description name what it loads.
|
||||
|
||||
Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's suites assert error codes and a few unrelated message substrings, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included.
|
||||
@@ -48,4 +50,4 @@ A value type publishes what its consumers call, so the span exposes the construc
|
||||
|
||||
The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor, so a reading that is not a finite number fails where it is produced instead of rendering text. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built.
|
||||
|
||||
Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch.
|
||||
Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own offset reading for display, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format. Five time sites keep their own code, each for a different reason. `packages/experimental/tool-session-query/src/input.ts` parses a wider profile than the shared entry — seconds optional, any number of fractional digits, and the digits below the millisecond kept so two instants can be compared exactly — so substituting the shared parser would narrow what it accepts, and moving that profile into the library is a design step of its own. The `Date.parse(record.scheduledAt)` reads in `dsh-schedule` parse a value the write path already validated, so a discriminated decode would add a rejection branch that cannot occur in practice and change what a corrupt record reports. The `everySeconds`, `afterSeconds`, and `refreshMarginSeconds` multiplications each sit beside a validation that raises the package's own error, so routing them through a constructor that throws its own type would change the failure a caller sees. The UTC offset a client reports is computed in `packages/client/ui-settings-account/src/client/client-metadata.ts` instead of shared: the client bundle admits a cross-plugin value import only for the packages it lists as inline-safe, `dsh-util-time` is not one of them, and it imports the `Temporal` polyfill at module scope, so inlining it would ship that polyfill to the browser. The Desktop host computes the same offset from the shared library, where no such rule applies. `dsh-workspace` writes its record timestamps with the platform's `toISOString`, which is the canonical rendering for every representable epoch: the extended-year and fractional-truncation cases a shared writer would refuse are unreachable from `Date.now()` and from a `SessionHeader` epoch, so adopting the shared formatter there would add a module and a dependency without changing a byte.
|
||||
@@ -20,6 +20,8 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec
|
||||
|
||||
`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结,不覆盖任何强制转换钩子,且时间段受可精确表示的毫秒范围约束——这正是拆分精确的前提。公开面就是 `fromMilliseconds` 与整数量拆解 `decompose()`,即已迁移消费方所调用的两个入口。
|
||||
|
||||
最后一份规范时刻剖面副本也一并迁移。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 原本用自己的一段正则与往返比对校验持久记录里的调度时刻;共享的 `fourDigitYearEpoch` 拥有同一剖面,因此该迁移改为询问它,并保留自己的错误类型与文案。由于这个迁移要读取已发布的日志,其接受集是兼容性承诺:`packages/session/session-format-v0-to-v1/tests/validation.spec.ts` 接受四位年份两端,并拒绝只靠正则会放进来的每种拼写(`0000`、`2026-02-30`、四位小数、数字偏移、首尾空白)。
|
||||
|
||||
受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此 `packages/README.md` 与 `AGENTS.md` 里的 `util/` 一行保留该说法,而 `packages/util/README.md` 与本包自己的描述写明它加载了什么。
|
||||
|
||||
可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试断言错误码以及少量无关的消息子串,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。
|
||||
@@ -48,4 +50,4 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec
|
||||
|
||||
耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出,于是非有限的读数在产生它的地方失败,而不是渲染出一段文本。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。
|
||||
|
||||
本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。
|
||||
本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与用于展示的偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有持久读数字格式。有五处时间代码各自保留实现,原因各不相同。`packages/experimental/tool-session-query/src/input.ts` 解析的剖面比共享入口更宽——秒可省略、小数位不限、并保留下毫秒数字以便精确比较两个时刻——因此换成共享解析器会收窄它接受的输入,而把那条剖面搬进本库是另一项独立的设计。`dsh-schedule` 里的 `Date.parse(record.scheduledAt)` 读的是写入路径已经校验过的值,换成带判别值的解码会在每个读取点新增一个实际不可能发生的拒绝分支,并改变损坏记录的报告方式。`everySeconds`、`afterSeconds` 与 `refreshMarginSeconds` 三处乘法旁边都紧跟一条抛本包自有错误的校验,因此把它们改走会抛自身错误类型的构造函数,会改变调用方看到的失败。客户端上报的 UTC 偏移由 `packages/client/ui-settings-account/src/client/client-metadata.ts` 自行计算而不共享:客户端 bundle 只允许对列入 inline-safe 的包做跨插件值导入,`dsh-util-time` 不在其中,而且它在模块顶层静态导入 `Temporal` polyfill,内联它等于把该 polyfill 打进浏览器;Desktop 的宿主侧则用共享库计算同一个偏移,那里没有这条限制。`dsh-workspace` 用平台的 `toISOString` 写记录时间戳,对每个可表示的纪元那已是规范渲染:共享写入器会拒绝的扩展年份与小数截断两种情形,从 `Date.now()` 与 `SessionHeader` 的纪元出发都不可达,因此在那边采用共享格式化器只会增加一个模块与一条依赖,却不会改变任何字节。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-27-web-search-offered-by-provider-readiness.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-27-web-search-offered-by-provider-readiness.md
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready:
|
||||
en: 9163af91ea0e030d
|
||||
zh: 92e44043b999d32f
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/problem:
|
||||
en: 6fac3c73a4415c83
|
||||
zh: f65b61cc1705348b
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/decision:
|
||||
en: 458416ed7e47f644
|
||||
zh: 8b4dd820228e9dfd
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/alternatives-considered:
|
||||
en: b731254983be656d
|
||||
zh: 5dee2b09f7faa537
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/consequences:
|
||||
en: 798faeaa0b406eac
|
||||
zh: 97bbfdaebc84b1d4
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Offer web_search and web_fetch only while the selected provider is ready
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-27-web-search-offered-by-provider-readiness.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-web` registers `web_search` whenever search is enabled, and the [web capability seam decision](2026-06-24-web-capability-seam.md) keeps it visible while the selected provider cannot run. The shipped search provider, `dsh-web-search-deepseek`, authenticates a search with the DeepSeek account token only for a model request on the `deepseek-account` route; every other route needs a configured API key. A Session on an API-key route without a search key was therefore offered a tool whose every call failed with `WEB_PROVIDER_CREDENTIAL_MISSING`. Sessions in one Host can use different routes, a Session can switch routes between requests, and the Web app registers `web_search` once per agent preset, so no registration decision matches every request. The Search and Fetch APIs of `dsh-web-deepseek-api`, which the Web search settings switch selects together, need that credential for both operations, so the same Session was also offered a `web_fetch` whose every call failed.
|
||||
|
||||
## Decision
|
||||
|
||||
Each role of the web capability seam owns one part of a per-step check:
|
||||
|
||||
- **Service Definition.** `WebSearchProvider` and `WebFetchProvider` have an optional `ready(llmProvider)` that answers whether an operation made for a model request on that chat route has a credential to send. It checks configuration only and makes no network calls. `ctx.web.searchReady(llmProvider?)` and `ctx.web.fetchReady(llmProvider?)` resolve the provider with the same rules as `search()` and `fetch()` and return its answer. A provider without `ready()` counts as ready, and a selection failure answers true because the operation reports that failure as a structured `WebError`.
|
||||
- **Service Provider.** `dsh-web-search-deepseek` answers true when the route is `deepseek-account` and `ctx.deepseekAccount.resolveToken()` returns a token for the configured search endpoint, when the section carries a literal `apiKey`, or when `ctx.credentials.describe()` reports the `apiKeyEnv` reference configured (without that service, when the launching environment carries it). It checks credentials in the order `search()` resolves them, and a failed account-token or credential-store read answers false, because `search()` fails on that read instead of trying the next credential. The Search and Fetch API providers of `dsh-web-deepseek-api` share one credential and answer the same way for their API base; their transport repeats these checks, so neither package depends on the other. The providers name no tool or prompt section.
|
||||
- **Consumer.** `dsh-tool-web` registers `web_search` and `web_fetch` with [step activation conditions](../../../../packages/core/tools/README.md#conditional-tools) that ask `ctx.web.searchReady()` and `ctx.web.fetchReady()` about the route of the step's model selection. The agent loop resolves conditions before it assembles the step, so the declared tools, the `tool:web_search` and `tool:web_fetch` prompt sections, PTC program bindings, and dispatch all read the same admitted set.
|
||||
|
||||
The route decides only whether the account token is checked before the API key, which happens on the `deepseek-account` route; an API key counts on every route, so a conversation on another provider's model is offered the DeepSeek web tools whenever a DeepSeek key is configured. The providers do not compare the chat route's endpoint with their own: each operation sends the configured key, `DEEPSEEK_API_KEY` by default, to the configured endpoint, even when the official route chats through a gateway.
|
||||
|
||||
Only presence decides: no API key value is read and no endpoint is contacted, so a wrong key or an endpoint that does not serve the native `web_search_20250305` server tool keeps the tool admitted. A step that does not admit a web tool also refuses calls to it as `UNKNOWN_TOOL`, so a model that repeats an earlier call reaches no provider; an operation made outside a model step resolves its credential when it runs and fails with `WEB_PROVIDER_CREDENTIAL_MISSING` when there is none.
|
||||
|
||||
The request header records the assembled tools, so the agent loop announces each change through tool-registry updates. A route switch, a stored key, or a removed key changes the next request; DeepSeek routes load a returning tool as a deferred addition, and routes without tool-update support receive the complete tool list.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Let the DeepSeek provider remove `web_search` in its own assemble listener.** The seam stays unchanged, but the provider then names another package's tool, prompt section, and PTC transport, and it removes the tool even when the deployment selects another search provider, such as Exa, whose searches would succeed.
|
||||
|
||||
**Extend `available()`.** It is synchronous and takes no request, while this check reads an asynchronous credential store and depends on the request's route. A route-dependent `available()` would also make provider selection depend on the conversation.
|
||||
|
||||
**Read the route from the Session's latest `request/context` event instead of passing it.** That event records the previous request, so a new Session's first request and the first request after a model switch would be decided for the wrong route or for none.
|
||||
|
||||
**Register or unregister `web_search` in `dsh-tool-web`.** Registration is shared by every Session of an agent preset, while credentials depend on each request's route. Unregistering would also make `toolOrder` entries and subagent `toolFilter` restrictions that name `web_search` fail.
|
||||
|
||||
**Remove `web_search` from the assembled request.** A `system-prompt/assemble` listener could drop the declaration and its section, but the tool would stay executable, so a model that repeats an earlier call would still run it, and PTC programs would still bind it. Prompt assembly also no longer carries tool declarations.
|
||||
|
||||
**Offer search only on the DeepSeek account route.** Account tokens are issued only for the official endpoint, so a gateway key could never reach DeepSeek. But every shipped profile defaults to the `deepseek-official` route, so search would be off by default, including the official API-key case.
|
||||
|
||||
**Withhold the tools from other providers' conversations unless search has a key of its own.** A search from another model's conversation bills the DeepSeek key, but DeepSeek search is one of the services that key pays for, and a user who configured it expects search to work whatever model chats. The Web search settings page saves its key under `apiKeyEnv`, `DEEPSEEK_API_KEY` by default, which the chat route also reads, so a key entered there could never serve another model; a `routes` setting that opens them would be one more thing a user has to find.
|
||||
|
||||
**Compare the official chat route's endpoint with the search endpoint.** Withholding the tools or the key when the two endpoints differ in origin keeps a gateway's key away from DeepSeek, but the providers would have to find the chat route's configuration themselves. Taking the first active `dsh-llm-deepseek-api-key` Loader row answers for an unrelated instance when a composition isolates several, which disables a correct route or lets a gateway key through; only the route's owner knows which endpoint serves a request, and an endpoint field in `dsh-llm` would reach the browser through the Remote `listProviders()`. The check would also withhold search from users who chat through a proxy or development endpoint with a key the official search endpoint accepts. Without it, a gateway user's search fails at the configured endpoint.
|
||||
|
||||
**Keep the tool offered and fail at execution.** The model spends a call on a search that cannot authenticate, and the failure repeats on every attempt.
|
||||
|
||||
**Probe the key or endpoint before offering the tool.** A network request per model request adds latency and cost; the user controls configuration, not its validity at that moment.
|
||||
|
||||
## Consequences
|
||||
|
||||
`dsh-web` gains public members: the optional `ready()` of both provider interfaces, `WebRuntime.searchReady()`, and `WebRuntime.fetchReady()`. Each readiness method is the smallest query its one consumer needs, which is how the [observation-surface decision](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) lets a status query return; neither reports availability or selection failures. The Exa, Perplexity, and HTTP fetch providers implement no `ready()`, so their behavior is unchanged, and `web_fetch` stays admitted whenever the switch leaves HTTP fetch selected. A step that does not admit a web tool leaves it out of PTC program bindings too. An endpoint that cannot serve the native server tool keeps the tool admitted and fails each call. Removing a tool on an unchanged route changes the declared tools and misses the prompt cache once; a model switch already invalidates it. Searches and fetches from other providers' conversations bill the configured DeepSeek key, and a deployment whose official route chats through a gateway sends the gateway's key to the configured endpoint, the official one by default, where each operation fails unless that endpoint accepts the key. The DeepSeek web packages read no Loader rows and depend on no LLM package.
|
||||
|
||||
Unit tests pin each role: `packages/web/web/tests/web.spec.ts` covers which provider `searchReady()` and `fetchReady()` ask and their answer on selection failure, `packages/web/web-search-deepseek/tests/deepseek.spec.ts` and `packages/web/web-deepseek-api/tests/readiness.spec.ts` cover each credential case of `ready()` on the account route and on other routes, and `packages/web/tool-web/tests/tool-web.spec.ts` resolves step admission of both tools through the real prompt, tool, and executor services, including the `UNKNOWN_TOOL` refusal of a call in a step that withheld the tool. In real compositions, the shipped headless keyless smoke test and the `web-search-unconfigured` recorded-session snapshot, whose search key references are unset, pin the withheld search tool. Snapshot replays supply a placeholder DeepSeek key, so their tool lists do not depend on the runner's environment; with it, the `pi-ai-conversation-updates` conversation on a pi-ai route keeps `web_search`.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Agent Note: 仅在选中的提供方就绪时提供 web_search 与 web_fetch
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-27-web-search-offered-by-provider-readiness.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-web` 在启用搜索时总会注册 `web_search`,而 [web 能力 seam 决策](2026-06-24-web-capability-seam.zh.md)让它在选中的提供方无法运行时仍然可见。发行版的搜索提供方 `dsh-web-search-deepseek` 只对 `deepseek-account` 路由上的模型请求使用 DeepSeek 账号 token 鉴权;其他路由都需要已配置的 API 密钥。因此,API 密钥路由上没有搜索密钥的会话会获得一个每次调用都以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败的工具。同一 Host 中的会话可以使用不同路由,一个会话可以在请求之间切换路由,而 Web 应用按 agent preset 只注册一次 `web_search`,因此任何注册层面的决定都无法与每个请求相符。Web 搜索设置开关会同时选中 `dsh-web-deepseek-api` 的 Search 与 Fetch API,两个操作都需要这份凭据,因此同一会话还会获得一个每次调用都失败的 `web_fetch`。
|
||||
|
||||
## Decision
|
||||
|
||||
web 能力 seam 的每个角色各负责逐步骤检查的一部分:
|
||||
|
||||
- **Service Definition。** `WebSearchProvider` 与 `WebFetchProvider` 有一个可选的 `ready(llmProvider)`,回答为该对话路由上的模型请求发起的操作是否有可发送的凭据。它只检查配置,不发起网络调用。`ctx.web.searchReady(llmProvider?)` 与 `ctx.web.fetchReady(llmProvider?)` 按与 `search()`、`fetch()` 相同的规则解析提供方并返回其回答。没有 `ready()` 的提供方视为就绪;选择失败时回答 true,因为对应操作会把该失败报告为结构化 `WebError`。
|
||||
- **Service Provider。** `dsh-web-search-deepseek` 在以下任一情况下回答 true:路由为 `deepseek-account` 且 `ctx.deepseekAccount.resolveToken()` 为配置的搜索端点返回 token;配置段带有字面 `apiKey`;或 `ctx.credentials.describe()` 报告 `apiKeyEnv` 引用已配置(没有该服务时,启动环境中存在该变量)。它按 `search()` 解析凭据的顺序检查凭据,读取账号 token 或凭据存储失败时回答 false,因为 `search()` 会在该次读取上失败,而不会尝试下一种凭据。`dsh-web-deepseek-api` 的 Search 与 Fetch API 提供方共用一份凭据,并对其 API 基础地址以同样方式回答;它们的传输层重复了这些检查,因此两个包互不依赖。提供方不点名任何工具或提示词段落。
|
||||
- **Consumer。** `dsh-tool-web` 注册 `web_search` 与 `web_fetch` 时各带一个[步骤激活条件](../../../../packages/core/tools/README.zh.md#conditional-tools),以该步骤模型选择的路由询问 `ctx.web.searchReady()` 与 `ctx.web.fetchReady()`。agent loop 在组装该步骤之前解析各条件,因此声明的工具、`tool:web_search` 与 `tool:web_fetch` 提示词段落、PTC 程序绑定与派发都读取同一个准入集合。
|
||||
|
||||
路由只决定是否在 API 密钥之前检查账号 token,这只发生在 `deepseek-account` 路由上;API 密钥在任何路由上都算数,因此只要配置了 DeepSeek 密钥,使用其他提供方模型的对话也会获得 DeepSeek Web 工具。提供方不比较聊天路由的地址与自己的端点:即使官方路由经网关聊天,每次操作也把配置的密钥(默认 `DEEPSEEK_API_KEY`)发往配置的端点。
|
||||
|
||||
只看凭据是否存在:不读取 API 密钥的值,也不访问任何端点,因此错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,仍会让该工具保持准入。未准入某个 Web 工具的步骤也会以 `UNKNOWN_TOOL` 拒绝对它的调用,因此模型重复先前的调用也到达不了任何提供方;在模型步骤之外发起的操作在运行时解析凭据,没有凭据时以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败。
|
||||
|
||||
请求头记录组装后的工具,因此 agent loop(智能体循环)通过工具注册表更新宣告每次变化。路由切换、存储密钥或删除密钥都会改变下一个请求;DeepSeek 路由以延迟加载的新增方式载入重新提供的工具,不支持工具更新的路由则接收完整工具列表。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**让 DeepSeek 提供方在自己的 assemble 监听器中移除 `web_search`。** seam 保持不变,但提供方因此要点名另一个包的工具、提示词段落与 PTC 传输工具;而且即使部署选择了另一个搜索成功的提供方(例如 Exa),它也会移除该工具。
|
||||
|
||||
**扩展 `available()`。** 它是同步的且不接收请求,而这项检查要读取异步凭据存储,并取决于请求的路由。依赖路由的 `available()` 还会让提供方选择取决于对话。
|
||||
|
||||
**从会话最近的 `request/context` 事件读取路由,而不是传入路由。** 该事件记录的是上一个请求,因此新会话的首个请求,以及模型切换后的首个请求,会按错误的路由判断,或根本没有路由可用。
|
||||
|
||||
**在 `dsh-tool-web` 中注册或注销 `web_search`。** 注册由同一 agent preset 的所有会话共享,而凭据取决于每个请求的路由。注销还会使点名 `web_search` 的 `toolOrder` 条目和 subagent `toolFilter` 限制失败。
|
||||
|
||||
**从组装好的请求中移除 `web_search`。** `system-prompt/assemble` 监听器可以删掉该声明及其段落,但工具仍可执行,因此模型重复先前的调用时仍会运行它,PTC 程序也仍能绑定它。提示词组装也已不再携带工具声明。
|
||||
|
||||
**只在 DeepSeek 账号路由上提供搜索。** 账号 token 只对官方端点签发,因此网关的密钥不可能到达 DeepSeek。但所有发行 profile 的默认路由都是 `deepseek-official`,搜索会因此默认关闭,连官方 API 密钥的情况也一并关闭。
|
||||
|
||||
**除非搜索有自己的密钥,否则不向其他提供方的对话提供这些工具。** 其他模型对话中的搜索会由 DeepSeek 密钥计费,但 DeepSeek 搜索本就是该密钥付费使用的服务之一,配置了它的用户也期望无论用哪个模型对话,搜索都能用。Web 搜索设置页把密钥存在 `apiKeyEnv` 指向的引用下,默认是聊天路由也在读取的 `DEEPSEEK_API_KEY`,因此在那里输入的密钥永远无法服务其他模型;再加一个开放这些路由的 `routes` 配置项,只会让用户多找一样东西。
|
||||
|
||||
**比较官方聊天路由的端点与搜索端点。** 两个端点不同源时撤下工具或不发送密钥,能让网关的密钥远离 DeepSeek,但提供方得自己找出聊天路由的配置。取第一条激活的 `dsh-llm-deepseek-api-key` Loader 行时,如果组合隔离了多个实例,回答的可能是无关的实例,从而关掉正确的路由,或放过网关的密钥;只有路由的所有者知道哪个端点服务某个请求,而在 `dsh-llm` 加地址字段会让它经 Remote 方法 `listProviders()` 进入浏览器。这项检查还会让经代理或开发端点对话、而密钥能被官方搜索端点接受的用户失去搜索。不做这项检查时,网关用户的搜索会在配置的端点上失败。
|
||||
|
||||
**保持工具可见,在执行时失败。** 模型会把一次调用花在无法鉴权的搜索上,并且每次尝试都会重复失败。
|
||||
|
||||
**提供工具前探测密钥或端点。** 每个模型请求都发起一次网络请求会增加延迟与成本;用户控制的是配置本身,而不是它在那一刻是否有效。
|
||||
|
||||
## Consequences
|
||||
|
||||
`dsh-web` 增加以下公共成员:两个提供方接口上可选的 `ready()`、`WebRuntime.searchReady()` 与 `WebRuntime.fetchReady()`。每个就绪方法都是其唯一消费方所需的最小查询,这正是[观测接口决策](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)允许状态查询回归的方式;它们既不报告可用性,也不报告选择失败。Exa、Perplexity 与 HTTP 抓取提供方没有实现 `ready()`,其行为不变;只要开关让 HTTP 抓取保持选中,`web_fetch` 就始终准入。未准入某个 Web 工具的步骤,其 PTC 程序绑定中也没有它。无法提供原生服务器工具的端点仍会让工具保持准入,并使每次调用失败。在路由不变时移除工具会改变声明的工具,并使提示词缓存失效一次;模型切换本身已经使缓存失效。其他提供方对话中的搜索与抓取由配置的 DeepSeek 密钥计费;官方路由经网关对话的部署会把网关的密钥发往配置的端点(默认是官方端点),除非该端点接受这个密钥,否则每次操作都会失败。DeepSeek Web 包不读取 Loader 的行,也不依赖任何 LLM 包。
|
||||
|
||||
单元测试分别固定每个角色:`packages/web/web/tests/web.spec.ts` 覆盖 `searchReady()` 与 `fetchReady()` 询问哪个提供方以及选择失败时的回答,`packages/web/web-search-deepseek/tests/deepseek.spec.ts` 与 `packages/web/web-deepseek-api/tests/readiness.spec.ts` 覆盖 `ready()` 在账号路由与其他路由上的每种凭据情况,`packages/web/tool-web/tests/tool-web.spec.ts` 通过真实的提示词、工具与执行器服务解析两个工具的步骤准入,包括在撤下该工具的步骤中调用它时以 `UNKNOWN_TOOL` 拒绝。在真实组合中,发行版 headless 无密钥冒烟测试与 `web-search-unconfigured` 录制会话快照的搜索密钥引用都未设置,二者固定了搜索工具的撤下行为。快照回放提供占位 DeepSeek 密钥,因此其工具列表不依赖运行环境;借助该密钥,`pi-ai-conversation-updates` 中 pi-ai 路由上的对话保留 `web_search`。
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
en: a15c7c13f167e5ac
|
||||
zh: 63bcc2ef60c2f344
|
||||
/agent-note-boot-the-preview-worker-from-the-shipped-profile-and-fetch-lazy-chunks-on-demand/decision/boot-the-same-profile-as-the-served-web-deployment:
|
||||
en: caf1ca77192dcb20
|
||||
zh: d663e2cbb05fe4fc
|
||||
en: 9b957e37f290abaf
|
||||
zh: 06a4599c8fe449ad
|
||||
/agent-note-boot-the-preview-worker-from-the-shipped-profile-and-fetch-lazy-chunks-on-demand/decision/node-internal-module-interfaces-in-the-worker:
|
||||
en: 37d8e809eaa21d25
|
||||
zh: 8c7c31bd6f45c823
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ Measured: with no bundle enabled, the two compositions differ in only one of 188
|
||||
The served launch path (apps/cli, app-boot) carries no Preview-specific code; the webworker packages reproduce it:
|
||||
|
||||
- **packer**
|
||||
- Package roster = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + the rows named in those bundles' patch files; the bundle packages themselves also enter the image (patches, locale, and icon through the publish view). Both tables are read from app-boot, the same source the Plugins page uses.
|
||||
- Package roster = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + the rows named in those bundles' patch files; the bundle packages themselves also enter the image (patches, locale, and icon through the publish view). Both tables are read from `@deepseek-ai/dsh-official-bundles`, the same source the Plugins page uses.
|
||||
- The image carries a manifest-only `node_modules/@deepseek-ai/dsh/package.json` (a verbatim copy of `apps/cli/package.json`) as the installation anchor, and the image manifest names it in `installation`.
|
||||
- The image carries no composed configuration and no `config/cordis.yml`; the image manifest records the `profile` the Worker boots.
|
||||
- The image carries a home patch, `home/cordis.patch.yml`: for each row the runtime's `PLATFORM_ENTRY_CONFIG` names (`session-persistence-jsonl`), the config the template's bundle layers compose with the required fields over it (`compression: none`). A patch replaces a row's whole config, so the composed fields stay; the bundle files read at pack time are the ones in the image.
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ Status: implemented
|
||||
正式启动链路(apps/cli、app-boot)不含 Preview 专用代码;由 webworker 包复现这条链路:
|
||||
|
||||
- **packer**
|
||||
- 收包清单 = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + 这些 bundle 补丁文件里的行;bundle 包本身也打进镜像(补丁、locale、icon 走 publish view)。两张表从 app-boot 读,与插件页同一来源。
|
||||
- 收包清单 = `PROFILE_TEMPLATES[profile].bundles` + `OPTIONAL_BUNDLES` + 这些 bundle 补丁文件里的行;bundle 包本身也打进镜像(补丁、locale、icon 走 publish view)。两张表从 `@deepseek-ai/dsh-official-bundles` 读,与插件页同一来源。
|
||||
- 镜像里放一份只含清单的 `node_modules/@deepseek-ai/dsh/package.json`(原样复制 `apps/cli/package.json`),作为安装锚点,镜像 manifest 在 `installation` 里记录它。
|
||||
- 镜像不带组合好的配置,也没有 `config/cordis.yml`;镜像 manifest 记录 Worker 要启动的 `profile`。
|
||||
- 镜像带一份 home patch `home/cordis.patch.yml`:对运行时 `PLATFORM_ENTRY_CONFIG` 列出的每一行(`session-persistence-jsonl`),写入模板各 bundle 层合成出的配置,并在其上设置所需字段(`compression: none`)。补丁会整体替换一行的配置,所以保留合成出的字段;打包时读取的 bundle 文件就是镜像里的那些。
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-21-replaceable-icon-sprite.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-21-replaceable-icon-sprite.md
|
||||
/agent-note-replaceable-product-icons-through-an-svg-sprite:
|
||||
en: 2b527e550e071451
|
||||
zh: 7de256912270cae0
|
||||
/agent-note-replaceable-product-icons-through-an-svg-sprite/problem:
|
||||
en: d55557fe41ab87c8
|
||||
zh: eea3f7d0c82561bf
|
||||
/agent-note-replaceable-product-icons-through-an-svg-sprite/decision:
|
||||
en: 6ddd80eae56693dd
|
||||
zh: 4d654a76eb64c50a
|
||||
/agent-note-replaceable-product-icons-through-an-svg-sprite/alternatives-considered:
|
||||
en: 2e25ece88bccdb22
|
||||
zh: bb24a28bef7ea7ce
|
||||
/agent-note-replaceable-product-icons-through-an-svg-sprite/consequences:
|
||||
en: 0ad6db37bbb76ddb
|
||||
zh: 0ca09d55a2167970
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Replaceable product icons through an SVG sprite
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-21-replaceable-icon-sprite.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Long-term skin customization needs themes to replace icons, not only colors. Product icons were React components that drew inline SVG paths, so a theme registered through `ctx.theme` could recolor them through `currentColor` but could not change their artwork. About 150 Client files import these components.
|
||||
|
||||
## Decision
|
||||
|
||||
Each product glyph weight is one sprite symbol, `dsw-icon-<glyph>-<weight>`. ui-primitives keeps the default SVG documents as `ICON_SYMBOLS`; an icon component keeps its export name, `size`, and `className`, and renders an `<svg>` box with one `<use>` of its symbol. ui-theme keeps one hidden sprite in the document whose `<symbol>` children come from the active theme's `ThemeDefinition.icons` replacements or the defaults. Artwork reached through `<use>` inherits `currentColor` and CSS custom properties from the use site, so a replacement can be multi-color, can paint with `--dsw-*` color tokens, and follows light and dark palettes without separate values.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Icons as CSS image tokens painted through `mask-image`.** Rejected after design review: a mask keeps only the image's alpha, so every icon stays one color, and a `url()` image cannot read CSS variables, so multi-color artwork could not follow color tokens. It would have reused the token override layer without a new theme field.
|
||||
- **A runtime icon registry service with React components as values.** Rejected: components cannot read services or React context, so every icon import site would need a new prop or slot.
|
||||
- **A slot per icon.** Rejected for the same caller cost; slots carry composition, not artwork.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Icon DOM contains a `<use>` reference instead of paths; tests identify glyphs through `data-icon` and inspect artwork through `ICON_SYMBOLS`.
|
||||
- Without ui-theme, the sprite is absent and icons render nothing.
|
||||
- Every symbol shares the document id space, so `register` requires element ids inside a replacement to start with `<symbol id>-`; `<use>` in another document, such as an iframe, cannot reach the sprite.
|
||||
- Replacement markup is trusted like the registering plugin's code; `register` rejects `<style>` and `<script>` only because they would apply to the whole document.
|
||||
- Gradients and other paint servers resolve their stops where the sprite sits, so they follow palette tokens but not use-site color overrides.
|
||||
- Default and token-colored replacement symbols render pixel-identically through `<use>` and inline in Chromium, WebKit, and Firefox.
|
||||
- File-type, plugin, site, and brand artwork still render inline and are not replaceable yet.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 通过 SVG sprite 实现可替换的产品图标
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-21-replaceable-icon-sprite.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
长期的皮肤定制需要主题能替换图标,而不只是颜色。产品图标原先是绘制内联 SVG 路径的 React 组件,因此通过 `ctx.theme` 注册的主题可以借助 `currentColor` 给它们换色,却无法更换图形。约 150 个 Client 文件导入了这些组件。
|
||||
|
||||
## 决策
|
||||
|
||||
每个产品图形的每种线重对应一个 sprite symbol `dsw-icon-<glyph>-<weight>`。ui-primitives 以 `ICON_SYMBOLS` 保存默认 SVG 文档;图标组件保留导出名、`size` 与 `className`,渲染一个 `<svg>` 框,其中包含一个引用自身 symbol 的 `<use>`。ui-theme 在文档中保留一个隐藏的 sprite,其 `<symbol>` 子元素来自活动主题的 `ThemeDefinition.icons` 替换项或默认值。通过 `<use>` 引用的图形会从使用处继承 `currentColor` 与 CSS 自定义属性,因此替换图形可以是多色的,可以用 `--dsw-*` 颜色 token 绘制,并且无需单独的值就能跟随浅色与深色调色板。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **把图标作为通过 `mask-image` 绘制的 CSS 图像 token。** 经设计评审后否决:遮罩只保留图像的 alpha,因此每个图标都只能是单色;`url()` 图像无法读取 CSS 变量,因此多色图形无法跟随颜色 token。该方案本可复用 token 覆盖层,而无需新增主题字段。
|
||||
- **以 React 组件为值的运行时图标注册服务。** 否决:组件无法读取服务或 React context,因此每个图标导入处都需要新的 prop 或 slot。
|
||||
- **每个图标一个 slot。** 因同样的调用方成本而否决;slot 承载的是组合,而不是图形。
|
||||
|
||||
## 后果
|
||||
|
||||
- 图标 DOM 包含 `<use>` 引用而不是路径;测试通过 `data-icon` 识别图形,通过 `ICON_SYMBOLS` 检查图形。
|
||||
- 没有 ui-theme 时,sprite 不存在,图标不渲染任何内容。
|
||||
- 所有 symbol 共享文档的 id 空间,因此 `register` 要求替换图形内的元素 id 以 `<symbol id>-` 开头;另一个文档(例如 iframe)中的 `<use>` 无法引用该 sprite。
|
||||
- 替换标记与注册它的插件代码同等受信任;`register` 仅拒绝 `<style>` 与 `<script>`,因为它们会作用于整个文档。
|
||||
- 渐变及其他绘制服务器在 sprite 所在位置解析其色标,因此它们跟随调色板 token,但不跟随使用处的颜色覆盖。
|
||||
- 默认 symbol 与使用 token 着色的替换 symbol 在 Chromium、WebKit 与 Firefox 中通过 `<use>` 渲染与内联渲染的结果逐像素一致。
|
||||
- 文件类型、插件、站点与品牌图形仍以内联方式渲染,暂不可替换。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-22-deepseek-official-model-listing.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.md
|
||||
/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized:
|
||||
en: 20de6c2a1bc643b4
|
||||
zh: 2fe21450cebf7f32
|
||||
/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/problem:
|
||||
en: 51a513e6134e6ad9
|
||||
zh: e0b19412c9b88e24
|
||||
/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/decision:
|
||||
en: ceb2f960085577c3
|
||||
zh: 1bbc833b7cf24070
|
||||
/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/alternatives-considered:
|
||||
en: fa126e4f4e0790b4
|
||||
zh: c4f6d266d1ba6753
|
||||
/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/consequences:
|
||||
en: 047e30a0f55acb24
|
||||
zh: 804e15532d172157
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: The official DeepSeek route follows the official model listing until its catalog is customized
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-22-deepseek-official-model-listing.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The `deepseek-official` route offered whatever its `models` setting held, and that setting defaults to the catalog bundled with each build. A model the DeepSeek API added reached API-key users only through a release, while the account route already followed the service's `GET /models` listing at the same origin. Unlike the account route's catalog, the official route's catalog is user-editable: the Models page stores an edited catalog in `models`, and a gateway behind `baseURL` declares its models there.
|
||||
|
||||
## Decision
|
||||
|
||||
- [`llm-deepseek-api-key`](../../../../packages/llm/llm-deepseek-api-key/README.md) follows `GET https://api.deepseek.com/models`, authenticated with the API key in `x-api-key`, only while the Messages origin is `https://api.deepseek.com` and `models` equals the built-in catalog. Any other endpoint or catalog sends no listing request.
|
||||
- Loading, refresh, fallback, merging, and the client-version gate are the [account route's](2026-09-24-deepseek-account-remote-model-catalog.md): a first access waits up to `modelsTimeoutMs`, a listing older than `modelsMaxAgeMs` refreshes in the background on the next access, a failure keeps the last good listing or the built-in catalog, and the cache lives in memory, keyed by endpoint and key fingerprint.
|
||||
- A customized `models` list wins whole: it replaces the catalog and stops listing requests. A list is customized when it resolves differently from the built-in catalog, because the schema default materializes an absent `models` as the built-in list and the Models page shows that default as its inherited rows.
|
||||
- Switching between a customized catalog and the listing invalidates the cache from a `loader/volatile-update` listener, so Restore defaults reloads the listing on the next access even when the cached listing is fresh.
|
||||
- The listing request, its cache, catalog resolution, and the gate live in the shared [Messages transport](../../../../packages/llm/llm-deepseek/README.md) because two routes consume them; the transport's provider hooks remain `connect` and `admit`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Follow the listing on every endpoint.** A gateway that publishes an OpenAI-style `/models` returns entries without `api_capabilities.anthropic_messages`, which the decoder hides, so the route would offer no models. Every such gateway would also receive an extra request carrying the key.
|
||||
|
||||
**Merge the customized catalog with the listing by model id.** Customized users would keep receiving new listed models, but a model the user deleted would return, and the Models page cannot hide one listed model.
|
||||
|
||||
**Mark an unset `models` by removing its schema default.** An absent value would select the listing without comparing catalogs, but the Models page would lose the inherited rows it shows and copies, with their hidden `systemPromptUpdate` and `toolUpdate` fields, into a customized catalog. Rows copied from the generic model-discovery API would drop those fields.
|
||||
|
||||
**Fetch once per process, or refresh on a timer.** A single fetch leaves a long-running Web or desktop host on its startup catalog, and one transient failure pins it to the built-in catalog. A timer sends requests while the route is idle. Access-triggered background refresh matches the account route.
|
||||
|
||||
**Reuse the cached listing after Restore defaults.** The cache could serve a listing loaded before the customization for up to `modelsMaxAgeMs`; the request is cheap, so restoring always reloads it. The reload starts on the next access rather than inside the listener, which keeps credential lookup and its failures on the request path; the Web page's settings refresh makes that access right after the save.
|
||||
|
||||
## Consequences
|
||||
|
||||
- API-key users on the official endpoint receive listed models without a release, and an older build hides or refuses a model that needs a newer client, as account users' builds do.
|
||||
- A customized catalog receives no later listed models until Restore defaults, and a `models` list identical to the built-in catalog counts as uncustomized. The comparison uses the running version's built-in catalog, so a saved list equal to an earlier version's built-in catalog counts as customized once the built-in catalog changes.
|
||||
- The Models page shows the built-in rows while the listing is followed, so a model only the listing offers appears only in model selectors, and editing the page's catalog drops it.
|
||||
- The first catalog access on the official endpoint can wait up to `modelsTimeoutMs`, and a suite that boots the route there must answer the listing itself to stay off the network.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Agent Note: DeepSeek 官方路由在目录未自定义时跟随官方模型列表
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-22-deepseek-official-model-listing.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`deepseek-official` 路由提供的是 `models` 设置里的内容,而这个设置默认是每个构建自带的目录。DeepSeek API 新增的模型只能随发版到达 API key 用户,而账号路由已经在同一个 origin 上跟随服务端的 `GET /models` 列表。与账号路由不同,官方路由的目录可以由用户编辑:模型页把编辑后的目录存进 `models`,`baseURL` 后面的网关也在这里声明自己的模型。
|
||||
|
||||
## 决策
|
||||
|
||||
- [`llm-deepseek-api-key`](../../../../packages/llm/llm-deepseek-api-key/README.zh.md) 只在 Messages 的 origin 是 `https://api.deepseek.com`、且 `models` 等于内置目录时,跟随 `GET https://api.deepseek.com/models`,并用 `x-api-key` 携带 API key 鉴权。其他端点或目录不发列表请求。
|
||||
- 加载、刷新、兜底、合并与客户端版本门控沿用[账号路由](2026-09-24-deepseek-account-remote-model-catalog.zh.md)的规则:首次访问最多等待 `modelsTimeoutMs`,列表超过 `modelsMaxAgeMs` 后在下一次访问时后台刷新,失败时保留上一份成功的列表或使用内置目录,缓存只在内存中,以端点和 key 指纹为键。
|
||||
- 自定义的 `models` 列表整份优先:它替换目录,并停止列表请求。解析结果与内置目录不同的列表才算自定义,因为 schema 默认值会把缺省的 `models` 物化为内置列表,模型页也把这个默认值显示为继承的行。
|
||||
- 在自定义目录与列表之间切换时,一个 `loader/volatile-update` 监听器会让缓存失效,因此点恢复默认后,即使缓存的列表尚未过期,下一次访问也会重新加载。
|
||||
- 列表请求、缓存、目录解析和门控放在共享的 [Messages 传输包](../../../../packages/llm/llm-deepseek/README.zh.md)中,因为现在有两条路由使用它们;传输层的提供方钩子仍然只有 `connect` 和 `admit`。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**在所有端点上跟随列表。** 发布 OpenAI 风格 `/models` 的网关返回的条目没有 `api_capabilities.anthropic_messages`,解码器会隐藏它们,路由就不提供任何模型。每个这样的网关还会多收到一个携带 key 的请求。
|
||||
|
||||
**按模型 id 合并自定义目录与列表。** 自定义用户会继续收到新列出的模型,但用户删掉的模型会重新出现,而模型页无法隐藏某一个列出的模型。
|
||||
|
||||
**去掉 `models` 的 schema 默认值来标记未设置。** 缺省值不用比较目录就能选中列表,但模型页会失去它显示的继承行;这些行连同隐藏的 `systemPromptUpdate`、`toolUpdate` 字段会被复制进自定义目录。从通用模型发现 API 复制的行会丢掉这些字段。
|
||||
|
||||
**每个进程只获取一次,或按定时器刷新。** 只获取一次会让常驻的 Web 或桌面宿主停留在启动时的目录,一次偶发失败就会让它固定在内置目录。定时器会在路由空闲时发请求。由访问触发的后台刷新与账号路由一致。
|
||||
|
||||
**恢复默认后沿用缓存的列表。** 缓存可能在 `modelsMaxAgeMs` 内一直提供自定义之前加载的列表;列表请求代价很小,所以恢复默认总是重新加载。重新加载从下一次访问开始,而不是在监听器里进行,这样凭据查询及其失败都留在请求路径上;Web 页面保存后的设置刷新会立即发起这次访问。
|
||||
|
||||
## 影响
|
||||
|
||||
- 使用官方端点的 API key 用户无需发版就能看到列出的模型,旧构建会像账号用户的构建一样隐藏或拒绝需要新版客户端的模型。
|
||||
- 自定义目录在恢复默认之前收不到之后列出的模型;与内置目录完全相同的 `models` 列表视为未自定义。比较的是当前版本的内置目录,所以保存的列表若等于旧版本的内置目录,内置目录变化后就算作自定义。
|
||||
- 跟随列表期间,模型页仍显示内置的行,因此只有列表提供的模型只出现在模型选择器里,在模型页编辑目录会丢掉它。
|
||||
- 官方端点上的第一次目录访问可能等待最多 `modelsTimeoutMs`,在该端点上启动路由的测试套件必须自己应答列表请求,才能不访问网络。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-23-independent-session-forks.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-23-independent-session-forks.md
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship:
|
||||
en: 543a84d51e8aea24
|
||||
zh: 24aeec7c3cdf5900
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship/problem:
|
||||
en: d2edaafdd83e5974
|
||||
zh: 57ce9ad368c404c2
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship/decision:
|
||||
en: aae0053e9dc3be9f
|
||||
zh: bd37095fd5c43c3d
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship/compatibility:
|
||||
en: 8393e8526f927134
|
||||
zh: c957cbde9dc6712d
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship/alternatives-considered:
|
||||
en: 748aa21283186e02
|
||||
zh: 51d3b0207b250784
|
||||
/agent-note-ordinary-forks-have-no-durable-creator-relationship/consequences:
|
||||
en: adcb06ccf7503e55
|
||||
zh: 30e02c6104eefe01
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Ordinary forks have no durable creator relationship
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-23-independent-session-forks.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Using `parentSession` for both copied history and delegation makes an ordinary fork appear to be a subagent. Consumers can emit false child-start notifications, hide resumable sessions, or impose source-relative ordering. Removing the writer alone leaves historical ordinary forks misclassified and exposes a V4 reader assumption: inherited delivery acknowledgements require a parent id even though the validated seed range already identifies copied events.
|
||||
|
||||
## Decision
|
||||
|
||||
Ordinary forks preserve their complete selected prefix, `isSeeded`, `inheritedEventCount`, and inherited end-seed marker, but record no source id. Subagents retain their direct creator, including when their backend copies history. `origin: subagent` supplies coarse classification; a valid own-range descriptor separately supplies continuation identity. Historical parent metadata remains readable and unchanged. Runtime Agent ownership remains governed by the [explicit identity decision](2026-08-31-explicit-agent-runtime-identity.md).
|
||||
|
||||
The V4 reader permits another Session's delivery id only inside the validated inherited range. Own-range markers still require the current id, and every active-generation `throughSeq` must precede its marker. These checks do not require a persistent source relationship.
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is a same-version behavior change with no declared persistence-type change. The generated inventory reports no schema transitions, so no new type acknowledgement or finalization checkpoint is needed. Existing ordinary forks and subagent logs retain their meanings, and the new reader accepts both parent-bearing and parentless seeds without rewriting either. Frozen schemas and historical generations remain untouched.
|
||||
|
||||
The released `dsh-v0.2.0-rc.2` validator and the reviewed master baseline share the parent-dependent delivery check. Before changing that check, real plain and compressed JSONL probes verified rejection through both read and write opens for parentless ordinary and nested forks, with byte-identical files afterward. Rejection occurs during completed-log relationship validation, before a handle is granted or tail repair runs. Older readers report corruption rather than an upgrade diagnostic; that diagnostic limitation is explicit in the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.md). Parentless seeds without active inherited delivery markers remain readable. This observed admission behavior meets the [safe-refusal rule](../process/2026-10-08-session-reader-compatibility-review.md); optionality alone is not the justification.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the source under another field.** A replacement such as `forkSourceId` preserves the durable relationship the user chose to remove, along with its downstream maintenance cost.
|
||||
|
||||
**Erase parent metadata from existing logs.** Historical mutation loses recorded information and violates immutable-generation guarantees. Consumers distinguish old ordinary forks without cleanup.
|
||||
|
||||
**Bump the format solely because older readers reject some new logs.** The existing inherited range and complete-log check prevent unsafe execution and truncation. A global bump and adjacent copy add no protection to this transition; same-version refusal has a documented diagnostic limitation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary-fork source tracing is intentionally unavailable in new data. Manual ordering preserves saved positions and treats new forks like other ordinary sessions. Operation-local source access preserves title increments and workspace selection. The [side-session proposal](../../proposed/feature/2026-07-08-interactive-side-sessions.md) retains its separate handback idea but cannot use a stored ordinary-fork parent. Runtime ownership, projection-cache identity, and Team membership decisions remain independently useful and are not superseded.
|
||||
|
||||
Verification covers real fork/persist/reopen paths for ordinary, nested, historical, and subagent seeds, rejection without writes for foreign own-range delivery and invalid coordinates, SDK notification classification, ACP resume, client ordering, and title generation. TypeScript and Python recorded SDK scenarios create ordinary and nested forks plus historical parent-bearing metadata while retaining the expected real-subagent lifecycle.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 普通 fork 不保留持久创建者关系
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-23-independent-session-forks.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
同时用 `parentSession` 表示复制历史和委派,会让普通 fork 看起来像子代理。消费方可能发送错误的子代理启动通知、隐藏可恢复会话,或施加相对来源的排序。仅删除写入仍会误分类历史普通 fork,并暴露 V4 reader 的一项假设:继承投递确认要求父 ID,尽管已验证的 seed 区间已经标识复制事件。
|
||||
|
||||
## 决策
|
||||
|
||||
普通 fork 保留完整的选定前缀、`isSeeded`、`inheritedEventCount` 及继承结束标记,但不记录来源 ID。子代理保留直接创建者,包括 backend 复制历史的情况。`origin: subagent` 提供粗粒度分类;有效的自身区间 descriptor 单独提供继续运行身份。历史父元数据保持可读且不变。运行时 Agent 归属仍遵循[显式身份决策](2026-08-31-explicit-agent-runtime-identity.zh.md)。
|
||||
|
||||
V4 reader 仅在已验证的继承区间中允许其他 Session 的投递 ID。自身区间标记仍要求当前 ID,每个活动代际的 `throughSeq` 必须早于其标记。这些检查不需要持久来源关系。
|
||||
|
||||
## 兼容性
|
||||
|
||||
这是没有声明持久化类型变化的同版本行为变更。生成清单报告没有 schema 转换,因此不需要新增类型确认或定稿检查点。已有普通 fork 和子代理日志保留其含义,新 reader 接受有父关系及无父关系的 seed,且不改写任一种。冻结 schema 与历史代际保持不变。
|
||||
|
||||
已发布的 `dsh-v0.2.0-rc.2` 校验器与所审查 master 基线具有相同的依赖父关系的投递检查。修改此检查之前,真实的普通及压缩 JSONL 探针验证:无父关系的普通和嵌套 fork 在 read、write 打开时都被拒绝,之后文件字节完全一致。拒绝发生在完整日志的关系校验期间,早于授予句柄或尾部修复。旧 reader 报告损坏而非升级诊断;[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.3/independent-session-forks/guide.zh.md)明确说明这一诊断限制。没有活动继承投递标记的无父关系 seed 仍然可读。这一已观察到的接纳行为满足[安全拒绝规则](../process/2026-10-08-session-reader-compatibility-review.zh.md);可选字段本身不是依据。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**用其他字段保留来源。** `forkSourceId` 等替代字段保留了用户决定移除的持久关系及其下游维护成本。
|
||||
|
||||
**清除已有日志的父元数据。** 修改历史会丢失已记录信息,并违反代际不可变保证。消费方可以在不清理的情况下区分旧普通 fork。
|
||||
|
||||
**仅因旧 reader 拒绝部分新日志就升级格式。** 现有继承区间和完整日志检查阻止不安全执行与截断。全局升级和相邻复制不会为这次转换增加保护;同版本拒绝的诊断限制已有明确记录。
|
||||
|
||||
## 影响
|
||||
|
||||
新数据有意不支持普通 fork 的来源追踪。手动排序保留已保存位置,将新 fork 当作其他普通会话处理。操作期间访问来源保留标题递增和工作区选择。[侧会话提案](../../proposed/feature/2026-07-08-interactive-side-sessions.zh.md)保留独立的交回设想,但不能使用普通 fork 的持久父字段。运行时归属、投影缓存身份和 Team 成员关系决策仍分别有用,未被取代。
|
||||
|
||||
验证覆盖普通、嵌套、历史及子代理 seed 的真实 fork/持久化/重新打开路径,自身区间外来投递及非法坐标的无写入拒绝,SDK 通知分类、ACP 恢复、客户端排序与标题生成。TypeScript 和 Python 录制 SDK 场景创建普通及嵌套 fork 和携带历史父元数据的会话,同时保留真正子代理的预期生命周期。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-21-subagent-end-turn-guidance.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-10-21-subagent-end-turn-guidance.md
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph:
|
||||
en: bfafb6d348cf2d21
|
||||
zh: 191bedbfa12b6329
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/problem:
|
||||
en: 36ff1fff46915956
|
||||
zh: 198c8ca7efaeb5b6
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/decision:
|
||||
en: 47b44ed9313c2ccf
|
||||
zh: 7ad254f8ef88351d
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/alternatives-considered:
|
||||
en: d52fa1af379dea5b
|
||||
zh: 4e404994f7a607dc
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/verification:
|
||||
en: be076216381ea64d
|
||||
zh: 2da8d9819c20e2fe
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/consequences:
|
||||
en: d7c144cbe750a873
|
||||
zh: 602c06d08572481c
|
||||
@@ -0,0 +1,44 @@
|
||||
# Agent Note: Subagent end-turn guidance stays in the delegation paragraph
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-21-subagent-end-turn-guidance.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The delegation guidance paragraph ended with `When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn.` A later prompt revert removed that sentence together with unrelated prompt changes.
|
||||
|
||||
Before the sentence existed, one long Session slept seven times for 150–240 seconds each (about 25 minutes) waiting for review subagents that finished in 2–9 minutes (#6057). A controlled probe on 2026-10-21 measured the sentence's effect on the model's next message after a child starts: `deepseek-v4-flash` and `deepseek-v4-pro`, 150 requests per arm per model, minimal system prompt, real `subagent` and `bash` schemas, history ending with `started subagent reviewer-1`.
|
||||
|
||||
| Arm | Sleep call | Any bash call | Ended turn |
|
||||
|---|---|---|---|
|
||||
| With the sentence | 0/300 | 7/300 | 291/300 |
|
||||
| Without the sentence | 29/300 | 120/300 | 179/300 |
|
||||
|
||||
The per-model difference is `0/150` vs `6/150` for flash (p=0.030) and `0/150` vs `23/150` for pro (p=9.6e-8), two-sided Fisher exact. The two remaining samples in the with-sentence arm called `subagent` again instead of ending the turn.
|
||||
|
||||
## Decision
|
||||
|
||||
The delegation paragraph keeps the end-turn sentence, byte-identical to the text introduced for #6057. Ending the turn does not lose the result: a settled child delivers a user-role notice to its parent, and a notice that reaches an idle parent starts one independent model request ([`dsh-subagent`](../../../../packages/subagent/subagent/README.md)). Hosts that keep children progressing alternate `agent.whenIdle()` and `waitForChildren(agent)` ([ACP `session.ts`](../../../../packages/acp/acp/src/session.ts)), and `dsh-goal-round-driver` waits for running descendants before starting the next goal round.
|
||||
|
||||
The sentence stays one literal line. Rewording `the next result starts a new turn` to `each result starts a new turn` would re-render 83 prompt sidecars without a measured behavior change.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Rely on the tool description.** The `subagent` tool description states that the runtime notifies the model when the child finishes. The probe measured 29/300 sleep calls without the paragraph sentence, so the tool description does not replace it.
|
||||
|
||||
**Move the guidance into the tool description.** The paragraph is assembled from the set of visible delegation tools and lists their names; the tool description describes one tool. Moving the sentence changes ownership without changing the behavior it prevents.
|
||||
|
||||
**Restore every prompt phrase reverted together with this sentence.** The read guidance and the `run_code` argument order are separate decisions with their own evidence; this note records only the delegation paragraph.
|
||||
|
||||
## Verification
|
||||
|
||||
- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` asserts the rendered paragraph for each visible-tool combination, including the restored sentence (9 expectations).
|
||||
- 83 `snapshots/**/system-prompt*.expected.md` sidecars carry the restored text and replay through the shipped headless profile in `test:snapshot`.
|
||||
- The probe is a one-off controlled measurement and is not committed; its two arms differ only in the guidance sentence.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every request with a visible delegation tool carries one more sentence, about 30 tokens.
|
||||
- The model ends its turn while only waiting for a delegated result instead of calling `bash sleep` or polling.
|
||||
- A future prompt-slimming pass must treat this sentence as load-bearing; the probe table above is the evidence for keeping it.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Agent Note: 委派指导段保留结束本轮说明
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-21-subagent-end-turn-guidance.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
委派指导段以 `When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn.` 结尾。后来的一次提示词回退把这句和不相关的提示词改动一起删掉了。
|
||||
|
||||
这句出现之前,一个长 Session 为了等待 2–9 分钟就结束的 review 子代理,七次 `sleep` 150–240 秒,累计约 25 分钟(#6057)。2026-10-21 的受控探针测量了这句对「子代理启动后模型下一条消息」的影响:`deepseek-v4-flash` 与 `deepseek-v4-pro`,每臂每模型 150 次请求,最小 system prompt,真实 `subagent` 与 `bash` schema,历史以 `started subagent reviewer-1` 结束。
|
||||
|
||||
| 组 | sleep 调用 | 任何 bash 调用 | 结束本轮 |
|
||||
|---|---|---|---|
|
||||
| 有这句 | 0/300 | 7/300 | 291/300 |
|
||||
| 无这句 | 29/300 | 120/300 | 179/300 |
|
||||
|
||||
分模型差异:flash `0/150` vs `6/150`(p=0.030),pro `0/150` vs `23/150`(p=9.6e-8),双侧 Fisher 精确检验。有这句的一组里剩余 2 次是重新调用 `subagent`,而不是结束本轮。
|
||||
|
||||
## 决策
|
||||
|
||||
委派段保留结束本轮这句,文本与 #6057 引入的版本逐字一致。结束本轮不会丢失结果:子代理结束后会向父级投递 user-role 通知,通知到达空闲父级会起一次独立的模型请求([`dsh-subagent`](../../../../packages/subagent/subagent/README.zh.md))。在子代理继续推进期间,宿主交替等待 `agent.whenIdle()` 与 `waitForChildren(agent)`([ACP `session.ts`](../../../../packages/acp/acp/src/session.ts)),`dsh-goal-round-driver` 也会等运行中的后代结束再开下一轮 goal。
|
||||
|
||||
这句保持为一行字面文本。把 `the next result starts a new turn` 改成 `each result starts a new turn` 会重录 83 个提示词 sidecar,而测不到行为变化。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**只依赖工具描述。** `subagent` 工具描述写明运行时会在子代理结束时通知模型。探针测得没有这句时 sleep 为 29/300,说明工具描述替代不了它。
|
||||
|
||||
**把说明移进工具描述。** 这段指导按可见委派工具集合组装并列出工具名,工具描述只描述单个工具。移动这句会改变归属,但不改变它阻止的行为。
|
||||
|
||||
**把与这句一起被回退的所有提示词都恢复。** read 指导与 `run_code` 参数顺序是各自有证据的独立决定;本记录只覆盖委派段。
|
||||
|
||||
## 验证
|
||||
|
||||
- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` 对每种可见工具组合断言渲染出的指导段,包含恢复后的这句(9 条断言)。
|
||||
- 83 个 `snapshots/**/system-prompt*.expected.md` sidecar 带有恢复后的文本,由 `test:snapshot` 通过随附 headless profile 回放。
|
||||
- 探针是一次性受控测量,未入库;两组只差这一句指导。
|
||||
|
||||
## 影响
|
||||
|
||||
- 只要存在可见委派工具,每个请求多一句,约 30 token。
|
||||
- 模型在只剩等待委派结果时结束本轮,不再调用 `bash sleep` 或轮询。
|
||||
- 后续的提示词精简必须把这句当作承重文本;上面的探针表是保留它的证据。
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-22-series-start-unchanged-prompt-prefix.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-10-22-series-start-unchanged-prompt-prefix.md
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node:
|
||||
en: f62e2980951e0cb3
|
||||
zh: 3cd4fd49a89fae40
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node/problem:
|
||||
en: b2b7d5e9a4850ece
|
||||
zh: cbd91b67969bca02
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node/decision:
|
||||
en: 8a3404207a36ecfb
|
||||
zh: d8bc1decefa60a20
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node/verification:
|
||||
en: 1970ea84337ce9b5
|
||||
zh: da5b9928606c07c6
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node/alternatives-considered:
|
||||
en: 2e87f55619f04607
|
||||
zh: 7ca5a8e9a42a799a
|
||||
/agent-note-a-series-start-with-an-unchanged-prompt-keeps-the-surviving-system-node/consequences:
|
||||
en: bf8a9d6c56722efd
|
||||
zh: 18dfdf8a7b9bcb5c
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: A series start with an unchanged prompt keeps the surviving system node
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-22-series-start-unchanged-prompt-prefix.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
On an in-history route, a prompt change is appended as a later `system/message`, so node 0 stays stale while the latest surviving node carries the rendering. A surface replacement (compaction, tool-result prune, image offload) starts a new request series. Re-baselining node 0 at a series start while the rendering already matched that latest node emptied the later node and wrote the identical text into node 0. The wire request then differed at its first message, so the provider prefix cache missed from the first token although no prompt text changed.
|
||||
|
||||
Real session `session-da55bbc9-…`, turn 710 step 2: `compaction/prune` replaced one tool result (seq 34669, 34670); the next request wrote 7,618 characters identical to the node it emptied (seq 34672, 34673), `request/header` logged reason `series` (seq 34674), and the step billed 722,332 uncached input tokens with 1,152 cache-read tokens (0.16% hit) against about 99.9% on the neighboring steps. A build update at turn 707 had appended the prompt node; nothing changed the rendering at turn 710.
|
||||
|
||||
## Decision
|
||||
|
||||
`SystemPromptProjection.project` re-baselines node 0 only when the effective text must move: an incapable route, an empty rendering, or a series start whose latest surviving non-empty node text differs from the rendering. When the route is in-history, the rendering is non-empty, and the latest surviving node already holds it, the projection returns no updates and the node stays where the cached history carried it. The loop still records the request series through `request/header` (`series`, or `change` with `startsSeries: true`) and rebuilds declarations as before; when the unchanged-rendering skip coincides with a tool-schema series start, the header records the declaration change without `startsSeries`.
|
||||
|
||||
Retry recovery keeps its guarantee: when a retry compaction shadows the latest node, the effective text falls back to an older node, differs from the rendering, and the head is rewritten before users are admitted again. The projection never appends an update after already-admitted users.
|
||||
|
||||
## Verification
|
||||
|
||||
- [`system-prompt-projection.spec.ts`](../../../../packages/core/agent-loop/tests/system-prompt-projection.spec.ts) pins the no-operation decision for a capable series start whose surviving node already matches.
|
||||
- [`system-prompt-admission.spec.ts`](../../../../packages/core/agent-loop/tests/system-prompt-admission.spec.ts) pins the wire request: a compaction replacement with an unchanged rendering keeps both system messages, the previous request messages remain a prefix, and the neighboring steps regain the provider prefix.
|
||||
- The [`in-history-series-start`](../../../../snapshots/session/in-history-series-start/) recorded session replays the shipped headless profile through a prompt change and a later surface replacement, pinning the unchanged-rendering series start to two retained prompt versions and no `system/message` event.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Always keep the appended node and never re-baseline.** Simpler, but a route that cannot append prompts, or a prompt that must clear, still needs a single current head, and a changed rendering at a series start must not append after users already admitted by a failed attempt. Rejected.
|
||||
|
||||
**Skip consolidation whenever the head differs.** A stale head is authoritative again on an incapable destination route, because that route ignores later system nodes; the route capability, not the head text, decides whether the later node is effective. Rejected.
|
||||
|
||||
**Stop treating local surface replacements as series starts.** A prune changes one tool result, so the cache loss is local, but image-offload and compaction decisions still legitimately start a series; the projection must not depend on which replacement produced the boundary. Rejected: the projection owns the prompt decision.
|
||||
|
||||
**Compare reconstructed requests instead of surface text.** The loop derives requests from surface events, so comparing past wire messages would duplicate request reconstruction and still need the same effective-text comparison. Rejected.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A capable route can retain earlier prompt versions in model history after a series start; they cost input tokens until a later consolidation or compaction shadows them.
|
||||
- A tool-schema series start on a route without tool updates no longer writes a `system/message` event, so its header records the declaration change without `startsSeries`. The request still carries the complete active declaration set through the unsupported-route projection (`projectToolUpdates`), while `Session.toolHistory()` keeps its incremental baseline instead of resetting.
|
||||
- Consolidation still applies when the rendering actually differs: route switches, prompt updates concurrent with compaction, and retries that shadow the latest node.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Agent Note: 渲染未变的新序列保留存活的系统节点
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-22-series-start-unchanged-prompt-prefix.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
在 in-history 路由上,提示词变化会追加为后续的 `system/message`,因此 node 0 保持陈旧,由最新存活节点承载渲染文本。surface 替换(压缩、工具结果剪枝、图片省略)会开启新的请求序列。在渲染文本已经与该最新节点一致时重写 node 0,会清空该节点并把完全相同的文本写进 node 0。于是 wire 请求的第一条消息发生变化,提供方前缀缓存从第一个 token 起未命中,尽管提示词文本没有任何变化。
|
||||
|
||||
真实会话 `session-da55bbc9-…` 第 710 轮第 2 步:`compaction/prune` 替换了一条工具结果(seq 34669、34670);下一次请求写入的 7,618 个字符与它清空的节点完全相同(seq 34672、34673),`request/header` 记录 reason `series`(seq 34674),该步计费 722,332 个未命中输入 token、1,152 个缓存读取 token(命中率 0.16%),而相邻步骤约为 99.9%。第 707 轮的构建更新追加了提示词节点;第 710 轮没有任何东西改变渲染文本。
|
||||
|
||||
## Decision
|
||||
|
||||
`SystemPromptProjection.project` 只在有效文本必须移动时重写 node 0:不具备能力的路由、空渲染文本,或最新存活非空节点文本与渲染文本不同的新序列开始。当路由为 in-history、渲染文本非空、且最新存活节点已经持有它时,投影不产生任何更新,节点保持在已缓存历史携带它的位置。loop 仍通过 `request/header` 记录请求序列(`series`,或带 `startsSeries: true` 的 `change`),并照旧重建声明;当「渲染未变则跳过」与工具 schema 起因的新序列重合时,header 记录声明变化但不带 `startsSeries`。
|
||||
|
||||
重试恢复保持原有保证:重试压缩遮蔽最新节点后,有效文本回退到更旧的节点,与渲染文本不同,于是头节点在用户消息再次被接纳之前重写。投影从不会在已接纳的用户消息之后追加更新。
|
||||
|
||||
## Verification
|
||||
|
||||
- [`system-prompt-projection.spec.ts`](../../../../packages/core/agent-loop/tests/system-prompt-projection.spec.ts) 固定「能力路由的新序列开始时存活节点文本已一致则不产生操作」的决策。
|
||||
- [`system-prompt-admission.spec.ts`](../../../../packages/core/agent-loop/tests/system-prompt-admission.spec.ts) 固定 wire 请求:渲染文本未变的压缩替换保留两条系统消息,上一次请求的消息仍构成前缀,相邻步骤重新获得提供方前缀。
|
||||
- [`in-history-series-start`](../../../../snapshots/session/in-history-series-start/) 录制会话通过 shipped headless profile 回放「提示词变化 + 后续 surface 替换」,固定渲染未变的新序列保留两份提示词版本且不写 `system/message` 事件。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**永远保留追加节点、从不重写。** 更简单,但无法追加提示词的路由、或必须清空的提示词仍需要单一当前头节点;新序列开始时若渲染文本确实变化,也不能在失败尝试已接纳的用户消息之后追加。拒绝。
|
||||
|
||||
**只要 head 不同就跳过归并。** 在目标路由变为不具备能力时,陈旧 head 会重新成为权威,因为该路由忽略后续系统节点;决定后续节点是否生效的是路由能力,而不是 head 文本。拒绝。
|
||||
|
||||
**让局部 surface 替换不再算新序列。** 剪枝只改一条工具结果的文本,缓存损失是局部的,但图片省略与压缩决定仍合理地开启新序列;投影不应依赖是哪种替换制造了边界。拒绝:提示词决策属于投影。
|
||||
|
||||
**比较重建后的请求而不是 surface 文本。** loop 从 surface 事件派生请求,比较历史 wire 消息会重复请求重建,而且仍然需要同样的有效文本比较。拒绝。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 具备能力的路由在新序列开始后可能在模型历史中保留更早的提示词版本;在被后续归并或压缩遮蔽之前,它们会计入输入 token。
|
||||
- 不支持工具更新的路由上,工具 schema 变化开启的新序列不再写出 `system/message` 事件,因此其 header 记录声明变化但不带 `startsSeries`。请求仍通过不支持更新的路由投影(`projectToolUpdates`)携带完整的生效声明集合,而 `Session.toolHistory()` 保留其增量基线、不再重置。
|
||||
- 渲染文本确实不同时归并仍然生效:路由切换、与压缩同时发生的提示词更新,以及遮蔽最新节点的重试。
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
en: 2f23fa55e1ce652c
|
||||
zh: 335695fc654ab1db
|
||||
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/decision/the-decision-rule:
|
||||
en: 554d01427ebebee1
|
||||
zh: 664c9bf0a9337a04
|
||||
en: bd990101d220c905
|
||||
zh: 98f54d523b9ffe52
|
||||
/agent-note-in-history-system-prompt-replacement-for-cache-stable-prompt-changes/decision/presentation-and-accounting:
|
||||
en: 800d579e2fcf2cce
|
||||
zh: 1e92354d9161c50a
|
||||
|
||||
+5
-4
@@ -24,18 +24,19 @@ The loop records the mode in the session: `RequestContext.systemPromptUpdate` jo
|
||||
|
||||
### The decision rule
|
||||
|
||||
`SystemPromptProjection.project(rendered, { inHistory, startsSeries })` in `packages/core/agent-loop/src/runtime-context.ts` scans the surviving `system/message` nodes of the current surface on every call. It returns ordered per-node commits. With no surviving system node it reserves the head even for an empty rendering. Effective text comes from the latest non-empty system node, falling back to the head; dormant empty tails neither supply effective text nor need another empty replacement. An empty rendering clears every active system node, regardless of route or series state. For a non-empty rendering on an incapable route or at a new request series, consolidation applies even when the effective text is unchanged. Otherwise matching effective text emits nothing. The operations are:
|
||||
`SystemPromptProjection.project(rendered, { inHistory, startsSeries })` in `packages/core/agent-loop/src/runtime-context.ts` scans the surviving `system/message` nodes of the current surface on every call. It returns ordered per-node commits. With no surviving system node it reserves the head even for an empty rendering. Effective text comes from the latest non-empty system node, falling back to the head; dormant empty tails neither supply effective text nor need another empty replacement. An empty rendering clears every active system node, regardless of route or series state. For a non-empty rendering, an incapable route or a new request series whose effective text differs consolidates; a series start whose latest surviving node already holds the rendering emits nothing and keeps that node in place. Otherwise matching effective text emits nothing. The unchanged-text case is owned by the [series-start prefix decision](../bug-fix/2026-10-22-series-start-unchanged-prompt-prefix.md). The operations are:
|
||||
|
||||
| Route capability | Prefix state | Operation |
|
||||
|---|---|---|
|
||||
| none | non-empty rendering, any prefix state | log an empty replacement for each non-empty later system node, then rewrite the first system node with the rendering if needed |
|
||||
| `in-history` | the current request series continues | append a new `system/message` before the step's `user/message` events; the append alone needs no `request/header` |
|
||||
| `in-history` | non-empty rendering, a new series starts | log empty replacements for non-empty later system nodes, then rewrite the first system node if needed, even when the latest effective text is unchanged |
|
||||
| `in-history` | non-empty rendering, a new series starts and the latest effective text differs | log empty replacements for non-empty later system nodes, then rewrite the first system node if needed |
|
||||
| `in-history` | non-empty rendering, a new series starts with the rendering already current | no operation: the surviving node stays where the cached history carried it |
|
||||
| any | the rendered prompt is empty | log empty replacements for non-empty later system nodes, then empty the head if needed; no prompt version remains in derived messages |
|
||||
|
||||
`startsSeries` is true when the `agent/pre-step` decision declares `startsRequestSeries`, when the surface replace generation moved since the last request (a compaction or any other replacement), or when the visible tool-schema set changed on a route without `toolUpdate` support. Supported tool additions can accompany a prompt append without starting a series. A provider or model swap alone is not a series start for this rule: on a capable destination route the changed prompt is appended, which costs nothing because the route change already misses the cache. A series start already costs the cache, so consolidation keeps only the current prompt in model history. Logged per-node empty replacements remove later prompts from derived messages without a surface delete operation or any replacement of intervening conversation nodes. This also keeps compaction recovery from appending a system update after users already admitted by the failed attempt.
|
||||
`startsSeries` is true when the `agent/pre-step` decision declares `startsRequestSeries`, when the surface replace generation moved since the last request (a compaction or any other replacement), or when the visible tool-schema set changed on a route without `toolUpdate` support. Supported tool additions can accompany a prompt append without starting a series. A provider or model swap alone is not a series start for this rule: on a capable destination route the changed prompt is appended, which costs nothing because the route change already misses the cache. A series start does not by itself discard the whole provider prefix: a replacement or image-offload decision may invalidate only the tail, so consolidation waits until the effective text must move; when it does, the series is the point to keep only the current prompt in model history and to avoid appending after already-admitted users on a retry. Logged per-node empty replacements remove later prompts from derived messages without a surface delete operation or any replacement of intervening conversation nodes. This also keeps compaction recovery from appending a system update after users already admitted by the failed attempt.
|
||||
|
||||
The first attempt admits the prompt after assembly, an accepted `agent/pre-step` decision, `step/start`, the `agent/request` waterfall, and `prepareCall()`. A rejected or empty first input opens no step. Neither async request phase commits the pending system prompt or accepted users, and cancellation during either commits neither. Every attempt synchronously reconciles the same rendered assembly after its own `agent/request` and `prepareCall()`, appends the accepted user batch only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming through the same prepared call. Retries do not repeat assembly, `agent/pre-step`, or user admission. Reconciliation sees both pre-step compaction (`compaction-basic` with `auto: true`) and recovery compaction, and consolidates non-empty prompt text at the head when either starts a new series. Resume is series-continuing — the `resume` header is not a series start — so a prompt that changed across a restart is appended; the provider cache may still be warm across a process boundary.
|
||||
The first attempt admits the prompt after assembly, an accepted `agent/pre-step` decision, `step/start`, the `agent/request` waterfall, and `prepareCall()`. A rejected or empty first input opens no step. Neither async request phase commits the pending system prompt or accepted users, and cancellation during either commits neither. Every attempt synchronously reconciles the same rendered assembly after its own `agent/request` and `prepareCall()`, appends the accepted user batch only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming through the same prepared call. Retries do not repeat assembly, `agent/pre-step`, or user admission. Reconciliation sees both pre-step compaction (`compaction-basic` with `auto: true`) and recovery compaction, and consolidates non-empty prompt text at the head when either starts a new series whose effective text differs. Resume is series-continuing — the `resume` header is not a series start — so a prompt that changed across a restart is appended; the provider cache may still be warm across a process boundary.
|
||||
|
||||
An empty head with no active later system node represents no prompt. Dormant empty tails do not supply effective text, so repeated clearing and resume cannot resurrect an older prompt. Restoring non-empty text uses the same admission rule: a continuing capable route may append it; an incapable route or a new series refills the head. Clearing uses ordinary per-node replacements, not a surface delete or an initial empty-head creation.
|
||||
|
||||
|
||||
+5
-4
@@ -24,18 +24,19 @@ Status: implemented
|
||||
|
||||
### 决策规则
|
||||
|
||||
`packages/core/agent-loop/src/runtime-context.ts` 中的 `SystemPromptProjection.project(rendered, { inHistory, startsSeries })` 每次调用都扫描当前 surface 上存活的 `system/message` 节点。它返回有序的逐节点提交。没有存活的系统节点时,即使渲染文本为空也预留头节点。有效文本取自最新的非空系统节点,没有时回退到头节点;未生效的空尾节点既不提供有效文本,也无需再次以空内容替换。无论路由或序列状态如何,空渲染文本都会清除每个生效的系统节点。不具备能力的路由或新请求序列面对非空渲染文本时,即使有效文本未变也执行归并。除此之外,有效文本相同时不产生事件。具体操作如下:
|
||||
`packages/core/agent-loop/src/runtime-context.ts` 中的 `SystemPromptProjection.project(rendered, { inHistory, startsSeries })` 每次调用都扫描当前 surface 上存活的 `system/message` 节点。它返回有序的逐节点提交。没有存活的系统节点时,即使渲染文本为空也预留头节点。有效文本取自最新的非空系统节点,没有时回退到头节点;未生效的空尾节点既不提供有效文本,也无需再次以空内容替换。无论路由或序列状态如何,空渲染文本都会清除每个生效的系统节点。面对非空渲染文本,不具备能力的路由或有效文本不同的新请求序列执行归并;新序列开始时若最新存活节点已经持有该渲染文本,则不产生事件并保留该节点。除此之外,有效文本相同时不产生事件。有效文本未变的新序列由[系列开始前缀决策](../bug-fix/2026-10-22-series-start-unchanged-prompt-prefix.zh.md)拥有。具体操作如下:
|
||||
|
||||
| 路由能力 | 前缀状态 | 操作 |
|
||||
|---|---|---|
|
||||
| 无 | 非空渲染文本,任意前缀状态 | 为每个非空的后续系统节点记录空内容替换,随后按需用渲染文本重写首个系统节点 |
|
||||
| `in-history` | 当前请求序列延续 | 在该步骤的 `user/message` 事件之前追加新的 `system/message`;仅追加本身不需要记录 `request/header` |
|
||||
| `in-history` | 非空渲染文本,新序列开始 | 为非空的后续系统节点记录空内容替换,再按需重写首个系统节点,即使最新有效文本未变也执行 |
|
||||
| `in-history` | 非空渲染文本,新序列开始且最新有效文本不同 | 为非空的后续系统节点记录空内容替换,再按需重写首个系统节点 |
|
||||
| `in-history` | 非空渲染文本,新序列开始但渲染文本已经是当前文本 | 不产生操作:存活节点保持在已缓存历史携带它的位置 |
|
||||
| 任意 | 渲染后的提示词为空 | 为非空的后续系统节点记录空内容替换,再按需清空头节点;派生消息中不保留任何提示词版本 |
|
||||
|
||||
`startsSeries` 在以下情况为真:`agent/pre-step` 决定声明了 `startsRequestSeries`、surface 的替换代数自上次请求以来发生了移动(压缩或任何其他替换)、不支持 `toolUpdate` 的路由上可见工具 schema 集合发生了变化。受支持的工具添加可以与提示词追加一起发生,无需开启新序列。仅 provider 或 model 切换对本规则不算序列开始:目标路由具备能力时,变更后的提示词被追加,这不花任何代价,因为路由变更本身已经使缓存未命中。序列开始已经付出了缓存代价,因此归并让模型历史只保留当前提示词。有日志记录的逐节点空内容替换会从派生消息中移除后续提示词,无需 surface 删除操作,也不替换其间的对话节点。这也使压缩恢复不会在失败尝试已接纳的用户消息之后追加系统更新。
|
||||
`startsSeries` 在以下情况为真:`agent/pre-step` 决定声明了 `startsRequestSeries`、surface 的替换代数自上次请求以来发生了移动(压缩或任何其他替换)、不支持 `toolUpdate` 的路由上可见工具 schema 集合发生了变化。受支持的工具添加可以与提示词追加一起发生,无需开启新序列。仅 provider 或 model 切换对本规则不算序列开始:目标路由具备能力时,变更后的提示词被追加,这不花任何代价,因为路由变更本身已经使缓存未命中。序列开始本身并不必然使整段提供方前缀作废:替换或图片省略决定可能只使尾部失效,因此归并会等到有效文本必须移动时才执行;此时借助该序列只保留当前提示词,并避免重试时在已接纳的用户消息之后追加指令。有日志记录的逐节点空内容替换会从派生消息中移除后续提示词,无需 surface 删除操作,也不替换其间的对话节点。这也使压缩恢复不会在失败尝试已接纳的用户消息之后追加系统更新。
|
||||
|
||||
首次尝试在组装、被接纳的 `agent/pre-step` 决策、`step/start`、`agent/request` waterfall 与 `prepareCall()` 之后才接纳提示词。被拒绝或为空的首次输入不打开步骤。两个异步请求阶段都不提交待处理的系统提示词与已接纳用户消息,在任一阶段取消都不会提交这两者。每次尝试都在各自的 `agent/request` 与 `prepareCall()` 之后同步协调同一份已渲染组装结果、仅在首次尝试追加已接纳用户批次、按需记录 header/context、派生并冻结请求,再通过同一个已准备调用发起流式请求。重试不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 压缩(`auto: true` 的 `compaction-basic`)与恢复压缩,并在任一种压缩开启新序列时将非空提示词文本归并到头部。恢复属于序列延续——`resume` header 不是序列开始——因此跨重启发生变化的提示词被追加;提供方缓存在进程边界之后可能仍是热的。
|
||||
首次尝试在组装、被接纳的 `agent/pre-step` 决策、`step/start`、`agent/request` waterfall 与 `prepareCall()` 之后才接纳提示词。被拒绝或为空的首次输入不打开步骤。两个异步请求阶段都不提交待处理的系统提示词与已接纳用户消息,在任一阶段取消都不会提交这两者。每次尝试都在各自的 `agent/request` 与 `prepareCall()` 之后同步协调同一份已渲染组装结果、仅在首次尝试追加已接纳用户批次、按需记录 header/context、派生并冻结请求,再通过同一个已准备调用发起流式请求。重试不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 压缩(`auto: true` 的 `compaction-basic`)与恢复压缩,并在任一种压缩开启有效文本不同的新序列时将非空提示词文本归并到头部。恢复属于序列延续——`resume` header 不是序列开始——因此跨重启发生变化的提示词被追加;提供方缓存在进程边界之后可能仍是热的。
|
||||
|
||||
空头节点且没有生效的后续系统节点表示没有提示词。未生效的空尾节点不提供有效文本,因此重复清除与恢复会话都不会使旧提示词重新生效。重新提供非空文本使用同一准入规则:延续中的具备能力路由可以追加它;不具备能力的路由或新序列则重新填充头节点。清除使用普通的逐节点替换,而非 surface 删除或初次创建空头节点。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 329c6ff947559b11
|
||||
zh: 5c4c1053236d319b
|
||||
/agent-note-plugin-market-discover-page-and-index-reader/decision:
|
||||
en: cc4d17a51dd479b7
|
||||
zh: 1fecf669b117f51c
|
||||
en: dd32bedaa763a4cb
|
||||
zh: 27b1a81bd744d8c1
|
||||
/agent-note-plugin-market-discover-page-and-index-reader/alternatives-considered:
|
||||
en: 62e7691dc8efd5c0
|
||||
zh: b13769634979b950
|
||||
|
||||
@@ -12,7 +12,7 @@ The plugin manager owns profile package operations and must not remount while an
|
||||
|
||||
## Decision
|
||||
|
||||
The feature ships as the experimental optional [custom plugin markets bundle](../../../../packages/experimental/plugin-market-bundle/README.md), off by default, which mounts both rows below; with it off, neither the service, the Discover tab, nor the `plugin_market` tool exists, and the Plugins page shows no tabs. A separate Host service, [`pluginMarket`](../../../../packages/boot/plugin-market/README.md), reads indexes. It requires `pluginManager` for read-only calls and never writes the profile. Its `indexes` configuration field is volatile: Settings writes only volatile fields, and the Loader commits a volatile-only change into the running reference without remounting any plugin. Other fields (`fetchTimeoutMs`, `maxBytes`, `cacheTtlMs`) are ordinary configuration.
|
||||
The feature ships as the experimental optional [custom plugin indexes bundle](../../../../packages/experimental/plugin-market-bundle/README.md), off by default, which mounts both rows below; with it off, neither the service, the Discover tab, nor the `plugin_market` tool exists, and the Plugins page shows no tabs. A separate Host service, [`pluginMarket`](../../../../packages/boot/plugin-market/README.md), reads indexes. It requires `pluginManager` for read-only calls and never writes the profile. Its `indexes` configuration field is volatile: Settings writes only volatile fields, and the Loader commits a volatile-only change into the running reference without remounting any plugin. Other fields (`fetchTimeoutMs`, `maxBytes`, `cacheTtlMs`) are ordinary configuration.
|
||||
|
||||
Index URLs use `http:` or `https:`, without credentials or fragment. `file:` is excluded because relative icon references would resolve to URLs a browser cannot load. `search` and `entry` accept only a configured URL, so the Host never fetches an address a client supplies. The Host fetches through the process proxy, refuses redirects, bounds the body, and validates with the shared format package; an invalid index affects only its own URL.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
该功能以实验性的可选[自定义插件市场组合包](../../../../packages/experimental/plugin-market-bundle/README.zh.md)发布,默认关闭,并挂载下面两行;关闭时,服务、「发现」标签页和 `plugin_market` 工具都不存在,插件页也不显示标签页。由独立的 Host 服务 [`pluginMarket`](../../../../packages/boot/plugin-market/README.zh.md) 读取索引。它依赖 `pluginManager` 进行只读调用,从不写入 profile。它的 `indexes` 配置字段是 volatile 字段:Settings 只写入 volatile 字段,Loader 把只涉及 volatile 字段的变更提交到运行中的引用,不重新挂载任何插件。其他字段(`fetchTimeoutMs`、`maxBytes`、`cacheTtlMs`)是普通配置。
|
||||
该功能以实验性的可选[自定义插件索引组合包](../../../../packages/experimental/plugin-market-bundle/README.zh.md)发布,默认关闭,并挂载下面两行;关闭时,服务、「发现」标签页和 `plugin_market` 工具都不存在,插件页也不显示标签页。由独立的 Host 服务 [`pluginMarket`](../../../../packages/boot/plugin-market/README.zh.md) 读取索引。它依赖 `pluginManager` 进行只读调用,从不写入 profile。它的 `indexes` 配置字段是 volatile 字段:Settings 只写入 volatile 字段,Loader 把只涉及 volatile 字段的变更提交到运行中的引用,不重新挂载任何插件。其他字段(`fetchTimeoutMs`、`maxBytes`、`cacheTtlMs`)是普通配置。
|
||||
|
||||
索引 URL 使用 `http:` 或 `https:`,不含凭据或片段。排除 `file:`,因为相对图标引用会解析为浏览器无法加载的 URL。`search` 和 `entry` 只接受已配置的 URL,因此 Host 从不获取客户端提供的地址。Host 通过进程代理获取,拒绝重定向,限制响应体大小,并用共享的格式包校验;无效索引只影响它自己的 URL。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: dc43ba84345e1405
|
||||
zh: dbc03572ab7710d2
|
||||
/agent-note-ship-optional-bundles-with-the-installation/decision:
|
||||
en: 1362533f342b31f4
|
||||
zh: 90689262acefb43d
|
||||
en: 2f20e356517715a6
|
||||
zh: 35c7ff8c4cd8a712
|
||||
/agent-note-ship-optional-bundles-with-the-installation/alternatives-considered:
|
||||
en: 95b48b61757f1f13
|
||||
zh: e9fb2d6ea492a4bc
|
||||
|
||||
@@ -10,7 +10,7 @@ The Web plugin page manages only the bundles a person installed into the profile
|
||||
|
||||
## Decision
|
||||
|
||||
The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts`, beside the profile templates) the bundles the installation ships for a person to switch on. Each must be a runtime dependency of `apps/cli` that declares `dsh.bundle.patch`, and no shipped profile template selects it. The plugin manager's `listBundles` reports such a bundle as `optional`: switched off until selected, never removable, resolved from the installation like any installation-supplied bundle. The Web plugin page presents offered optional bundles in its Official group, tagged experimental when the package is experimental, ahead of the profile's own installed bundles. The [persistent-terminal bundle](../../../../packages/experimental/terminal-bundle/README.md) remains available to the Host but has no page switch, even when enabled; its existing profile selection remains intact.
|
||||
The launcher names in `OPTIONAL_BUNDLES` (`packages/util/official-bundles/src/bundles.ts`, beside the profile templates) the bundles the installation ships for a person to switch on. Each must be a runtime dependency of `apps/cli` that declares `dsh.bundle.patch`, and no shipped profile template selects it. The plugin manager's `listBundles` reports such a bundle as `optional`: switched off until selected, never removable, resolved from the installation like any installation-supplied bundle. The Web plugin page presents offered optional bundles in its Official group, tagged experimental when the package is experimental, ahead of the profile's own installed bundles. The [persistent-terminal bundle](../../../../packages/experimental/terminal-bundle/README.md) remains available to the Host but has no page switch, even when enabled; its existing profile selection remains intact.
|
||||
|
||||
Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样
|
||||
|
||||
## 决策
|
||||
|
||||
启动器在 `OPTIONAL_BUNDLES`(`packages/boot/app-boot/src/profile.ts`,与 profile 模板并列)里点名安装随附、供用户开启的组合包。每一个都必须是 `apps/cli` 声明了 `dsh.bundle.patch` 的运行时依赖,且不被任何随附 profile 模板选中。插件管理器的 `listBundles` 把这类组合包报告为 `optional`:选中前保持关闭、永不可卸载、像其他安装提供的组合包一样从安装目录解析。Web 插件页在「官方」分组展示可供选择的可选组合包,属于实验包的带实验性标签,排在 profile 自己已安装的组合包之前。[持久终端组合包](../../../../packages/experimental/terminal-bundle/README.zh.md)仍可供 Host 使用,但即使已启用也不提供页面开关;已有的 profile 选择保持不变。
|
||||
启动器在 `OPTIONAL_BUNDLES`(`packages/util/official-bundles/src/bundles.ts`,与 profile 模板并列)里点名安装随附、供用户开启的组合包。每一个都必须是 `apps/cli` 声明了 `dsh.bundle.patch` 的运行时依赖,且不被任何随附 profile 模板选中。插件管理器的 `listBundles` 把这类组合包报告为 `optional`:选中前保持关闭、永不可卸载、像其他安装提供的组合包一样从安装目录解析。Web 插件页在「官方」分组展示可供选择的可选组合包,属于实验包的带实验性标签,排在 profile 自己已安装的组合包之前。[持久终端组合包](../../../../packages/experimental/terminal-bundle/README.zh.md)仍可供 Host 使用,但即使已启用也不提供页面开关;已有的 profile 选择保持不变。
|
||||
|
||||
默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。
|
||||
|
||||
|
||||
+18
-18
@@ -2,21 +2,21 @@
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-20-weighted-approval-status-gate-with-check-run-label.md
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run:
|
||||
en: a1f0f9fd657c7d35
|
||||
zh: e8fa02fb5bde6075
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/problem:
|
||||
en: 8e854872edfc0729
|
||||
zh: 6f3ed495fcb690a4
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/decision:
|
||||
en: 03de94946d7532d7
|
||||
zh: cf2f585f715d3295
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/verification:
|
||||
en: 8298cff64cce6467
|
||||
zh: a121d509c2302c00
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/alternatives-considered:
|
||||
en: 197255cd82aabef7
|
||||
zh: dc03f4bf77150f22
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/consequences:
|
||||
en: 25e1ac1c8cab089a
|
||||
zh: 84e10cccb1b9eda7
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app:
|
||||
en: 1ea2e345d6bc1058
|
||||
zh: 65f872a0606660b8
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/problem:
|
||||
en: f2dc92b64d32bd68
|
||||
zh: 7f24eaaa49a5776f
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/decision:
|
||||
en: 5f7ebd56d43445cf
|
||||
zh: 869e144309b85c28
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/verification:
|
||||
en: c80da76f52dac191
|
||||
zh: 1f7f8458211e1f5b
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/alternatives-considered:
|
||||
en: e3b37e68b3e76d05
|
||||
zh: bb6c917c3fc6b685
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/consequences:
|
||||
en: 79a97b43e268dd04
|
||||
zh: b85691eac68c313f
|
||||
+15
-11
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Weighted approval keeps its commit status gate and adds a decision check run
|
||||
# Agent Note: Weighted approval keeps its commit status gate and publishes the decision label from an App
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,32 +6,36 @@ English | [中文](2026-10-20-weighted-approval-status-gate-with-check-run-label
|
||||
|
||||
## Problem
|
||||
|
||||
The `weighted approval` commit status is the record this repository's branch rules require, and below the approval threshold it stays `pending`. A commit status has no action-required state, so readers cannot tell an approval wait from running work. Moving the gate to a check run does not work: after that change, the check run completed as `success` on the pull-request head and both the REST API and `gh pr checks` listed it, while the merge box kept reporting the same `weighted approval decision` context as `Expected — Waiting for status to be reported`, and two pull requests needed an administrator bypass. That change was reverted.
|
||||
The `weighted approval` commit status is the record this repository's branch rules require, and below the approval threshold it stays `pending`. A commit status has no action-required state, so readers cannot tell an approval wait from running work.
|
||||
|
||||
A check run carries that state, but GitHub attaches a check run created through the API to the earliest Actions check suite on the head, and a check suite created by a `pull_request_target` run is not associated with the pull request. Measured on 2026-10-20 and 2026-10-21: when the earliest suite belonged to a `pull_request` workflow the merge box listed the label, and when it belonged to the `pull_request_target` publisher it listed none, while the Checks page listed it either way; which suite is earliest is a race among the workflows a push starts. Moving the gate to a required check run published with the job token hit the same race: the merge box reported the context as `Expected — Waiting for status to be reported` while the REST API and `gh pr checks` listed it complete, so two pull requests needed an administrator bypass; that change was reverted.
|
||||
|
||||
## Decision
|
||||
|
||||
The publisher keeps writing the `weighted approval` commit status exactly as before: `pending` while the policy is unmet, `success` when it holds, and `error` when setup or evaluation fails. Branch rules keep requiring that status. That behavior, including keeping an unmet policy `pending`, is what the [archived decision](../../archived/process/2026-09-09-blocked-weighted-approvals-remain-pending.md) records; this change adds the label beside it instead of changing the gate.
|
||||
|
||||
The publisher additionally writes a `weighted approval decision` check run for the same evaluation: `action_required` while the policy is unmet, `success` when it holds, and `failure` when setup or evaluation fails. The check run's title carries the score or the blocker. Branch rules do not require it, so it never blocks a merge; it names the blocked state beside the status that does.
|
||||
The publisher writes the `weighted approval decision` check run with a token minted from a dedicated approval App (`actions/create-github-app-token` with `vars.DSH_APPROVAL_APP_CLIENT_ID`, `secrets.DSH_APPROVAL_APP_PRIVATE_KEY`, and `vars.DSH_APPROVAL_APP_SLUG`, scoped to `checks: write`). The check suite then belongs to the App and is associated with the pull-request head, so the merge box lists the label without depending on which Actions workflow started first. The conclusion is `action_required` while the policy is unmet, `success` when it holds, and `failure` when setup or evaluation fails; the title carries the score or the blocker. Branch rules do not require the check run, so it never blocks a merge.
|
||||
|
||||
A head keeps one GitHub Actions run with the check-run name: the publisher updates the head's existing run and creates one only when none exists. The setup phase creates the run as `in_progress` on a head without one and deals with a published success by rewriting its conclusion to `action_required`, because GitHub accepts but does not apply a return to `in_progress`. A failed check-run write is logged and never changes the status, so a token without the checks permission, or a check-run API failure, leaves the gate as the status wrote it. The publisher identifies its own runs by the `external_id` marker it sets on creation, which excludes check runs that workflow jobs produce for a same-named job. It revokes a published `success` before dependency setup, because GitHub accepts but does not apply a return to `in_progress`.
|
||||
The status and the check run are written through separate API clients: the status gate uses the job token, and only the check-run calls use the App token. A failed App token step or check-run write is logged and never changes the status. When the token or the slug is unavailable the publisher keeps the job token and updates `github-actions` runs only, so the gate keeps working and the label stays on the Checks page; the job token cannot update an App run, so such a head keeps the App run it already has.
|
||||
|
||||
The publisher selects the check runs created by the publishing token by `app.slug`: the App token matches the App's runs and the job token matches `github-actions` runs, because neither token can update a run the other created. It updates every selected run of that app and creates one only when none exists; job-token runs written by earlier evaluations stay untouched and can coexist with the App run on a head created before the App was configured. The setup phase creates the run as `in_progress` on a head without one and deals with a published success by rewriting its conclusion to `action_required`, because GitHub accepts but does not apply a return to `in_progress`. Runs the publisher creates carry the `external_id` marker it sets on creation.
|
||||
|
||||
## Verification
|
||||
|
||||
[Approval policy tests](../../../../.github/review-ownership/check-approval.test.mjs) pin the unchanged status states, the check-run conclusion for a met and an unmet policy, the phase writes, revoking a published success through its conclusion, one run per head, and invalid list responses. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the `checks: write` and `statuses: write` permissions and the setup phase commands.
|
||||
[Approval policy tests](../../../../.github/review-ownership/check-approval.test.mjs) pin the unchanged status states, the check-run conclusion for a met and an unmet policy, the phase writes, revoking a published success through its conclusion, one run per head, invalid list responses, the status fallback when the label write fails, and the split between the status client and the check-run client. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the `checks: write` and `statuses: write` permissions, the App token step and its single secret reference, the `DECISION_TOKEN` wiring, and the setup phase commands.
|
||||
|
||||
The check-run limit was measured on 2026-10-20 with the reverted attempt on this repository: the `weighted approval decision` check run completed as `success` on the pull-request head, the REST API and `gh pr checks` listed it, and the merge box kept reporting the same context as expected.
|
||||
The check-run association limit was measured on 2026-10-20 with the reverted and the previous attempts on this repository: a required check run published with the job token stayed `Expected` in the merge box, and a non-required check run published with the job token was absent from the merge box's check list while the Checks page listed it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Require the check run instead of the commit status.** Measured above: the ruleset does not match a check run published through the default-branch workflow's token, so the gate cannot be satisfied.
|
||||
**Publish the label with the job token and accept the Checks page only.** That is the state this change replaces; the merge box never lists the label, so the conversation page shows only the pending status.
|
||||
|
||||
**Publish the commit status as `failure` while the policy is unmet.** The status would complete, but this changes the required record's meaning and would report an unmet, reversible condition as a malfunction.
|
||||
**Require the App check run instead of the commit status.** The App check run is associated with the pull request, so it could carry the gate. That needs a ruleset change to the App's integration and a migration for open heads; this change keeps the proven status gate and only moves the label onto the App.
|
||||
|
||||
**Publish the commit status as `failure` while the policy is unmet.** The status would complete, but this changes the required record's meaning and reports an unmet, reversible condition as a malfunction.
|
||||
|
||||
**Publish only the check run and leave the status untouched.** The status would keep its last state from an earlier evaluation, so the gate and the label could disagree.
|
||||
|
||||
**Publish the required check run from a dedicated GitHub App.** A check run whose app the ruleset names would let the required entry carry the action-required conclusion itself. The repository has no App with the checks permission; with one, the label could move onto the required record and the status could be retired.
|
||||
|
||||
## Consequences
|
||||
|
||||
A job that ends between the `pending` write and the fallback step, such as a lost runner or a cancellation that skips the final step, leaves the status `pending`; that is the original symptom of this status, not a regression from the added label. While reviewers are pending, the merge box reports `weighted approval` as pending (the gate) and the `weighted approval decision` entry as `action_required`, with the score or blocker in its title; `gh pr checks` lists both. A published success is revoked by rewriting the check run's conclusion, and the status follows the unchanged policy states.
|
||||
The merge box now lists `weighted approval` (the gate, pending while reviewers are pending) and `weighted approval decision` (`action_required`, with the score or blocker in its title). If the App token cannot be minted, the label falls back to the job token and stays on the Checks page only; the gate is unaffected. A job that ends between the `pending` write and the fallback step, such as a lost runner or a cancellation that skips the final step, leaves the status `pending`; that is the original symptom of this status, not a regression.
|
||||
+14
-10
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Weighted approval 保留其 commit status 门禁并新增决策 check run
|
||||
# Agent Note: Weighted approval 保留 commit status 门禁,并由 App 发布决策标签
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,32 +6,36 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`weighted approval` commit status 是本仓库分支规则要求的记录,在批准阈值下方时保持 `pending`。commit status 没有 action-required 状态,读者无法区分等待批准与正在运行的工作。把门禁改成 check run 并不可行:该改动后 check run 在 PR head 上以 `success` 完成,REST API 与 `gh pr checks` 都能列出它,而合并框始终把同一上下文报告为 `Expected — Waiting for status to be reported`,两个 PR 需要管理员绕过才能合并。该改动已回退。
|
||||
`weighted approval` commit status 是本仓库分支规则要求的记录,在批准阈值下方时保持 `pending`。commit status 没有 action-required 状态,读者无法区分等待批准与正在运行的工作。
|
||||
|
||||
check run 可以承载这个状态,但 GitHub 会把通过 API 创建的 check run 挂到该 head 上最早的 Actions check suite,而 `pull_request_target` 的 run 创建的 check suite 不与该 PR 关联。2026-10-20 与 2026-10-21 实测:最早 suite 属于 `pull_request` workflow 时合并框会列出该标签,属于 `pull_request_target` 的 publisher 时合并框一条都不列,而 Checks 页两种情况下都能列出;哪个 suite 最早是同一次 push 触发的多个 workflow 之间的竞态。把门禁改成用 job token 发布的必需 check run 也撞上同一个竞态:合并框把该上下文报告为 `Expected — Waiting for status to be reported`,而 REST API 与 `gh pr checks` 都显示已完成,两个 PR 需要管理员绕过,该改动已回退。
|
||||
|
||||
## 决定
|
||||
|
||||
publisher 继续完全按原样写 `weighted approval` commit status:策略未满足时为 `pending`,满足时为 `success`,安装或评估失败时为 `error`。分支规则继续要求该 status。该行为——包括未满足的策略保持 `pending`——正是[已归档决定](../../archived/process/2026-09-09-blocked-weighted-approvals-remain-pending.md)记录的;本次变更在该门禁旁边新增标签,而不改变门禁。
|
||||
|
||||
publisher 为同一次评估额外写一个 `weighted approval decision` check run:策略未满足时为 `action_required`,满足时为 `success`,安装或评估失败时为 `failure`。check run 的标题携带分数或 blocker。分支规则不要求它,因此它从不阻止合并;它在会阻止合并的那条 status 旁边命名被阻止的状态。
|
||||
publisher 用从专用批准 App 铸造的 token(`actions/create-github-app-token`,使用 `vars.DSH_APPROVAL_APP_CLIENT_ID`、`secrets.DSH_APPROVAL_APP_PRIVATE_KEY` 与 `vars.DSH_APPROVAL_APP_SLUG`,权限限定为 `checks: write`)写 `weighted approval decision` check run。check suite 因此归 App 所有并与 PR head 关联,合并框会列出该标签,不再取决于哪个 Actions workflow 先启动。策略未满足时结论为 `action_required`,满足时为 `success`,安装或评估失败时为 `failure`;标题携带分数或 blocker。分支规则不要求该 check run,因此它从不阻止合并。
|
||||
|
||||
每个 head 只保留一个同名 GitHub Actions run:publisher 更新 head 上已有的 run,仅在不存在时创建。setup 阶段对没有 run 的 head 以 `in_progress` 创建,并把已发布的 success 的 conclusion 改写为 `action_required`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。check run 写入失败会被记录,且从不改变 status,因此没有 checks 权限的 token 或 check-run API 失败时,门禁保持 status 写入的结果。publisher 用创建时设置的 `external_id` 标记识别自己的 run,从而排除 workflow job 为同名 job 产生的 check run。它在依赖安装前撤销已发布的 `success`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。
|
||||
status 与 check run 通过两个独立的 API client 写入:status 门禁使用 job token,只有 check-run 调用使用 App token。token 步骤失败或 check-run 写入失败会被记录,且从不改变 status,因此 App 权限缺失时门禁照常工作,标签只出现在 Checks 页。
|
||||
|
||||
publisher 按 `app.slug` 选择由当前发布 token 创建的 run:App token 只匹配 App 的 run,job token 只匹配 `github-actions` 的 run,因为两种 token 都无法更新对方创建的 run。它更新所选 app 的所有同名 run,仅在不存在时创建;App 配置之前由 job token 写入的 run 保持不变,可能与 App run 在同一个 head 上共存。setup 阶段对没有 run 的 head 以 `in_progress` 创建,并把已发布的 success 的 conclusion 改写为 `action_required`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。publisher 创建的 run 带创建时设置的 `external_id` 标记。
|
||||
|
||||
## 验证
|
||||
|
||||
[批准策略测试](../../../../.github/review-ownership/check-approval.test.mjs)锁定未改动的 status 状态、策略满足与未满足两种情况下 check run 的 conclusion、阶段写入、通过 conclusion 撤销已发布的成功、每个 head 只有一个 run,以及无效的列表响应。[工作流测试](../../../../scripts/ci-workflow.spec.ts)锁定 `checks: write` 与 `statuses: write` 权限以及 setup 阶段命令。
|
||||
[批准策略测试](../../../../.github/review-ownership/check-approval.test.mjs)锁定未改动的 status 状态、策略满足与未满足两种情况下 check run 的 conclusion、阶段写入、通过 conclusion 撤销已发布的成功、每个 head 只有一个 run、无效的列表响应、标签写入失败时 status 的兜底,以及 status client 与 check-run client 的分离。[工作流测试](../../../../scripts/ci-workflow.spec.ts)锁定 `checks: write` 与 `statuses: write` 权限、App token 步骤及其唯一的密钥引用、`DECISION_TOKEN` 的接线,以及 setup 阶段命令。
|
||||
|
||||
check run 的限制于 2026-10-20 在本仓库用被回退的尝试实测:`weighted approval decision` check run 在 PR head 上以 `success` 完成,REST API 与 `gh pr checks` 都能列出它,而合并框始终把同一个 `weighted approval decision` 上下文报告为 expected。
|
||||
check run 的关联限制于 2026-10-20 在本仓库用回退的尝试与上一版尝试实测:用 job token 发布的必需 check run 在合并框里保持 `Expected`;用 job token 发布的非必需 check run 不在合并框的检查列表中,而 Checks 页能列出它。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把 check run 作为必需检查。** 见上面的实测:ruleset 不匹配默认分支 workflow 的 token 发布的 check run,门禁无法满足。
|
||||
**用 job token 发布标签、只接受 Checks 页可见。** 这正是本次变更要替换的状态;合并框永远不列该标签,会话页只显示 pending 的 status。
|
||||
|
||||
**把 App check run 设为必需检查来替代 commit status。** App check run 已关联 PR,可以承载门禁。但那需要把 ruleset 改成该 App 的 integration 并为开放 head 做迁移;本次保留已验证的 status 门禁,只把标签移到 App。
|
||||
|
||||
**策略未满足时把 commit status 发布为 `failure`。** status 会完成,但改变了必需记录的含义,并把尚未满足且可撤销的条件报告为故障。
|
||||
|
||||
**只发布 check run 而不动 status。** status 会保留上一次评估的状态,门禁与标签可能不一致。
|
||||
|
||||
**由专用 GitHub App 发布必需 check run。** 由 ruleset 指定 app 的 check run 可以让必需条目本身携带 action-required 结论。本仓库没有具备 checks 权限的 App;有了之后可以把标签移到必需记录上并停用 status。
|
||||
|
||||
## 后果
|
||||
|
||||
job 在 `pending` 写入与兜底步骤之间结束(例如 runner 丢失,或取消导致最后一步未执行)会让 status 保持 `pending`;这是该 status 既有的表现,不是新增标签带来的回归。评审未完成时,合并框把 `weighted approval` 报告为 pending(门禁),并把 `weighted approval decision` 条目报告为 `action_required`,标题里是分数或 blocker;`gh pr checks` 会列出两者。已发布的 success 通过改写 check run 的 conclusion 撤销,status 继续按未改动的策略状态变化。
|
||||
合并框现在同时列出 `weighted approval`(门禁,评审未完成时为 pending)与 `weighted approval decision`(`action_required`,标题里是分数或 blocker)。App token 步骤失败或 check-run 写入失败会被记录,且从不改变 status。缺少 token 或 slug 时 publisher 保留 job token、只更新 `github-actions` 的 run,因此门禁照常工作、标签留在 Checks 页;job token 无法更新 App run,这类 head 会保留已有的 App run。job 在 `pending` 写入与兜底步骤之间结束(例如 runner 丢失,或取消导致最后一步未执行)会让 status 保持 `pending`;这是该 status 既有的表现,不是新增标签带来的回归。
|
||||
@@ -9,14 +9,14 @@
|
||||
en: 212258ada8f174eb
|
||||
zh: 9a02b100533118fe
|
||||
/agent-note-interactive-side-sessions-and-merge-back/proposal:
|
||||
en: 5fa7d2000db6b265
|
||||
zh: a892b74299e0f984
|
||||
en: 7f24e61247bf2d84
|
||||
zh: aaa397f7d4aaf351
|
||||
/agent-note-interactive-side-sessions-and-merge-back/alternatives-considered:
|
||||
en: de50cab213a61b91
|
||||
zh: 85b912193092dfe2
|
||||
/agent-note-interactive-side-sessions-and-merge-back/acceptance-criteria:
|
||||
en: 17d0937a1f28b1ac
|
||||
zh: 9710ab0af3268da1
|
||||
en: 3dbfd9c97279519f
|
||||
zh: 719f54d2c0a68e0a
|
||||
/agent-note-interactive-side-sessions-and-merge-back/risks:
|
||||
en: 2435d8e2aba0d1c1
|
||||
zh: 3854c476bea061d4
|
||||
@@ -12,7 +12,7 @@ A user may want to explore a question from a live session without changing its m
|
||||
|
||||
A **side session** is an ordinary live session forked at the source's last completed turn, attached to its own agent, framed as a read-only advisor, and able to **merge back** one condensed note.
|
||||
|
||||
- **Fork and attach:** create the child with the parent's balanced completed-turn prefix, `parentSession`, `meta.isSeeded: true`, and the exact sibling `inheritedEventCount`. This composes `ctx.agents.create({ seed, inheritedEventCount, meta })`; it adds no core service or session-store method.
|
||||
- **Fork and attach:** create the child with the parent's balanced completed-turn prefix, `meta.isSeeded: true`, and the exact sibling `inheritedEventCount`. The caller retains any handback target separately from the copied Session under the [independent-fork decision](../../implemented/architecture/2026-10-23-independent-session-forks.md). This composes `ctx.agents.create({ seed, inheritedEventCount, meta })`; it adds no core service or session-store method.
|
||||
- **Advisor framing:** inject one plugin-sourced `context/message` after creation that tells the child to explain without mutating or continuing the task. Keeping the system prompt byte-identical preserves the provider prefix cache over inherited history.
|
||||
- **Merge-back:** ask the child for a length-capped handback, then inject one plugin-sourced `context/message` into the parent. The next parent request sees it at its logged position, preserving replay and [request reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) without a new session event.
|
||||
- **Presentation:** invocation, session switching, and handback rendering belong to the first client UI. This Agent Note specifies only the client-independent mechanics.
|
||||
@@ -28,7 +28,7 @@ Rewind productization, session-tree views, a model-facing side-session tool, and
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Forking leaves the source untouched and creates a child with the balanced completed-turn prefix, `parentSession`, `isSeeded: true`, the exact `inheritedEventCount`, and a byte-identical system prompt.
|
||||
- Forking leaves the source untouched and creates a child with the balanced completed-turn prefix, `isSeeded: true`, the exact `inheritedEventCount`, and a byte-identical system prompt.
|
||||
- Advisor framing adds exactly one plugin-sourced `context/message` at the head of the child's appended history, rather than changing its system prompt.
|
||||
- Merge-back adds exactly one length-capped `context/message` with source `plugin: sidechat`; the next parent request and replay see it at the same position.
|
||||
- Parent and child run concurrently without log or stream cross-talk.
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: proposed
|
||||
|
||||
**侧会话(side session)**是一个普通的活跃会话,从源会话的最后一个已完成轮次 fork 而来,绑定到自己的 agent(智能体),定位为只读顾问,并能**合并回写**一条精简笔记。
|
||||
|
||||
- **Fork 并绑定:**用 parent 的平衡 completed-turn prefix、`parentSession`、`meta.isSeeded: true` 与精确 sibling `inheritedEventCount` 创建 child。这组合 `ctx.agents.create({ seed, inheritedEventCount, meta })`;不新增 core service 或 Session-store method。
|
||||
- **Fork 并绑定:**用 parent 的平衡 completed-turn prefix、`meta.isSeeded: true` 与精确 sibling `inheritedEventCount` 创建 child。这组合 `ctx.agents.create({ seed, inheritedEventCount, meta })`;不新增 core service 或 Session-store method。 依据[独立 fork 决策](../../implemented/architecture/2026-10-23-independent-session-forks.zh.md),调用方将交回目标保存在所复制 Session 之外。
|
||||
- **顾问定位:**创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,可在继承的历史上保留提供方的前缀缓存。
|
||||
- **合并回写:**向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在日志所记录的位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.zh.md),无需新增会话事件。
|
||||
- **呈现:**调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 Agent Note 仅规定与界面无关的机制。
|
||||
@@ -28,7 +28,7 @@ Status: proposed
|
||||
|
||||
## 验收标准
|
||||
|
||||
- Fork 不改变 source Session,创建的 child 带有平衡 completed-turn prefix、`parentSession`、`isSeeded: true`、精确 `inheritedEventCount` 与逐字节相同 system prompt。
|
||||
- Fork 不改变 source Session,创建的 child 带有平衡 completed-turn prefix、`isSeeded: true`、精确 `inheritedEventCount` 与逐字节相同 system prompt。
|
||||
- 顾问定位在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。
|
||||
- 合并回写恰好添加一条有长度上限的 `context/message`,来源为 `plugin: sidechat`;父会话的下一次请求与回放在相同位置看到它。
|
||||
- 父会话与子会话并发运行,日志和流之间无串扰。
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
---
|
||||
name: dsh-session-projection
|
||||
description: Use when creating, extending, or reviewing a DeepSeek Harness session projection, or migrating a fold helper to one.
|
||||
description: Use when creating, extending, or reviewing a DeepSeek Harness session projection, or migrating session-derived recovery state to one.
|
||||
---
|
||||
|
||||
# DeepSeek Harness Session Projections
|
||||
# Session Projection Rules
|
||||
|
||||
Follow the [projection API](../../../packages/session/session-projection/README.md) for registration and lifecycle details.
|
||||
## Summary
|
||||
|
||||
1. Decide whether to extend an existing projection or create one; check whether an existing owner already maintains the needed state.
|
||||
2. Follow the README's [service ownership rules](../../../packages/session/session-projection/README.md#register-and-read): keep projection definitions and handles private to the owning service's implementation, and expose domain methods to consumers.
|
||||
3. Require `sessionProjections` when the service needs projected state to function. Optional registration is for contributions the service can operate without.
|
||||
4. Evaluate retained state size as the session grows. Do not accumulate state proportional to session length, including collections of event sequence numbers. Growth is acceptable only when slow enough to remain negligible. When the data would keep growing, consider incremental loading instead of retaining the complete state.
|
||||
5. `apply` is synchronous; historical reads belong in asynchronous consumers. When later reads need event payloads, consider retaining sequence numbers within rule 4's state-size limits and letting consumers resolve them asynchronously.
|
||||
6. When replacing a helper such as `foldSessionTitle` with a projection, remove the superseded helper.
|
||||
7. After migrating to Session persistence, stop using `fold` in names and descriptions of the migrated logic.
|
||||
8. Do not add calls to deprecated synchronous readers (`snapshotEvents`, `eventAt`, `ownEvents`) or wrappers exposing the same access ([decision](../../notes/implemented/architecture/2026-09-09-deprecate-synchronous-session-event-reads.md)). Assume the full event log is not resident in memory and random reads may require asynchronous disk access.
|
||||
9. Consider state size, replay cost, and per-event work; add complexity only for a concrete performance need.
|
||||
10. Asynchronous `register()` is usually called in `[Service.init]`. If event listeners are registered afterward, check initialization ordering: when needed, install listeners before awaiting registration; if a listener needs the returned handle, await its readiness in a callback that supports asynchronous waiting. Otherwise, an event may fire before the listener is installed and be missed. Determine whether this race exists from the actual initialization flow and event trigger paths; do not move listeners earlier or add waits mechanically.
|
||||
Keep recoverable session facts in Projection and expose them through their owning services. Read the [API and lifecycle](../../../packages/session/session-projection/README.md) for the supported preparation, registration, read, and cleanup contracts.
|
||||
|
||||
1. Put all persisted or restorable session-derived state in Projection, including host-only state. Reuse an existing owner when suitable. Keep external resources and process-scoped ownership outside persisted state. Persist facts, not registration handles, promises, or runtime resources.
|
||||
|
||||
2. Do not expose projection definitions or handles as production read APIs. Keep the ready handle private; consumers use the owning service's domain methods. A handle is a process-local reader, recreated during activation. Require `sessionProjections` when the service depends on it; optional registration is only for optional contributions. Do not hide a missing required dependency with another recovery path.
|
||||
|
||||
3. State recovery through `init` and `apply` is synchronous and deterministic. Do not perform I/O or trigger Cordis hooks, models, or tools. Runtime hooks own those effects. Replay must not repeat them. Return the same state reference for unrelated events. Keep views synchronous and preserve their references when visible values do not change.
|
||||
|
||||
4. Persist JSON state that its schema accepts. Follow the README's state-version and checkpoint-watermark rules. Restoring a checkpoint and replaying its suffix must match full replay. Change the version when state fields or recovery semantics change.
|
||||
|
||||
5. Prefer fixed-size or sublinear state. Growth is acceptable only when slow enough to remain negligible. Necessary linear state must have a current use and explicit memory, replay, and per-event costs. Do not retain history-sized indexes, including event sequence collections, by habit. Do not discard required data or change behavior to reduce size. Start with a complete implementation; optimize for a concrete need.
|
||||
|
||||
6. Caches must be discardable and rebuildable without changing behavior. A sequence reference alone is insufficient when a synchronous consumer needs the referenced payload. Store the required current value within rule 5, or prepare the payload through an explicit asynchronous path. Do not hide full-log reads in a cache miss. Do not add `snapshotEvents`, `eventAt`, `ownEvents`, or equivalent wrappers: history need not stay in memory. An async wrapper does not make synchronous history reads asynchronous. Existing host replay may remain for named supported callers, with an owner and removal condition; this does not permit [Client cache-miss full-log reads](../../../docs/defensive-patterns/conversation-and-session.md#cold-reads-are-the-last-resort). Remove superseded recovery helpers; cache reconstruction must not duplicate recovery rules.
|
||||
|
||||
7. Await required registration before exposing synchronous readers or installing callbacks that use its handle, usually in `[Service.init]`. Let registration rejection fail that activation. Events can arrive during the await; install listeners earlier only when needed to avoid missed events. Await only in callbacks that support it. Do not move listeners or add waits mechanically. Keep fiber cleanup ownership, await explicit disposal, and do not create effects after the owner is disposed.
|
||||
|
||||
8. Keep registration readiness and Session recovery separate. The asynchronous Session-loading or preparation owner completes historical recovery before it publishes the Session to synchronous readers. Use the registry's supported lifecycle; do not add hydration or hidden I/O to domain getters. Recovery prepares state; getters read it. Preserve the registry's error scope for each unit and Session.
|
||||
|
||||
9. Identify all official consumers before switching a read path, including plugins, CLI/GUI, and both SDKs. Preserve synchronous callers until they migrate explicitly; a Promise cannot implement a synchronous API. A new required service or activation step needs consumer updates and an upgrade guide. Keep compatibility tied to actual supported callers, not test-only compositions.
|
||||
|
||||
10. Verify values, notifications, error scope and timing, and recovery through affected real entry points. Keep existing behavior assertions. Do not move consumer-only errors into eager Projection processing. For lifecycle changes, test delayed registration, rejection, replacement, and disposal during an await. Use actual Session/store lifecycles and explicit barriers instead of partial Session mocks or microtask counts. Check error logs as well as released state. Recovery tests cover usable, missing, and incompatible checkpoints plus suffix replay, without runtime effects.
|
||||
@@ -16,7 +16,7 @@ The [`weighted-approval` workflow](../workflows/weighted-approval.yml) publishes
|
||||
|
||||
## Approval scoring
|
||||
|
||||
The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. The publisher marks the head pending before Python setup or lexer installation, so failed setup or an interrupted history fetch cannot leave a previous success in place. Setup and evaluation failures publish an error status. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status. The publisher also writes a `weighted approval decision` check run that reports the same decision as a conclusion — `action_required` while the policy is unmet, `success` when it holds, and `failure` on setup or evaluation failures — with the score or blocker in its title. Branch rules do not require that check run, so it never blocks a merge; it names the blocked state beside the status that gates the merge. A failed check-run write is logged and never changes the status.
|
||||
The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. The publisher marks the head pending before Python setup or lexer installation, so failed setup or an interrupted history fetch cannot leave a previous success in place. Setup and evaluation failures publish an error status. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status. The publisher also writes a `weighted approval decision` check run that reports the same decision as a conclusion — `action_required` while the policy is unmet, `success` when it holds, and `failure` on setup or evaluation failures — with the score or blocker in its title. Branch rules do not require that check run, so it never blocks a merge; it names the blocked state beside the status that gates the merge. The check run is published with a token minted from a dedicated approval App (`DSH_APPROVAL_APP_CLIENT_ID`, `DSH_APPROVAL_APP_PRIVATE_KEY`, `DSH_APPROVAL_APP_SLUG`, scope `checks: write`): GitHub attaches a check run created with the job token to the earliest Actions check suite on the head, and when that suite belongs to this `pull_request_target` workflow the merge box omits it, so the App identity makes the association stable. The App publishes only runs whose `app.slug` is the App; job-token runs from earlier evaluations stay untouched and can coexist on a head. When the token or slug is missing, the publisher falls back to the job token and updates job-token runs only, which keeps the label on the Checks page. The App token step runs with `continue-on-error`, so a missing App configuration fails only that step and leaves the job-token fallback; a failed check-run write is logged by the publisher and never changes the status.
|
||||
|
||||
Reviewers whose calculated base repository permission is `write` or `admin` count. The [approval policy](approval-policy.json) gives `@07akioni`, `@imccyu`, `@tianyicui`, `@tianyicui-bot`, `@turtle1999`, and `@turtle2099` two points each; every other write-capable reviewer gets one point. The pull-request author’s own review and reviewers without write permission do not count.
|
||||
|
||||
@@ -44,7 +44,7 @@ Each sender contributes at most once, using their own fixed weight or production
|
||||
|
||||
## Security
|
||||
|
||||
All actions in the status-writing job are pinned to commit SHAs. The job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. Only when there is no blocker, reviewer points plus author credit are insufficient, and an approval has the policy’s default weight, it fetches complete history using the job token, passes Git objects to the trusted classifier as data, and resolves commit authors in batches of 50. Fetch credentials exist only in the Git child environment. Missing history, parsing failures, or incomplete author queries fail evaluation rather than producing a partial score. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews and comments are treated as API data and escaped in logs. All publisher triggers and steps, including dependency setup, have pull-request write permission; the policy uses it to dismiss eligible senders' prior decision reviews and request recipients. Every evaluation verifies command authors and latest editors by immutable account ID through GraphQL, matching the body to the REST comment history before counting points or acting. Missing or inconsistent editor metadata fails evaluation. Non-author edits are ignored. Only active eligible delegations are reconciled; superseded or revoked commands have no effects. Dismissal failures publish an error; request failures remain separate from the approval decision.
|
||||
All actions in the status-writing job are pinned to commit SHAs. The job checks out only the repository default branch. It does not check out or execute pull-request code. Its only secret is the dedicated approval App private key, used to mint a token scoped to `checks: write` for the decision check run; the status gate keeps using the job token. Only when there is no blocker, reviewer points plus author credit are insufficient, and an approval has the policy’s default weight, it fetches complete history using the job token, passes Git objects to the trusted classifier as data, and resolves commit authors in batches of 50. Fetch credentials exist only in the Git child environment. Missing history, parsing failures, or incomplete author queries fail evaluation rather than producing a partial score. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews and comments are treated as API data and escaped in logs. All publisher triggers and steps, including dependency setup, have pull-request write permission; the policy uses it to dismiss eligible senders' prior decision reviews and request recipients. Every evaluation verifies command authors and latest editors by immutable account ID through GraphQL, matching the body to the REST comment history before counting points or acting. Missing or inconsistent editor metadata fails evaluation. Non-author edits are ignored. Only active eligible delegations are reconciled; superseded or revoked commands have no effects. Dismissal failures publish an error; request failures remain separate from the approval decision.
|
||||
|
||||
Approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing the program or policy for its own run.
|
||||
|
||||
|
||||
@@ -308,10 +308,10 @@ export async function evaluateApproval({ event, policySource, api, getOwnership
|
||||
|
||||
/**
|
||||
* Evaluate and publish the required commit status, publishing an error status when evaluation fails.
|
||||
* @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, runUrl: string, write?: (line: string) => void, getOwnership?: typeof productionOwnership, getMergedCount?: typeof countMergedAuthorPulls}} options Runtime inputs.
|
||||
* @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, checkApi?: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, checkAppSlug?: string, runUrl: string, write?: (line: string) => void, getOwnership?: typeof productionOwnership, getMergedCount?: typeof countMergedAuthorPulls}} options Runtime inputs; checkApi publishes the decision check run, defaulting to api, and checkAppSlug selects the runs that client can update.
|
||||
* @returns {Promise<Awaited<ReturnType<typeof evaluateApproval>>>} Published approval decision.
|
||||
*/
|
||||
export async function runApprovalCheck({ event, policySource, api, runUrl, getOwnership = productionOwnership, getMergedCount = countMergedAuthorPulls, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
export async function runApprovalCheck({ event, policySource, api, checkApi = api, checkAppSlug = 'github-actions', runUrl, getOwnership = productionOwnership, getMergedCount = countMergedAuthorPulls, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
const pull = pullRequestFromEvent(event)
|
||||
await publishStatus(api, pull, 'pending', 'Evaluating approval points.', runUrl)
|
||||
let result
|
||||
@@ -334,7 +334,7 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
}
|
||||
} catch (error) {
|
||||
await publishStatus(api, pull, 'error', 'Approval evaluation failed.', runUrl)
|
||||
await tryPublishCheck(api, pull, 'failure', 'Approval evaluation failed.', runUrl, write)
|
||||
await tryPublishCheck(checkApi, pull, 'failure', 'Approval evaluation failed.', runUrl, write, checkAppSlug)
|
||||
throw error
|
||||
}
|
||||
write(result.authorCredit
|
||||
@@ -346,7 +346,7 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
writeList(write, 'Blocking change requests', result.blockers.map(login => `@${login}`))
|
||||
writeList(write, 'Ignored reviewers without write access', result.ignoredReviewers.map(login => `@${login}`))
|
||||
await publishStatus(api, pull, result.state, result.description, runUrl)
|
||||
await tryPublishCheck(api, pull, result.state === 'success' ? 'success' : 'action_required', result.description, runUrl, write)
|
||||
await tryPublishCheck(checkApi, pull, result.state === 'success' ? 'success' : 'action_required', result.description, runUrl, write, checkAppSlug)
|
||||
write(`Published ${JSON.stringify(STATUS_CONTEXT)} status ${JSON.stringify(result.state)}.`)
|
||||
try {
|
||||
await requestDelegatedReviews(result, api, write)
|
||||
@@ -358,15 +358,15 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
|
||||
/**
|
||||
* Revoke a previous success before dependency installation, or report its failure.
|
||||
* @param {{event: unknown, api: (path: string, options: object) => Promise<unknown>, runUrl: string, phase: string, write?: (line: string) => void}} options Publication inputs.
|
||||
* @param {{event: unknown, api: (path: string, options: object) => Promise<unknown>, checkApi?: (path: string, options: object) => Promise<unknown>, checkAppSlug?: string, runUrl: string, phase: string, write?: (line: string) => void}} options Publication inputs; checkApi publishes the decision check run and checkAppSlug selects the runs it can update.
|
||||
* @returns {Promise<void>} Completion of the status write and the corresponding check-run write.
|
||||
*/
|
||||
export async function publishApprovalPhase({ event, api, runUrl, phase, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
export async function publishApprovalPhase({ event, api, checkApi = api, checkAppSlug = 'github-actions', runUrl, phase, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
const pull = pullRequestFromEvent(event)
|
||||
if (phase === 'pending') {
|
||||
await publishStatus(api, pull, 'pending', 'Preparing approval evaluation.', runUrl)
|
||||
try {
|
||||
await revokeApproval(api, pull, runUrl)
|
||||
await revokeApproval(checkApi, pull, runUrl, checkAppSlug)
|
||||
} catch (error) {
|
||||
write(`Decision check run not revoked; the commit status is unchanged: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`)
|
||||
}
|
||||
@@ -374,7 +374,7 @@ export async function publishApprovalPhase({ event, api, runUrl, phase, write =
|
||||
}
|
||||
if (phase === 'error') {
|
||||
await publishStatus(api, pull, 'error', 'Approval setup or evaluation failed.', runUrl)
|
||||
return tryPublishCheck(api, pull, 'failure', 'Approval setup or evaluation failed.', runUrl, write)
|
||||
return tryPublishCheck(checkApi, pull, 'failure', 'Approval setup or evaluation failed.', runUrl, write, checkAppSlug)
|
||||
}
|
||||
throw new Error('invalid approval setup phase')
|
||||
}
|
||||
@@ -486,20 +486,20 @@ async function reviewerPermission(api, repository, login) {
|
||||
// The required gate is the commit status; this check run reports the same decision as a conclusion
|
||||
// label and is not required. A failed label write is reported and never changes the gate, including
|
||||
// for a replayed workflow whose token has no checks permission.
|
||||
async function tryPublishCheck(api, pull, conclusion, title, runUrl, write) {
|
||||
async function tryPublishCheck(checkApi, pull, conclusion, title, runUrl, write, appSlug) {
|
||||
try {
|
||||
await publishCheck(api, pull, conclusion, title, runUrl)
|
||||
await publishCheck(checkApi, pull, conclusion, title, runUrl, appSlug)
|
||||
} catch (error) {
|
||||
write(`Decision check run not published; the commit status is unchanged: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function publishCheck(api, pull, conclusion, title, runUrl) {
|
||||
async function publishCheck(checkApi, pull, conclusion, title, runUrl, appSlug) {
|
||||
requireHttpsRunUrl(runUrl)
|
||||
const body = { status: 'completed', conclusion, details_url: runUrl, output: { title, summary: `${title} [Workflow run](${runUrl})` } }
|
||||
const runs = await decisionRuns(api, pull)
|
||||
const runs = await decisionRuns(checkApi, pull, appSlug)
|
||||
if (runs.length === 0) {
|
||||
await api(`/repos/${pull.repository}/check-runs`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs`, {
|
||||
method: 'POST',
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, external_id: CHECK_RUN_MARKER, ...body },
|
||||
})
|
||||
@@ -507,21 +507,21 @@ async function publishCheck(api, pull, conclusion, title, runUrl) {
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (!Number.isSafeInteger(run.id)) throw new Error('check run has no valid id')
|
||||
await api(`/repos/${pull.repository}/check-runs/${run.id}`, { method: 'PATCH', body })
|
||||
await checkApi(`/repos/${pull.repository}/check-runs/${run.id}`, { method: 'PATCH', body })
|
||||
}
|
||||
}
|
||||
|
||||
// A head without a decision starts in progress; a published success becomes action_required, so an
|
||||
// interrupted evaluation cannot leave a success that its current inputs no longer support.
|
||||
async function revokeApproval(api, pull, runUrl) {
|
||||
async function revokeApproval(checkApi, pull, runUrl, appSlug) {
|
||||
requireHttpsRunUrl(runUrl)
|
||||
const title = 'Preparing approval evaluation.'
|
||||
const output = { title, summary: `${title} [Workflow run](${runUrl})` }
|
||||
const runs = await decisionRuns(api, pull)
|
||||
const runs = await decisionRuns(checkApi, pull, appSlug)
|
||||
if (runs.length === 0) {
|
||||
await api(`/repos/${pull.repository}/check-runs`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs`, {
|
||||
method: 'POST',
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, status: 'in_progress', details_url: runUrl, output },
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, external_id: CHECK_RUN_MARKER, status: 'in_progress', details_url: runUrl, output },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -529,7 +529,7 @@ async function revokeApproval(api, pull, runUrl) {
|
||||
if (!Number.isSafeInteger(run.id)) throw new Error('check run has no valid id')
|
||||
// The label is not a gate; only a published success has to stop claiming the policy holds.
|
||||
if (run.conclusion !== 'success') continue
|
||||
await api(`/repos/${pull.repository}/check-runs/${run.id}`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs/${run.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { status: 'completed', conclusion: 'action_required', details_url: runUrl, output },
|
||||
})
|
||||
@@ -537,13 +537,14 @@ async function revokeApproval(api, pull, runUrl) {
|
||||
}
|
||||
|
||||
// A completed check run never returns to in_progress: GitHub answers 200 and leaves it completed.
|
||||
async function decisionRuns(api, pull) {
|
||||
const listed = await api(`/repos/${pull.repository}/commits/${pull.headSha}/check-runs?check_name=${encodeURIComponent(CHECK_NAME)}&filter=all&per_page=100`)
|
||||
async function decisionRuns(checkApi, pull, appSlug) {
|
||||
const listed = await checkApi(`/repos/${pull.repository}/commits/${pull.headSha}/check-runs?check_name=${encodeURIComponent(CHECK_NAME)}&filter=all&per_page=100`)
|
||||
if (!isRecord(listed) || !Array.isArray(listed.check_runs) || listed.total_count !== listed.check_runs.length) {
|
||||
throw new Error('check-run list response is invalid or incomplete')
|
||||
}
|
||||
return listed.check_runs.filter(run => isRecord(run) && isRecord(run.app) && run.app.slug === 'github-actions'
|
||||
&& (run.external_id === CHECK_RUN_MARKER || run.external_id === ''))
|
||||
// Only runs created by the publishing token can be updated: the job token cannot patch an App run
|
||||
// and the App token cannot patch an Actions run, so the slug selects the runs this token owns.
|
||||
return listed.check_runs.filter(run => isRecord(run) && isRecord(run.app) && run.app.slug === appSlug)
|
||||
}
|
||||
|
||||
function requireHttpsRunUrl(runUrl) {
|
||||
@@ -640,6 +641,17 @@ async function main() {
|
||||
token: process.env.GITHUB_TOKEN ?? '',
|
||||
apiUrl: process.env.GITHUB_API_URL,
|
||||
})
|
||||
// The decision check run is published by the dedicated approval App so GitHub associates its check
|
||||
// suite with the pull request instead of whichever Actions suite started first. Both the token and
|
||||
// the App slug must be configured; otherwise the publisher keeps the job token, whose runs only
|
||||
// show on the Checks page.
|
||||
const appToken = process.env.DECISION_TOKEN
|
||||
const appSlug = process.env.DECISION_APP_SLUG
|
||||
const useApp = Boolean(appToken && appSlug)
|
||||
const checkApi = useApp
|
||||
? createGitHubApi({ token: appToken, apiUrl: process.env.GITHUB_API_URL })
|
||||
: api
|
||||
const checkAppSlug = useApp ? appSlug : 'github-actions'
|
||||
if (isRecord(event) && isRecord(event.issue)) {
|
||||
event = await approvalEventFromComment({ event, api })
|
||||
} else if (isRecord(event) && isRecord(event.workflow_run)) {
|
||||
@@ -653,7 +665,7 @@ async function main() {
|
||||
}
|
||||
const phase = process.argv[2]
|
||||
if (phase) {
|
||||
await publishApprovalPhase({ event, api, runUrl: process.env.GITHUB_RUN_URL ?? '', phase })
|
||||
await publishApprovalPhase({ event, api, checkApi, checkAppSlug, runUrl: process.env.GITHUB_RUN_URL ?? '', phase })
|
||||
if (phase === 'pending' && process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, 'active=true\n')
|
||||
return
|
||||
}
|
||||
@@ -661,6 +673,8 @@ async function main() {
|
||||
event,
|
||||
policySource,
|
||||
api,
|
||||
checkApi,
|
||||
checkAppSlug,
|
||||
runUrl: process.env.GITHUB_RUN_URL ?? '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -497,6 +497,92 @@ test('publishes setup phases without evaluating or installing dependencies', asy
|
||||
await assert.rejects(publishApprovalPhase({ ...options, phase: 'success' }), /invalid approval setup phase/u)
|
||||
})
|
||||
|
||||
test('updates the App-owned run instead of creating another', async () => {
|
||||
const calls = []
|
||||
const appRuns = [{ id: 42, app: { slug: 'dsh-weighted-approval' }, conclusion: 'success', external_id: 'weighted-approval-decision' }]
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
checkAppSlug: 'dsh-weighted-approval',
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path, options = {}) => {
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
calls.push({ path, method: options.method, body: options.body })
|
||||
if (isCheckRunsList(path)) return { total_count: appRuns.length, check_runs: appRuns }
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.deepEqual(calls.map(call => call.path), [
|
||||
`/repos/deepseek-harness/deepseek-harness/commits/${HEAD_SHA}/check-runs?check_name=weighted%20approval%20decision&filter=all&per_page=100`,
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/42',
|
||||
])
|
||||
assert.equal(calls[1].method, 'PATCH')
|
||||
assert.equal(calls[1].body.name, undefined)
|
||||
})
|
||||
|
||||
test('ignores job-token runs when the App token publishes', async () => {
|
||||
const calls = []
|
||||
const jobRuns = [{ id: 7, app: { slug: 'github-actions' }, conclusion: 'success', external_id: 'weighted-approval-decision' }]
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
checkAppSlug: 'dsh-weighted-approval',
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path) => {
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
calls.push({ path, method: options.method, body: options.body })
|
||||
if (isCheckRunsList(path)) return { total_count: jobRuns.length, check_runs: jobRuns }
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.equal(calls.at(-1).method, 'POST')
|
||||
assert.equal(calls.at(-1).body.name, 'weighted approval decision')
|
||||
assert.equal(calls.at(-1).body.external_id, 'weighted-approval-decision')
|
||||
})
|
||||
|
||||
test('publishes the check run through the App token when one is provided', async () => {
|
||||
const statusCalls = []
|
||||
const checkCalls = []
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path, options = {}) => {
|
||||
statusCalls.push({ path, body: options.body })
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
checkCalls.push({ path, body: options.body })
|
||||
if (isCheckRunsList(path)) return emptyCheckRuns
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.equal(statusCalls.at(-1).body.context, 'weighted approval')
|
||||
assert.equal(checkCalls.at(-1).body.name, 'weighted approval decision')
|
||||
assert.equal(checkCalls.at(-1).body.conclusion, 'action_required')
|
||||
assert.ok(statusCalls.every(call => !call.path.includes('/check-runs')))
|
||||
assert.ok(checkCalls.every(call => !call.path.includes('/statuses/')))
|
||||
})
|
||||
|
||||
test('keeps the status gate when the label write fails', async () => {
|
||||
const writes = []
|
||||
const states = []
|
||||
@@ -561,6 +647,7 @@ test('revokes a published success check run and updates one run per head', async
|
||||
`/repos/deepseek-harness/deepseek-harness/statuses/${HEAD_SHA}`,
|
||||
`/repos/deepseek-harness/deepseek-harness/commits/${HEAD_SHA}/check-runs?check_name=weighted%20approval%20decision&filter=all&per_page=100`,
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/7',
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/10',
|
||||
])
|
||||
for (const call of calls.slice(2)) {
|
||||
assert.equal(call.method, 'PATCH')
|
||||
|
||||
@@ -37,10 +37,26 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
# The decision check run is published by a dedicated approval App so GitHub associates its
|
||||
# check suite with the pull request and no other workflow owns that identity; the job token
|
||||
# alone keeps the run out of the merge box. The App holds only Checks write. A missing App
|
||||
# permission leaves the token empty and the publisher falls back to the job token.
|
||||
- name: Create decision check token
|
||||
id: decision-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
with:
|
||||
client-id: ${{ vars.DSH_APPROVAL_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.DSH_APPROVAL_APP_PRIVATE_KEY }}
|
||||
owner: deepseek-harness
|
||||
repositories: deepseek-harness
|
||||
permission-checks: write
|
||||
- name: Revoke previous approval status
|
||||
id: revoke
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs pending
|
||||
# SECURITY: dependency setup runs in the status-writing job.
|
||||
@@ -57,11 +73,15 @@ jobs:
|
||||
if: steps.revoke.outputs.active == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs
|
||||
- name: Publish approval setup failure
|
||||
if: failure() && steps.revoke.outputs.active == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs error
|
||||
@@ -160,6 +160,7 @@ pnpm applies local patches to the following packages at install time, so shipped
|
||||
- `@yao-pkg/pkg@6.21.0` — [`patches/@yao-pkg__pkg@6.21.0.patch`](patches/@yao-pkg__pkg@6.21.0.patch)
|
||||
- `app-builder-lib@26.17.0` — [`patches/app-builder-lib@26.17.0.patch`](patches/app-builder-lib@26.17.0.patch)
|
||||
- `exceljs@4.4.0` — [`patches/exceljs@4.4.0.patch`](patches/exceljs@4.4.0.patch)
|
||||
- `lexical@0.49.0` — [`patches/lexical@0.49.0.patch`](patches/lexical@0.49.0.patch)
|
||||
- `node-pty@1.2.0-beta.15` — [`patches/node-pty@1.2.0-beta.15.patch`](patches/node-pty@1.2.0-beta.15.patch)
|
||||
|
||||
## Chrome DevTools frontend
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.2.1-rc.2",
|
||||
"version": "0.2.1-rc.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -48,6 +48,7 @@
|
||||
"@deepseek-ai/dsh-jobs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:*",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:*",
|
||||
"@deepseek-ai/dsh-official-bundles": "workspace:*",
|
||||
"@deepseek-ai/dsh-persona": "workspace:*",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:*",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:*",
|
||||
@@ -106,7 +107,8 @@
|
||||
"@deepseek-ai/dsh-experimental-session-search": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*"
|
||||
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-personalized-recommendations": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-skill-badge": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/** Profile package management and explicit, exact-version compatibility approvals. */
|
||||
import { runPluginCommand, runProfilePnpm, setProfileVersionExemption, type PackageOperationOptions } from '@deepseek-ai/dsh-plugin-manager/operations'
|
||||
import { INSTALL_ANCHOR } from './profile-boot.ts'
|
||||
import { DEFAULT_PROFILE_BUNDLES, initProfile, PROFILE_TEMPLATES, readProfileCompatibility, resolveProfileDir, type ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { initProfile, readProfileCompatibility, resolveProfileDir, type ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES, DEFAULT_PROFILE_BUNDLES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
|
||||
@@ -26,12 +26,12 @@ import {
|
||||
reportSkippedBundles,
|
||||
PluginPackages,
|
||||
PROFILE_PATCH_FILENAME,
|
||||
PROFILE_TEMPLATES,
|
||||
resolveProfileDir,
|
||||
type ProfileContext,
|
||||
type Profile,
|
||||
type RuntimeResolution,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
||||
import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
|
||||
@@ -443,6 +443,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('serves the SDK protocol with an absolute-path overlay plugin and exits after shutdown', async () => {
|
||||
// On the official endpoint the route requests its model listing with this key;
|
||||
// the loopback mock keeps the run offline and records any model call.
|
||||
const server = await startMockLlmServer({ sequence: ['success'] })
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-'))
|
||||
const pluginPath = join(home, 'plugin #100%.mjs')
|
||||
const marker = join(home, 'plugin-loaded')
|
||||
@@ -465,6 +468,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
DSH_HOME: home,
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
DEEPSEEK_API_KEY: 'built-sdk-profile-no-call',
|
||||
DEEPSEEK_BASE_URL: server.baseURL,
|
||||
},
|
||||
extendEnv: false,
|
||||
})
|
||||
@@ -504,10 +508,12 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(result.signal, stderr).toBeUndefined()
|
||||
expect(result.exitCode, stderr).toBe(0)
|
||||
expect(stderr).toBe('')
|
||||
expect(server.requests).toEqual([])
|
||||
expect(readFileSync(marker, 'utf8')).toBe('loaded')
|
||||
} finally {
|
||||
child.kill('SIGKILL')
|
||||
await child
|
||||
await server.close()
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
@@ -13,7 +13,8 @@ import { setTimeout as delay } from 'node:timers/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import WebSocket from 'ws'
|
||||
import { initProfile, PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot'
|
||||
import { initProfile } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
|
||||
const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
const BUILT_BIN = join(REPO_ROOT, 'apps/cli/lib/bin.js')
|
||||
|
||||
@@ -3,7 +3,8 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { getDshRuntimeVersion, initProfile, PROFILE_TEMPLATES, readProfileVersionExemptions } from '@deepseek-ai/dsh-app-boot'
|
||||
import { getDshRuntimeVersion, initProfile, readProfileVersionExemptions } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { runPluginCommand, runProfilePnpm } from '@deepseek-ai/dsh-plugin-manager/operations'
|
||||
import { runPlugin } from '../src/plugin.ts'
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
initProfile,
|
||||
PROFILE_PATCH_FILENAME,
|
||||
PROFILE_TEMPLATES,
|
||||
readProfileManifest,
|
||||
resolveProfileDir,
|
||||
writeProfileManifest,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { execa } from 'execa'
|
||||
import { initializeProfileFromDefault } from '../src/profile-boot.ts'
|
||||
|
||||
@@ -5,7 +5,8 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { composeEntries, loadProfile, PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot'
|
||||
import { composeEntries, loadProfile } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-official-bundles'
|
||||
|
||||
const installAnchor = fileURLToPath(new URL('../package.json', import.meta.url))
|
||||
const resourcePackage = '@deepseek-ai/dsh-mcp-resources'
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -41,3 +41,9 @@
|
||||
config:
|
||||
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
|
||||
workspaceRoot: !!js process.cwd()
|
||||
|
||||
# A search key reference no runner sets: the turn runs as a deployment without
|
||||
# web search credentials, whatever the launching environment exports.
|
||||
- id: web-search-deepseek
|
||||
config:
|
||||
apiKeyEnv: DSH_KEYLESS_SMOKE_SEARCH_KEY
|
||||
@@ -60,7 +60,9 @@ describe('headless-agent keyless smoke', () => {
|
||||
})
|
||||
expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
|
||||
expect(persistedHeader).toMatchObject({ type: 'session' })
|
||||
expect(persistedToolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
|
||||
expect(persistedToolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch']))
|
||||
// Search has no key in this composition, so the model request does not offer it.
|
||||
expect(persistedToolNames).not.toContain('web_search')
|
||||
expect(persistedToolNames).not.toContain('str_replace_editor')
|
||||
}, PRODUCTION_PROFILE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
fixtureContext,
|
||||
normalizeSessionSnapshot,
|
||||
normalizeSessionSnapshots,
|
||||
REPLAY_CREDENTIAL_ENV,
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-session-snapshot'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
@@ -104,6 +105,7 @@ describe('parent-only override inheritance snapshot', () => {
|
||||
binArgs: [configPath, task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
...REPLAY_CREDENTIAL_ENV,
|
||||
// The primary fixture path must exist for llm-replay's config guard;
|
||||
// the override sidecar fully replaces the derived parent script.
|
||||
DSH_SNAPSHOT_FILE: replayOverride,
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
{
|
||||
"path": "../../packages/boot/app-boot"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/official-bundles"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/boot/cmdline"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop-host",
|
||||
"description": "Private Node-mode host process for the Electron desktop application",
|
||||
"version": "0.2.1-rc.2",
|
||||
"version": "0.2.1-rc.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 372297005b57a08b
|
||||
zh: 074a635ac17e4362
|
||||
/deepseek-harness-desktop/installation-ownership:
|
||||
en: 7cbb1f9e5006bd34
|
||||
zh: 01e9e15a75a90450
|
||||
en: 160b500cdcd6a39a
|
||||
zh: 6c267f1762ec3689
|
||||
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
|
||||
en: a1a47b09f4a72ba4
|
||||
zh: 7b1cf27f0b72cf31
|
||||
@@ -45,8 +45,8 @@
|
||||
en: faf7676ba18bbf2f
|
||||
zh: f1d7b75ad348c548
|
||||
/deepseek-harness-desktop/develop/startup-onboarding:
|
||||
en: d87d51b901b72ecd
|
||||
zh: 525da1c4d56af659
|
||||
en: 67dc7ef3e293236a
|
||||
zh: 1b224db684a2d1d7
|
||||
/deepseek-harness-desktop/develop/welcome-window-appearance:
|
||||
en: 617a89aeb28131b9
|
||||
zh: 4d84bd5d86501c86
|
||||
|
||||
@@ -152,7 +152,7 @@ The product UI retains Web actions, including "Open In..." through the shared au
|
||||
|
||||
Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version.
|
||||
|
||||
Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. English, Simplified Chinese, and Japanese resources must have matching keys and format placeholders; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
|
||||
Electron chooses typed shell copy in the eight [shipped Client languages](../../packages/client/locale/README.md) from its application locale and falls back to English. The macOS application bundle declares all eight languages in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. All shipped language resources must have matching keys, format placeholders, and command examples; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
|
||||
|
||||
Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance.
|
||||
|
||||
@@ -220,7 +220,7 @@ Repeated launches and `dsh://open` keep the workspace hidden until the startup c
|
||||
|
||||
Desktop checks configured model API-key references after the Host starts and before opening the workspace. With no configured key, the welcome window offers the API-key page. Save and continue writes through the existing credential service using the official DeepSeek provider's configured reference, then opens the workspace. Set up later opens the workspace without saving the draft or a completion setting; the next process launch checks credentials again. Back to sign in returns to the entry and clears the unsaved key and validation message. Buttons keep their labels and block competing actions while saving or opening the workspace. The Desktop preload marker suppresses the Web credential dialog, while retaining the Models settings page and the welcome notice.
|
||||
|
||||
The welcome window reads the shared `locale.preference` before it appears. An explicit English, Chinese, or Japanese choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector.
|
||||
The welcome window reads the shared `locale.preference` before it appears. An explicit supported language choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector.
|
||||
|
||||
The browser-login waiting page offers a copy-link action for the current pending authorization, a loading indicator, and cancellation. Clipboard failures leave the copy action available for retry. Copy feedback resets after two seconds; the copied state disables the link until it resets. Welcome text uses Montserrat Light with system fallbacks; large action buttons keep the system font, while text buttons use Montserrat Light. English introductory copy is 24px throughout. Chinese introductory copy is 24px with a 26px product name. Login action buttons are 240px wide with 14px labels. Authorization status headings use 20px Montserrat Regular text. The API-key page uses a 20px heading and a 14px back action, with 84px between the secondary button’s bottom edge and the window bottom.
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm
|
||||
|
||||
升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。
|
||||
|
||||
Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。 Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。英文、简体中文和日语资源的键与格式占位符必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
|
||||
Electron 根据应用语言选择类型化的八种[内置 Client 语言](../../packages/client/locale/README.zh.md)的 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持全部八种语言,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。全部内置语言资源的键、格式占位符和命令示例必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
|
||||
|
||||
Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器同步全屏状态,让浮层调整顶栏避让距离。
|
||||
|
||||
@@ -220,7 +220,7 @@ API Key 输入框初始为空,并通过 `autocomplete="new-password"` 请求 C
|
||||
|
||||
Desktop 在 Host 启动后、打开工作区前检查模型 API Key 引用是否已配置。没有已配置的密钥时,欢迎窗口提供 API Key 页面。“保存并继续”通过现有凭证服务写入 DeepSeek 官方提供方配置的引用,然后打开工作区。“稍后配置”打开工作区,但不保存草稿或完成标记;下次进程启动时会重新检查凭证。“返回登录”回到入口并清空未保存的密钥和校验提示。保存或打开工作区期间,按钮保持原文案并禁用竞争操作。Desktop preload 标记使 Web 凭证弹窗不再显示,同时保留模型设置页和欢迎须知。
|
||||
|
||||
欢迎窗口在显示前读取共享的 `locale.preference`。用户明确选择的英文、中文或日文优先;否则 Desktop 按系统语言顺序匹配支持的语言,并以英文兜底。主界面在挂载前通过隔离 preload 读取同一偏好和系统语言顺序。在设置中切换语言会更新桌面壳的当前词典和菜单;自动选择不会写入偏好。欢迎窗口不提供语言切换入口。
|
||||
欢迎窗口在显示前读取共享的 `locale.preference`。用户明确选择的受支持语言优先;否则 Desktop 按系统语言顺序匹配支持的语言,并以英文兜底。主界面在挂载前通过隔离 preload 读取同一偏好和系统语言顺序。在设置中切换语言会更新桌面壳的当前词典和菜单;自动选择不会写入偏好。欢迎窗口不提供语言切换入口。
|
||||
|
||||
等待浏览器登录时,欢迎页提供当前待授权请求的链接复制入口、加载指示和取消操作;剪贴板写入失败后可以重试复制,复制结果提示在两秒后恢复;已复制状态下链接禁用,恢复后可再次点击。Welcome 文字使用 Montserrat Light 并回退到系统字体,底部大按钮保留系统字体,文字按钮使用 Montserrat Light。英文欢迎正文及产品名均为 24px,中文欢迎正文为 24px、产品名为 26px。登录操作按钮宽 240px,文字为 14px。授权状态标题使用 20px Montserrat Regular 字重。API Key 页的标题为 20px,返回操作为 14px,次级按钮底边距窗口底部 84px。
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nsisLanguage": "GERMAN",
|
||||
"strings": {
|
||||
"INSTALLER_INSTALL": "Jetzt installieren",
|
||||
"INSTALLER_FINISH": "Fertigstellen",
|
||||
"INSTALLER_NATIVE_PROGRESS": "Installation wird vorbereitet… %d%%",
|
||||
"INSTALLER_PROGRESS_EXTRACT": "Dateien extrahieren… %d%%",
|
||||
"INSTALLER_PROGRESS_COPY": "Dateien werden installiert… %d%%",
|
||||
"INSTALLER_PROGRESS_REGISTER": "Abschluss der Installation… %d%%",
|
||||
"INSTALLER_PROGRESS_CLEAN": "Bereinigen temporärer Dateien… %d%%",
|
||||
"INSTALLER_CHOOSE_PATH": "Standort wählen",
|
||||
"INSTALLER_BROWSE": "Durchsuchen",
|
||||
"INSTALLER_LAUNCH": "Jetzt starten",
|
||||
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness konnte nicht gestartet werden. Versuchen Sie es noch einmal oder deaktivieren Sie „Jetzt starten“ und schließen Sie das Installationsprogramm.",
|
||||
"INSTALLER_UI_ERROR": "Das Installationsprogramm konnte sein Fenster nicht initialisieren. Schließen Sie es und versuchen Sie es erneut.",
|
||||
"INSTALLER_THEME_ERROR": "Verwenden Sie /THEME=auto, /THEME=light oder /THEME=dark.",
|
||||
"INSTALLER_FAILED": "Die Installation wurde nicht abgeschlossen. Schließen Sie dieses Installationsprogramm und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, laden Sie das Installationsprogramm erneut herunter.",
|
||||
"INSTALLER_RUNNING": "DeepSeek Harness läuft. Beenden Sie es über die Taskleiste und versuchen Sie es erneut.",
|
||||
"INSTALLER_DIRECTORY_FAILED": "Das Installationsverzeichnis konnte nicht ersetzt werden.",
|
||||
"INSTALLER_DIRECTORY_HINT": "Überprüfen Sie den Zugriff auf das Installationsverzeichnis und schließen Sie Programme mithilfe seiner Dateien. Führen Sie dann das Installationsprogramm erneut aus. Kopieren Sie die Fehlerdetails, wenn das Problem weiterhin besteht.",
|
||||
"INSTALLER_DIRECTORY_RESTORE_HINT": "Die vorherige Installation konnte nicht wiederhergestellt werden. Behalten Sie das in den Fehlerdetails angezeigte Sicherungsverzeichnis bei und wenden Sie sich vor der erneuten Installation an den Support.",
|
||||
"INSTALLER_PATH_INVALID": "Wählen Sie einen vollständigen lokalen Ordnerpfad. Laufwerksstämme, Systemordner, Links und Sonderzeichen werden nicht unterstützt.",
|
||||
"INSTALLER_PATH_OWNERSHIP": "Wählen Sie einen leeren Ordner oder den registrierten DeepSeek Harness-Installationsordner.",
|
||||
"INSTALLER_PATH_WRITABLE": "Dieser Ort ist nicht beschreibbar. Wählen Sie einen Ordner, der für den aktuellen Benutzer verfügbar ist.",
|
||||
"INSTALLER_DISK_SPACE": "Es ist nicht genügend freier Speicherplatz vorhanden. Wählen Sie einen anderen Standort.",
|
||||
"INSTALLER_PER_USER": "Dieses Installationsprogramm unterstützt nur den aktuellen Benutzer. Deinstallieren Sie zunächst die vorhandene All-User-Installation.",
|
||||
"INSTALLER_EXTRACT_FAILED": "Die Anwendungsdateien konnten nicht extrahiert werden.",
|
||||
"INSTALLER_EXTRACT_HINT": "Möglicherweise hat die Sicherheitssoftware eine Datei blockiert oder die Festplatte ist voll. Schließen Sie dieses Installationsprogramm und versuchen Sie es erneut. Wenn es weiterhin fehlschlägt, senden Sie uns die Fehlerdetails unten.",
|
||||
"INSTALLER_EXTRACT_COPY": "Fehlerdetails kopieren",
|
||||
"INSTALLER_EXTRACT_EXPAND": "Details anzeigen",
|
||||
"INSTALLER_EXTRACT_COLLAPSE": "Details ausblenden",
|
||||
"INSTALLER_EXTRACT_SAVED": "Der vollständige Bericht wurde unter %s gespeichert",
|
||||
"INSTALLER_EXTRACT_UNSAVED": "Der vollständige Bericht konnte nicht gespeichert werden. Verwenden Sie „Fehlerdetails kopieren“, um sie beizubehalten.",
|
||||
"INSTALLER_EXTRACT_COPIED": "Fehlerdetails wurden in die Zwischenablage kopiert."
|
||||
},
|
||||
"uninstaller": {
|
||||
"welcome": "Willkommen",
|
||||
"finished": "entfernt worden"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nsisLanguage": "SPANISHINTERNATIONAL",
|
||||
"strings": {
|
||||
"INSTALLER_INSTALL": "Instalar ahora",
|
||||
"INSTALLER_FINISH": "Finalizar",
|
||||
"INSTALLER_NATIVE_PROGRESS": "Preparando la instalación… %d%%",
|
||||
"INSTALLER_PROGRESS_EXTRACT": "Extrayendo archivos… %d%%",
|
||||
"INSTALLER_PROGRESS_COPY": "Instalando archivos… %d%%",
|
||||
"INSTALLER_PROGRESS_REGISTER": "Terminando la instalación… %d%%",
|
||||
"INSTALLER_PROGRESS_CLEAN": "Limpieza de archivos temporales… %d%%",
|
||||
"INSTALLER_CHOOSE_PATH": "Elige ubicación",
|
||||
"INSTALLER_BROWSE": "Examinar",
|
||||
"INSTALLER_LAUNCH": "Lanzar ahora",
|
||||
"INSTALLER_LAUNCH_FAILED": "No se pudo iniciar DeepSeek Harness. Vuelva a intentarlo o desmarque Iniciar ahora y cierre el instalador.",
|
||||
"INSTALLER_UI_ERROR": "El instalador no pudo inicializar su ventana. Ciérralo y vuelve a intentarlo.",
|
||||
"INSTALLER_THEME_ERROR": "Utilice /THEME=auto, /THEME=light o /THEME=dark.",
|
||||
"INSTALLER_FAILED": "La instalación no terminó. Cierre este instalador y vuelva a intentarlo. Si el problema persiste, descargue el instalador nuevamente.",
|
||||
"INSTALLER_RUNNING": "DeepSeek Harness se está ejecutando. Salga de la bandeja del sistema y vuelva a intentarlo.",
|
||||
"INSTALLER_DIRECTORY_FAILED": "No se pudo reemplazar el directorio de instalación.",
|
||||
"INSTALLER_DIRECTORY_HINT": "Verifique el acceso al directorio de instalación y cierre los programas usando sus archivos, luego ejecute el instalador nuevamente. Copie los detalles del error si el problema persiste.",
|
||||
"INSTALLER_DIRECTORY_RESTORE_HINT": "No se pudo restaurar la instalación anterior. Mantenga el directorio de respaldo que se muestra en los detalles del error y comuníquese con el soporte antes de volver a instalar.",
|
||||
"INSTALLER_PATH_INVALID": "Elija una ruta de carpeta local completa. No se admiten raíces de unidades, carpetas del sistema, enlaces ni caracteres especiales.",
|
||||
"INSTALLER_PATH_OWNERSHIP": "Elija una carpeta vacía o la carpeta de instalación registrada de DeepSeek Harness.",
|
||||
"INSTALLER_PATH_WRITABLE": "Esta ubicación no se puede escribir. Elija una carpeta disponible para el usuario actual.",
|
||||
"INSTALLER_DISK_SPACE": "No hay suficiente espacio libre en disco. Elija otra ubicación.",
|
||||
"INSTALLER_PER_USER": "Este instalador solo admite el usuario actual. Primero desinstale la instalación existente para todos los usuarios.",
|
||||
"INSTALLER_EXTRACT_FAILED": "No se pudieron extraer los archivos de la aplicación.",
|
||||
"INSTALLER_EXTRACT_HINT": "Es posible que el software de seguridad haya bloqueado un archivo o que el disco esté lleno. Cierra este instalador y vuelve a intentarlo. Si sigue fallando, envíenos los detalles del error a continuación.",
|
||||
"INSTALLER_EXTRACT_COPY": "Copiar detalles del error",
|
||||
"INSTALLER_EXTRACT_EXPAND": "Mostrar detalles",
|
||||
"INSTALLER_EXTRACT_COLLAPSE": "Ocultar detalles",
|
||||
"INSTALLER_EXTRACT_SAVED": "El informe completo se guardó en %s.",
|
||||
"INSTALLER_EXTRACT_UNSAVED": "No se pudo guardar el informe completo. Utilice Copiar detalles del error para conservarlo.",
|
||||
"INSTALLER_EXTRACT_COPIED": "Los detalles del error se copiaron al portapapeles."
|
||||
},
|
||||
"uninstaller": {
|
||||
"welcome": "Bienvenido",
|
||||
"finished": "ha sido desinstalado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nsisLanguage": "FRENCH",
|
||||
"strings": {
|
||||
"INSTALLER_INSTALL": "Installer maintenant",
|
||||
"INSTALLER_FINISH": "Terminer",
|
||||
"INSTALLER_NATIVE_PROGRESS": "Préparation de l'installation… %d%%",
|
||||
"INSTALLER_PROGRESS_EXTRACT": "Extraction de fichiers… %d%%",
|
||||
"INSTALLER_PROGRESS_COPY": "Installation des fichiers… %d%%",
|
||||
"INSTALLER_PROGRESS_REGISTER": "Finition de l'installation… %d%%",
|
||||
"INSTALLER_PROGRESS_CLEAN": "Nettoyage des fichiers temporaires… %d%%",
|
||||
"INSTALLER_CHOOSE_PATH": "Choisissez un emplacement",
|
||||
"INSTALLER_BROWSE": "Parcourir",
|
||||
"INSTALLER_LAUNCH": "Lancez-vous maintenant",
|
||||
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness n'a pas pu être lancé. Réessayez ou désactivez Lancer maintenant et fermez le programme d'installation.",
|
||||
"INSTALLER_UI_ERROR": "Le programme d'installation n'a pas pu initialiser sa fenêtre. Fermez-le et réessayez.",
|
||||
"INSTALLER_THEME_ERROR": "Utilisez /THEME=auto, /THEME=light ou /THEME=dark.",
|
||||
"INSTALLER_FAILED": "L'installation n'est pas terminée. Fermez ce programme d'installation et réessayez. Si le problème persiste, téléchargez à nouveau le programme d'installation.",
|
||||
"INSTALLER_RUNNING": "DeepSeek Harness est en cours d'exécution. Quittez-le de la barre d'état système et réessayez.",
|
||||
"INSTALLER_DIRECTORY_FAILED": "Le répertoire d'installation n'a pas pu être remplacé.",
|
||||
"INSTALLER_DIRECTORY_HINT": "Vérifiez l'accès au répertoire d'installation et fermez les programmes à l'aide de ses fichiers, puis réexécutez le programme d'installation. Copiez les détails de l'erreur si le problème persiste.",
|
||||
"INSTALLER_DIRECTORY_RESTORE_HINT": "L'installation précédente n'a pas pu être restaurée. Conservez le répertoire de sauvegarde indiqué dans les détails de l'erreur et contactez l'assistance avant de procéder à une nouvelle installation.",
|
||||
"INSTALLER_PATH_INVALID": "Choisissez un chemin de dossier local complet. Les racines du lecteur, les dossiers système, les liens et les caractères spéciaux ne sont pas pris en charge.",
|
||||
"INSTALLER_PATH_OWNERSHIP": "Choisissez un dossier vide ou le dossier d'installation DeepSeek Harness enregistré.",
|
||||
"INSTALLER_PATH_WRITABLE": "Cet emplacement n'est pas accessible en écriture. Choisissez un dossier disponible pour l'utilisateur actuel.",
|
||||
"INSTALLER_DISK_SPACE": "Il n'y a pas assez d'espace disque libre. Choisissez un autre emplacement.",
|
||||
"INSTALLER_PER_USER": "Ce programme d'installation prend en charge uniquement l'utilisateur actuel. Désinstallez d’abord l’installation existante pour tous les utilisateurs.",
|
||||
"INSTALLER_EXTRACT_FAILED": "Les fichiers de l'application n'ont pas pu être extraits.",
|
||||
"INSTALLER_EXTRACT_HINT": "Un logiciel de sécurité a peut-être bloqué un fichier ou le disque est peut-être plein. Fermez ce programme d'installation et réessayez. Si l'échec persiste, envoyez-nous les détails de l'erreur ci-dessous.",
|
||||
"INSTALLER_EXTRACT_COPY": "Copier les détails de l'erreur",
|
||||
"INSTALLER_EXTRACT_EXPAND": "Afficher les détails",
|
||||
"INSTALLER_EXTRACT_COLLAPSE": "Masquer les détails",
|
||||
"INSTALLER_EXTRACT_SAVED": "Le rapport complet a été enregistré dans %s",
|
||||
"INSTALLER_EXTRACT_UNSAVED": "Le rapport complet n'a pas pu être enregistré. Utilisez Copier les détails de l'erreur pour le conserver.",
|
||||
"INSTALLER_EXTRACT_COPIED": "Les détails de l'erreur ont été copiés dans le presse-papiers."
|
||||
},
|
||||
"uninstaller": {
|
||||
"welcome": "Bienvenue",
|
||||
"finished": "a été supprimé"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nsisLanguage": "PORTUGUESEBR",
|
||||
"strings": {
|
||||
"INSTALLER_INSTALL": "Instale agora",
|
||||
"INSTALLER_FINISH": "Concluir",
|
||||
"INSTALLER_NATIVE_PROGRESS": "Preparando a instalação… %d%%",
|
||||
"INSTALLER_PROGRESS_EXTRACT": "Extraindo arquivos… %d%%",
|
||||
"INSTALLER_PROGRESS_COPY": "Instalando arquivos… %d%%",
|
||||
"INSTALLER_PROGRESS_REGISTER": "Concluindo a instalação… %d%%",
|
||||
"INSTALLER_PROGRESS_CLEAN": "Limpando arquivos temporários… %d%%",
|
||||
"INSTALLER_CHOOSE_PATH": "Escolha o local",
|
||||
"INSTALLER_BROWSE": "Procurar",
|
||||
"INSTALLER_LAUNCH": "Lançar agora",
|
||||
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness não pôde ser iniciado. Tente novamente ou desmarque Iniciar agora e feche o instalador.",
|
||||
"INSTALLER_UI_ERROR": "O instalador não conseguiu inicializar sua janela. Feche-o e tente novamente.",
|
||||
"INSTALLER_THEME_ERROR": "Use /THEME=auto, /THEME=light ou /THEME=dark.",
|
||||
"INSTALLER_FAILED": "A instalação não foi concluída. Feche este instalador e tente novamente. Se o problema persistir, baixe o instalador novamente.",
|
||||
"INSTALLER_RUNNING": "DeepSeek Harness está em execução. Saia da bandeja do sistema e tente novamente.",
|
||||
"INSTALLER_DIRECTORY_FAILED": "O diretório de instalação não pôde ser substituído.",
|
||||
"INSTALLER_DIRECTORY_HINT": "Verifique o acesso ao diretório de instalação e feche os programas usando seus arquivos e execute o instalador novamente. Copie os detalhes do erro se o problema persistir.",
|
||||
"INSTALLER_DIRECTORY_RESTORE_HINT": "A instalação anterior não pôde ser restaurada. Mantenha o diretório de backup mostrado nos detalhes do erro e entre em contato com o suporte antes de instalar novamente.",
|
||||
"INSTALLER_PATH_INVALID": "Escolha um caminho completo para a pasta local. Raízes de unidade, pastas de sistema, links e caracteres especiais não são suportados.",
|
||||
"INSTALLER_PATH_OWNERSHIP": "Escolha uma pasta vazia ou a pasta de instalação DeepSeek Harness registrada.",
|
||||
"INSTALLER_PATH_WRITABLE": "Este local não é gravável. Escolha uma pasta disponível para o usuário atual.",
|
||||
"INSTALLER_DISK_SPACE": "Não há espaço livre suficiente em disco. Escolha outro local.",
|
||||
"INSTALLER_PER_USER": "Este instalador oferece suporte apenas ao usuário atual. Desinstale primeiro a instalação existente para todos os usuários.",
|
||||
"INSTALLER_EXTRACT_FAILED": "Os arquivos do aplicativo não puderam ser extraídos.",
|
||||
"INSTALLER_EXTRACT_HINT": "O software de segurança pode ter bloqueado um arquivo ou o disco pode estar cheio. Feche este instalador e tente novamente. Se continuar falhando, envie-nos os detalhes do erro abaixo.",
|
||||
"INSTALLER_EXTRACT_COPY": "Copiar detalhes do erro",
|
||||
"INSTALLER_EXTRACT_EXPAND": "Mostrar detalhes",
|
||||
"INSTALLER_EXTRACT_COLLAPSE": "Ocultar detalhes",
|
||||
"INSTALLER_EXTRACT_SAVED": "O relatório completo foi salvo em %s",
|
||||
"INSTALLER_EXTRACT_UNSAVED": "Não foi possível salvar o relatório completo. Use Copiar detalhes do erro para mantê-lo.",
|
||||
"INSTALLER_EXTRACT_COPIED": "Os detalhes do erro foram copiados para a área de transferência."
|
||||
},
|
||||
"uninstaller": {
|
||||
"welcome": "Bem-vindo",
|
||||
"finished": "foi desinstalado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"nsisLanguage": "RUSSIAN",
|
||||
"strings": {
|
||||
"INSTALLER_INSTALL": "Установить сейчас",
|
||||
"INSTALLER_FINISH": "Готово",
|
||||
"INSTALLER_NATIVE_PROGRESS": "Подготовка установки… %d%%",
|
||||
"INSTALLER_PROGRESS_EXTRACT": "Извлечение файлов… %d%%",
|
||||
"INSTALLER_PROGRESS_COPY": "Установка файлов… %d%%",
|
||||
"INSTALLER_PROGRESS_REGISTER": "Завершение установки… %d%%",
|
||||
"INSTALLER_PROGRESS_CLEAN": "Очистка временных файлов… %d%%",
|
||||
"INSTALLER_CHOOSE_PATH": "Выберите местоположение",
|
||||
"INSTALLER_BROWSE": "Обзор",
|
||||
"INSTALLER_LAUNCH": "Запустить сейчас",
|
||||
"INSTALLER_LAUNCH_FAILED": "DeepSeek Harness не удалось запустить. Повторите попытку или снимите флажок «Запустить сейчас» и закройте программу установки.",
|
||||
"INSTALLER_UI_ERROR": "Установщику не удалось инициализировать свое окно. Закройте его и попробуйте еще раз.",
|
||||
"INSTALLER_THEME_ERROR": "Используйте /THEME=auto, /THEME=light или /THEME=dark.",
|
||||
"INSTALLER_FAILED": "Установка не завершена. Закройте этот установщик и повторите попытку. Если проблема не устранена, загрузите установщик еще раз.",
|
||||
"INSTALLER_RUNNING": "DeepSeek Harness работает. Закройте его из системного трея и повторите попытку.",
|
||||
"INSTALLER_DIRECTORY_FAILED": "Не удалось заменить каталог установки.",
|
||||
"INSTALLER_DIRECTORY_HINT": "Проверьте доступ к каталогу установки и закройте программы, использующие его файлы, затем снова запустите установщик. Скопируйте сведения об ошибке, если проблема не устранена.",
|
||||
"INSTALLER_DIRECTORY_RESTORE_HINT": "Предыдущую установку восстановить не удалось. Сохраните каталог резервной копии, указанный в сведениях об ошибке, и обратитесь в службу поддержки перед повторной установкой.",
|
||||
"INSTALLER_PATH_INVALID": "Выберите полный путь к локальной папке. Корни дисков, системные папки, ссылки и специальные символы не поддерживаются.",
|
||||
"INSTALLER_PATH_OWNERSHIP": "Выберите пустую папку или зарегистрированную папку установки DeepSeek Harness.",
|
||||
"INSTALLER_PATH_WRITABLE": "Это местоположение недоступно для записи. Выберите папку, доступную текущему пользователю.",
|
||||
"INSTALLER_DISK_SPACE": "Недостаточно свободного места на диске. Выберите другое место.",
|
||||
"INSTALLER_PER_USER": "Этот установщик поддерживает только текущего пользователя. Сначала удалите существующую установку для всех пользователей.",
|
||||
"INSTALLER_EXTRACT_FAILED": "Файлы приложения не удалось извлечь.",
|
||||
"INSTALLER_EXTRACT_HINT": "Возможно, программное обеспечение безопасности заблокировало файл или диск переполнен. Закройте этот установщик и повторите попытку. Если ошибка продолжает возникать, отправьте нам сведения об ошибке ниже.",
|
||||
"INSTALLER_EXTRACT_COPY": "Скопировать сведения об ошибке",
|
||||
"INSTALLER_EXTRACT_EXPAND": "Показать детали",
|
||||
"INSTALLER_EXTRACT_COLLAPSE": "Скрыть детали",
|
||||
"INSTALLER_EXTRACT_SAVED": "Полный отчет сохранен в %s.",
|
||||
"INSTALLER_EXTRACT_UNSAVED": "Полный отчет не удалось сохранить. Используйте Скопировать сведения об ошибке, чтобы сохранить их.",
|
||||
"INSTALLER_EXTRACT_COPIED": "Подробности об ошибке скопированы в буфер обмена."
|
||||
},
|
||||
"uninstaller": {
|
||||
"welcome": "Вас приветствует мастер удаления",
|
||||
"finished": "удалена"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop",
|
||||
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
|
||||
"version": "0.2.1-rc.2",
|
||||
"version": "0.2.1-rc.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
@@ -57,6 +57,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-settings-general": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar-browser": "workspace:*",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:*",
|
||||
"@deepseek-ai/dsh-official-bundles": "workspace:*",
|
||||
"@deepseek-ai/node-addon-system": "workspace:~",
|
||||
"@electron/get": "^5.1.0",
|
||||
"@electron/notarize": "2.5.0",
|
||||
|
||||
@@ -165,7 +165,7 @@ export function createElectronBuilderConfig(
|
||||
category: 'public.app-category.developer-tools',
|
||||
// macOS matches the application locale against this bundle, not Electron Framework resources.
|
||||
extendInfo: {
|
||||
CFBundleLocalizations: ['en', 'zh_CN', 'ja'],
|
||||
CFBundleLocalizations: ['en', 'zh_CN', 'ja', 'es', 'fr', 'de', 'ru', 'pt'],
|
||||
NSMicrophoneUsageDescription: 'DeepSeek Harness uses your microphone to transcribe speech into message drafts.',
|
||||
},
|
||||
// Ad-hoc signing keeps the modified Electron executable runnable without a Developer ID.
|
||||
|
||||
@@ -10,7 +10,8 @@ const defaultDirectory = fileURLToPath(new URL('../installer/locales/', import.m
|
||||
* @returns {{id: string, nsisLanguage: string, strings: Record<string, string>, uninstaller: Record<string, string>}[]} Complete language resources.
|
||||
*/
|
||||
export function readInstallerLocales(directory = defaultDirectory) {
|
||||
const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort().map(name => {
|
||||
const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort((a, b) =>
|
||||
a === 'en_US.json' ? -1 : b === 'en_US.json' ? 1 : a.localeCompare(b)).map(name => {
|
||||
if (!/^[a-z]{2}_[A-Z]{2}\.json$/u.test(name)) throw new Error(`${name}: invalid installer language filename`)
|
||||
/** @type {unknown} */
|
||||
const resource = JSON.parse(readFileSync(`${directory}/${name}`, 'utf8'))
|
||||
@@ -25,6 +26,7 @@ export function readInstallerLocales(directory = defaultDirectory) {
|
||||
const result = {}
|
||||
for (const [key, text] of Object.entries(value)) {
|
||||
if (typeof text !== 'string' || text.trim() === '') throw new Error(`${name}: ${key} must be nonempty text`)
|
||||
if (/[\u200b\ufeff]/u.test(text)) throw new Error(`${name}: ${key} contains an invisible locale character`)
|
||||
result[key] = text
|
||||
}
|
||||
return result
|
||||
@@ -34,7 +36,7 @@ export function readInstallerLocales(directory = defaultDirectory) {
|
||||
return { id: name.slice(0, -5), nsisLanguage: resource.nsisLanguage, strings,
|
||||
uninstaller: dictionary('uninstaller' in resource ? resource.uninstaller : undefined) }
|
||||
})
|
||||
for (const id of ['en_US', 'zh_CN', 'ja_JP']) {
|
||||
for (const id of ['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR']) {
|
||||
if (!locales.some(locale => locale.id === id)) throw new Error(`installer locales: missing ${id}.json`)
|
||||
}
|
||||
const english = locales.find(locale => locale.id === 'en_US')
|
||||
@@ -49,8 +51,8 @@ export function readInstallerLocales(directory = defaultDirectory) {
|
||||
const actual = locale[field]
|
||||
if (Object.keys(expected).sort().join(',') !== Object.keys(actual).sort().join(',')) throw new Error(`${locale.id}: ${field} keys differ from English`)
|
||||
for (const [key, text] of Object.entries(expected)) {
|
||||
const placeholders = (/** @type {string} */ value) => (value.match(/%.?/gu) ?? []).join(',')
|
||||
if (placeholders(text) !== placeholders(actual[key])) throw new Error(`${locale.id}: ${key} changes format placeholders`)
|
||||
const placeholders = (/** @type {string} */ value) => (value.match(/%.?|(?<![A-Za-z0-9/])\/[A-Z][A-Z_]*(?:=[\w-]+)?/gu) ?? []).join(',')
|
||||
if (placeholders(text) !== placeholders(actual[key])) throw new Error(`${locale.id}: ${key} changes format placeholders or commands`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const DESKTOP_RUNTIME_PATCHES: Readonly<Record<string, DesktopRuntimePatc
|
||||
'@yao-pkg/pkg@6.21.0': { scope: 'workspace-only', reason: 'The executable packer executes on the build host.' },
|
||||
'app-builder-lib@26.17.0': { scope: 'workspace-only', reason: 'DMG format validation executes on the build host.' },
|
||||
'exceljs@4.4.0': { scope: 'workspace-only', reason: 'The patched parser is embedded in the client bundle and worker.' },
|
||||
'lexical@0.49.0': { scope: 'workspace-only', reason: 'The patched editor is embedded in the client bundle.' },
|
||||
'node-pty@1.2.0-beta.15': { scope: 'shared', reason: 'Runtime terminals need helper resolution and Windows worker cleanup.' },
|
||||
}
|
||||
|
||||
|
||||
+871
-8
@@ -1,4 +1,4 @@
|
||||
/** Typed English, Chinese, and Japanese copy owned by the Electron shell. */
|
||||
/** Typed shell copy for every shipped language owned by the Electron shell. */
|
||||
|
||||
export const en = {
|
||||
cliCommandMenu: 'Manage dsh Command…',
|
||||
@@ -517,19 +517,882 @@ export const ja = {
|
||||
mandatoryNotification: 'アプリに戻り、インストールと再起動を確認してください。',
|
||||
} satisfies Record<keyof typeof en, string>
|
||||
|
||||
/** Spanish dictionary, checked against the English keys. */
|
||||
export const es = {
|
||||
cliCommandMenu: 'Administrar el comando dsh…',
|
||||
cliCommandTitle: 'Administrar el comando dsh',
|
||||
cliCommandLocation: 'Comando de escritorio: {path}',
|
||||
cliCommandSelected: 'Comando dsh actual: {path}',
|
||||
cliCommandTarget: 'Objetivo actual del lanzador: {path}',
|
||||
cliCommandShadowed: 'Otro dsh tiene prioridad. Elimine o reordene esa instalación para usar el comando Escritorio de forma predeterminada. También puede ejecutar el comando Escritorio por su ruta completa.',
|
||||
cliCommandSelectionUnknown: 'No se pudo verificar su comando de shell. Un alias u otra instalación de dsh puede tener prioridad.',
|
||||
cliCommandInstalled: 'El comando Escritorio está instalado.',
|
||||
cliCommandNotInstalled: 'Agregue el comando Escritorio a su terminal.',
|
||||
cliCommandBroken: 'El comando de escritorio necesita reparación.',
|
||||
cliCommandInstall: 'Instalar',
|
||||
cliCommandRepair: 'Reparación',
|
||||
cliCommandRemove: 'Quitar',
|
||||
cliCommandClose: 'Cerrar',
|
||||
cliCommandSwitch: '¿Continuar con el comando Escritorio?',
|
||||
cliCommandPreserve: 'Se conservará un comando existente. Otras instalaciones y archivos de inicio del shell no se modificarán.',
|
||||
cliCommandContinue: 'Continuar',
|
||||
cliCommandInstallApp: 'Instale Desktop en su carpeta de Aplicaciones antes de administrar el comando dsh.',
|
||||
cliCommandUpdating: 'Se está instalando una actualización. Administre el comando una vez finalizada la instalación.',
|
||||
cliCommandNewTerminal: 'Abra una nueva terminal y ejecute dsh --version.',
|
||||
cliCommandRemoved: 'Se eliminó el registro de comandos de escritorio.',
|
||||
cliCommandPreviousRestored: 'El lanzador anterior ha sido restaurado.',
|
||||
cliCommandOtherKept: 'Otras instalaciones de mando se han dejado en su lugar.',
|
||||
cliCommandBackupKept: 'Se conserva un lanzador anterior en: {path}',
|
||||
cliCommandChanged: 'El comando o RUTA cambió mientras este cuadro de diálogo estaba abierto. Abra Administrar comando dsh nuevamente para revisar el estado actual.',
|
||||
cliCommandOwnershipError: 'El registro del comando o la copia de seguridad cambió. No se eliminó ningún comando no relacionado.',
|
||||
cliCommandFailed: 'El comando no se pudo actualizar. Verifique que la aplicación esté instalada y que se pueda escribir en el destino, luego vuelva a intentarlo.',
|
||||
application: 'Solicitud',
|
||||
fileMenu: 'Archivo',
|
||||
closePage: 'Cerrar página o ventana',
|
||||
aboutMenu: 'Acerca de DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'Versión V{version}',
|
||||
hideApplication: 'Ocultar DeepSeek Harness',
|
||||
hideOtherApplications: 'Ocultar otros',
|
||||
showAllApplications: 'Mostrar todo',
|
||||
quitApplication: 'Salir de DeepSeek Harness',
|
||||
openApplication: 'Abrir DeepSeek Harness',
|
||||
quit: 'Salir',
|
||||
cancel: 'Cancelar',
|
||||
quitTitle: '¿Salir de DeepSeek Harness?',
|
||||
quitActiveTasks: 'Las tareas en ejecución se interrumpirán.',
|
||||
quitScheduledTasks: 'Las tareas programadas no se ejecutarán mientras la aplicación esté cerrada.',
|
||||
quitActiveAndScheduledTasks: 'Las tareas en ejecución se interrumpirán y las tareas programadas no se ejecutarán mientras la aplicación esté cerrada.',
|
||||
backgroundNoticeBody: 'Las tareas en ejecución continuarán. Puede volver a abrir la ventana desde la bandeja del sistema.',
|
||||
backgroundNoticeConfirm: 'Confirmar',
|
||||
edit: 'Editar',
|
||||
menuBar: 'Menú de aplicaciones',
|
||||
delete: 'Eliminar',
|
||||
undo: 'Deshacer',
|
||||
redo: 'Rehacer',
|
||||
cut: 'cortar',
|
||||
copy: 'Copiar',
|
||||
paste: 'Pegar',
|
||||
selectAll: 'Seleccionar todo',
|
||||
startupFailed: 'DeepSeek Harness no está disponible',
|
||||
fatalSummary: 'La aplicación no pudo iniciarse o se detuvo inesperadamente.',
|
||||
startupAddressInUse: 'Se está ejecutando otra instancia de DSH (como la web de dsh o la aplicación de escritorio). No pueden empezar al mismo tiempo. Salga de la otra instancia DSH en ejecución y luego reinicie.',
|
||||
diagnosticTruncated: '… Detalles del error acortados.',
|
||||
reportWrittenTo: 'Informe de diagnóstico: {path}',
|
||||
startupReinstallAdvice: 'Si faltan archivos de la aplicación o están dañados, ciérrela y vuelva a instalarla. Sus tareas se almacenan por separado.',
|
||||
exitApplication: 'Salir',
|
||||
restartApplication: 'Reiniciar',
|
||||
recoveryOperationFailed: 'La operación de recuperación falló',
|
||||
disableThirdPartyPlugins: 'Deshabilite los complementos de terceros, haga una copia de seguridad del parche del perfil y reinicie',
|
||||
welcomeTitle: 'DeepSeek Harness',
|
||||
welcomeBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineBefore: 'Bienvenido a ',
|
||||
welcomeTaglineBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineAfter: '',
|
||||
welcomeDescription: 'Desarrollar potencial. Explora la inteligencia.',
|
||||
welcomeAuthStarting: 'Abriendo inicio de sesión…',
|
||||
welcomeAuthWaiting: '¿El navegador no se abrió automáticamente?',
|
||||
welcomeAuthWaitingDescription: 'Copie el enlace de inicio de sesión y ábralo en su navegador para iniciar sesión.',
|
||||
welcomeAuthExchanging: 'Completando el inicio de sesión…',
|
||||
welcomeAuthExpired: 'Se agotó el tiempo de inicio de sesión',
|
||||
welcomeAuthExpiredDescription: 'Inicia sesión nuevamente para continuar',
|
||||
welcomeAuthFailed: 'No se pudo completar el inicio de sesión. Inténtelo de nuevo.',
|
||||
welcomeAuthCopyLink: 'Copiar enlace de inicio de sesión',
|
||||
welcomeAuthCopied: 'Copiado',
|
||||
welcomeAuthCopyFailed: 'No se pudo copiar. Intentar otra vez.',
|
||||
welcomeAuthCancel: 'Cancelar',
|
||||
welcomeAuthRetry: 'Iniciar sesión nuevamente',
|
||||
welcomeSignIn: 'Iniciar sesión',
|
||||
welcomeApiKey: 'Agregar clave API',
|
||||
welcomeKeyTitle: 'Agregue una clave API para comenzar',
|
||||
welcomeKeyDescription: 'Configura los modelos oficiales DeepSeek para empezar a utilizar Harness',
|
||||
welcomeKeyPlaceholder: 'Ingrese la clave API',
|
||||
welcomeKeySave: 'Guardar y continuar',
|
||||
welcomeKeyLater: 'Configurar más tarde',
|
||||
welcomeKeyBack: 'Volver para iniciar sesión',
|
||||
welcomeSessionExpired: 'Has cerrado sesión en tu cuenta, inicia sesión nuevamente.',
|
||||
welcomeKeyBlank: 'Ingrese una clave API.',
|
||||
welcomeKeyInvalid: 'Ingrese la clave API, sin comillas, espacios ni una asignación de variable de entorno.',
|
||||
welcomeKeyFailed: 'No se pudo guardar la clave API. Por favor inténtalo de nuevo.',
|
||||
welcomeContinueFailed: 'No se pudo abrir el espacio de trabajo. Por favor inténtalo de nuevo.',
|
||||
checkUpdatesMenu: 'Buscar actualizaciones...',
|
||||
reloadPageMenu: 'Recargar página',
|
||||
restartAppHostMenu: 'Reiniciar la aplicación y el host',
|
||||
updateCheckFailedTitle: 'Error en la comprobación de actualización',
|
||||
updateCheckFailed: 'No se pudo buscar actualizaciones. Inténtelo de nuevo más tarde.',
|
||||
updateDownloadFailed: 'No se pudo descargar la actualización. Por favor inténtalo de nuevo.',
|
||||
updateInstallFailed: 'No se pudo instalar la actualización. Inténtelo de nuevo más tarde.',
|
||||
updateCheckNetworkFailed: 'No se pudo buscar actualizaciones. Comprueba tu conexión y vuelve a intentarlo.',
|
||||
updateDownloadNetworkFailed: 'No se pudo descargar la actualización. Comprueba tu conexión y vuelve a intentarlo.',
|
||||
updateInstallNetworkFailed: 'No se pudo instalar la actualización. Comprueba tu conexión y vuelve a intentarlo.',
|
||||
unknownError: 'Error desconocido',
|
||||
updateCheckTitle: 'Buscar actualizaciones',
|
||||
updateCurrentDetail: 'Versión actual: {version}',
|
||||
updateCurrent: '¡Estás al día!',
|
||||
updateChecking: 'Buscando actualizaciones...',
|
||||
updateDownload: 'Descargar actualización',
|
||||
updateDownloadedTitle: 'La versión {version} está lista para instalar.',
|
||||
updateDownloadedDetail: 'La aplicación se cerrará durante la actualización y se volverá a abrir automáticamente cuando se complete.',
|
||||
updateDownloadedDetailWindows: 'La aplicación se cerrará temporalmente durante la actualización y se volverá a abrir automáticamente cuando se complete.\n\nLa actualización puede tardar algún tiempo. Espere y no vuelva a iniciar la aplicación durante la instalación.',
|
||||
updateClose: 'Cerrar',
|
||||
updateAcknowledge: 'OK',
|
||||
updateLater: 'Actualizar más tarde',
|
||||
updateDownloading: 'Descargando {percent}%…',
|
||||
updateVerifying: 'Verificando archivos de actualización...',
|
||||
updateInstalling: 'Preparándose para reiniciar…',
|
||||
updateRetry: 'Reintentar actualización',
|
||||
updateActiveTasks: 'Las tareas aún están en progreso',
|
||||
updateActiveTasksDetail: 'La actualización detendrá las tareas en curso y reiniciará la aplicación. ¿Continuar?',
|
||||
updateStopTasks: 'Detener tareas y actualizar',
|
||||
updateTasksChanged: 'Han comenzado nuevas tareas. Confirme nuevamente para detener las tareas y actualizar.',
|
||||
updateTasksUnavailable: 'El estado de la tarea no está disponible. Intente actualizar nuevamente cuando el espacio de trabajo esté listo.',
|
||||
updateFullscreenTimeout: 'No se pudo salir de la pantalla completa. Salga de la pantalla completa y luego vuelva a intentar la actualización.',
|
||||
updateStopFailed: 'No se pudieron detener las tareas de forma segura. La actualización no se ha instalado. Inténtelo de nuevo más tarde.',
|
||||
updateTechnicalDetails: 'Ver detalles técnicos',
|
||||
updateTitle: 'Actualización DeepSeek Harness',
|
||||
updateAvailable: 'Nueva versión disponible: {version}',
|
||||
updateDetail: 'Una vez que se completa la descarga, puede instalar la actualización y reiniciar la aplicación.',
|
||||
installAndRestart: 'Instalar y reiniciar',
|
||||
later: 'Más tarde',
|
||||
updateFailedTitle: 'Actualización fallida',
|
||||
mandatoryTitle: 'Actualizar para continuar',
|
||||
mandatoryDetail: 'Esta versión ya no es compatible. Actualiza para continuar. Las tareas en curso seguirán ejecutándose hasta que confirme la instalación y reinicie.',
|
||||
mandatoryUnavailable: 'No se pudieron verificar los requisitos de actualización. Inténtelo de nuevo más tarde.',
|
||||
policyLoginTitle: 'Inicie sesión en el entorno de prueba',
|
||||
policyLoginRequired: 'Inicie sesión con Feishu para verificar los requisitos de actualización para esta versión de prueba. Iniciar sesión no descargará ni instalará una actualización.',
|
||||
policyLogin: 'Iniciar sesión con Feishu',
|
||||
policyLoginFailed: 'El inicio de sesión de Feishu no se completó. Por favor inténtalo de nuevo.',
|
||||
policyLoginLoading: 'Cargando página de inicio de sesión...',
|
||||
mandatoryNoRelease: 'No se encontró ninguna actualización compatible. Verifique nuevamente o comuníquese con el soporte.',
|
||||
mandatoryRefresh: 'Comprobar de nuevo',
|
||||
mandatoryPage: 'Descargar desde el sitio web oficial.',
|
||||
mandatoryCopy: 'Copiar enlace de descarga',
|
||||
mandatoryPageFailed: 'No se pudo abrir la página de descarga oficial. Copie el enlace y ábralo en su navegador.',
|
||||
mandatoryViewDownload: 'Abrir página de descarga',
|
||||
mandatoryViewPageFailed: 'No se pudo abrir la página de descarga. Por favor inténtalo de nuevo.',
|
||||
mandatoryActionFailed: 'La acción fracasó. Por favor inténtalo de nuevo. Puede utilizar la aplicación nuevamente una vez completada la actualización.',
|
||||
mandatoryReady: 'Actualización lista',
|
||||
mandatoryVersion: 'Nueva versión: {version}',
|
||||
mandatoryReadyDetail: 'La aplicación se cerrará durante la actualización y se volverá a abrir automáticamente cuando se complete.',
|
||||
mandatoryDeferred: 'Las tareas en curso seguirán ejecutándose. Complete la actualización antes de volver a utilizar la aplicación.',
|
||||
mandatoryContinue: 'Continuar actualizando',
|
||||
mandatoryInspecting: 'Comprobando tareas...',
|
||||
mandatoryStopping: 'Deteniendo tareas…',
|
||||
mandatoryRestarting: 'La aplicación se reiniciará en breve. Espere por favor.',
|
||||
mandatoryDownloadFailed: 'Los archivos de actualización no se pudieron descargar ni preparar. Vuelva a intentarlo.',
|
||||
mandatoryInstallFailed: 'La actualización no se ha instalado. Verifique las tareas nuevamente y vuelva a intentarlo.',
|
||||
mandatoryOpenHelp: '¿La página no se abrió?',
|
||||
mandatoryReopen: 'Abre la página de descarga oficial nuevamente.',
|
||||
mandatoryCopied: 'Enlace copiado',
|
||||
mandatoryCopyFailed: 'No se pudo copiar el enlace. Selecciónelo y cópielo a continuación.',
|
||||
mandatoryAddress: 'Enlace de descarga',
|
||||
mandatoryNotification: 'Regrese a la aplicación para confirmar la instalación y reiniciar.',
|
||||
} satisfies DesktopMessages
|
||||
|
||||
/** French dictionary, checked against the English keys. */
|
||||
export const fr = {
|
||||
cliCommandMenu: 'Gérer la commande dsh…',
|
||||
cliCommandTitle: 'Gérer la commande dsh',
|
||||
cliCommandLocation: 'Commande de bureau : {path}',
|
||||
cliCommandSelected: 'Commande dsh actuelle : {path}',
|
||||
cliCommandTarget: 'Cible actuelle du lanceur : {path}',
|
||||
cliCommandShadowed: 'Un autre dsh est prioritaire. Supprimez ou réorganisez cette installation pour utiliser la commande Bureau par défaut. Vous pouvez également exécuter la commande Desktop par son chemin complet.',
|
||||
cliCommandSelectionUnknown: "Votre commande shell n'a pas pu être vérifiée. Un alias ou une autre installation dsh peut être prioritaire.",
|
||||
cliCommandInstalled: 'La commande Bureau est installée.',
|
||||
cliCommandNotInstalled: 'Ajoutez la commande Bureau à votre terminal.',
|
||||
cliCommandBroken: 'La commande Bureau doit être réparée.',
|
||||
cliCommandInstall: 'Installer',
|
||||
cliCommandRepair: 'Réparer',
|
||||
cliCommandRemove: 'Supprimer',
|
||||
cliCommandClose: 'Fermer',
|
||||
cliCommandSwitch: 'Continuer avec la commande Bureau ?',
|
||||
cliCommandPreserve: 'Une commande existante sera conservée. Les autres installations et fichiers de démarrage du shell ne seront pas modifiés.',
|
||||
cliCommandContinue: 'Continuer',
|
||||
cliCommandInstallApp: 'Installez Desktop dans votre dossier Applications avant de gérer la commande dsh.',
|
||||
cliCommandUpdating: "Une mise à jour est en cours d'installation. Gérez la commande une fois l'installation terminée.",
|
||||
cliCommandNewTerminal: 'Ouvrez un nouveau terminal et exécutez dsh --version.',
|
||||
cliCommandRemoved: 'Enregistrement des commandes de bureau supprimé.',
|
||||
cliCommandPreviousRestored: 'Le lanceur précédent a été restauré.',
|
||||
cliCommandOtherKept: "D'autres installations de commandement ont été laissées en place.",
|
||||
cliCommandBackupKept: 'Un lanceur précédent est conservé à : {path}',
|
||||
cliCommandChanged: "La commande ou PATH a changé pendant que cette boîte de dialogue était ouverte. Ouvrez à nouveau la commande Gérer dsh pour vérifier l'état actuel.",
|
||||
cliCommandOwnershipError: "L'enregistrement ou la sauvegarde de la commande a été modifiée. Aucune commande sans rapport n'a été supprimée.",
|
||||
cliCommandFailed: "La commande n'a pas pu être mise à jour. Vérifiez que l'application est installée et que la destination est accessible en écriture, puis réessayez.",
|
||||
application: 'Application',
|
||||
fileMenu: 'Fichier',
|
||||
closePage: 'Fermer la page ou la fenêtre',
|
||||
aboutMenu: 'À propos de DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'VersionV{version}',
|
||||
hideApplication: 'Masquer DeepSeek Harness',
|
||||
hideOtherApplications: 'Masquer les autres',
|
||||
showAllApplications: 'Afficher tout',
|
||||
quitApplication: 'Quitter DeepSeek Harness',
|
||||
openApplication: 'Ouvrir DeepSeek Harness',
|
||||
quit: 'Quitter',
|
||||
cancel: 'Annuler',
|
||||
quitTitle: 'Quitter DeepSeek Harness ?',
|
||||
quitActiveTasks: 'Les tâches en cours seront interrompues.',
|
||||
quitScheduledTasks: "Les tâches planifiées ne s'exécuteront pas lorsque l'application est fermée.",
|
||||
quitActiveAndScheduledTasks: "Les tâches en cours d'exécution seront interrompues et les tâches planifiées ne s'exécuteront pas tant que l'application sera fermée.",
|
||||
backgroundNoticeBody: "Les tâches en cours continueront. Vous pouvez rouvrir la fenêtre à partir de la barre d'état système.",
|
||||
backgroundNoticeConfirm: 'Confirmer',
|
||||
edit: 'Modifier',
|
||||
menuBar: 'Menu des applications',
|
||||
delete: 'Supprimer',
|
||||
undo: 'Annuler',
|
||||
redo: 'Refaire',
|
||||
cut: 'Couper',
|
||||
copy: 'Copier',
|
||||
paste: 'Coller',
|
||||
selectAll: 'Sélectionner tout',
|
||||
startupFailed: "DeepSeek Harness n'est pas disponible",
|
||||
fatalSummary: "L'application n'a pas pu démarrer ou s'est arrêtée de manière inattendue.",
|
||||
startupAddressInUse: "Une autre instance DSH (telle que le Web dsh ou l'application de bureau) est en cours d'exécution. Ils ne peuvent pas démarrer en même temps. Quittez l'autre instance DSH en cours d'exécution, puis redémarrez.",
|
||||
diagnosticTruncated: '… Détails de l’erreur raccourcis.',
|
||||
reportWrittenTo: 'Rapport de diagnostic : {path}',
|
||||
startupReinstallAdvice: "Si les fichiers de l'application sont manquants ou endommagés, fermez l'application et réinstallez-la. Vos tâches sont stockées séparément.",
|
||||
exitApplication: 'Quitter',
|
||||
restartApplication: 'Redémarrer',
|
||||
recoveryOperationFailed: "L'opération de récupération a échoué",
|
||||
disableThirdPartyPlugins: 'Désactivez les plugins tiers, sauvegardez le correctif de profil et redémarrez',
|
||||
welcomeTitle: 'DeepSeek Harness',
|
||||
welcomeBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineBefore: 'Bienvenue à ',
|
||||
welcomeTaglineBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineAfter: '',
|
||||
welcomeDescription: "Développer le potentiel. Explorez l'intelligence.",
|
||||
welcomeAuthStarting: 'Ouverture de la connexion…',
|
||||
welcomeAuthWaiting: "Le navigateur ne s'est pas ouvert automatiquement ?",
|
||||
welcomeAuthWaitingDescription: 'Copiez le lien de connexion et ouvrez-le dans votre navigateur pour vous connecter.',
|
||||
welcomeAuthExchanging: 'Connexion terminée…',
|
||||
welcomeAuthExpired: 'La connexion a expiré',
|
||||
welcomeAuthExpiredDescription: 'Connectez-vous à nouveau pour continuer',
|
||||
welcomeAuthFailed: 'Impossible de terminer la connexion. Veuillez réessayer.',
|
||||
welcomeAuthCopyLink: 'Copier le lien de connexion',
|
||||
welcomeAuthCopied: 'Copié',
|
||||
welcomeAuthCopyFailed: 'Impossible de copier. Essayer à nouveau.',
|
||||
welcomeAuthCancel: 'Annuler',
|
||||
welcomeAuthRetry: 'Connectez-vous à nouveau',
|
||||
welcomeSignIn: 'Connectez-vous',
|
||||
welcomeApiKey: 'Ajouter une clé API',
|
||||
welcomeKeyTitle: 'Ajoutez une clé API pour commencer',
|
||||
welcomeKeyDescription: 'Configurez les modèles DeepSeek officiels pour commencer à utiliser Harness',
|
||||
welcomeKeyPlaceholder: 'Entrez la clé API',
|
||||
welcomeKeySave: 'Enregistrer et continuer',
|
||||
welcomeKeyLater: 'Configurer plus tard',
|
||||
welcomeKeyBack: 'Retour pour vous connecter',
|
||||
welcomeSessionExpired: 'Vous vous êtes déconnecté de votre compte, veuillez vous reconnecter.',
|
||||
welcomeKeyBlank: 'Entrez une clé API.',
|
||||
welcomeKeyInvalid: "Entrez la clé API elle-même, sans guillemets, espaces ni affectation de variable d'environnement.",
|
||||
welcomeKeyFailed: "Impossible d'enregistrer la clé API. Veuillez réessayer.",
|
||||
welcomeContinueFailed: "Impossible d'ouvrir l'espace de travail. Veuillez réessayer.",
|
||||
checkUpdatesMenu: 'Vérifiez les mises à jour…',
|
||||
reloadPageMenu: 'Recharger la page',
|
||||
restartAppHostMenu: "Redémarrez l'application et l'hôte",
|
||||
updateCheckFailedTitle: 'Échec de la vérification de la mise à jour',
|
||||
updateCheckFailed: 'Impossible de vérifier les mises à jour. Veuillez réessayer plus tard.',
|
||||
updateDownloadFailed: 'Impossible de télécharger la mise à jour. Veuillez réessayer.',
|
||||
updateInstallFailed: "Impossible d'installer la mise à jour. Veuillez réessayer plus tard.",
|
||||
updateCheckNetworkFailed: 'Impossible de vérifier les mises à jour. Vérifiez votre connexion et réessayez.',
|
||||
updateDownloadNetworkFailed: 'Impossible de télécharger la mise à jour. Vérifiez votre connexion et réessayez.',
|
||||
updateInstallNetworkFailed: "Impossible d'installer la mise à jour. Vérifiez votre connexion et réessayez.",
|
||||
unknownError: 'Erreur inconnue',
|
||||
updateCheckTitle: 'Vérifier les mises à jour',
|
||||
updateCurrentDetail: 'Version actuelle : {version}',
|
||||
updateCurrent: 'Vous êtes à jour !',
|
||||
updateChecking: 'Vérification des mises à jour…',
|
||||
updateDownload: 'Télécharger la mise à jour',
|
||||
updateDownloadedTitle: 'La version {version} est prête à installer',
|
||||
updateDownloadedDetail: "L'application se fermera pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.",
|
||||
updateDownloadedDetailWindows: "L'application se fermera temporairement pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.\n\nLa mise à jour peut prendre un certain temps. Veuillez patienter et ne pas relancer l'application pendant l'installation.",
|
||||
updateClose: 'Fermer',
|
||||
updateAcknowledge: 'OK',
|
||||
updateLater: 'Mettre à jour plus tard',
|
||||
updateDownloading: 'Téléchargement de {percent}%…',
|
||||
updateVerifying: 'Vérification des fichiers de mise à jour…',
|
||||
updateInstalling: 'Préparation du redémarrage…',
|
||||
updateRetry: 'Réessayez la mise à jour',
|
||||
updateActiveTasks: 'Les tâches sont toujours en cours',
|
||||
updateActiveTasksDetail: "La mise à jour arrêtera les tâches en cours et redémarrera l'application. Continuer?",
|
||||
updateStopTasks: 'Arrêter les tâches et mettre à jour',
|
||||
updateTasksChanged: 'De nouvelles tâches ont commencé. Confirmez à nouveau pour arrêter les tâches et mettre à jour.',
|
||||
updateTasksUnavailable: "L'état de la tâche n'est pas disponible. Essayez à nouveau de mettre à jour lorsque l'espace de travail est prêt.",
|
||||
updateFullscreenTimeout: 'Impossible de quitter le mode plein écran. Quittez le mode plein écran, puis réessayez la mise à jour.',
|
||||
updateStopFailed: "Impossible d'arrêter les tâches en toute sécurité. La mise à jour n'a pas été installée. Veuillez réessayer plus tard.",
|
||||
updateTechnicalDetails: 'Voir les détails techniques',
|
||||
updateTitle: 'Mise à jour DeepSeek Harness',
|
||||
updateAvailable: 'Nouvelle version disponible : {version}',
|
||||
updateDetail: "Une fois le téléchargement terminé, vous pouvez installer la mise à jour et redémarrer l'application.",
|
||||
installAndRestart: 'Installer et redémarrer',
|
||||
later: 'Plus tard',
|
||||
updateFailedTitle: 'Échec de la mise à jour',
|
||||
mandatoryTitle: 'Mettre à jour pour continuer',
|
||||
mandatoryDetail: "Cette version n'est plus prise en charge. Mettez à jour pour continuer. Les tâches en cours continueront à s'exécuter jusqu'à ce que vous confirmiez l'installation et redémarriez.",
|
||||
mandatoryUnavailable: 'Impossible de vérifier les exigences de mise à jour. Veuillez réessayer plus tard.',
|
||||
policyLoginTitle: "Connectez-vous à l'environnement de test",
|
||||
policyLoginRequired: 'Connectez-vous avec Feishu pour vérifier les exigences de mise à jour pour cette version de test. La connexion ne téléchargera ni n’installera de mise à jour.',
|
||||
policyLogin: 'Connectez-vous avec Feishu',
|
||||
policyLoginFailed: 'La connexion à Feishu n’a pas abouti. Veuillez réessayer.',
|
||||
policyLoginLoading: 'Chargement de la page de connexion…',
|
||||
mandatoryNoRelease: "Aucune mise à jour compatible n'a été trouvée. Vérifiez à nouveau ou contactez l'assistance.",
|
||||
mandatoryRefresh: 'Vérifiez à nouveau',
|
||||
mandatoryPage: 'Télécharger depuis le site officiel',
|
||||
mandatoryCopy: 'Copier le lien de téléchargement',
|
||||
mandatoryPageFailed: "Impossible d'ouvrir la page de téléchargement officielle. Copiez le lien et ouvrez-le dans votre navigateur.",
|
||||
mandatoryViewDownload: 'Ouvrir la page de téléchargement',
|
||||
mandatoryViewPageFailed: "Impossible d'ouvrir la page de téléchargement. Veuillez réessayer.",
|
||||
mandatoryActionFailed: "L'action a échoué. Veuillez réessayer. Vous pourrez réutiliser l'application une fois la mise à jour terminée.",
|
||||
mandatoryReady: 'Mise à jour prête',
|
||||
mandatoryVersion: 'Nouvelle version : {version}',
|
||||
mandatoryReadyDetail: "L'application se fermera pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.",
|
||||
mandatoryDeferred: "Les tâches en cours continueront à s'exécuter. Terminez la mise à jour avant d'utiliser à nouveau l'application.",
|
||||
mandatoryContinue: 'Continuer la mise à jour',
|
||||
mandatoryInspecting: 'Vérification des tâches…',
|
||||
mandatoryStopping: 'Arrêt des tâches…',
|
||||
mandatoryRestarting: "L'application redémarrera sous peu. S'il vous plaît, attendez.",
|
||||
mandatoryDownloadFailed: "Les fichiers de mise à jour n'ont pas pu être téléchargés ou préparés. Veuillez réessayer.",
|
||||
mandatoryInstallFailed: "La mise à jour n'a pas été installée. Vérifiez à nouveau les tâches et réessayez.",
|
||||
mandatoryOpenHelp: "La page ne s'est pas ouverte ?",
|
||||
mandatoryReopen: 'Ouvrez à nouveau la page de téléchargement officielle',
|
||||
mandatoryCopied: 'Lien copié',
|
||||
mandatoryCopyFailed: 'Impossible de copier le lien. Sélectionnez-le et copiez-le ci-dessous.',
|
||||
mandatoryAddress: 'Lien de téléchargement',
|
||||
mandatoryNotification: "Revenez à l'application pour confirmer l'installation et redémarrer.",
|
||||
} satisfies DesktopMessages
|
||||
|
||||
/** German dictionary, checked against the English keys. */
|
||||
export const de = {
|
||||
cliCommandMenu: 'dsh-Befehl verwalten…',
|
||||
cliCommandTitle: 'dsh-Befehl verwalten',
|
||||
cliCommandLocation: 'Desktop-Befehl: {path}',
|
||||
cliCommandSelected: 'Aktueller dsh-Befehl: {path}',
|
||||
cliCommandTarget: 'Aktuelles Launcher-Ziel: {path}',
|
||||
cliCommandShadowed: 'Ein anderer dsh hat Vorrang. Entfernen Sie diese Installation oder ordnen Sie sie neu an, um standardmäßig den Desktop-Befehl zu verwenden. Sie können den Desktop-Befehl auch über seinen vollständigen Pfad ausführen.',
|
||||
cliCommandSelectionUnknown: 'Ihr Shell-Befehl konnte nicht überprüft werden. Ein Alias oder eine andere dsh-Installation kann Vorrang haben.',
|
||||
cliCommandInstalled: 'Der Desktop-Befehl ist installiert.',
|
||||
cliCommandNotInstalled: 'Fügen Sie den Desktop-Befehl zu Ihrem Terminal hinzu.',
|
||||
cliCommandBroken: 'Der Desktop-Befehl muss repariert werden.',
|
||||
cliCommandInstall: 'Installieren',
|
||||
cliCommandRepair: 'Reparieren',
|
||||
cliCommandRemove: 'Entfernen',
|
||||
cliCommandClose: 'Schließen',
|
||||
cliCommandSwitch: 'Mit dem Desktop-Befehl fortfahren?',
|
||||
cliCommandPreserve: 'Ein vorhandener Befehl bleibt erhalten. Andere Installationen und Shell-Startdateien werden nicht geändert.',
|
||||
cliCommandContinue: 'Weiter',
|
||||
cliCommandInstallApp: 'Installieren Sie Desktop in Ihrem Anwendungsordner, bevor Sie den Befehl dsh verwalten.',
|
||||
cliCommandUpdating: 'Ein Update wird installiert. Verwalten Sie den Befehl nach Abschluss der Installation.',
|
||||
cliCommandNewTerminal: 'Öffnen Sie ein neues Terminal und führen Sie dsh --version aus.',
|
||||
cliCommandRemoved: 'Desktop-Befehlsregistrierung entfernt.',
|
||||
cliCommandPreviousRestored: 'Der vorherige Launcher wurde wiederhergestellt.',
|
||||
cliCommandOtherKept: 'Andere Befehlsinstallationen wurden beibehalten.',
|
||||
cliCommandBackupKept: 'Ein früherer Launcher ist unter folgender Adresse erhalten: {path}',
|
||||
cliCommandChanged: 'Der Befehl oder PATH hat sich geändert, während dieses Dialogfeld geöffnet war. Öffnen Sie den Befehl „dsh verwalten“ erneut, um den aktuellen Status zu überprüfen.',
|
||||
cliCommandOwnershipError: 'Die Befehlsregistrierung oder Sicherung wurde geändert. Es wurde kein nicht verwandter Befehl entfernt.',
|
||||
cliCommandFailed: 'Der Befehl konnte nicht aktualisiert werden. Überprüfen Sie, ob die Anwendung installiert ist und das Ziel beschreibbar ist, und versuchen Sie es dann erneut.',
|
||||
application: 'Anwendung',
|
||||
fileMenu: 'Datei',
|
||||
closePage: 'Seite oder Fenster schließen',
|
||||
aboutMenu: 'Über DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'Version V{version}',
|
||||
hideApplication: 'DeepSeek Harness ausblenden',
|
||||
hideOtherApplications: 'Andere ausblenden',
|
||||
showAllApplications: 'Alle einblenden',
|
||||
quitApplication: 'DeepSeek Harness beenden',
|
||||
openApplication: 'DeepSeek Harness öffnen',
|
||||
quit: 'Beenden',
|
||||
cancel: 'Abbrechen',
|
||||
quitTitle: 'DeepSeek Harness beenden?',
|
||||
quitActiveTasks: 'Laufende Aufgaben werden unterbrochen.',
|
||||
quitScheduledTasks: 'Geplante Aufgaben werden nicht ausgeführt, während die App geschlossen ist.',
|
||||
quitActiveAndScheduledTasks: 'Laufende Aufgaben werden unterbrochen und geplante Aufgaben werden nicht ausgeführt, während die App geschlossen ist.',
|
||||
backgroundNoticeBody: 'Laufende Aufgaben werden fortgesetzt. Sie können das Fenster über die Taskleiste erneut öffnen.',
|
||||
backgroundNoticeConfirm: 'Bestätigen',
|
||||
edit: 'Bearbeiten',
|
||||
menuBar: 'Anwendungsmenü',
|
||||
delete: 'Löschen',
|
||||
undo: 'Rückgängig machen',
|
||||
redo: 'Wiederholen',
|
||||
cut: 'Ausschneiden',
|
||||
copy: 'Kopieren',
|
||||
paste: 'Einfügen',
|
||||
selectAll: 'Alles auswählen',
|
||||
startupFailed: 'DeepSeek Harness ist nicht verfügbar',
|
||||
fatalSummary: 'Die Anwendung konnte nicht gestartet werden oder wurde unerwartet gestoppt.',
|
||||
startupAddressInUse: 'Eine andere DSH-Instanz (z. B. dsh Web oder die Desktop-App) wird ausgeführt. Sie können nicht gleichzeitig starten. Beenden Sie die andere laufende DSH-Instanz und starten Sie sie neu.',
|
||||
diagnosticTruncated: '… Fehlerdetails gekürzt.',
|
||||
reportWrittenTo: 'Diagnosebericht: {path}',
|
||||
startupReinstallAdvice: 'Wenn Anwendungsdateien fehlen oder beschädigt sind, schließen Sie die Anwendung und installieren Sie sie erneut. Ihre Aufgaben werden separat gespeichert.',
|
||||
exitApplication: 'Beenden',
|
||||
restartApplication: 'Neustart',
|
||||
recoveryOperationFailed: 'Der Wiederherstellungsvorgang ist fehlgeschlagen',
|
||||
disableThirdPartyPlugins: 'Deaktivieren Sie Plugins von Drittanbietern, sichern Sie den Profil-Patch und starten Sie neu',
|
||||
welcomeTitle: 'DeepSeek Harness',
|
||||
welcomeBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineBefore: 'Willkommen bei ',
|
||||
welcomeTaglineBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineAfter: '',
|
||||
welcomeDescription: 'Potenzial aufbauen. Entdecken Sie Intelligenz.',
|
||||
welcomeAuthStarting: 'Anmeldung wird geöffnet…',
|
||||
welcomeAuthWaiting: 'Browser wurde nicht automatisch geöffnet?',
|
||||
welcomeAuthWaitingDescription: 'Kopieren Sie den Anmeldelink und öffnen Sie ihn in Ihrem Browser, um sich anzumelden.',
|
||||
welcomeAuthExchanging: 'Anmeldung wird abgeschlossen…',
|
||||
welcomeAuthExpired: 'Zeitüberschreitung bei der Anmeldung',
|
||||
welcomeAuthExpiredDescription: 'Melden Sie sich erneut an, um fortzufahren',
|
||||
welcomeAuthFailed: 'Die Anmeldung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.',
|
||||
welcomeAuthCopyLink: 'Anmeldelink kopieren',
|
||||
welcomeAuthCopied: 'Kopiert',
|
||||
welcomeAuthCopyFailed: 'Konnte nicht kopiert werden. Versuchen Sie es erneut.',
|
||||
welcomeAuthCancel: 'Abbrechen',
|
||||
welcomeAuthRetry: 'Erneut anmelden',
|
||||
welcomeSignIn: 'Anmelden',
|
||||
welcomeApiKey: 'API-Schlüssel hinzufügen',
|
||||
welcomeKeyTitle: 'Fügen Sie einen API-Schlüssel hinzu, um loszulegen',
|
||||
welcomeKeyDescription: 'Konfigurieren Sie offizielle DeepSeek-Modelle für die Verwendung von Harness',
|
||||
welcomeKeyPlaceholder: 'API-Schlüssel eingeben',
|
||||
welcomeKeySave: 'Speichern und fortfahren',
|
||||
welcomeKeyLater: 'Später einrichten',
|
||||
welcomeKeyBack: 'Zurück zur Anmeldung',
|
||||
welcomeSessionExpired: 'Sie haben sich von Ihrem Konto abgemeldet. Bitte melden Sie sich erneut an.',
|
||||
welcomeKeyBlank: 'Geben Sie einen API-Schlüssel ein.',
|
||||
welcomeKeyInvalid: 'Geben Sie den API-Schlüssel selbst ein, ohne Anführungszeichen, Leerzeichen oder eine Umgebungsvariablenzuweisung.',
|
||||
welcomeKeyFailed: 'Der API-Schlüssel konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.',
|
||||
welcomeContinueFailed: 'Der Arbeitsbereich konnte nicht geöffnet werden. Bitte versuchen Sie es erneut.',
|
||||
checkUpdatesMenu: 'Nach Updates suchen…',
|
||||
reloadPageMenu: 'Seite neu laden',
|
||||
restartAppHostMenu: 'Anwendung und Host neu starten',
|
||||
updateCheckFailedTitle: 'Update-Überprüfung fehlgeschlagen',
|
||||
updateCheckFailed: 'Es konnte nicht nach Updates gesucht werden. Bitte versuchen Sie es später noch einmal.',
|
||||
updateDownloadFailed: 'Das Update konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.',
|
||||
updateInstallFailed: 'Das Update konnte nicht installiert werden. Bitte versuchen Sie es später noch einmal.',
|
||||
updateCheckNetworkFailed: 'Es konnte nicht nach Updates gesucht werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.',
|
||||
updateDownloadNetworkFailed: 'Das Update konnte nicht heruntergeladen werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.',
|
||||
updateInstallNetworkFailed: 'Das Update konnte nicht installiert werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.',
|
||||
unknownError: 'Unbekannter Fehler',
|
||||
updateCheckTitle: 'Nach Updates suchen',
|
||||
updateCurrentDetail: 'Aktuelle Version: {version}',
|
||||
updateCurrent: 'Sie verwenden die neueste Version.',
|
||||
updateChecking: 'Suche nach Updates…',
|
||||
updateDownload: 'Update herunterladen',
|
||||
updateDownloadedTitle: 'Die Version {version} ist zur Installation bereit',
|
||||
updateDownloadedDetail: 'Die App wird während des Updates geschlossen und nach Abschluss automatisch wieder geöffnet.',
|
||||
updateDownloadedDetailWindows: 'Die App wird während des Updates vorübergehend geschlossen und nach Abschluss automatisch wieder geöffnet.\n\nDas Update kann einige Zeit dauern. Bitte warten Sie und starten Sie die App während der Installation nicht erneut.',
|
||||
updateClose: 'Schließen',
|
||||
updateAcknowledge: 'OK',
|
||||
updateLater: 'Später aktualisieren',
|
||||
updateDownloading: '{percent}% wird heruntergeladen…',
|
||||
updateVerifying: 'Aktualisierungsdateien werden überprüft…',
|
||||
updateInstalling: 'Neustart wird vorbereitet…',
|
||||
updateRetry: 'Update erneut versuchen',
|
||||
updateActiveTasks: 'Aufgaben sind noch in Bearbeitung',
|
||||
updateActiveTasksDetail: 'Durch die Aktualisierung werden die laufenden Aufgaben gestoppt und die App neu gestartet. Weitermachen?',
|
||||
updateStopTasks: 'Aufgaben beenden und Update installieren',
|
||||
updateTasksChanged: 'Neue Aufgaben haben begonnen. Bestätigen Sie erneut, um die Aufgaben zu stoppen und zu aktualisieren.',
|
||||
updateTasksUnavailable: 'Der Aufgabenstatus ist nicht verfügbar. Versuchen Sie erneut, die Aktualisierung durchzuführen, wenn der Arbeitsbereich bereit ist.',
|
||||
updateFullscreenTimeout: 'Der Vollbildmodus konnte nicht beendet werden. Beenden Sie den Vollbildmodus und versuchen Sie dann erneut, das Update durchzuführen.',
|
||||
updateStopFailed: 'Die Aufgaben konnten nicht sicher gestoppt werden. Das Update wurde nicht installiert. Bitte versuchen Sie es später noch einmal.',
|
||||
updateTechnicalDetails: 'Technische Details anzeigen',
|
||||
updateTitle: 'DeepSeek Harness-Update',
|
||||
updateAvailable: 'Neue Version verfügbar: {version}',
|
||||
updateDetail: 'Sobald der Download abgeschlossen ist, können Sie das Update installieren und die App neu starten.',
|
||||
installAndRestart: 'Installieren und neu starten',
|
||||
later: 'Später',
|
||||
updateFailedTitle: 'Aktualisierung fehlgeschlagen',
|
||||
mandatoryTitle: 'Aktualisieren, um fortzufahren',
|
||||
mandatoryDetail: 'Diese Version wird nicht mehr unterstützt. Aktualisieren, um fortzufahren. Laufende Aufgaben werden so lange ausgeführt, bis Sie die Installation bestätigen und neu starten.',
|
||||
mandatoryUnavailable: 'Update-Anforderungen konnten nicht überprüft werden. Bitte versuchen Sie es später noch einmal.',
|
||||
policyLoginTitle: 'Melden Sie sich bei der Testumgebung an',
|
||||
policyLoginRequired: 'Melden Sie sich bei Feishu an, um die Update-Anforderungen für diesen Test-Build zu prüfen. Durch die Anmeldung wird kein Update heruntergeladen oder installiert.',
|
||||
policyLogin: 'Melden Sie sich mit Feishu an',
|
||||
policyLoginFailed: 'Die Feishu-Anmeldung wurde nicht abgeschlossen. Bitte versuchen Sie es erneut.',
|
||||
policyLoginLoading: 'Anmeldeseite wird geladen…',
|
||||
mandatoryNoRelease: 'Es wurde kein kompatibles Update gefunden. Überprüfen Sie es erneut oder wenden Sie sich an den Support.',
|
||||
mandatoryRefresh: 'Erneut prüfen',
|
||||
mandatoryPage: 'Von der offiziellen Website herunterladen',
|
||||
mandatoryCopy: 'Download-Link kopieren',
|
||||
mandatoryPageFailed: 'Die offizielle Download-Seite konnte nicht geöffnet werden. Kopieren Sie den Link und öffnen Sie ihn in Ihrem Browser.',
|
||||
mandatoryViewDownload: 'Download-Seite öffnen',
|
||||
mandatoryViewPageFailed: 'Die Download-Seite konnte nicht geöffnet werden. Bitte versuchen Sie es erneut.',
|
||||
mandatoryActionFailed: 'Die Aktion ist fehlgeschlagen. Bitte versuchen Sie es erneut. Nach Abschluss des Updates können Sie die App wieder verwenden.',
|
||||
mandatoryReady: 'Update bereit',
|
||||
mandatoryVersion: 'Neue Version: {version}',
|
||||
mandatoryReadyDetail: 'Die App wird während des Updates geschlossen und nach Abschluss automatisch wieder geöffnet.',
|
||||
mandatoryDeferred: 'Laufende Aufgaben werden weiterhin ausgeführt. Schließen Sie das Update ab, bevor Sie die App erneut verwenden.',
|
||||
mandatoryContinue: 'Update fortsetzen',
|
||||
mandatoryInspecting: 'Aufgaben prüfen…',
|
||||
mandatoryStopping: 'Aufgaben stoppen…',
|
||||
mandatoryRestarting: 'Die App wird in Kürze neu gestartet. Bitte warten.',
|
||||
mandatoryDownloadFailed: 'Die Update-Dateien konnten nicht heruntergeladen oder vorbereitet werden. Bitte versuchen Sie es erneut.',
|
||||
mandatoryInstallFailed: 'Das Update wurde nicht installiert. Überprüfen Sie die Aufgaben erneut und versuchen Sie es erneut.',
|
||||
mandatoryOpenHelp: 'Seite wurde nicht geöffnet?',
|
||||
mandatoryReopen: 'Offizielle Downloadseite erneut öffnen',
|
||||
mandatoryCopied: 'Link kopiert',
|
||||
mandatoryCopyFailed: 'Der Link konnte nicht kopiert werden. Wählen Sie es aus und kopieren Sie es unten.',
|
||||
mandatoryAddress: 'Download-Link',
|
||||
mandatoryNotification: 'Kehren Sie zur Anwendung zurück, um die Installation zu bestätigen und neu zu starten.',
|
||||
} satisfies DesktopMessages
|
||||
|
||||
/** Russian dictionary, checked against the English keys. */
|
||||
export const ru = {
|
||||
cliCommandMenu: 'Управление командой dsh…',
|
||||
cliCommandTitle: 'Управление командой dsh',
|
||||
cliCommandLocation: 'Команда Desktop: {path}',
|
||||
cliCommandSelected: 'Текущая команда dsh: {path}.',
|
||||
cliCommandTarget: 'Текущая цель лаунчера: {path}',
|
||||
cliCommandShadowed: 'Другой dsh имеет приоритет. Удалите или измените порядок установки, чтобы использовать команду «Desktop» по умолчанию. Вы также можете запустить команду «Desktop» по ее полному пути.',
|
||||
cliCommandSelectionUnknown: 'Не удалось проверить вашу команду оболочки. Псевдоним или другая установка dsh может иметь приоритет.',
|
||||
cliCommandInstalled: 'Команда «Desktop» установлена.',
|
||||
cliCommandNotInstalled: 'Добавьте команду «Desktop» в свой терминал.',
|
||||
cliCommandBroken: 'Команда Desktop требует ремонта.',
|
||||
cliCommandInstall: 'Установить',
|
||||
cliCommandRepair: 'Исправить',
|
||||
cliCommandRemove: 'Удалить',
|
||||
cliCommandClose: 'Закрыть',
|
||||
cliCommandSwitch: 'Продолжить команду «Desktop»?',
|
||||
cliCommandPreserve: 'Существующая команда будет сохранена. Другие установки и файлы запуска оболочки не будут изменены.',
|
||||
cliCommandContinue: 'Продолжить',
|
||||
cliCommandInstallApp: 'Установите Desktop в папку «Приложения», прежде чем управлять командой dsh.',
|
||||
cliCommandUpdating: 'Идет установка обновления. Управляйте командой после завершения установки.',
|
||||
cliCommandNewTerminal: 'Откройте новый терминал и запустите dsh --version.',
|
||||
cliCommandRemoved: 'Регистрация команд Desktop удалена.',
|
||||
cliCommandPreviousRestored: 'Предыдущий лаунчер восстановлен.',
|
||||
cliCommandOtherKept: 'Остальные командные установки остались на месте.',
|
||||
cliCommandBackupKept: 'Предыдущий лаунчер сохранился по адресу: {path}.',
|
||||
cliCommandChanged: 'Команда или PATH изменилась, пока это диалоговое окно было открыто. Откройте команду «Управление dsh» еще раз, чтобы просмотреть текущее состояние.',
|
||||
cliCommandOwnershipError: 'Регистрация команд или резервное копирование изменены. Ни одна несвязанная команда не была удалена.',
|
||||
cliCommandFailed: 'Не удалось обновить команду. Убедитесь, что приложение установлено и место назначения доступно для записи, затем повторите попытку.',
|
||||
application: 'Приложение',
|
||||
fileMenu: 'Файл',
|
||||
closePage: 'Закрыть страницу или окно',
|
||||
aboutMenu: 'О DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'Версия V{version}',
|
||||
hideApplication: 'Скрыть DeepSeek Harness',
|
||||
hideOtherApplications: 'Скрыть других',
|
||||
showAllApplications: 'Показать все',
|
||||
quitApplication: 'Выйти из DeepSeek Harness',
|
||||
openApplication: 'Открыть DeepSeek Harness',
|
||||
quit: 'Выйти',
|
||||
cancel: 'Отмена',
|
||||
quitTitle: 'Выйти из DeepSeek Harness?',
|
||||
quitActiveTasks: 'Выполнение задач будет прервано.',
|
||||
quitScheduledTasks: 'Запланированные задачи не будут выполняться, пока приложение закрыто.',
|
||||
quitActiveAndScheduledTasks: 'Выполнение задач будет прервано, а запланированные задачи не будут выполняться, пока приложение закрыто.',
|
||||
backgroundNoticeBody: 'Выполнение задач продолжится. Вы можете снова открыть окно из системного трея.',
|
||||
backgroundNoticeConfirm: 'Подтвердить',
|
||||
edit: 'Редактировать',
|
||||
menuBar: 'Меню приложения',
|
||||
delete: 'Удалить',
|
||||
undo: 'Отменить',
|
||||
redo: 'Повторить',
|
||||
cut: 'Вырезать',
|
||||
copy: 'Копировать',
|
||||
paste: 'Вставить',
|
||||
selectAll: 'Выбрать все',
|
||||
startupFailed: 'DeepSeek Harness недоступен.',
|
||||
fatalSummary: 'Приложение не могло запуститься или неожиданно остановилось.',
|
||||
startupAddressInUse: 'Другой экземпляр DSH (например, веб-приложение dsh или настольное приложение) запущен. Они не могут начаться одновременно. Закройте другой работающий экземпляр DSH, а затем перезапустите.',
|
||||
diagnosticTruncated: '… Подробности об ошибках сокращены.',
|
||||
reportWrittenTo: 'Диагностический отчет: {path}',
|
||||
startupReinstallAdvice: 'Если файлы приложения отсутствуют или повреждены, закройте приложение и переустановите его. Ваши задачи хранятся отдельно.',
|
||||
exitApplication: 'Выход',
|
||||
restartApplication: 'Перезапустить',
|
||||
recoveryOperationFailed: 'Операция восстановления не удалась',
|
||||
disableThirdPartyPlugins: 'Отключите сторонние плагины, создайте резервную копию патча профиля и перезапустите.',
|
||||
welcomeTitle: 'DeepSeek Harness',
|
||||
welcomeBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineBefore: 'Добро пожаловать в ',
|
||||
welcomeTaglineBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineAfter: '',
|
||||
welcomeDescription: 'Развивайте потенциал. Исследуйте интеллект.',
|
||||
welcomeAuthStarting: 'Открытие входа в систему…',
|
||||
welcomeAuthWaiting: 'Браузер не открылся автоматически?',
|
||||
welcomeAuthWaitingDescription: 'Скопируйте ссылку для входа и откройте ее в браузере, чтобы войти.',
|
||||
welcomeAuthExchanging: 'Завершение входа…',
|
||||
welcomeAuthExpired: 'Время входа в систему истекло',
|
||||
welcomeAuthExpiredDescription: 'Войдите еще раз, чтобы продолжить',
|
||||
welcomeAuthFailed: 'Не удалось завершить вход. Повторите попытку.',
|
||||
welcomeAuthCopyLink: 'Скопировать ссылку для входа',
|
||||
welcomeAuthCopied: 'Скопировано',
|
||||
welcomeAuthCopyFailed: 'Не удалось скопировать. Попробуйте еще раз.',
|
||||
welcomeAuthCancel: 'Отмена',
|
||||
welcomeAuthRetry: 'Войдите снова',
|
||||
welcomeSignIn: 'Войти',
|
||||
welcomeApiKey: 'Добавить ключ API',
|
||||
welcomeKeyTitle: 'Добавьте ключ API, чтобы начать работу.',
|
||||
welcomeKeyDescription: 'Настройте официальные модели DeepSeek, чтобы начать использовать Harness.',
|
||||
welcomeKeyPlaceholder: 'Введите ключ API',
|
||||
welcomeKeySave: 'Сохранить и продолжить',
|
||||
welcomeKeyLater: 'Настроить позже',
|
||||
welcomeKeyBack: 'Вернуться для входа в систему',
|
||||
welcomeSessionExpired: 'Вы вышли из своей учетной записи, пожалуйста, войдите снова.',
|
||||
welcomeKeyBlank: 'Введите ключ API.',
|
||||
welcomeKeyInvalid: 'Введите сам ключ API без кавычек, пробелов и присвоения переменной среды.',
|
||||
welcomeKeyFailed: 'Не удалось сохранить ключ API. Пожалуйста, попробуйте еще раз.',
|
||||
welcomeContinueFailed: 'Не удалось открыть рабочую область. Пожалуйста, попробуйте еще раз.',
|
||||
checkUpdatesMenu: 'Проверить наличие обновлений…',
|
||||
reloadPageMenu: 'Обновить страницу',
|
||||
restartAppHostMenu: 'Перезапустите приложение и хост.',
|
||||
updateCheckFailedTitle: 'Проверка обновления не удалась',
|
||||
updateCheckFailed: 'Не удалось проверить наличие обновлений. Пожалуйста, повторите попытку позже.',
|
||||
updateDownloadFailed: 'Не удалось загрузить обновление. Пожалуйста, попробуйте еще раз.',
|
||||
updateInstallFailed: 'Не удалось установить обновление. Пожалуйста, повторите попытку позже.',
|
||||
updateCheckNetworkFailed: 'Не удалось проверить наличие обновлений. Проверьте подключение и повторите попытку.',
|
||||
updateDownloadNetworkFailed: 'Не удалось загрузить обновление. Проверьте подключение и повторите попытку.',
|
||||
updateInstallNetworkFailed: 'Не удалось установить обновление. Проверьте подключение и повторите попытку.',
|
||||
unknownError: 'Неизвестная ошибка',
|
||||
updateCheckTitle: 'Проверьте наличие обновлений',
|
||||
updateCurrentDetail: 'Текущая версия: {version}.',
|
||||
updateCurrent: 'У вас последняя версия.',
|
||||
updateChecking: 'Проверка обновлений…',
|
||||
updateDownload: 'Скачать обновление',
|
||||
updateDownloadedTitle: 'Версия {version} готова к установке.',
|
||||
updateDownloadedDetail: 'Приложение закроется во время обновления и автоматически откроется после его завершения.',
|
||||
updateDownloadedDetailWindows: 'Приложение временно закроется во время обновления и автоматически откроется после его завершения.\n\nОбновление может занять некоторое время. Пожалуйста, подождите и не запускайте приложение снова во время установки.',
|
||||
updateClose: 'Закрыть',
|
||||
updateAcknowledge: 'OK',
|
||||
updateLater: 'Обновить позже',
|
||||
updateDownloading: 'Загрузка {percent}%…',
|
||||
updateVerifying: 'Проверка файлов обновлений…',
|
||||
updateInstalling: 'Подготовка к перезапуску…',
|
||||
updateRetry: 'Повторить обновление',
|
||||
updateActiveTasks: 'Задачи еще выполняются',
|
||||
updateActiveTasksDetail: 'Обновление остановит выполняемые задачи и перезапустит приложение. Продолжать?',
|
||||
updateStopTasks: 'Остановить задачи и обновить',
|
||||
updateTasksChanged: 'Начались новые задачи. Подтвердите еще раз, чтобы остановить выполнение задач и выполнить обновление.',
|
||||
updateTasksUnavailable: 'Статус задачи недоступен. Попробуйте обновить еще раз, когда рабочая область будет готова.',
|
||||
updateFullscreenTimeout: 'Не удалось выйти из полноэкранного режима. Выйдите из полноэкранного режима и повторите попытку обновления.',
|
||||
updateStopFailed: 'Не удалось безопасно остановить задачи. Обновление не установлено. Пожалуйста, повторите попытку позже.',
|
||||
updateTechnicalDetails: 'Посмотреть технические подробности',
|
||||
updateTitle: 'DeepSeek Harness Обновление',
|
||||
updateAvailable: 'Доступна новая версия: {version}.',
|
||||
updateDetail: 'После завершения загрузки вы можете установить обновление и перезапустить приложение.',
|
||||
installAndRestart: 'Установить и перезапустить',
|
||||
later: 'Позже',
|
||||
updateFailedTitle: 'Обновление не выполнено',
|
||||
mandatoryTitle: 'Обновите, чтобы продолжить',
|
||||
mandatoryDetail: 'Эта версия больше не поддерживается. Обновите, чтобы продолжить. Выполняемые задачи будут продолжать выполняться до тех пор, пока вы не подтвердите установку и не перезапустите.',
|
||||
mandatoryUnavailable: 'Не удалось проверить требования к обновлению. Пожалуйста, повторите попытку позже.',
|
||||
policyLoginTitle: 'Войдите в тестовую среду',
|
||||
policyLoginRequired: 'Войдите в систему с помощью Feishu, чтобы проверить требования к обновлению для этой тестовой сборки. При входе в систему обновление не загружается и не устанавливается.',
|
||||
policyLogin: 'Войти через Feishu',
|
||||
policyLoginFailed: 'Вход в Feishu не завершен. Пожалуйста, попробуйте еще раз.',
|
||||
policyLoginLoading: 'Загрузка страницы входа…',
|
||||
mandatoryNoRelease: 'Совместимое обновление не найдено. Проверьте еще раз или обратитесь в службу поддержки.',
|
||||
mandatoryRefresh: 'Проверьте еще раз',
|
||||
mandatoryPage: 'Скачать с официального сайта',
|
||||
mandatoryCopy: 'Скопировать ссылку для скачивания',
|
||||
mandatoryPageFailed: 'Не удалось открыть официальную страницу загрузки. Скопируйте ссылку и откройте ее в браузере.',
|
||||
mandatoryViewDownload: 'Открыть страницу загрузки',
|
||||
mandatoryViewPageFailed: 'Не удалось открыть страницу загрузки. Пожалуйста, попробуйте еще раз.',
|
||||
mandatoryActionFailed: 'Не удалось выполнить действие. Повторите попытку. Вы сможете снова использовать приложение после завершения обновления.',
|
||||
mandatoryReady: 'Обновление готово',
|
||||
mandatoryVersion: 'Новая версия: {version}',
|
||||
mandatoryReadyDetail: 'Приложение закроется во время обновления и автоматически откроется после его завершения.',
|
||||
mandatoryDeferred: 'Выполняемые задачи будут продолжать выполняться. Завершите обновление, прежде чем снова использовать приложение.',
|
||||
mandatoryContinue: 'Продолжить обновление',
|
||||
mandatoryInspecting: 'Проверка задач…',
|
||||
mandatoryStopping: 'Остановка задач…',
|
||||
mandatoryRestarting: 'Приложение вскоре перезапустится. Пожалуйста, подождите.',
|
||||
mandatoryDownloadFailed: 'Файлы обновления не удалось загрузить или подготовить. Пожалуйста, повторите попытку.',
|
||||
mandatoryInstallFailed: 'Обновление не установлено. Проверьте задачи еще раз и повторите попытку.',
|
||||
mandatoryOpenHelp: 'Страница не открылась?',
|
||||
mandatoryReopen: 'Откройте официальную страницу загрузки еще раз.',
|
||||
mandatoryCopied: 'Ссылка скопирована',
|
||||
mandatoryCopyFailed: 'Не удалось скопировать ссылку. Выделите и скопируйте его ниже.',
|
||||
mandatoryAddress: 'Ссылка для скачивания',
|
||||
mandatoryNotification: 'Вернитесь в приложение, чтобы подтвердить установку и перезапустить.',
|
||||
} satisfies DesktopMessages
|
||||
|
||||
/** Portuguese dictionary, checked against the English keys. */
|
||||
export const pt = {
|
||||
cliCommandMenu: 'Gerenciar comando dsh…',
|
||||
cliCommandTitle: 'Gerenciar comando dsh',
|
||||
cliCommandLocation: 'Comando da área de trabalho: {path}',
|
||||
cliCommandSelected: 'Comando dsh atual: {path}',
|
||||
cliCommandTarget: 'Alvo atual do iniciador: {path}',
|
||||
cliCommandShadowed: 'Outro dsh tem precedência. Remova ou reordene essa instalação para usar o comando Desktop por padrão. Você também pode executar o comando Desktop pelo caminho completo.',
|
||||
cliCommandSelectionUnknown: 'Seu comando shell não pôde ser verificado. Um alias ou outra instalação do dsh pode ter precedência.',
|
||||
cliCommandInstalled: 'O comando Desktop está instalado.',
|
||||
cliCommandNotInstalled: 'Adicione o comando Desktop ao seu terminal.',
|
||||
cliCommandBroken: 'O comando Desktop precisa de reparo.',
|
||||
cliCommandInstall: 'Instalar',
|
||||
cliCommandRepair: 'Reparar',
|
||||
cliCommandRemove: 'Remover',
|
||||
cliCommandClose: 'Fechar',
|
||||
cliCommandSwitch: 'Continuar com o comando Desktop?',
|
||||
cliCommandPreserve: 'Um comando existente será preservado. Outras instalações e arquivos de inicialização do shell não serão alterados.',
|
||||
cliCommandContinue: 'Continuar',
|
||||
cliCommandInstallApp: 'Instale o Desktop na pasta Aplicativos antes de gerenciar o comando dsh.',
|
||||
cliCommandUpdating: 'Uma atualização está sendo instalada. Gerencie o comando após a conclusão da instalação.',
|
||||
cliCommandNewTerminal: 'Abra um novo terminal e execute dsh --version.',
|
||||
cliCommandRemoved: 'Registro de comando da área de trabalho removido.',
|
||||
cliCommandPreviousRestored: 'O inicializador anterior foi restaurado.',
|
||||
cliCommandOtherKept: 'Outras instalações de comando foram mantidas.',
|
||||
cliCommandBackupKept: 'Um inicializador anterior é preservado em: {path}',
|
||||
cliCommandChanged: 'O comando ou PATH foi alterado enquanto esta caixa de diálogo estava aberta. Abra o comando Gerenciar dsh novamente para revisar o estado atual.',
|
||||
cliCommandOwnershipError: 'O registro ou backup do comando foi alterado. Nenhum comando não relacionado foi removido.',
|
||||
cliCommandFailed: 'O comando não pôde ser atualizado. Verifique se o aplicativo está instalado e se o destino é gravável e tente novamente.',
|
||||
application: 'Aplicativo',
|
||||
fileMenu: 'Arquivo',
|
||||
closePage: 'Fechar página ou janela',
|
||||
aboutMenu: 'Sobre DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'Versão V{version}',
|
||||
hideApplication: 'Ocultar DeepSeek Harness',
|
||||
hideOtherApplications: 'Ocultar outros',
|
||||
showAllApplications: 'Mostrar tudo',
|
||||
quitApplication: 'Sair de DeepSeek Harness',
|
||||
openApplication: 'Abra DeepSeek Harness',
|
||||
quit: 'Sair',
|
||||
cancel: 'Cancelar',
|
||||
quitTitle: 'Sair de DeepSeek Harness?',
|
||||
quitActiveTasks: 'As tarefas em execução serão interrompidas.',
|
||||
quitScheduledTasks: 'As tarefas agendadas não serão executadas enquanto o aplicativo estiver fechado.',
|
||||
quitActiveAndScheduledTasks: 'As tarefas em execução serão interrompidas e as tarefas agendadas não serão executadas enquanto o aplicativo estiver fechado.',
|
||||
backgroundNoticeBody: 'As tarefas em execução continuarão. Você pode reabrir a janela na bandeja do sistema.',
|
||||
backgroundNoticeConfirm: 'Confirmar',
|
||||
edit: 'Editar',
|
||||
menuBar: 'Menu de aplicativos',
|
||||
delete: 'Excluir',
|
||||
undo: 'Desfazer',
|
||||
redo: 'Refazer',
|
||||
cut: 'Cortar',
|
||||
copy: 'Copiar',
|
||||
paste: 'Colar',
|
||||
selectAll: 'Selecionar tudo',
|
||||
startupFailed: 'DeepSeek Harness não está disponível',
|
||||
fatalSummary: 'O aplicativo não pôde ser iniciado ou parou inesperadamente.',
|
||||
startupAddressInUse: 'Outra instância DSH (como dsh web ou aplicativo de desktop) está em execução. Eles não podem começar ao mesmo tempo. Saia da outra instância DSH em execução e reinicie.',
|
||||
diagnosticTruncated: '… Detalhes do erro reduzidos.',
|
||||
reportWrittenTo: 'Relatório de diagnóstico: {path}',
|
||||
startupReinstallAdvice: 'Se os arquivos do aplicativo estiverem ausentes ou danificados, feche o aplicativo e reinstale-o. Suas tarefas são armazenadas separadamente.',
|
||||
exitApplication: 'Sair',
|
||||
restartApplication: 'Reiniciar',
|
||||
recoveryOperationFailed: 'A operação de recuperação falhou',
|
||||
disableThirdPartyPlugins: 'Desative plug-ins de terceiros, faça backup do patch do perfil e reinicie',
|
||||
welcomeTitle: 'DeepSeek Harness',
|
||||
welcomeBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineBefore: 'Bem-vindo ao ',
|
||||
welcomeTaglineBrand: 'DeepSeek Harness',
|
||||
welcomeTaglineAfter: '',
|
||||
welcomeDescription: 'Construir potencial. Explore a inteligência.',
|
||||
welcomeAuthStarting: 'Abrindo login…',
|
||||
welcomeAuthWaiting: 'O navegador não abriu automaticamente?',
|
||||
welcomeAuthWaitingDescription: 'Copie o link de login e abra-o em seu navegador para fazer login.',
|
||||
welcomeAuthExchanging: 'Concluindo login…',
|
||||
welcomeAuthExpired: 'O tempo limite de login expirou',
|
||||
welcomeAuthExpiredDescription: 'Faça login novamente para continuar',
|
||||
welcomeAuthFailed: 'Não foi possível concluir o login. Tente novamente.',
|
||||
welcomeAuthCopyLink: 'Copiar link de login',
|
||||
welcomeAuthCopied: 'Copiado',
|
||||
welcomeAuthCopyFailed: 'Não foi possível copiar. Tente novamente.',
|
||||
welcomeAuthCancel: 'Cancelar',
|
||||
welcomeAuthRetry: 'Faça login novamente',
|
||||
welcomeSignIn: 'Faça login',
|
||||
welcomeApiKey: 'Adicionar chave API',
|
||||
welcomeKeyTitle: 'Adicione uma chave API para começar',
|
||||
welcomeKeyDescription: 'Configure modelos DeepSeek oficiais para começar a usar o Harness',
|
||||
welcomeKeyPlaceholder: 'Insira a chave de API',
|
||||
welcomeKeySave: 'Salvar e continuar',
|
||||
welcomeKeyLater: 'Configurar mais tarde',
|
||||
welcomeKeyBack: 'Voltar para fazer login',
|
||||
welcomeSessionExpired: 'Você saiu da sua conta, faça login novamente.',
|
||||
welcomeKeyBlank: 'Insira uma chave API.',
|
||||
welcomeKeyInvalid: 'Insira a própria chave API, sem aspas, espaços ou atribuição de variável de ambiente.',
|
||||
welcomeKeyFailed: 'Não foi possível salvar a chave API. Por favor, tente novamente.',
|
||||
welcomeContinueFailed: 'Não foi possível abrir o espaço de trabalho. Por favor, tente novamente.',
|
||||
checkUpdatesMenu: 'Verifique se há atualizações…',
|
||||
reloadPageMenu: 'Recarregar página',
|
||||
restartAppHostMenu: 'Reinicie o aplicativo e o host',
|
||||
updateCheckFailedTitle: 'Falha na verificação de atualização',
|
||||
updateCheckFailed: 'Não foi possível verificar se há atualizações. Por favor, tente novamente mais tarde.',
|
||||
updateDownloadFailed: 'Não foi possível baixar a atualização. Por favor, tente novamente.',
|
||||
updateInstallFailed: 'Não foi possível instalar a atualização. Por favor, tente novamente mais tarde.',
|
||||
updateCheckNetworkFailed: 'Não foi possível verificar se há atualizações. Verifique sua conexão e tente novamente.',
|
||||
updateDownloadNetworkFailed: 'Não foi possível baixar a atualização. Verifique sua conexão e tente novamente.',
|
||||
updateInstallNetworkFailed: 'Não foi possível instalar a atualização. Verifique sua conexão e tente novamente.',
|
||||
unknownError: 'Erro desconhecido',
|
||||
updateCheckTitle: 'Verifique se há atualizações',
|
||||
updateCurrentDetail: 'Versão atual: {version}',
|
||||
updateCurrent: 'Você está atualizado!',
|
||||
updateChecking: 'Verificando atualizações…',
|
||||
updateDownload: 'Baixar atualização',
|
||||
updateDownloadedTitle: 'A versão {version} está pronta para instalação',
|
||||
updateDownloadedDetail: 'O aplicativo será fechado durante a atualização e reaberto automaticamente quando for concluída.',
|
||||
updateDownloadedDetailWindows: 'O aplicativo será fechado temporariamente durante a atualização e reaberto automaticamente quando for concluída.\n\nA atualização pode levar algum tempo. Aguarde e não inicie o aplicativo novamente durante a instalação.',
|
||||
updateClose: 'Fechar',
|
||||
updateAcknowledge: 'OK',
|
||||
updateLater: 'Atualizar mais tarde',
|
||||
updateDownloading: 'Baixando {percent}%…',
|
||||
updateVerifying: 'Verificando arquivos de atualização…',
|
||||
updateInstalling: 'Preparando para reiniciar…',
|
||||
updateRetry: 'Tentar atualizar novamente',
|
||||
updateActiveTasks: 'As tarefas ainda estão em andamento',
|
||||
updateActiveTasksDetail: 'A atualização interromperá as tarefas em andamento e reiniciará o aplicativo. Continuar?',
|
||||
updateStopTasks: 'Pare tarefas e atualize',
|
||||
updateTasksChanged: 'Novas tarefas foram iniciadas. Confirme novamente para interromper as tarefas e atualizar.',
|
||||
updateTasksUnavailable: 'O status da tarefa não está disponível. Tente atualizar novamente quando o espaço de trabalho estiver pronto.',
|
||||
updateFullscreenTimeout: 'Não foi possível sair da tela cheia. Saia da tela inteira e tente atualizar novamente.',
|
||||
updateStopFailed: 'Não foi possível interromper as tarefas com segurança. A atualização não foi instalada. Por favor, tente novamente mais tarde.',
|
||||
updateTechnicalDetails: 'Ver detalhes técnicos',
|
||||
updateTitle: 'DeepSeek Harness Atualização',
|
||||
updateAvailable: 'Nova versão disponível: {version}',
|
||||
updateDetail: 'Assim que o download for concluído, você pode instalar a atualização e reiniciar o aplicativo.',
|
||||
installAndRestart: 'Instalar e reiniciar',
|
||||
later: 'Mais tarde',
|
||||
updateFailedTitle: 'Falha na atualização',
|
||||
mandatoryTitle: 'Atualizar para continuar',
|
||||
mandatoryDetail: 'Esta versão não é mais suportada. Atualize para continuar. As tarefas em andamento continuarão em execução até você confirmar a instalação e reiniciar.',
|
||||
mandatoryUnavailable: 'Não foi possível verificar os requisitos de atualização. Por favor, tente novamente mais tarde.',
|
||||
policyLoginTitle: 'Faça login no ambiente de teste',
|
||||
policyLoginRequired: 'Faça login com Feishu para verificar os requisitos de atualização para esta versão de teste. O login não baixará nem instalará uma atualização.',
|
||||
policyLogin: 'Faça login com Feishu',
|
||||
policyLoginFailed: 'O login do Feishu não foi concluído. Por favor, tente novamente.',
|
||||
policyLoginLoading: 'Carregando página de login…',
|
||||
mandatoryNoRelease: 'Nenhuma atualização compatível foi encontrada. Verifique novamente ou entre em contato com o suporte.',
|
||||
mandatoryRefresh: 'Verifique novamente',
|
||||
mandatoryPage: 'Baixe do site oficial',
|
||||
mandatoryCopy: 'Copiar link de download',
|
||||
mandatoryPageFailed: 'Não foi possível abrir a página oficial de download. Copie o link e abra-o no seu navegador.',
|
||||
mandatoryViewDownload: 'Abrir página de download',
|
||||
mandatoryViewPageFailed: 'Não foi possível abrir a página de download. Por favor, tente novamente.',
|
||||
mandatoryActionFailed: 'A ação falhou. Por favor, tente novamente. Você poderá usar o aplicativo novamente após a conclusão da atualização.',
|
||||
mandatoryReady: 'Atualização pronta',
|
||||
mandatoryVersion: 'Nova versão: {version}',
|
||||
mandatoryReadyDetail: 'O aplicativo será fechado durante a atualização e reaberto automaticamente quando for concluída.',
|
||||
mandatoryDeferred: 'As tarefas em andamento continuarão em execução. Conclua a atualização antes de usar o aplicativo novamente.',
|
||||
mandatoryContinue: 'Continuar atualização',
|
||||
mandatoryInspecting: 'Verificando tarefas…',
|
||||
mandatoryStopping: 'Parando tarefas…',
|
||||
mandatoryRestarting: 'O aplicativo será reiniciado em breve. Por favor, aguarde.',
|
||||
mandatoryDownloadFailed: 'Os arquivos de atualização não puderam ser baixados ou preparados. Por favor, tente novamente.',
|
||||
mandatoryInstallFailed: 'A atualização não foi instalada. Verifique as tarefas novamente e tente novamente.',
|
||||
mandatoryOpenHelp: 'A página não abriu?',
|
||||
mandatoryReopen: 'Abra a página oficial de download novamente',
|
||||
mandatoryCopied: 'Link copiado',
|
||||
mandatoryCopyFailed: 'Não foi possível copiar o link. Selecione e copie abaixo.',
|
||||
mandatoryAddress: 'Link de download',
|
||||
mandatoryNotification: 'Retorne ao aplicativo para confirmar a instalação e reinicie.',
|
||||
} satisfies DesktopMessages
|
||||
|
||||
const dictionaries = { en, zh, ja, es, fr, de, ru, pt }
|
||||
|
||||
/** Locale payload exposed to the Desktop-owned renderer. */
|
||||
export interface DesktopLocale {
|
||||
readonly id: 'en' | 'zh-CN' | 'ja'
|
||||
readonly id: 'en' | 'zh-CN' | 'ja' | 'es' | 'fr' | 'de' | 'ru' | 'pt'
|
||||
readonly messages: DesktopMessages
|
||||
}
|
||||
|
||||
/** Resolve Electron's locale to one shipped Desktop dictionary. */
|
||||
export function resolveDesktopLocale(locale: string): DesktopLocale {
|
||||
const primary = locale.toLowerCase().split('-')[0]
|
||||
if (primary === 'ja') return { id: 'ja', messages: ja }
|
||||
return primary === 'zh'
|
||||
? { id: 'zh-CN', messages: zh }
|
||||
: { id: 'en', messages: en }
|
||||
if (primary !== undefined && Object.hasOwn(dictionaries, primary)) {
|
||||
const id = primary as keyof typeof dictionaries
|
||||
return { id: id === 'zh' ? 'zh-CN' : id, messages: dictionaries[id] }
|
||||
}
|
||||
return { id: 'en', messages: en }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,10 +1403,10 @@ export function resolveDesktopLocale(locale: string): DesktopLocale {
|
||||
*/
|
||||
export function resolveDesktopStartupLocale(preference: string | null, languages: readonly string[]): DesktopLocale {
|
||||
const selected = preference?.toLowerCase()
|
||||
if (selected === 'zh' || selected === 'en' || selected === 'ja') return resolveDesktopLocale(selected)
|
||||
if (selected !== undefined && Object.hasOwn(dictionaries, selected)) return resolveDesktopLocale(selected)
|
||||
for (const language of languages) {
|
||||
const primary = language.toLowerCase().split('-')[0]
|
||||
if (primary === 'zh' || primary === 'en' || primary === 'ja') return resolveDesktopLocale(primary)
|
||||
if (primary !== undefined && Object.hasOwn(dictionaries, primary)) return resolveDesktopLocale(primary)
|
||||
}
|
||||
return resolveDesktopLocale('en')
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import {
|
||||
import type { DesktopPaths } from './paths.ts'
|
||||
import type { DesktopRelease } from './release.ts'
|
||||
import { readDesktopRuntime } from './runtime-tree.ts'
|
||||
import {
|
||||
initProfile, PROFILE_TEMPLATES, removeLinkProjections, sanitizeProfile, type ProfileTemplate,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { initProfile, removeLinkProjections, sanitizeProfile } from '@deepseek-ai/dsh-app-boot'
|
||||
import { PROFILE_TEMPLATES, type ProfileTemplate } from '@deepseek-ai/dsh-official-bundles'
|
||||
|
||||
const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
|
||||
const DSH_PACKAGE = '@deepseek-ai/dsh'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts'
|
||||
import { createRequire } from 'node:module'
|
||||
/** Per-language installer resources feed both the packaging language list and the NSIS compiler. */
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -8,7 +10,7 @@ import { readInstallerLocales, renderInstallerStrings } from '../scripts/install
|
||||
function fixture(): string {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-installer-locales-'))
|
||||
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
|
||||
for (const id of ['en_US', 'zh_CN', 'ja_JP']) {
|
||||
for (const id of ['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR']) {
|
||||
writeFileSync(join(directory, `${id}.json`), readFileSync(new URL(`../installer/locales/${id}.json`, import.meta.url)))
|
||||
}
|
||||
return directory
|
||||
@@ -16,17 +18,17 @@ function fixture(): string {
|
||||
|
||||
it('generates Japanese NSIS strings from the same files that select packaged languages', () => {
|
||||
const locales = readInstallerLocales()
|
||||
expect(locales.map(locale => locale.id)).toEqual(['en_US', 'ja_JP', 'zh_CN'])
|
||||
expect(locales.map(locale => locale.id)).toEqual(['en_US', 'de_DE', 'es_ES', 'fr_FR', 'ja_JP', 'pt_BR', 'ru_RU', 'zh_CN'])
|
||||
const source = renderInstallerStrings(locales)
|
||||
expect(source).toContain('LangString INSTALLER_INSTALL ${LANG_JAPANESE} "今すぐインストール"')
|
||||
expect(source).toContain('LangString INSTALLER_NATIVE_PROGRESS ${LANG_JAPANESE} "インストールを準備しています… %d%%"')
|
||||
expect(source).toContain('!ifdef LANG_JAPANESE\n')
|
||||
expect(source.match(/^LangString /gmu)).toHaveLength(93)
|
||||
expect(source.match(/^LangString /gmu)).toHaveLength(248)
|
||||
const japanese = locales.filter(locale => locale.id === 'ja_JP')
|
||||
expect(renderInstallerStrings(japanese)).not.toContain('${LANG_ENGLISH}')
|
||||
})
|
||||
|
||||
it.each(['en_US', 'zh_CN', 'ja_JP'])('rejects a missing %s configuration', (id) => {
|
||||
it.each(['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR'])('rejects a missing %s configuration', (id) => {
|
||||
const directory = fixture()
|
||||
rmSync(join(directory, `${id}.json`))
|
||||
expect(() => readInstallerLocales(directory)).toThrow(`missing ${id}.json`)
|
||||
@@ -39,7 +41,10 @@ it.each([
|
||||
['changed placeholder', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%s%%' }, 'format placeholders'],
|
||||
['missing percent escape', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%' }, 'format placeholders'],
|
||||
['extra format specifier', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%% %q' }, 'format placeholders'],
|
||||
['translated command', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_THEME_ERROR = 'Use /THEME=claro.' }, 'format placeholders'],
|
||||
['duplicate language', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.nsisLanguage = 'ENGLISH' }, 'duplicate NSIS language'],
|
||||
['invisible character', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_INSTALL = 'Install\u200b' }, 'invisible'],
|
||||
['byte-order mark', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.strings.INSTALLER_INSTALL = 'Install\ufeff' }, 'invisible'],
|
||||
['invalid compiler token', (resource: ReturnType<typeof readInstallerLocales>[number]) => { resource.nsisLanguage = 'JA\n!error' }, 'invalid NSIS language'],
|
||||
])('rejects %s', (_name, mutate, diagnostic) => {
|
||||
const directory = fixture()
|
||||
@@ -54,3 +59,18 @@ it('escapes literal NSIS syntax without changing translated text', () => {
|
||||
expect(renderInstallerStrings([{ ...locale, strings: { INSTALLER_INSTALL: '"$name"\nnext\rline' } }]))
|
||||
.toContain('"$\\"$$name$\\"$\\nnext$\\rline"')
|
||||
})
|
||||
|
||||
it('uses the real electron-builder language identifiers and English fallback', () => {
|
||||
const require = createRequire(import.meta.url)
|
||||
const { LangConfigurator, createAddLangsMacro } = require('app-builder-lib/out/targets/nsis/nsisLang.js') as {
|
||||
LangConfigurator: new (options: { installerLanguages: string[] }) => object
|
||||
createAddLangsMacro: (generator: { macro: (name: string, lines: string[]) => void }, languages: object) => void
|
||||
}
|
||||
const locales = readInstallerLocales()
|
||||
expect(locales[0]?.id).toBe('en_US')
|
||||
expect(locales.map(locale => locale.id.split('_')[0]).sort()).toEqual([...SHIPPED_LOCALE_IDS].sort())
|
||||
createAddLangsMacro({ macro: (_name, lines) => {
|
||||
expect(lines.map(line => /MUI_LANGUAGE "([^" ]+)"/u.exec(line)?.[1]?.toUpperCase()))
|
||||
.toEqual(locales.map(locale => locale.nsisLanguage))
|
||||
} }, new LangConfigurator({ installerLanguages: locales.map(locale => locale.id) }))
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readInstallerLocales } from '../scripts/installer-locales.mjs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { Arch, Platform } from 'electron-builder'
|
||||
@@ -54,7 +55,7 @@ describe('installer preparation preserves application dependencies', () => {
|
||||
expect(appInfo.copyright).toBe(`Copyright © ${new Date().getFullYear()} DeepSeek Harness`)
|
||||
if (platform === 'win32') {
|
||||
expect(expandMacro(config.nsis.uninstallDisplayName, null, appInfo, {}, false)).toBe('DeepSeek Harness')
|
||||
expect(config.nsis.installerLanguages).toEqual(['en_US', 'ja_JP', 'zh_CN'])
|
||||
expect(config.nsis.installerLanguages).toEqual(readInstallerLocales().map(locale => locale.id))
|
||||
}
|
||||
const aboutIcon = config.extraResources.find(resource => resource.to === 'icon.png')
|
||||
expect(aboutIcon).toBeDefined()
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { en, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale, ja, zh } from '../src/locale.ts'
|
||||
import { en, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale, ja, zh, es, fr, de, ru, pt } from '../src/locale.ts'
|
||||
|
||||
describe('desktop locale dictionaries', () => {
|
||||
it('ships the same key set in English, Chinese, and Japanese', () => {
|
||||
it('ships baseline and Japanese keys and falls back for unsupported languages', () => {
|
||||
expect(Object.keys(zh)).toEqual(Object.keys(en))
|
||||
expect(Object.keys(ja)).toEqual(Object.keys(en))
|
||||
expect(resolveDesktopLocale('JA-jp')).toEqual({ id: 'ja', messages: ja })
|
||||
expect(resolveDesktopLocale('zh-Hans-CN').messages).toEqual(zh)
|
||||
expect(resolveDesktopLocale('en-US').messages).toEqual(en)
|
||||
expect(resolveDesktopLocale('fr-FR').messages).toEqual(en)
|
||||
expect(resolveDesktopLocale('it-IT').messages).toEqual(en)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['es', 'es-MX', es], ['fr', 'fr-CA', fr], ['de', 'de-DE', de],
|
||||
['ru', 'ru-RU', ru], ['pt', 'pt-BR', pt],
|
||||
] as const)('resolves %s, preserves placeholders, and honors saved preferences', (id, tag, messages) => {
|
||||
expect(resolveDesktopLocale(tag.toUpperCase())).toEqual({ id, messages })
|
||||
expect(resolveDesktopStartupLocale(id, ['zh-CN']).id).toBe(id)
|
||||
expect(resolveDesktopStartupLocale(null, ['it-IT', tag, 'en-US']).id).toBe(id)
|
||||
expect(Object.keys(messages)).toEqual(Object.keys(en))
|
||||
for (const key of Object.keys(en) as (keyof typeof en)[]) {
|
||||
const placeholders = (text: string): string[] => (text.match(/\{\w+\}/gu) ?? []).sort()
|
||||
expect(placeholders(messages[key]), key).toEqual(placeholders(en[key]))
|
||||
}
|
||||
})
|
||||
|
||||
it.each(SHIPPED_LOCALE_IDS)('resolves every shipped language %s without fallback', (id) => {
|
||||
expect(resolveDesktopLocale(id).id).toBe(id === 'zh' ? 'zh-CN' : id)
|
||||
expect(resolveDesktopStartupLocale(null, [id]).id).toBe(id === 'zh' ? 'zh-CN' : id)
|
||||
})
|
||||
|
||||
it('formats named values without consuming unknown placeholders', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -51,7 +52,7 @@ describe('desktop macOS release signature', () => {
|
||||
expect(config.protocols).toEqual([{ name: 'DeepSeek Harness', schemes: ['dsh'] }])
|
||||
expect(portablePath(config.directories.output)).toContain('/.desktop-build/targets/mac-arm64/artifacts')
|
||||
expect(config.mac.extendInfo.NSMicrophoneUsageDescription).toContain('microphone')
|
||||
expect(config.mac.extendInfo.CFBundleLocalizations).toEqual(['en', 'zh_CN', 'ja'])
|
||||
expect(config.mac.extendInfo.CFBundleLocalizations).toEqual(SHIPPED_LOCALE_IDS.map(id => id === 'zh' ? 'zh_CN' : id))
|
||||
expect(config.mac.entitlementsInherit).toBe(config.mac.entitlements)
|
||||
const entitlements = readFileSync(config.mac.entitlements, 'utf8')
|
||||
for (const key of ['com.apple.security.cs.allow-jit', 'com.apple.security.cs.allow-unsigned-executable-memory',
|
||||
|
||||
@@ -43,8 +43,9 @@ it('classifies the current workspace and shares only runtime patch bytes', () =>
|
||||
const before = readFileSync(join(repository, 'pnpm-lock.yaml'))
|
||||
const plan = prepareRuntimePatches(project, repository)
|
||||
expect(plan.patches.map(entry => entry.spec)).toEqual(['@earendil-works/pi-ai@1.3.0', 'node-pty@1.2.0-beta.15'])
|
||||
expect(plan.workspaceOnly).toHaveLength(7)
|
||||
expect(plan.workspaceOnly).toHaveLength(8)
|
||||
expect(plan.workspaceOnly).toContain('@botiverse/oar')
|
||||
expect(plan.workspaceOnly).toContain('lexical')
|
||||
const settings = yaml.load(readFileSync(join(project, 'pnpm-workspace.yaml'), 'utf8')) as { overrides: Record<string, string> }
|
||||
expect(settings.overrides['@earendil-works/pi-ai']).toBeUndefined()
|
||||
expect(settings.overrides['@earendil-works/pi-ai@^1.3.0']).toBe('1.3.0')
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
{
|
||||
"path": "../../packages/boot/app-boot"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/official-bundles"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/home-paths"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-plugin-market-generator",
|
||||
"description": "Reference generator of plugin market index files from a list of npm packages and Git repositories",
|
||||
"version": "0.2.1-rc.2",
|
||||
"version": "0.2.1-rc.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-web-frontend",
|
||||
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
|
||||
"version": "0.2.1-rc.2",
|
||||
"version": "0.2.1-rc.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/** Shipped language selection, metadata and narrow settings through the assembled Web profile. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import { acknowledgeReloadConnectionLoss, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
|
||||
const languages = [
|
||||
['es', 'es-MX', 'Menú de cuenta', 'Configuración', 'Complementos', 'Idioma', 'Español'],
|
||||
['fr', 'fr-CA', 'Menu du compte', 'Paramètres', 'Plugins', 'Langue', 'Français'],
|
||||
['de', 'de-DE', 'Kontomenü', 'Einstellungen', 'Plugins', 'Sprache', 'Deutsch'],
|
||||
['ru', 'ru-RU', 'Меню аккаунта', 'Настройки', 'Плагины', 'Язык', 'Русский'],
|
||||
['pt', 'pt-BR', 'Menu da conta', 'Configurações', 'Plug-ins', 'Idioma', 'Português'],
|
||||
] as const
|
||||
|
||||
it.each(languages)('selects %s from the browser, localizes metadata and persists an explicit choice', async (id, tag, accountMenu, settings, plugins, language, label) => {
|
||||
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await resources.browser?.close() } finally { await resources.scaffold?.close() }
|
||||
})
|
||||
const scaffold = await launchWebScaffold()
|
||||
resources.scaffold = scaffold
|
||||
const browser = await chromium.launch()
|
||||
resources.browser = browser
|
||||
const page = await browser.newPage({ locale: tag, viewport: { width: 1440, height: 960 } })
|
||||
const tripwire = watchConsole(page)
|
||||
onTestFailed(() => saveFailureShot(page, `additional-locale-${id}`))
|
||||
await page.goto(scaffold.authenticatedUrl)
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe(id)
|
||||
await page.getByRole('button', { name: plugins, exact: true }).click()
|
||||
const panel = page.locator('[data-plugin-panel]')
|
||||
const metadata = JSON.parse(await readFile(new URL(`../../../packages/experimental/auto-review/locale/${id}.json`, import.meta.url), 'utf8')) as { meta: { title: string; description: string } }
|
||||
await panel.getByText(metadata.meta.title, { exact: true }).waitFor()
|
||||
await panel.getByText(metadata.meta.description, { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(fileURLToPath(new URL(`./expected/additional-locales/${id}.expected.md`, import.meta.url)),
|
||||
await captureStableAria(page, `[data-plugin-panel] li:has-text(${JSON.stringify(metadata.meta.title)})`, scaffold.workspaceCwd), webSnapshotMode())
|
||||
await page.getByRole('button', { name: accountMenu, exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: settings, exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: settings, exact: true })
|
||||
await dialog.getByText(language, { exact: true }).waitFor()
|
||||
await dialog.getByRole('button', { name: label, exact: true }).waitFor()
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await expect.poll(() => dialog.evaluate((element) => {
|
||||
const content = element.lastElementChild!
|
||||
return { panel: element.scrollWidth <= element.clientWidth + 1, content: content.scrollWidth <= content.clientWidth + 1 }
|
||||
})).toEqual({ panel: true, content: true })
|
||||
await dialog.getByRole('button', { name: label, exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: label, exact: true }).click()
|
||||
await expect.poll(() => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain(`preference: ${id}`)
|
||||
await dialog.getByRole('button', { name: label, exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: 'English', exact: true }).click()
|
||||
await expect.poll(() => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('preference: en')
|
||||
const warningsBeforeReload = tripwire.warnings.length
|
||||
await page.reload()
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBeforeReload)
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('en')
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
Loaded 100 of 3120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user