mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge master into worktree/oar-node24
This commit is contained in:
@@ -18,8 +18,8 @@
|
||||
en: b432f80a424c3e49
|
||||
zh: faa26925fdd98387
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/provider-availability-and-selection:
|
||||
en: dff78c46432311a1
|
||||
zh: 58d5db4c3c85b886
|
||||
en: 4e4ec068c2a22383
|
||||
zh: b7030d0f00bfdb74
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/search-request-and-result-schema:
|
||||
en: ec3703b7e7c6b23d
|
||||
zh: 325755e6e90092da
|
||||
|
||||
@@ -60,6 +60,7 @@ flowchart LR
|
||||
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
|
||||
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
|
||||
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
|
||||
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
|
||||
toolWeb -->|ctx.tools.register| webSearch["tool: web_search"]
|
||||
@@ -151,6 +152,8 @@ The "single provider auto-selects" rule is for tests, demos, and simple deployme
|
||||
|
||||
Operational overrides feed the same explicit selection path: `DSH_WEB_SEARCH_PROVIDER=perplexity` is equivalent to config `searchProvider: perplexity`, not a hidden priority chain inside `dsh-tool-web`.
|
||||
|
||||
Each search or fetch reads the configured id when it runs, so a saved settings change selects another provider for the next operation without remounting `dsh-web`. The Web search settings switch relies on this to move both capabilities to the [DeepSeek Search and Fetch APIs](../feature/2026-10-15-deepseek-search-fetch-api.md) at once.
|
||||
|
||||
`ctx.web.search()` and `ctx.web.fetch()` resolve the provider at execution time using the selection rules above. If the selected capability is unavailable, they throw `WebError` with a structured code such as `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_CONFIGURED_MISSING`, `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`, or `WEB_PROVIDER_AMBIGUOUS`. If no provider is explicitly configured and no usable provider exists, the execution error is the generic `WEB_PROVIDER_UNAVAILABLE` case; there is deliberately no diagnostic summary of every unavailable provider.
|
||||
|
||||
## Search request and result schema
|
||||
|
||||
@@ -60,6 +60,7 @@ flowchart LR
|
||||
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
|
||||
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
|
||||
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
|
||||
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
|
||||
toolWeb -->|ctx.tools.register| webSearch["tool: web_search"]
|
||||
@@ -151,6 +152,8 @@ interface WebRuntime {
|
||||
|
||||
运维覆盖走同一条显式选择路径:`DSH_WEB_SEARCH_PROVIDER=perplexity` 等同于配置 `searchProvider: perplexity`,而非 `dsh-tool-web` 内部的隐式优先级链。
|
||||
|
||||
每次搜索或抓取在执行时读取配置的 id,因此保存的设置变更无需重新挂载 `dsh-web` 即对下一次操作选择另一个提供方。网页搜索设置页的开关借此把两种能力同时切换到 [DeepSeek Search 与 Fetch API](../feature/2026-10-15-deepseek-search-fetch-api.zh.md)。
|
||||
|
||||
`ctx.web.search()` 和 `ctx.web.fetch()` 在执行时按上述选择规则解析提供方。如果选定的能力不可用,它们抛出带有结构化代码的 `WebError`,如 `WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE` 或 `WEB_PROVIDER_AMBIGUOUS`。如果未显式配置提供方且不存在可用提供方,执行错误是通用的 `WEB_PROVIDER_UNAVAILABLE` 情况;刻意不提供对每个不可用提供方的诊断汇总。
|
||||
|
||||
## 搜索请求与结果 schema
|
||||
|
||||
+6
-6
@@ -9,14 +9,14 @@
|
||||
en: 3238dfb7db7dfdd8
|
||||
zh: ec3bdd9a9da0a321
|
||||
/agent-note-user-authorized-subagent-model-routes/decision:
|
||||
en: 0166e452a774ade5
|
||||
zh: 747061b9424b3610
|
||||
en: 9eeb42f10be1bd28
|
||||
zh: 24e6cca2db77444a
|
||||
/agent-note-user-authorized-subagent-model-routes/alternatives-considered:
|
||||
en: e41c17226190ea8b
|
||||
zh: d3b1395fafe75f01
|
||||
/agent-note-user-authorized-subagent-model-routes/consequences:
|
||||
en: f1927d31ffc1dee9
|
||||
zh: b0fed04b80043c71
|
||||
en: 26244ba796f6ed70
|
||||
zh: 47e9744719b05f0b
|
||||
/agent-note-user-authorized-subagent-model-routes/related-decisions:
|
||||
en: 595a9485d91592c6
|
||||
zh: 7b32611ea380498c
|
||||
en: 0eead87ec1172d81
|
||||
zh: 4e776a583f5a00a5
|
||||
|
||||
+7
-7
@@ -12,11 +12,11 @@ Registering an LLM adapter makes its routes reachable, but does not authorize an
|
||||
|
||||
The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `session/modelCatalog`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process.
|
||||
|
||||
A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its model-selectable definitions can reach a request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions, while a legacy Session without the event remains disabled, including an explicitly empty restored Session.
|
||||
The Host settings service snapshots the route list for each fresh top-level Session in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently created top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route.
|
||||
|
||||
The fixed `list_subagent_models` schema does not enumerate the policy. At call time, provider and model listings are the intersection of the Session route list and the adapter's live advertised directory. An exact provider/model lookup first requires authorization, then resolves the adapter-owned model metadata and all advertised reasoning efforts. The delegation executor independently rejects any explicit provider, model, or effort selection whose effective provider/model route is outside the Session list before `resolveCallConfig()` validates adapter availability and effort support. A call that supplies no selection field retains configured or inherited routing because the model made no route choice.
|
||||
The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing.
|
||||
|
||||
Model selection has no unrestricted static mode. The default-off Host setting is the only authority, and an enabled Session always carries an exact allowlist. The primary spawn tool reads that setting; the shipped fork tool still exposes no route selection so inherited conversation prefixes remain eligible for provider-side KV Cache reuse.
|
||||
Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Each delegation tool’s `modelSelection` option controls only its route fields. One delegation instance per scope enables `modelDiscovery` to own discovery; the setting, discovery, and delegation read the same package-local authorization projection. Web and Desktop presets mount discovery by default, enable spawn route fields, and leave fork route fields disabled to preserve inherited-prefix reuse; custom fork compositions may enable them under the same Session authorization. Tool installation does not sample or expand authorization.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -33,11 +33,11 @@ Model selection has no unrestricted static mode. The default-off Host setting is
|
||||
## Consequences
|
||||
|
||||
- New adapter registrations and newly advertised models do not expand user authorization.
|
||||
- Adapter removals or catalog failures can reduce what discovery currently lists without deleting the saved route decision; an exact authorized route remains usable when its adapter accepts it even if the advisory catalog omits it.
|
||||
- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript.
|
||||
- Adapter removal or catalog lookup failure can make provider discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it.
|
||||
- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. The `modelSelection` option adds three route fields; `modelDiscovery` adds a fixed discovery schema. Both options default off; shipped headless and SDK profiles carry neither.
|
||||
- The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it.
|
||||
- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, discovery intersection, executor denial, live UI catalog invalidation, staged-route retention, connection-generation invalidation, staged whole-array writes, stale-revision rejection, and retry after scoped installation failure. The assembled Web scenario pins the real settings document and Plugins card flow.
|
||||
- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow.
|
||||
|
||||
## Related decisions
|
||||
|
||||
The route arguments, adapter preflight, discovery tool, and fork cache restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md).
|
||||
The route arguments, adapter preflight, discovery tool, and fork restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md).
|
||||
|
||||
+7
-7
@@ -12,11 +12,11 @@ Status: implemented
|
||||
|
||||
Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `session/modelCatalog` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。
|
||||
|
||||
设置启用时,新组合的顶层 Session 会在模型可选定义进入请求之前,把路由列表快照记录为 `subagent/model-selection-policy`。事件存在就表示模型选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一份精确列表,恢复的 Session 使用已记录事件而不是当前设置。因此,设置修改只影响之后组合的顶层 Session,而没有该事件的旧 Session 仍保持禁用,包括显式为空的恢复 Session。
|
||||
设置启用时,Host 设置服务为每个新顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后创建的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。
|
||||
|
||||
固定的 `list_subagent_models` schema 不会枚举该策略。调用时,提供方和模型列表是 Session 路由列表与适配器实时公布目录的交集。精确 provider/model 查询先要求授权,再解析适配器自有的模型元数据和全部已公布推理强度。委派执行器还会独立拒绝任何生效 provider/model 路由不在 Session 列表内的显式提供方、模型或强度选择,然后才由 `resolveCallConfig()` 校验适配器可用性与强度支持。完全没有选择字段的调用保留配置或继承路由,因为模型没有作出路由选择。
|
||||
固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。
|
||||
|
||||
模型选择不再有无限制的静态模式。默认关闭的 Host 设置是唯一授权来源,启用的 Session 始终携带精确允许列表。主 spawn 工具读取该设置;随附 fork 工具仍不公开路由选择,使继承的对话前缀继续符合提供方侧 KV Cache 复用条件。
|
||||
模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。各委派工具的 `modelSelection` 选项只控制自身路由字段。每个作用域的一个委派实例启用 `modelDiscovery` 并负责发现;设置、发现和委派读取包内同一个授权投影。Web 与 Desktop preset 默认挂载发现工具,启用 spawn 路由字段,并为 fork 保持关闭,以保留对继承前缀的复用;自定义 fork 组合可以在相同 Session 授权下启用这些字段。工具安装不采样或扩大授权。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -33,11 +33,11 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled`
|
||||
## Consequences
|
||||
|
||||
- 新适配器注册和新公布模型不会扩大用户授权。
|
||||
- 适配器移除或目录失败可以减少发现当前列出的内容,但不会删除已存路由决定;即使建议性目录省略某条精确已授权路由,只要适配器接受它,该路由仍然可用。
|
||||
- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。
|
||||
- 适配器移除或目录查询失败可能使提供方发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。
|
||||
- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。`modelSelection` 选项增加三个路由字段;`modelDiscovery` 增加固定发现 schema。两个选项都默认关闭;随附 headless 与 SDK profile 两者都不携带。
|
||||
- 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。
|
||||
- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、发现交集、执行器拒绝、UI 实时目录失效、暂存路由保留、连接换代失效、暂存后的整数组写入、陈旧 revision 拒绝,以及作用域安装失败后的重试。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。
|
||||
- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。
|
||||
|
||||
## Related decisions
|
||||
|
||||
路由参数、适配器预检、发现工具与 fork 缓存限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。
|
||||
路由参数、适配器预检、发现工具与 fork 限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Bilingual-pair consistency record for 2026-10-15-deepseek-search-fetch-api.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/feature/2026-10-15-deepseek-search-fetch-api.md
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis:
|
||||
en: fb19be7e7fc65810
|
||||
zh: 865aaed5b7f802cc
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/problem:
|
||||
en: d1aad580a4db9872
|
||||
zh: 1eb3bac59353291d
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision:
|
||||
en: e70efbb3aa9d53b6
|
||||
zh: 1d25eae2461346df
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/protocol-selection-and-deployment:
|
||||
en: ec653b59d62ed95f
|
||||
zh: 0d675616059930ba
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/search-results-and-model-input:
|
||||
en: a95618f172ef63ee
|
||||
zh: 306556be09c56bdf
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/fetch-metadata-and-ownership:
|
||||
en: 8051417b08739ed0
|
||||
zh: c11d35a346014436
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/default-migration:
|
||||
en: d5a8e36b545f1c04
|
||||
zh: d741fd0789401c62
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/supplementary-messages-fix:
|
||||
en: 738e32b488bbc1a1
|
||||
zh: f33afe570f62599a
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/alternatives-considered:
|
||||
en: 641cbfefc76f2f95
|
||||
zh: e3e718473eace85f
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/consequences:
|
||||
en: a55da02f4d3c06c0
|
||||
zh: 3c1a624f03f34f28
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/verification-and-remaining-api-adoption:
|
||||
en: 41f9a331c7aca397
|
||||
zh: 17a0fd53e133c2f5
|
||||
@@ -0,0 +1,84 @@
|
||||
# Agent Note: Direct DeepSeek Search and Fetch APIs
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-15-deepseek-search-fetch-api.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
DeepSeek web retrieval through Messages requires an auxiliary model request for each query. Harness extracts sources and citation excerpts but discards the generated answer. The dedicated Search and Fetch APIs provide structured sources and Markdown directly, so retrieval can run without Harness starting that auxiliary generation. Adopting these APIs requires explicit selection of both operations, independent deployment addresses, and a mapping of Fetch results onto the existing fetch result.
|
||||
|
||||
## Decision
|
||||
|
||||
The [DeepSeek Search and Fetch guide](https://api-docs.deepseek.com/zh-cn/guides/search), checked on 2026-10-16, defines the upstream API. The [web-deepseek-api plugin](../../../../packages/web/web-deepseek-api/README.md) implements it behind the existing `web_search` and `web_fetch` tools, and [web-search-deepseek](../../../../packages/web/web-search-deepseek/README.md) keeps Messages search. The [web capability decision](../architecture/2026-06-24-web-capability-seam.md) continues to own provider selection, tool responsibilities, and the fetch result fields.
|
||||
|
||||
### Protocol selection and deployment
|
||||
|
||||
The APIs register as `deepseek-search-api` and `deepseek-fetch-api`; Messages search keeps `deepseek-official`. The setting **Use the new Web Search API** (**启用新版 Web 搜索 API**) on the Web search page writes `web.searchProvider` and `web.fetchProvider` together: on selects both APIs, and off restores Messages search with local HTTP fetch, the shipped selection. The switch defaults off and takes effect after saving, because the web service reads both selections for each operation. Messages remains the default until the Search API launch is announced, so existing users keep working search without configuration. Each operation resolves one settings snapshot, including its endpoint and credentials. Both APIs read one credential and one API base, because the guide serves them from one base and the switch selects them together.
|
||||
|
||||
The two protocols retain independent API bases. Messages keeps `https://api.deepseek.com/anthropic/v1` and appends `/messages`. Direct retrieval uses `web-deepseek-api.baseURL`, which defaults to the guide's public base `https://api.deepseek.com/v1/tools`, and appends `/search` or `/fetch`; the verified development base is `http://api-dev.deepseek.com:8031/api/v1/tools`. These prefixes are different and are never inferred from the hostname, so another deployment enters its complete base. The switch is already the explicit opt-in, so the documented public deployment serves as its default. A blank or unparseable base leaves both providers unavailable.
|
||||
|
||||
The guide specifies API-key Bearer authentication, and requests send only the documented authorization and JSON content type. A Session on the DeepSeek account route sends its account token instead, as Messages search does; the public Search and Fetch routes accepted it on 2026-10-16, so account-only users can enable direct retrieval. An Anthropic-compatible gateway checked the same day served Messages web search and had no `/v1/tools` routes; such deployments keep Messages search.
|
||||
|
||||
### Search results and model input
|
||||
|
||||
Direct Search sends `query`, `count`, `need_content: false`, and `ori_question`. `count` is the configured value from 1 to 50, defaulting to the service's 10, unless the request's result bound is smaller; the tool always sends its limit, 8 by default. `ori_question` is the text of the initiating Session's latest human prompt with whitespace collapsed and cut to the API's 128-character limit. The API rejects blank or longer values, so a search without such a prompt omits the field, and `sendOriginalQuestion: false` disables it. In 2026-10-16 probes, different values left result order, scores, and summaries unchanged. Queries pass through unchanged, including the documented site, language, date, exclusion, and exact-phrase operators. The service enforces the 1–128 Unicode-character query constraint. Empty `results` is successful.
|
||||
|
||||
The provider preserves service order and maps URL, title, `snippet`, and nullable `date` into shared sources. The current tool gives the same short excerpt to the model and search card. It does not consume `summary`, full `content`, `site_name`, `site_icon`, `rerank_score`, or `authority`, and it does not send `localization`. Those omissions are integration limits, not absent API capabilities.
|
||||
|
||||
Direct retrieval emits no auxiliary model-request event because Harness makes no such request. The normal tool-call and tool-result events retain the query and model-visible result. This says nothing about the search service's internal use of models. Returned text remains external, untrusted data.
|
||||
|
||||
### Fetch metadata and ownership
|
||||
|
||||
Fetch sends a URL and returns Markdown without an extra Harness summarization request. The API supplies neither a final redirected URL nor a target HTTP status, and the shared result has no title field. The provider keeps the requested URL, returns the Markdown body, and reports HTTP 200 for a successful response: the Fetch API answers 404 when the URL is unavailable, blocked by site policy, or returns an error, or when no Markdown can be extracted. On 2026-10-16 an unresolvable host returned HTTP 500 instead, so every non-success status is a retrieval failure rather than a specific target diagnosis. The shared fetch result and its consumers stay unchanged.
|
||||
|
||||
The HTTP provider retains its own public-address validation, connection pinning, and resource limits. Selecting DeepSeek delegates page retrieval and target access policy to that service; Harness rejects redirects of the API request and retains cancellation and bounded tool output. The reported HTTP 200 therefore means the service retrieved the page, not that Harness observed the target response. The guide's Fetch 403 means crawler-policy rejection, while 404 means no usable page content.
|
||||
|
||||
### Default migration
|
||||
|
||||
Changing the default to the Search API is a separate change after the launch is announced. API keys and account tokens both reach the public routes; deployments behind a gateway without `/v1/tools` routes need an explicit `web.searchProvider: deepseek-official` before the default changes, and a stored Messages selection keeps Messages.
|
||||
|
||||
### Supplementary Messages fix
|
||||
|
||||
The retained Messages path explicitly sends `thinking: { type: 'disabled' }` and records that same body before dispatch. This avoids reasoning work in an auxiliary request whose generated answer is discarded; it does not implement or control the new retrieval API. `tool_choice` remains omitted. The [persistence acknowledgement](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.md) owns compatibility for the optional logged field.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep Messages as the only search implementation.** It preserves auxiliary generation and response extraction even when callers need only sources. Direct Search removes that Harness model request while retaining Messages as an explicit mode.
|
||||
|
||||
**Make the Search API the default now.** The launch is not announced, and some gateways expose only Messages; their users would lose working search.
|
||||
|
||||
**Reuse the Messages address or infer a route prefix.** The protocols and deployed prefixes differ. Separate bases preserve existing configurations and avoid sending a valid request to the wrong route.
|
||||
|
||||
**Require an explicit Search API base.** Enabling the switch is already an explicit choice, and an empty default would make every user copy the documented public address. Other deployments still override it.
|
||||
|
||||
**Keep the APIs inside the Messages provider behind a provider-local switch.** A switch inside `web-search-deepseek` cannot change fetch routing, so turning it on left `web_fetch` on HTTP unless the user also edited `web.fetchProvider`; it also tied the APIs' lifecycle to Messages search, which the default migration may retire. Separate registrations let the web service's existing selection express the switch, and each package can change or retire alone.
|
||||
|
||||
**Share one transport between Messages search and the new APIs.** Both resolve credentials through the same seams, `ctx.credentials` and `ctx.deepseekAccount`, so a shared module would save only the dispatch and failure-mapping code. It would make the Messages package depend on the new API package and tie their releases, while the default migration may retire Messages search. Each package keeps its own transport until then.
|
||||
|
||||
**Split Search and Fetch into separate packages.** The guide serves both APIs from one base with one credential, and the switch selects them together; separate packages would duplicate the settings a user edits once. Each operation lives in its own module, so either API can change without the other.
|
||||
|
||||
**List every registered provider in a settings dropdown.** A generic provider form derives editors from Config schemas and needs host-side display metadata on the provider interfaces. The page needs only the DeepSeek choice, and other providers keep their own settings pages.
|
||||
|
||||
**Let the model supply `ori_question` as a tool argument.** The API defines the field as the user's original question, which the latest human prompt already records. A tool argument would change the model-facing schema shared by every search provider and invite paraphrase.
|
||||
|
||||
**Keep HTTP fetch when the switch turns on.** The switch adopts the new DeepSeek retrieval APIs, and leaving fetch on HTTP made Fetch API adoption depend on a hand-edited `web.fetchProvider`. HTTP fetch, with its own access policy, remains the selection whenever the switch is off, and configuration can still select either fetch provider alone.
|
||||
|
||||
**Add an extraction tool or widen the fetch result.** The API retrieves one concrete web page as Markdown, which fits the existing fetch operation. Its success already implies a retrieved page, and the Markdown body carries the page heading, so widening the result would change every fetch consumer for little model-visible gain.
|
||||
|
||||
## Consequences
|
||||
|
||||
Retrieval no longer requires auxiliary model tokens in direct mode, but its results still consume the main model's context. One Messages web search through a gateway on 2026-10-16 consumed 8,154 input and 828 output tokens for a single server-side search. The guide prices Search and Fetch independently at CNY 5 per 1,000 successful requests with results; result count does not change a request's price. Multiple queries remain multiple requests. The stated Claude Code and Codex promotion does not establish a Harness exemption. Search API mode also sends up to 128 characters of the user's prompt to the search service with each query unless `sendOriginalQuestion` is off.
|
||||
|
||||
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The current tool can issue four queries concurrently and fails the batch if one query fails. The provider has no shared rate scheduler, automatic retries, or fallback; other Sessions or processes can exhaust the same account allowance.
|
||||
|
||||
## Verification and remaining API adoption
|
||||
|
||||
Provider tests cover request mapping, empty results, malformed responses, independent and default bases, account-token authentication, cancellation, and blank configuration. Settings tests and the [plugin configuration browser scenario](../../../../apps/web/tests/plugin-config.e2e.ts) cover staged saves, the default base, and switching both selections. The [web search replay](../../../../apps/web/tests/web-search-round.e2e.ts) and [Fetch Session replay](../../../../snapshots/session/deepseek-fetch/snapshot.yml) pin model-visible results. On 2026-10-16 the [real provider tests](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts) passed Search and Fetch against the public base, and a headless round on the shipped profile returned eight sources and fetched Markdown without an auxiliary request event. The account-token, gateway, and status observations above come from manual probes the same day.
|
||||
|
||||
The following capabilities remain outside the implemented integration:
|
||||
|
||||
- Separate model-facing `summary` from display `snippet`, with per-source and total output budgets and matching Session records. The guide's typical summary length is not a maximum: returned summaries measured 1,000–2,300 characters on 2026-10-16.
|
||||
- Expose search syntax in provider-appropriate tool guidance and define a bounded localization input before forwarding it. API `localization` is an IP address, not an inferred city or UI language.
|
||||
- Retain quality metadata before defining selection policies. `rerank_score` and `authority` describe service assessments, not guarantees of truth; a hard cutoff can discard useful niche sources.
|
||||
- Add cancellation-aware scheduling and bounded retry ownership. The guide recommends waiting before retrying 408, 429, and 5xx; 402 requires funding first, and other 4xx require a changed request. Local pacing cannot enforce an account-wide quota across unrelated processes.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Agent Note: 直接接入 DeepSeek Search 与 Fetch API
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-15-deepseek-search-fetch-api.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
通过 Messages 获取 DeepSeek 网页搜索结果时,每个查询都需要一次辅助模型请求。Harness 提取来源与引用摘录,却丢弃生成的回答。独立 Search 和 Fetch API 直接提供结构化来源与 Markdown,使检索无需由 Harness 发起这次辅助生成。接入这些 API 需要明确选择两种操作的提供方、独立配置部署地址,并把 Fetch 结果映射到现有抓取结果。
|
||||
|
||||
## 决策
|
||||
|
||||
2026-10-16 核对的 [DeepSeek Search 与 Fetch 指南](https://api-docs.deepseek.com/zh-cn/guides/search)定义上游 API。[web-deepseek-api 插件](../../../../packages/web/web-deepseek-api/README.zh.md)通过现有 `web_search` 和 `web_fetch` 工具接入它,[web-search-deepseek](../../../../packages/web/web-search-deepseek/README.zh.md) 保留 Messages 搜索。[Web 能力决策](../architecture/2026-06-24-web-capability-seam.zh.md)继续负责提供方选择、工具职责和抓取结果字段。
|
||||
|
||||
### 协议选择与部署
|
||||
|
||||
两个 API 注册为 `deepseek-search-api` 和 `deepseek-fetch-api`;Messages 搜索保留 `deepseek-official`。网页搜索页上的设置 **Use the new Web Search API**(**启用新版 Web 搜索 API**)同时写入 `web.searchProvider` 和 `web.fetchProvider`:开启时选择两个 API,关闭时恢复内置选择,即 Messages 搜索与本地 HTTP 抓取。开关默认关闭,保存后生效,因为 Web 服务在每次操作时读取两项选择。在 Search API 正式发布公告之前,Messages 仍是默认值,已有用户无需配置即可继续使用搜索。每次操作解析一份设置快照,包含端点和凭据。两个 API 读取同一份凭据和同一个 API 基础地址,因为指南从同一基础地址提供它们,开关也同时选择它们。
|
||||
|
||||
两种协议分别保存 API 基础地址。Messages 保留 `https://api.deepseek.com/anthropic/v1`,追加 `/messages`。直接检索使用 `web-deepseek-api.baseURL`,默认值为指南中的公网地址 `https://api.deepseek.com/v1/tools`,追加 `/search` 或 `/fetch`;已验证的开发地址为 `http://api-dev.deepseek.com:8031/api/v1/tools`。两者前缀不同,程序不会根据主机名推断,其他部署需填写完整基础地址。开关本身已是显式启用,因此以文档中的公网部署作为默认值。基础地址为空白或无法解析时,两个提供方都不可用。
|
||||
|
||||
指南规定使用 API 密钥进行 Bearer 鉴权,请求只发送文档规定的鉴权与 JSON 内容类型请求头。DeepSeek 账号路由上的会话改为发送其账号 token,与 Messages 搜索一致;2026-10-16 公网 Search 与 Fetch 路由已接受该 token,因此只登录账号的用户也可以启用直接检索。同日检查的一个 Anthropic 兼容网关提供 Messages 网页搜索,但没有 `/v1/tools` 路由;此类部署继续使用 Messages 搜索。
|
||||
|
||||
### 搜索结果与模型输入
|
||||
|
||||
直接 Search 发送 `query`、`count`、`need_content: false` 和 `ori_question`。`count` 为 1 至 50 的配置值,默认采用服务的 10;请求的结果上限更小时以后者为准,工具总会发送其上限,默认为 8。`ori_question` 是发起会话中最近一条用户提示的文本,空白折叠后截取至 API 的 128 个字符上限。API 拒绝空白或更长的值,因此没有此类提示的搜索不发送该字段,`sendOriginalQuestion: false` 可将其关闭。2026-10-16 的探测中,不同取值未改变结果顺序、评分或摘要。查询原样传递,包括文档中的站点、语言、日期、排除和精确短语操作符。服务端执行查询长度为 1–128 个 Unicode 字符的约束。空 `results` 表示成功。
|
||||
|
||||
提供方保留服务端顺序,将 URL、标题、`snippet` 和可为 null 的 `date` 映射为共享来源。当前工具将同一段短摘要提供给模型和搜索卡片。它不使用 `summary`、完整 `content`、`site_name`、`site_icon`、`rerank_score` 或 `authority`,也不发送 `localization`。这些是当前接入的限制,并非 API 缺少这些能力。
|
||||
|
||||
直接检索不产生辅助模型请求事件,因为 Harness 没有发起该请求。普通工具调用与工具结果事件继续保留查询及模型可见的结果。这不代表搜索服务内部不使用模型。返回文本仍是外部不受信任的数据。
|
||||
|
||||
### 抓取元数据与职责
|
||||
|
||||
Fetch 发送 URL,返回 Markdown,无需 Harness 额外发起摘要请求。API 既不提供重定向后的最终 URL,也不提供目标 HTTP 状态,而共享结果没有标题字段。提供方保留请求 URL,返回 Markdown 正文,并对成功响应报告 HTTP 200:URL 不可用、被站点策略拦截、目标站点返回错误或无法提取 Markdown 时,Fetch API 会返回 404。2026-10-16 无法解析的主机实际返回 HTTP 500,因此任何非成功状态都视为抓取失败,而不是对目标的具体诊断。共享抓取结果及其消费方保持不变。
|
||||
|
||||
HTTP 提供方继续负责自身的公开地址校验、连接固定和资源限制。选择 DeepSeek 会将页面抓取与目标访问策略交给该服务;Harness 拒绝 API 请求重定向,并保留取消与工具输出限额。因此报告的 HTTP 200 表示服务已获取页面,而不是 Harness 观察到了目标响应。指南中的 Fetch 403 表示抓取器策略拒绝,404 则表示没有可用的页面内容。
|
||||
|
||||
### 默认值迁移
|
||||
|
||||
将默认值改为 Search API 是发布公告之后的另一项变更。API 密钥和账号 token 都能访问公网路由;位于没有 `/v1/tools` 路由的网关之后的部署,需要在默认值变更前显式设置 `web.searchProvider: deepseek-official`,已保存的 Messages 选择仍使用 Messages。
|
||||
|
||||
### 附带的 Messages 修复
|
||||
|
||||
保留的 Messages 路径显式发送 `thinking: { type: 'disabled' }`,并在发送前记录同一请求体。这避免了辅助请求在生成回答被丢弃的情况下执行思考;它不实现或控制新的检索 API。`tool_choice` 仍不传递。[持久化确认记录](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.zh.md)负责日志中可选字段的兼容性。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**仅保留 Messages 搜索实现。** 即使调用方只需要来源,这也保留了辅助生成和响应提取。直接 Search 去掉这次 Harness 模型请求,同时保留 Messages 供显式选择。
|
||||
|
||||
**立即将 Search API 设为默认值。** 尚未发布公告,部分网关也只提供 Messages;这些用户会失去可用的搜索。
|
||||
|
||||
**复用 Messages 地址或推断路由前缀。** 协议与部署前缀不同。分别保存基础地址可保留现有配置,避免把有效请求发往错误路由。
|
||||
|
||||
**要求显式填写 Search API 基础地址。** 开启开关已是显式选择,空默认值会迫使每位用户复制文档中的公网地址。其他部署仍可覆盖它。
|
||||
|
||||
**把两个 API 放在 Messages 提供方内部,由提供方自己的开关切换。** `web-search-deepseek` 内的开关无法改变抓取路由,因此开启后除非用户另外修改 `web.fetchProvider`,`web_fetch` 仍走 HTTP;它还把两个 API 的生命周期绑在 Messages 搜索上,而默认值迁移后 Messages 搜索可能被移除。独立注册让 Web 服务现有的选择来表达这个开关,每个包都可以单独修改或移除。
|
||||
|
||||
**让 Messages 搜索与新 API 共用一套传输层。** 两者都通过同样的 seam,即 `ctx.credentials` 和 `ctx.deepseekAccount` 解析凭据,共享模块只能省下请求派发与失败映射代码。它会让 Messages 包依赖新 API 包、两者必须一起发版,而默认值迁移后 Messages 搜索可能被移除。在此之前,两个包各自保留传输层。
|
||||
|
||||
**把 Search 和 Fetch 拆成两个包。** 指南从同一基础地址、以同一份凭据提供两个 API,开关也同时选择它们;拆成两个包会让用户只需编辑一次的设置重复出现。每种操作位于独立模块,任一 API 都可以单独修改。
|
||||
|
||||
**在设置页用下拉框列出所有已注册的提供方。** 通用提供方表单从 Config schema 推导编辑控件,还需要在提供方接口上增加宿主侧展示元数据。该页面只需要 DeepSeek 的选择,其他提供方保留各自的设置页。
|
||||
|
||||
**让模型以工具参数提供 `ori_question`。** API 将该字段定义为用户的原始问题,而最近一条用户提示已记录了这段文本。工具参数会改变所有搜索提供方共用的面向模型的 schema,并引入改写。
|
||||
|
||||
**开启开关时保留 HTTP 抓取。** 该开关用于启用 DeepSeek 新的检索 API,保留 HTTP 抓取会让使用 Fetch API 依赖手动修改 `web.fetchProvider`。开关关闭时,抓取仍选择带有自身访问策略的 HTTP 提供方,配置中也仍可单独选择任一抓取提供方。
|
||||
|
||||
**新增提取工具或扩展抓取结果。** API 以 Markdown 获取一个具体网页,符合现有抓取操作。其成功本身即表示已获取页面,Markdown 正文也带有页面标题,因此扩展结果会改动每个抓取消费方,而模型可见的收益很小。
|
||||
|
||||
## 影响
|
||||
|
||||
直接模式中的检索不再需要辅助模型 token,但结果仍消耗主模型上下文。2026-10-16 通过网关进行的一次 Messages 网页搜索,为一次服务端搜索消耗了 8,154 个输入 token 和 828 个输出 token。指南对 Search 和 Fetch 分别按每 1,000 次成功且有结果的请求收取 5 元;结果条数不影响单次价格。多个查询仍对应多次请求。文档中的 Claude Code 与 Codex 优惠不能作为 Harness 免收费用的依据。除非关闭 `sendOriginalQuestion`,Search API 模式还会随每个查询把用户提示的至多 128 个字符发送给搜索服务。
|
||||
|
||||
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。当前工具可并发发送四个查询,任一查询失败会使整批失败。提供方没有共享限速调度器、自动重试或回退;其他会话或进程也会消耗同一账号的额度。
|
||||
|
||||
## 验证与尚未接入的 API 能力
|
||||
|
||||
提供方测试覆盖请求映射、空结果、格式错误的响应、独立地址与默认地址、账号 token 鉴权、取消与空白配置。设置测试及[插件配置浏览器场景](../../../../apps/web/tests/plugin-config.e2e.ts)覆盖暂存后保存、默认地址和同时切换两项选择。[网页搜索回放](../../../../apps/web/tests/web-search-round.e2e.ts)与 [Fetch Session 回放](../../../../snapshots/session/deepseek-fetch/snapshot.yml)固定模型可见结果。2026-10-16,[真实提供方测试](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts)在公网地址通过了 Search 与 Fetch 验证;在内置配置上运行的一轮 headless 对话返回了八个来源并抓取到 Markdown,且没有辅助请求事件。上文的账号 token、网关和状态码观察来自同日的手动探测。
|
||||
|
||||
以下能力尚未包含在已实现的接入中:
|
||||
|
||||
- 区分面向模型的 `summary` 与用于展示的 `snippet`,设置每个来源和总输出限额,并记录对应的 Session 内容。指南中的典型摘要长度不是最大值:2026-10-16 实测返回的摘要为 1,000–2,300 个字符。
|
||||
- 在适用于提供方的工具说明中介绍搜索语法,先定义有界的本地化输入,再进行转发。API 的 `localization` 是 IP 地址,不能用推测的城市或界面语言代替。
|
||||
- 先保留质量元数据,再定义筛选策略。`rerank_score` 与 `authority` 表示服务端评估,不保证内容真实;固定阈值可能丢弃有用的冷门来源。
|
||||
- 增加支持取消的调度并明确有限重试的职责。指南建议对 408、429 和 5xx 等待后重试;402 需要先充值,其他 4xx 需要修改请求。本地节流无法跨无关进程执行账号级配额。
|
||||
@@ -567,6 +567,15 @@ jobs:
|
||||
- name: Run blocking Windows builds
|
||||
shell: pwsh
|
||||
run: pnpm run check:ci:windows-blocking
|
||||
- name: Parse Windows runner supervision scripts
|
||||
shell: pwsh
|
||||
run: |
|
||||
foreach ($path in @('apps/desktop/scripts/windows-runner-task.ps1', 'apps/desktop/scripts/run-windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-task.ps1')) {
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $env:GITHUB_WORKSPACE $path), [ref]$tokens, [ref]$parseErrors)
|
||||
if ($parseErrors.Count) { throw ($parseErrors -join "`n") }
|
||||
}
|
||||
- name: Run Windows observational gates on built artifacts
|
||||
id: observational
|
||||
continue-on-error: true
|
||||
|
||||
@@ -31,6 +31,11 @@ jobs:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Reject debugging packages before dependency installation
|
||||
id: preflight
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: node apps/desktop/scripts/preflight-desktop-publish.mjs
|
||||
- uses: pnpm/action-setup@v5.0.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
@@ -41,6 +46,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
DESKTOP_PUBLISH_RUNNERS: ${{ vars.DESKTOP_PUBLISH_RUNNERS }}
|
||||
DESKTOP_PACKAGE_ATTEMPT: ${{ steps.preflight.outputs.attempt }}
|
||||
run: node --import tsx apps/desktop/scripts/resolve-desktop-publish-ci.ts
|
||||
|
||||
prepare:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
default: test
|
||||
required: true
|
||||
skip_notarization:
|
||||
description: '跳过公证和 stapling:仅限内部调试,不可用于发布或自动更新'
|
||||
description: '跳过公证和 stapling:仅限内部调试,禁止发布到 test / production 或用于自动更新;需要发布请勿勾选'
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
|
||||
@@ -10,10 +10,10 @@ on:
|
||||
default: test
|
||||
required: true
|
||||
signing:
|
||||
description: '签名方式:unsigned 仅供内部测试;signed 使用机器配置的 EV 硬件令牌'
|
||||
description: '签名方式:unsigned 仅限内部调试,禁止发布到 test / production 或用于自动更新;需要发布请选择 signed(EV 硬件令牌)'
|
||||
type: choice
|
||||
options: [unsigned, signed]
|
||||
default: unsigned
|
||||
options: [signed, unsigned]
|
||||
default: signed
|
||||
required: true
|
||||
test_release_id:
|
||||
description: '测试 release ID:32 位小写十六进制随机前缀(非鉴权);留空使用机器默认值,连续升级测试须保持相同值'
|
||||
|
||||
@@ -146,8 +146,6 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_subagent_fork_in_process
|
||||
plugin_dsh_base_tool_subagent_control["tool-subagent-control<br/>@deepseek-ai/dsh-tool-subagent-control"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_control
|
||||
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents<br/>@deepseek-ai/dsh-tool-subagent-control/list-agents"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_list_agents
|
||||
plugin_dsh_base_tool_subagent["tool-subagent<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
cfg --> plugin_dsh_base_tool_subagent
|
||||
plugin_dsh_base_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
@@ -180,6 +178,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_web
|
||||
plugin_dsh_base_web_search_deepseek["web-search-deepseek<br/>@deepseek-ai/dsh-web-search-deepseek"]
|
||||
cfg --> plugin_dsh_base_web_search_deepseek
|
||||
plugin_dsh_base_web_deepseek_api["web-deepseek-api<br/>@deepseek-ai/dsh-web-deepseek-api"]
|
||||
cfg --> plugin_dsh_base_web_deepseek_api
|
||||
plugin_dsh_base_web_fetch_http["web-fetch-http<br/>@deepseek-ai/dsh-web-fetch-http"]
|
||||
cfg --> plugin_dsh_base_web_fetch_http
|
||||
plugin_dsh_base_tool_web["tool-web<br/>@deepseek-ai/dsh-tool-web"]
|
||||
@@ -271,7 +271,6 @@ flowchart LR
|
||||
| `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` |
|
||||
| `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` |
|
||||
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
|
||||
| `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
|
||||
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `ptc-runtime` | `@deepseek-ai/dsh-ptc-runtime-node` |
|
||||
@@ -288,6 +287,7 @@ flowchart LR
|
||||
| `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` |
|
||||
| `web` | `@deepseek-ai/dsh-web` |
|
||||
| `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` |
|
||||
| `web-deepseek-api` | `@deepseek-ai/dsh-web-deepseek-api` |
|
||||
| `web-fetch-http` | `@deepseek-ai/dsh-web-fetch-http` |
|
||||
| `tool-web` | `@deepseek-ai/dsh-tool-web` |
|
||||
| `mcp-resources` | `@deepseek-ai/dsh-mcp-resources` |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.2.1-alpha.4",
|
||||
"version": "0.2.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/** Account catalog diagnostics preserve the shipped ACP profile's JSON-RPC stream. */
|
||||
import { once } from 'node:events'
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
||||
import { launchAcpTestAgent, type AgentUnderTest, type LaunchedAcpTestAgent } from '@deepseek-ai/dsh-session-snapshot'
|
||||
import { expect, it } from 'vitest'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
it('keeps account model-listing diagnostics on stderr during API-key session creation', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-acp-stdout-'))
|
||||
const token = 'acp-catalog-fixture-token'
|
||||
const requests: Array<{ method: string | undefined; path: string | undefined; token: string | string[] | undefined }> = []
|
||||
const server = createServer((request, response) => {
|
||||
requests.push({ method: request.method, path: request.url, token: request.headers['x-dsh-auth-token'] })
|
||||
request.resume()
|
||||
if (request.method !== 'GET' || request.url !== '/models') {
|
||||
response.writeHead(500).end('unexpected request')
|
||||
return
|
||||
}
|
||||
response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'deepseek-flash', name: 'Account fixture model', input_modalities: ['text'], output_modalities: ['text'],
|
||||
context_window: 1_048_576, max_output_tokens: 393_216, api_capabilities: { anthropic_messages: {} },
|
||||
},
|
||||
{ id: 'deepseek-v4-pro' },
|
||||
],
|
||||
}))
|
||||
})
|
||||
let launched: LaunchedAcpTestAgent | undefined
|
||||
try {
|
||||
server.listen(0, '127.0.0.1')
|
||||
await once(server, 'listening')
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('account catalog server has no port')
|
||||
const origin = `http://127.0.0.1:${address.port}`
|
||||
await mkdir(join(cwd, '.dsh'))
|
||||
await writeFile(join(cwd, '.dsh', '.credentials.yaml'), JSON.stringify({
|
||||
version: 1,
|
||||
records: {
|
||||
'deepseek-account-platform/default': { kind: 'grant', payload: { version: 1, token, issuer: origin } },
|
||||
},
|
||||
}) + '\n', { mode: 0o600 })
|
||||
const configPath = join(cwd, 'catalog.patch.yml')
|
||||
await writeFile(configPath, JSON.stringify([
|
||||
{ id: 'deepseek-account', config: { platformOrigin: origin, inferenceOrigin: origin, allowLoopbackHttp: true } },
|
||||
{ id: 'llm-deepseek-account', config: { baseURL: `${origin}/anthropic` } },
|
||||
{ id: 'llm-deepseek', config: { baseURL: `${origin}/anthropic`, models: [{ id: 'fixture-api-key-model' }] } },
|
||||
// Catalog discovery requires the account service to have loaded the stored grant.
|
||||
{
|
||||
id: 'acp', inject: ['acpAppStartup', 'deepseekAccount'],
|
||||
config: { provider: 'deepseek-official', model: 'fixture-api-key-model' },
|
||||
},
|
||||
{ id: 'session-persistence-jsonl', config: { root: join(cwd, 'sessions'), compression: 'none' } },
|
||||
]) + '\n')
|
||||
const agent: AgentUnderTest = {
|
||||
binScript: fileURLToPath(new URL('../../../../src/bin.ts', import.meta.url)),
|
||||
configPath,
|
||||
profile: 'acp',
|
||||
tsconfigPath: fileURLToPath(new URL('../../../../../../tsconfig.json', import.meta.url)),
|
||||
}
|
||||
launched = launchAcpTestAgent({
|
||||
agent,
|
||||
cwd,
|
||||
env: { DEEPSEEK_API_KEY: 'acp-fixture-api-key', DEEPSEEK_BASE_URL: `${origin}/anthropic`, DSH_TELEMETRY_DISABLED: '1' },
|
||||
})
|
||||
await launched.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
const created = await launched.client.newSession({ cwd, mcpServers: [] })
|
||||
expect(created.configOptions).toContainEqual(expect.objectContaining({
|
||||
id: 'model', currentValue: JSON.stringify(['deepseek-official', 'fixture-api-key-model']),
|
||||
}))
|
||||
await launched.client.closeSession({ sessionId: created.sessionId })
|
||||
// Closing joins both pipes; receiving a stdout response alone does not drain stderr.
|
||||
await launched.close()
|
||||
|
||||
expect(launched.child.signalCode).toBeNull()
|
||||
expect(launched.child.exitCode).toBe(0)
|
||||
expect(requests).toEqual([{ method: 'GET', path: '/models', token }])
|
||||
const frames = launched.rawStdout().trim().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
||||
expect(frames.length).toBeGreaterThanOrEqual(3)
|
||||
for (const frame of frames) expect(frame).toMatchObject({ jsonrpc: '2.0' })
|
||||
expect(launched.stderr()).toContain(`[llm-deepseek-account] model listing from ${origin}/models loaded 2 model(s), skipped 0 row(s)`)
|
||||
expect(launched.stderr()).toContain('[llm-deepseek-account] model "deepseek-v4-pro" declares no api_capabilities.anthropic_messages')
|
||||
expect(launched.stderr()).not.toContain(token)
|
||||
} finally {
|
||||
const cleanup = await Promise.allSettled([cleanupAcpExampleTest(launched, cwd)])
|
||||
server.closeAllConnections()
|
||||
cleanup.push(...await Promise.allSettled([
|
||||
server.listening ? new Promise<void>((resolve, reject) => server.close((error) => {
|
||||
if (error === undefined) resolve()
|
||||
else reject(error)
|
||||
})) : undefined,
|
||||
]))
|
||||
const failures: unknown[] = []
|
||||
for (const result of cleanup) if (result.status === 'rejected') failures.push(result.reason)
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'ACP catalog test cleanup failed')
|
||||
}
|
||||
})
|
||||
@@ -24,9 +24,6 @@
|
||||
- id: tool-subagent-control
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-list-agents
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-report
|
||||
disabled: true
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
|
||||
import { lastAssistantStreamChunk, type RetryPolicyConfig, type TokenUsage } from '@deepseek-ai/dsh-llm'
|
||||
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
@@ -22,6 +22,8 @@ interface ObservedRequest {
|
||||
step: number
|
||||
body: {
|
||||
system?: string
|
||||
max_tokens: number
|
||||
thinking: { type: string }
|
||||
messages: { role: string; content: { type: string; text?: string; tool?: { name: string } }[] }[]
|
||||
tools?: { name: string; defer_loading?: true }[]
|
||||
}
|
||||
@@ -70,7 +72,7 @@ function registryChanges(previous: ObservedRequest, request: ObservedRequest): {
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with real DeepSeek', () => {
|
||||
it.each([false, true])('keeps the preceding conversation cached after a tool addition (prompt update: %s)', { retry: 0 }, async (updatePrompt) => {
|
||||
it.each([false, true])('keeps the preceding conversation cached after a tool addition (prompt update: %s)', async (updatePrompt) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-sdk-tool-cache-'))
|
||||
onTestFinished(async () => { await rm(root, { recursive: true, force: true }) })
|
||||
const sampleLabel = `CACHE_SAMPLE_${randomUUID()}`
|
||||
@@ -80,7 +82,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea
|
||||
await writeFile(patch, JSON.stringify([
|
||||
{ id: 'llm-deepseek', config: {
|
||||
apiKeyEnv: 'DEEPSEEK_API_KEY', thinking: 'disabled', maxTokens: 1024,
|
||||
retryPolicy: { maxRetries: 0 },
|
||||
retryPolicy: { mode: 'normal', maxRetries: 0 } satisfies RetryPolicyConfig,
|
||||
models: [{ id: 'deepseek-flash', systemPromptUpdate: 'in-history', toolUpdate: 'addition-only' }],
|
||||
} },
|
||||
{ id: 'tools', config: { mode: 'native' } },
|
||||
@@ -101,13 +103,19 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea
|
||||
const run = async (prompt: string) => {
|
||||
const result = await session.run(prompt)
|
||||
events.push(...result.events)
|
||||
const failures = result.events.filter(event => event.type === 'assistant/attempt').map(event => ({
|
||||
turn: event.data.turn,
|
||||
step: event.data.step,
|
||||
reason: lastAssistantStreamChunk(event.data.stream, 'finish')?.reason,
|
||||
}))
|
||||
expect(failures, `Provider request failures: ${JSON.stringify(failures)}`).toEqual([])
|
||||
return result.finalResponse
|
||||
}
|
||||
await run('Do not call tools. Reply READY.')
|
||||
expect(await run('Do not call tools. Reply READY.')).toContain('READY')
|
||||
// Unique user history makes hits on shared profile instructions insufficient.
|
||||
const records = Array.from({ length: 96 }, (_, index) => `record-${index}: ${randomUUID()}`).join('\n')
|
||||
await run(`Keep these reference records. Do not call tools. Reply READY.\n${records}`)
|
||||
await run('Keep the reference records. Do not call tools. Reply READY.')
|
||||
expect(await run(`Keep these reference records. Do not call tools. Reply READY.\n${records}`)).toContain('READY')
|
||||
expect(await run('Keep the reference records. Do not call tools. Reply READY.')).toContain('READY')
|
||||
expect(await run(`Use cache_tool_control with action add. After it returns, call ${sampleTool} exactly once and report the generated sample label. This is synthetic example data for this run. Call these separately in order.`)).toContain(sampleLabel)
|
||||
expectCalledBefore(await recordedCalls(callsPath), 'add', 'sample')
|
||||
expect(await run('Use cache_tool_control with action remove. Whether it removes the sample tool or reports that it is not registered, reply REMOVED and do not call the sample tool.')).toContain('REMOVED')
|
||||
@@ -115,7 +123,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea
|
||||
expectCalledBefore(calls, 'sample', 'remove')
|
||||
|
||||
const requests = (await readFile(evidencePath, 'utf8')).trim().split('\n').map(line => JSON.parse(line) as ObservedRequest)
|
||||
expect(events.filter(event => event.type === 'assistant/attempt')).toHaveLength(0)
|
||||
expect(requests).toHaveLength(events.filter(event => event.type === 'assistant/message').length)
|
||||
const [initial, history, warm] = requests
|
||||
const addedIndex = requests.findIndex(request => request.stage === 'added')
|
||||
@@ -123,6 +130,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea
|
||||
const added = requests[addedIndex]
|
||||
const removed = requests.find(request => request.stage === 'removed')
|
||||
if (!initial || !history || !warm || !before || !added || !removed) throw new Error('Missing cache transition requests')
|
||||
expect(initial.body).toMatchObject({ max_tokens: 1024, thinking: { type: 'disabled' } })
|
||||
const historyInput = totalInput(usageFor(history, events))
|
||||
const precedingInput = totalInput(usageFor(before, events))
|
||||
const cached = usageFor(added, events).cacheReadTokens ?? 0
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('the shipped Web composition', () => {
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update',
|
||||
'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search',
|
||||
@@ -328,15 +328,21 @@ describe('the shipped Web composition', () => {
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models')
|
||||
expect(toolParameterNames(ctx, disabled.agent, 'subagent')).not.toEqual(expect.arrayContaining([
|
||||
expect(toolNames(ctx, disabled.agent)).toContain('list_subagent_models')
|
||||
expect(toolParameterNames(ctx, disabled.agent, 'subagent')).toEqual(expect.arrayContaining([
|
||||
'model', 'provider', 'reasoning_effort',
|
||||
]))
|
||||
expect(ctx.sessionProjections.stateOf(disabled.agent.session, 'subagentModelSelectionPolicy')).toBeNull()
|
||||
expect(ctx.sessionProjections.stateOf(enabled.agent.session, 'subagentModelSelectionPolicy')).toEqual([
|
||||
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
])
|
||||
expect(toolNames(ctx, enabled.agent)).toContain('list_subagent_models')
|
||||
expect(toolParameterNames(ctx, enabled.agent, 'subagent')).toEqual(expect.arrayContaining([
|
||||
'model', 'provider', 'reasoning_effort',
|
||||
]))
|
||||
expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models')
|
||||
for (const agent of [disabled.agent, enabled.agent]) {
|
||||
expect(toolParameterNames(ctx, agent, 'subagent_fork')).toEqual(['cwd', 'description', 'prompt'])
|
||||
}
|
||||
} finally {
|
||||
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
|
||||
await enabled.dispose()
|
||||
|
||||
@@ -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-alpha.4",
|
||||
"version": "0.2.1-rc.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -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-alpha.4",
|
||||
"version": "0.2.1-rc.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: f5dc36baf414ecc5
|
||||
zh: 8984741b356ac1d3
|
||||
/desktop-packaging/windows-actions-packaging:
|
||||
en: e3f51e7130a0fe05
|
||||
zh: 70fd6a40d5899871
|
||||
en: e1b101593f6f755e
|
||||
zh: 121f225de399de1e
|
||||
/desktop-packaging/macos-self-hosted-packaging:
|
||||
en: a3af9523fdc60036
|
||||
zh: 5e18d4740e9b7f57
|
||||
|
||||
@@ -103,7 +103,9 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t
|
||||
|
||||
## Windows Actions packaging
|
||||
|
||||
The `Package Desktop · Windows` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `unsigned` (default) or `signed`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `build_version` defaults to `auto`, using the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged.
|
||||
For supervised runner startup and recovery, follow [Windows runner machine setup](../windows-signing/README.md#machine-setup).
|
||||
|
||||
The `Package Desktop · Windows` [manual workflow](../../../.github/workflows/windows-package.yml) selects the dedicated Windows x64 runner label `dsh-win-package-trial` and the machine preset `test` or `production`. Choose `signed` (default) or `unsigned`; signed mode uses the existing EV token preflight, signature cache and failure interlock. It does not publish or upload a release. `build_version` defaults to `auto`, using the CI identifier in [Release versions](#release-versions); an explicit value may be the checkout's product version or an extended build version such as `0.2.1-alpha.1.20261009.1` for product `0.2.1-alpha.1`. The version prefix must match the selected checkout; validation errors show its current example. Manifests remain unchanged.
|
||||
|
||||
Select the workflow in Actions, choose the source branch, preset, signing mode and version, then click Run workflow. Before merging input changes, test the pushed branch with `gh workflow run windows-package.yml --ref <branch> -f preset=test -f signing=unsigned -f build_version=auto`; verify the web form after merging. There is no push or PR trigger. Logs and successful EXEs are retained for 14 days. Inspect the retained logs and installer before relying on packaging; installation is separate. Before checkout, Node removes the runner-owned source directory without following dependency junctions; the workflow validates its location and rejects linked ancestors. Never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run remains pending, and a newer dispatch replaces it. Diagnostics upload requires a successful clean checkout. Machine settings stay outside the checkout and are never uploaded as artifacts.
|
||||
|
||||
|
||||
@@ -103,7 +103,9 @@ macOS 正式发布和本地未签名 DMG 都通过 [electron-builder 配置](../
|
||||
|
||||
## Windows Actions 打包
|
||||
|
||||
`Package Desktop · Windows` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `unsigned`(默认)或 `signed`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`build_version` 默认为 `auto`,使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。
|
||||
runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-signing/README.zh.md#machine-setup)。
|
||||
|
||||
`Package Desktop · Windows` [手动工作流](../../../.github/workflows/windows-package.yml) 选择专用 Windows x64 runner 标签 `dsh-win-package-trial`,并使用机器 preset `test` 或 `production`。选择 `signed`(默认)或 `unsigned`;签名模式复用现有 EV token 预检、签名缓存与失败 interlock,不发布或上传 release。`build_version` 默认为 `auto`,使用[发布版本](#release-versions)中的 CI 标识;显式值可以是源码产品版本,或扩展构建版本,例如产品 `0.2.1-alpha.1` 对应 `0.2.1-alpha.1.20261009.1`。版本前缀必须匹配所选源码;校验错误显示其当前示例。manifest 保持不变。
|
||||
|
||||
在 Actions 选择工作流、源码分支、preset、签名模式和版本,再点击 Run workflow。输入变更合并前,用 `gh workflow run windows-package.yml --ref <branch> -f preset=test -f signing=unsigned -f build_version=auto` 测试已推送分支;合并后验证网页表单。它不监听 push 或 PR。日志和成功生成的 EXE 保留 14 天。依赖打包流程前检查日志和安装包;安装是独立操作。检出前,Node 删除 runner 专用源码目录,不跟随依赖 junction;工作流校验目标位置并拒绝链接祖先目录。不得指向日常开发目录。运行共享一个并发组,不取消正在进行的构建;最多保留一个待运行任务,新触发会替换它。仅在成功完成干净检出后上传诊断。机器配置位于 checkout 之外,不作为产物上传。
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
en: 1b6a4d7146879eec
|
||||
zh: f2520fc412cd2d2a
|
||||
/desktop-ci-publication/machine-setup:
|
||||
en: 990ff7e56dd6005a
|
||||
zh: 18f9dc23c520dc8c
|
||||
en: f80388c2b1ffef4c
|
||||
zh: f242ab2316bf38a1
|
||||
/desktop-ci-publication/package-and-publish:
|
||||
en: 2143e73bc7c65385
|
||||
zh: cd968050774f4bb3
|
||||
en: 0a26537845e89dda
|
||||
zh: 307378bb65e24650
|
||||
/desktop-ci-publication/recovery-and-records:
|
||||
en: e92bfad06162443e
|
||||
zh: 6c3e3d8ece673217
|
||||
en: 2e1e6b16d0d6bd41
|
||||
zh: 6ff4ad5a8f1f57b4
|
||||
/desktop-ci-publication/restore-a-channel:
|
||||
en: d82e2ccf70cce397
|
||||
zh: d93dcb77cc4b463e
|
||||
|
||||
@@ -23,7 +23,7 @@ Publish a completed package from its original machine without downloading its in
|
||||
|
||||
Keep `DSH_PACKAGING_CONFIG_DIR` pointed at the existing machine packaging directory. Completed attempts are copied to its sibling `releases/<run-id>/<attempt>/`; `DSH_DESKTOP_RELEASES_DIR` overrides that root with an absolute path. The inventory includes installers, update archives, blockmaps, completion metadata and a credential-free manifest. Debug packages are retained but cannot publish. Copies are checked before the attempt directory becomes visible, and an existing attempt cannot be replaced. GitHub retains the same file inventory for 14 days. Local files remain until an operator removes them; do not delete an attempt while it is being verified or published.
|
||||
|
||||
Create `publishing/test.json` and, when authorized, `publishing/production.json` below the configuration directory. Each declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. These files stay outside Git. The uploader supports permanent COS credentials, not STS session credentials. Bucket versioning must be disabled for immutable binary writes.
|
||||
Create `publishing/test.json` and, when authorized, `publishing/production.json` below the configuration directory. Each declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. These files stay outside Git. The uploader supports permanent COS credentials, not STS session credentials. Administrators must keep test and production buckets unversioned (neither enabled nor suspended). Publication assumes this deployment prerequisite and does not call `GetBucketVersioning` or require its permission. Immutable binary PUTs retain `x-cos-forbid-overwrite`; their protection depends on administrators maintaining the bucket configuration.
|
||||
|
||||
Set the repository variable `DESKTOP_PUBLISH_RUNNERS` to a JSON mapping of registered runner names to unique labels and targets, for example `{"mini":{"label":"dsh-mini-publish","target":"mac-arm64"}}`. Assign that label to exactly that runner. Windows uses target `win-x64`. The routing job checks the original successful job, and the publisher checks its own runner name against the recorded name. Machines need no shared filesystem or LAN connection to each other.
|
||||
|
||||
@@ -35,6 +35,8 @@ Environment approval gates this workflow, not arbitrary code running under the s
|
||||
|
||||
## Package and publish
|
||||
|
||||
For publication to either test or production, choose Windows `signing=signed` or leave macOS **Skip notarization** unchecked. Unsigned and unnotarized packages are only for internal debugging; they cannot publish or provide automatic updates. Before installing dependencies, the hosted routing job rejects debugging artifacts identified by the selected run and attempt. Missing artifact metadata does not establish eligibility: the original machine still validates its retained manifest and signatures. A debugging-package rejection requires a new signed/notarized package and its run ID / attempt; retrying publication cannot qualify the existing bytes.
|
||||
|
||||
Select the platform's `Package Desktop` workflow, preset, source, channel and version mode. `source_ref` accepts repository branch names (for example `feature/desktop-update`) or `dsh-v*` tags for test; production accepts only `master` or `dsh-v*` tags. Names matching `dsh-v` followed by a digit and letters, digits, dots or hyphens are treated as tags, even if a branch has that name. Tags must match the selected product version. We recommend dispatching the packaging workflow from `master` and enter the source separately; raw commit SHAs, PR refs and fork repositories are not supported. The selected source must contain the CI adapters, so older sources fail before dependency installation. Only select trusted branches: their build code runs on the signing machine. This is an operating convention, not enforced workflow-origin or credential isolation. Publication uses the recorded commit and requires it to remain in the selected branch history; normal branch advances are allowed, deletion or a diverging force-push rejects. Tag publication requires the tag to still resolve to the recorded commit. Dispatch the publisher from `master`; its hosted routing job rejects other refs before scheduling the original machine. The packaged source commit is recorded separately. `version_mode=default` selects product for production and auto for test; product preserves the selected checkout version, auto appends date/run/attempt, and manual requires `build_version`. A latest package requires a stable version, so test/latest needs product or a stable manual version. The optional `test_release_id` is the test URL prefix: blank retains the machine default, successive upgrade tests on that platform need the same value, and defaults should be rotated periodically. It is not a GitHub run ID. See [packaging configuration](../../packaging/README.md#configuration-and-local-builds) for its validation and rotation limits.
|
||||
|
||||
Packaging only retains files. After success, open **Publish Desktop · COS → Run workflow** on `master`, enter `package_run_id` from the packaging run URL, and optionally enter `package_run_attempt`. Blank selects the latest attempt, which must have succeeded; it never falls back to an older success. An explicit attempt selects that exact successful build. Windows must be signed; macOS must finish notarization and stapling. GitHub job evidence selects the original runner; its local manifest supplies the preset, version, source and channel before approval. No installer or request artifact is downloaded. Retained packages with the old `publishRequested=false` flag remain eligible; missing or changed local files stop publication.
|
||||
@@ -47,7 +49,7 @@ Before uploading, the publisher hashes all retained files, checks version and de
|
||||
|
||||
After resolving a failure, re-run the failed publishing job to keep its prepared package attempt, or start a new publication with the explicit original package run and attempt. Re-running all jobs with a blank attempt resolves the latest package attempt again and requires a new approval. Do not rebuild solely to retry an upload. Missing objects are uploaded, identical objects are skipped, and conflicting same-name bytes stop the operation. Unknown write outcomes are reconciled by reading remote bytes. A newer channel version prevents a stale upload from replacing it. Each package updates only its embedded channel: nightly writes `nightly.yml` / `nightly-mac.yml`, latest writes `latest.yml` / `latest-mac.yml`. Latest binaries use a `latest/` subdirectory under the target binary prefix, separate from existing nightly objects. Publishing the same source/version to both channels requires two packages; an existing release tag is reused only for the same source commit.
|
||||
|
||||
The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; raw provider responses are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate.
|
||||
The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; COS failures also identify `GetObject` or `PutObject`, plus a validated HTTP status and request ID when available. Access-denied reports name the permission to inspect. Raw provider responses and credentials are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate.
|
||||
|
||||
If the feed completed but tagging failed, the result is `published-tag-pending` and the job fails visibly. On retry, durable feed-completion evidence permits only tag recovery, even if a later release has advanced the channel. It never re-publishes old YAML. An existing conflicting tag requires administrator investigation and is never moved automatically.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ description: "在专用 macOS 和 Windows runner 上发布留存的 Desktop 安
|
||||
|
||||
保持 `DSH_PACKAGING_CONFIG_DIR` 指向现有机器打包目录。完成的构建复制到其同级 `releases/<run-id>/<attempt>/`;`DSH_DESKTOP_RELEASES_DIR` 可用绝对路径覆盖该根目录。文件清单包含安装包、更新归档、blockmap、完成记录和不含凭据的 manifest。调试包会留存,但不能发布。复制内容通过校验后才开放 attempt 目录,已有 attempt 不能被替换。GitHub 保留相同文件清单 14 天。本地文件保留到操作员删除;校验或发布期间不要删除对应 attempt。
|
||||
|
||||
在配置目录下创建 `publishing/test.json`,获得授权后再创建 `publishing/production.json`。每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。这些文件不进入 Git。上传器支持长期 COS 凭据,不支持 STS 会话凭据。不可覆盖的二进制写入要求 bucket 版本控制关闭。
|
||||
在配置目录下创建 `publishing/test.json`,获得授权后再创建 `publishing/production.json`。每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。这些文件不进入 Git。上传器支持长期 COS 凭据,不支持 STS 会话凭据。管理员必须维持 test 和 production bucket 未启用版本控制(不能是已启用或已暂停状态)。发布流程以此为部署前提,不调用 `GetBucketVersioning`,也不要求该权限。二进制 PUT 仍携带 `x-cos-forbid-overwrite`;防覆盖保证依赖管理员维持桶配置。
|
||||
|
||||
将仓库变量 `DESKTOP_PUBLISH_RUNNERS` 配置为已注册 runner 名称到唯一标签和目标的 JSON 映射,例如 `{"mini":{"label":"dsh-mini-publish","target":"mac-arm64"}}`。该标签只能分配给这台 runner。Windows 使用目标 `win-x64`。路由 job 检查原构建成功的 job,发布器再检查自身 runner 名称与记录一致。机器之间不需要共享文件系统或局域网连接。
|
||||
|
||||
@@ -35,6 +35,8 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
## 打包与发布
|
||||
|
||||
发布到 test 或 production 都必须在 Windows 选择 `signing=signed`,或在 macOS 不勾选**跳过公证**。无签名、无公证的包仅限内部调试,不能发布或用于自动更新。托管路由 job 会在安装依赖前,依据所选 run 和 attempt 对应的 artifact 名称拒绝已标明的调试包。缺少 artifact 元数据不代表允许发布:原机器仍会验证留存清单和签名。遇到调试包拒绝提示时,需重新完成签名或公证,再使用新包的 run ID / attempt 发布;重跑发布不能使现有产物获得发布资格。
|
||||
|
||||
选择对应平台的 `Package Desktop` 工作流、preset、源码、通道和版本模式。test 的 `source_ref` 接受仓库内分支名(如 `feature/desktop-update`)或 `dsh-v*` tag;production 仅接受 `master` 或 `dsh-v*` tag。符合 `dsh-v` 后接数字、其余为字母、数字、点或连字符的名称按 tag 处理,即使存在同名分支。tag 必须与所选产品版本一致。建议从 `master` 启动打包工作流,再单独填写打包源码;不支持直接填写 commit SHA、PR ref 或 fork 仓库。源码必须包含 CI 适配脚本,旧源码会在安装依赖前失败。仅选择可信分支:其构建代码将在签名机器上执行。这是操作约定,不提供工作流来源强制限制或凭据隔离。发布使用记录的 commit,并要求该提交仍在所选分支历史中;正常追加提交不影响发布,删除分支或导致历史分叉的强推会拒绝。tag 发布要求 tag 仍指向记录的提交。从 `master` 发起发布;托管路由 job 会在派发到原机器前拒绝其他 ref。实际打包源码提交单独记录。`version_mode=default` 对 production 选择 product,对 test 选择 auto;product 保留所选源码版本,auto 追加日期/run/attempt,manual 要求填写 `build_version`。latest 包要求稳定版本,因此 test/latest 需选择 product 或稳定的手填版本。可选 `test_release_id` 是测试 URL 前缀:留空沿用机器默认值,同一平台连续升级测试需保持相同值,并应定期轮换默认值。它不是 GitHub run ID。格式校验和轮换限制见[打包配置](../../packaging/README.zh.md#configuration-and-local-builds)。
|
||||
|
||||
打包只保留文件。成功后,在 `master` 上打开 **Publish Desktop · COS → Run workflow**,填写打包运行 URL 中的 `package_run_id`,并可填写 `package_run_attempt`。留空选择最新尝试,且该尝试必须成功,不会回退到较早的成功尝试;显式填写则选择对应的成功构建。Windows 必须完成签名,macOS 必须完成公证和 stapling。GitHub job 证据确定原 runner,本机清单在审批前提供 preset、版本、源码和通道。不会下载安装包或请求 artifact。旧清单的 `publishRequested=false` 不影响发布资格;本机文件缺失或变化会停止发布。
|
||||
@@ -47,7 +49,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
解决故障后重跑失败的发布 job,以保持准备阶段选定的打包尝试;也可显式填写原打包运行和尝试,发起新的发布。尝试留空时重跑所有 job 会重新解析最新打包尝试,并需要重新审批。不要仅为重试上传而重新打包。缺失对象会上传,相同对象跳过,同名但字节不同的对象会停止操作。写入结果不明时通过读取远端字节核对。频道已有更高版本时,旧上传不能替换它。每个包只更新自身内置通道:nightly 写入 `nightly.yml` / `nightly-mac.yml`,latest 写入 `latest.yml` / `latest-mac.yml`。latest 二进制位于目标二进制前缀下的 `latest/` 子目录,与已有 nightly 对象分离。同一源码和版本发布到两个通道需要分别打包;已有发布 tag 仅在源码提交相同时复用。
|
||||
|
||||
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断,不保存原始服务响应。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
|
||||
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断;COS 失败还标明 `GetObject` 或 `PutObject`,并在可用时记录经过校验的 HTTP 状态码和 request ID。访问拒绝提示会指出需检查的权限。不保存原始服务响应或凭据。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
|
||||
|
||||
频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。
|
||||
|
||||
|
||||
@@ -5,13 +5,40 @@ import { DesktopPublishError } from './desktop-publish.ts'
|
||||
const publicCodes = new Set(['AccessDenied', 'InternalError', 'NoSuchBucket', 'BadDigest', 'SignatureDoesNotMatch',
|
||||
'RequestTimeout', 'TimeoutError', 'AbortError', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ENOSPC', 'EACCES', 'EPERM', 'ENOENT'])
|
||||
|
||||
/** COS operation failure retaining its cause for safe diagnostics; a failed write may have an unknown remote outcome. */
|
||||
export class DesktopCosOperationError extends Error {
|
||||
constructor(readonly operation: 'GetObject' | 'PutObject', cause: unknown) {
|
||||
const failure = desktopPublicationFailure(cause, operation)
|
||||
const message = failure.code === 'AccessDenied'
|
||||
? `AccessDenied. Check name/cos:${operation} permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.`
|
||||
: failure.message
|
||||
super(`COS ${operation}: ${message}`, { cause })
|
||||
this.name = 'DesktopCosOperationError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep actionable local validation details without recording SDK response bodies or credentials.
|
||||
* @param error Failure from validation, filesystem, subprocess or remote transport.
|
||||
* @param phase Operation stage known by the entrypoint.
|
||||
* @returns Credential-free diagnostic suitable for stderr and an audit event.
|
||||
*/
|
||||
export function desktopPublicationFailure(error: unknown, phase: string): { phase: string; code: string; message: string } {
|
||||
export function desktopPublicationFailure(error: unknown, phase: string): {
|
||||
phase: string
|
||||
code: string
|
||||
message: string
|
||||
operation?: 'GetObject' | 'PutObject'
|
||||
requestId?: string
|
||||
httpStatus?: number
|
||||
} {
|
||||
if (error instanceof DesktopCosOperationError) {
|
||||
const cause = typeof error.cause === 'object' && error.cause !== null ? error.cause : {}
|
||||
const requestId = 'RequestId' in cause ? cause.RequestId : undefined
|
||||
const status = 'statusCode' in cause ? cause.statusCode : undefined
|
||||
return { ...desktopPublicationFailure(error.cause, phase), operation: error.operation, message: error.message,
|
||||
...(typeof requestId === 'string' && /^[\w+/=.-]{1,256}$/u.test(requestId) ? { requestId } : {}),
|
||||
...(typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599 ? { httpStatus: status } : {}) }
|
||||
}
|
||||
if (error instanceof DesktopPublishError) return { phase, code: error.code, message: error.message }
|
||||
if (error instanceof ZodError) return { phase, code: 'invalid-data',
|
||||
message: `Invalid fields: ${error.issues.map(issue => `${issue.path.join('.') || '(root)'} (${issue.code})`).join(', ')}` }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createDesktopCos, DESKTOP_COS_REGION, type DesktopCosCredentials } from
|
||||
import { cosOperation } from './cos-operation.ts'
|
||||
import type { DesktopUploadPlan } from './desktop-upload-plan.ts'
|
||||
import { desktopArtifactDigest, type DesktopPublishTransport } from './desktop-publish.ts'
|
||||
import { DesktopCosOperationError } from './desktop-publication-failure.ts'
|
||||
|
||||
function missing(error: unknown): boolean {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'NoSuchKey'
|
||||
@@ -14,6 +15,7 @@ function missing(error: unknown): boolean {
|
||||
|
||||
/**
|
||||
* Create a bounded COS transport restricted to the validated plan's object keys.
|
||||
* Administrators must keep bucket versioning absent; publication does not query bucket configuration.
|
||||
* @param plan Destination and exact object inventory; arbitrary keys are rejected.
|
||||
* @param credentials Credentials for this deployment only.
|
||||
* @param createClient Client factory; absent uses the shared production client, tests supply a loopback client.
|
||||
@@ -51,7 +53,7 @@ export function desktopPublishCos(
|
||||
return { size, sha512: sha512.digest('base64'), ...(channel ? { contents: Buffer.concat(chunks).toString('utf8') } : {}) }
|
||||
} catch (error) {
|
||||
if (missing(error)) return null
|
||||
throw error
|
||||
throw new DesktopCosOperationError('GetObject', error)
|
||||
} finally { output.destroy() }
|
||||
},
|
||||
async put(artifact, digest, forbidOverwrite) {
|
||||
@@ -62,13 +64,6 @@ export function desktopPublishCos(
|
||||
const actual = await desktopArtifactDigest(artifact)
|
||||
if (actual.size !== digest.size || actual.sha512 !== digest.sha512) throw new Error('desktop publish: local input changed')
|
||||
const cos = client()
|
||||
// COS overwrite prevention requires versioning to be absent, not enabled or suspended.
|
||||
if (forbidOverwrite) {
|
||||
const state = await cosOperation(cos, 30_000, () => cos.getBucketVersioning({ Bucket: plan.bucket, Region: DESKTOP_COS_REGION }))
|
||||
if (state.statusCode !== 200 || state.VersioningConfiguration.Status !== undefined) {
|
||||
throw new Error('desktop publish: immutable objects require bucket versioning to be disabled')
|
||||
}
|
||||
}
|
||||
const md5 = createHash('md5')
|
||||
const bytes = artifact.contents === undefined ? createReadStream(artifact.path) : [Buffer.from(artifact.contents)]
|
||||
for await (const chunk of bytes) md5.update(chunk)
|
||||
@@ -78,6 +73,8 @@ export function desktopPublishCos(
|
||||
...address, Body: body, ContentLength: digest.size, ContentType: artifact.contentType,
|
||||
Headers: { 'Content-MD5': md5.digest('base64'), ...(forbidOverwrite ? { 'x-cos-forbid-overwrite': 'true' } : {}) },
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new DesktopCosOperationError('PutObject', error)
|
||||
} finally { body.destroy() }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Explain why retained debugging packages cannot enter either publication environment. */
|
||||
/**
|
||||
* Return rebuilding instructions for debugging packages.
|
||||
* @param qualification Recorded package qualification.
|
||||
* @returns Diagnostic, or undefined for a non-debugging qualification.
|
||||
*/
|
||||
export function desktopPublishQualificationError(qualification: string): string | undefined
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Explain why retained debugging packages cannot enter either publication environment. */
|
||||
|
||||
/**
|
||||
* Return rebuilding instructions for debugging packages.
|
||||
* @param {string} qualification Recorded package qualification.
|
||||
* @returns {string | undefined} Diagnostic, or undefined for a non-debugging qualification.
|
||||
*/
|
||||
export function desktopPublishQualificationError(qualification) {
|
||||
const remedy = qualification === 'unsigned'
|
||||
? '该包未签名(unsigned)。请重新运行 Windows 打包并选择 signing=signed。'
|
||||
: qualification === 'unnotarized'
|
||||
? '该包跳过了公证(unnotarized)。请重新运行 macOS 打包,取消勾选“跳过公证”,完成公证和 stapling。'
|
||||
: undefined
|
||||
return remedy && `desktop publish: ${remedy}当前产物仅限内部调试,不能发布到 test 或 production,也不能用于自动更新。打包成功后使用新的 run ID / attempt 发布;重跑当前发布任务无效。`
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Validate a completed packaging attempt and route publication back to its original runner. */
|
||||
import { z } from 'zod'
|
||||
import { desktopPublishQualificationError } from './desktop-publish-qualification.mjs'
|
||||
|
||||
const positive = z.number().int().positive()
|
||||
const runSchema = z.object({
|
||||
@@ -12,7 +13,7 @@ const requestSchema = z.object({ source: z.object({
|
||||
workflowCommit: z.string(), sourceRef: z.string().min(1),
|
||||
channel: z.enum(['nightly', 'latest']),
|
||||
runnerName: z.string(), target: z.enum(['mac-arm64', 'win-x64']), preset: z.enum(['test', 'production']),
|
||||
qualification: z.enum(['signed', 'notarized']),
|
||||
qualification: z.enum(['signed', 'notarized', 'unsigned', 'unnotarized']),
|
||||
}).refine(source => source.preset === 'test' || /^(?:master|dsh-v[0-9][0-9A-Za-z.-]*)$/u.test(source.sourceRef),
|
||||
'Production source must be master or dsh-v*') })
|
||||
const jobsSchema = z.array(z.object({ name: z.string(), runner_name: z.string().nullable(), conclusion: z.string().nullable() }))
|
||||
@@ -45,6 +46,8 @@ export function resolveDesktopPublishRoute(
|
||||
): DesktopPublishRoute {
|
||||
const run = runSchema.parse(runValue)
|
||||
const { source } = requestSchema.parse(requestValue)
|
||||
const diagnostic = desktopPublishQualificationError(source.qualification)
|
||||
if (diagnostic) throw new Error(diagnostic)
|
||||
const jobs = jobsSchema.parse(jobsValue)
|
||||
const routes = routesSchema.parse(routesValue)
|
||||
const workflow = source.target === 'win-x64' ? '.github/workflows/windows-package.yml' : '.github/workflows/macos-package.yml'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Reject identifiable debugging artifacts before installing publication dependencies. */
|
||||
/**
|
||||
* Pin the requested attempt and reject its explicitly named debugging artifacts.
|
||||
* @param repository GitHub repository.
|
||||
* @param inputs Dispatch inputs.
|
||||
* @param api Parsed GitHub JSON.
|
||||
* @returns Exact attempt for subsequent routing; local verification remains required.
|
||||
*/
|
||||
export function preflightDesktopPublish(repository: string, inputs: { package_run_id: string; package_run_attempt?: string },
|
||||
api: (path: string, paginate?: boolean) => unknown): string
|
||||
@@ -0,0 +1,47 @@
|
||||
/** Reject identifiable debugging artifacts before installing publication dependencies. */
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { appendFile, readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { desktopPublishQualificationError } from './desktop-publish-qualification.mjs'
|
||||
|
||||
/**
|
||||
* Pin the requested attempt and reject its explicitly named debugging artifacts.
|
||||
* Artifact names only reject; local inventory and signature verification still authorize publication.
|
||||
* @param {string} repository GitHub repository.
|
||||
* @param {{ package_run_id: string, package_run_attempt?: string }} inputs Dispatch inputs.
|
||||
* @param {(path: string, paginate?: boolean) => unknown} api Parsed GitHub JSON; validated here without dependencies.
|
||||
* @returns {string} Exact attempt for subsequent routing.
|
||||
*/
|
||||
export function preflightDesktopPublish(repository, inputs, api) {
|
||||
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository) || !/^[1-9]\d*$/u.test(inputs.package_run_id)
|
||||
|| (inputs.package_run_attempt && !/^[1-9]\d*$/u.test(inputs.package_run_attempt))) {
|
||||
throw new Error('desktop publish: repository, package_run_id and package_run_attempt must identify a valid packaging attempt')
|
||||
}
|
||||
const base = `repos/${repository}/actions/runs/${inputs.package_run_id}`
|
||||
const latest = inputs.package_run_attempt ? undefined : api(base)
|
||||
const attempt = inputs.package_run_attempt || String(latest && typeof latest === 'object' && 'run_attempt' in latest ? latest.run_attempt : '')
|
||||
if (!/^[1-9]\d*$/u.test(attempt)) throw new Error('desktop publish: GitHub returned an invalid attempt')
|
||||
const pages = api(`${base}/artifacts?per_page=100`, true)
|
||||
if (!Array.isArray(pages) || pages.some(page => !Array.isArray(page.artifacts)
|
||||
|| page.artifacts.some(artifact => typeof artifact.name !== 'string'))) {
|
||||
throw new Error('desktop publish: GitHub returned invalid artifact metadata')
|
||||
}
|
||||
for (const { name } of pages.flatMap(page => page.artifacts)) {
|
||||
if (!name.endsWith(`-${inputs.package_run_id}-${attempt}`)) continue
|
||||
const qualification = name.startsWith('windows-x64-unsigned-') ? 'unsigned'
|
||||
: name.startsWith('macos-arm64-unnotarized-debug-') ? 'unnotarized' : ''
|
||||
const diagnostic = desktopPublishQualificationError(qualification)
|
||||
if (diagnostic) throw new Error(diagnostic)
|
||||
}
|
||||
return attempt
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) {
|
||||
const env = process.env
|
||||
if (!env.GITHUB_EVENT_PATH || !env.GITHUB_REPOSITORY || !env.GITHUB_OUTPUT) throw new Error('desktop publish: Actions context required')
|
||||
const event = JSON.parse(await readFile(env.GITHUB_EVENT_PATH, 'utf8'))
|
||||
const attempt = preflightDesktopPublish(env.GITHUB_REPOSITORY, event.inputs, (path, paginate) =>
|
||||
JSON.parse(execFileSync('gh', ['api', ...(paginate ? ['--paginate', '--slurp'] : []), path],
|
||||
{ encoding: 'utf8', timeout: 60_000, maxBuffer: 8 * 1024 * 1024 })))
|
||||
await appendFile(env.GITHUB_OUTPUT, `attempt=${attempt}\n`)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { publishDesktopRelease } from './desktop-publish.ts'
|
||||
import { desktopPublicationFailure } from './desktop-publication-failure.ts'
|
||||
import { desktopPublicationSummary } from './desktop-publication-summary.ts'
|
||||
import { publishDesktopTag } from './desktop-publish-tag.ts'
|
||||
import { desktopPublishQualificationError } from './desktop-publish-qualification.mjs'
|
||||
|
||||
const routeSchema = z.object({ runId: z.string(), attempt: z.string(), commit: z.string(), runnerName: z.string(),
|
||||
sourceRef: z.string(), channel: z.enum(['nightly', 'latest']),
|
||||
@@ -36,7 +37,9 @@ export async function prepareDesktopPublication(env: NodeJS.ProcessEnv, verifySi
|
||||
|| source.preset !== route.preset) {
|
||||
throw new Error('desktop publish: local package differs from the routed request')
|
||||
}
|
||||
if (source.qualification !== (source.target === 'win-x64' ? 'signed' : 'notarized')) throw new Error('desktop publish: debugging artifacts cannot be published')
|
||||
const diagnostic = desktopPublishQualificationError(source.qualification)
|
||||
if (diagnostic) throw new Error(diagnostic)
|
||||
if (source.qualification !== (source.target === 'win-x64' ? 'signed' : 'notarized')) throw new Error('desktop publish: package qualification does not match its platform')
|
||||
const config = await readDesktopPublishConfig(env.DSH_PACKAGING_CONFIG_DIR!, source.preset)
|
||||
const directory = retainedDesktopPath(root, route.runId, route.attempt)
|
||||
const environment = source.preset === 'test'
|
||||
|
||||
@@ -62,6 +62,7 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.fi
|
||||
const env = process.env
|
||||
if (!env.GITHUB_EVENT_PATH || !env.GITHUB_REPOSITORY || !env.GITHUB_OUTPUT) throw new Error('desktop publish: Actions context required')
|
||||
const event = z.object({ inputs: inputsSchema }).parse(JSON.parse(await readFile(env.GITHUB_EVENT_PATH, 'utf8')))
|
||||
const selected = selectDesktopPackage(env.GITHUB_REPOSITORY, event.inputs, JSON.parse(env.DESKTOP_PUBLISH_RUNNERS || '{}'), desktopPublishApi)
|
||||
const inputs = { ...event.inputs, package_run_attempt: env.DESKTOP_PACKAGE_ATTEMPT || event.inputs.package_run_attempt }
|
||||
const selected = selectDesktopPackage(env.GITHUB_REPOSITORY, inputs, JSON.parse(env.DESKTOP_PUBLISH_RUNNERS || '{}'), desktopPublishApi)
|
||||
await appendFile(env.GITHUB_OUTPUT, `selection=${JSON.stringify(selected)}\nrunner=${selected.runnerLabel}\n`)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { desktopBuildRecordFilename, desktopUpdateMetadataFilename, resolveDeskt
|
||||
import { loadDesktopPackageEnvironment } from './desktop-package-environment.mjs'
|
||||
import { retainDesktopRelease, type DesktopRetainedSource } from './desktop-release-store.ts'
|
||||
import { validateDesktopBuildVersion } from './desktop-build-version.mjs'
|
||||
import { desktopPublishQualificationError } from './desktop-publish-qualification.mjs'
|
||||
|
||||
function required(env: NodeJS.ProcessEnv, name: string): string {
|
||||
const value = env[name]
|
||||
@@ -66,8 +67,10 @@ export async function retainDesktopCi(env: NodeJS.ProcessEnv, appRoot = resolve(
|
||||
}
|
||||
const retained = await retainDesktopRelease(desktopReleasesRoot(env), source, names.map(name => join(artifacts, name)))
|
||||
if (env.GITHUB_OUTPUT) await appendFile(env.GITHUB_OUTPUT, `directory=${retained}\n`)
|
||||
const nextStep = desktopPublishQualificationError(qualification)
|
||||
?? `Publish separately with package_run_id=${env.GITHUB_RUN_ID} and package_run_attempt=${env.GITHUB_RUN_ATTEMPT}`
|
||||
if (env.GITHUB_STEP_SUMMARY) await appendFile(env.GITHUB_STEP_SUMMARY,
|
||||
`\nRetained package: ${env.GITHUB_RUN_ID}/${env.GITHUB_RUN_ATTEMPT}\n\nPublish separately with package_run_id=${env.GITHUB_RUN_ID} and package_run_attempt=${env.GITHUB_RUN_ATTEMPT}\n`)
|
||||
`\nRetained package: ${env.GITHUB_RUN_ID}/${env.GITHUB_RUN_ATTEMPT}\n\n${nextStep}\n`)
|
||||
return retained
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Repeating Task Scheduler triggers recover an exited launcher without relying on action exit-code retries.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$runnerRoot = [IO.Path]::GetFullPath((Split-Path -Parent $PSScriptRoot)).TrimEnd('\','/')
|
||||
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($runnerRoot.ToUpperInvariant())))
|
||||
$taskName = 'DSH-Packaging-' + $digest.Substring(0,16)
|
||||
$log = Join-Path $PSScriptRoot ('run-' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ') + '-' + $PID + '.log')
|
||||
try {
|
||||
@{ Pid = $PID; Started = (Get-Process -Id $PID).StartTime.ToUniversalTime().Ticks } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $PSScriptRoot 'process.json')
|
||||
$config = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'config.json') -Raw | ConvertFrom-Json
|
||||
$attemptFile = Join-Path $PSScriptRoot 'attempts.txt'
|
||||
$attempts = [int](Get-Content -LiteralPath $attemptFile -Raw)
|
||||
if ($attempts -lt 0 -or $config.RestartCount -lt 1 -or $config.RestartCount -gt 999) { throw 'Invalid restart budget; reinstall the task.' }
|
||||
if ($attempts -gt $config.RestartCount) {
|
||||
$null = Disable-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
Set-Content -LiteralPath $log -Value 'Restart budget exhausted. Task disabled; inspect runner logs, fix the cause and run start.'
|
||||
exit 1
|
||||
}
|
||||
Set-Content -LiteralPath $attemptFile -Value ($attempts + 1)
|
||||
& (Join-Path $PSHOME 'pwsh.exe') -NoProfile -NonInteractive -File (Join-Path $PSScriptRoot 'start.ps1') -RunnerRoot $config.RunnerRoot -ConfigRoot $config.ConfigRoot *> $log
|
||||
$code = $LASTEXITCODE
|
||||
Add-Content -LiteralPath $log -Value "Runner launcher exited with code $code."
|
||||
# An unexpected exit is a task failure even when run.cmd hides the listener's nonzero status; the log retains its code.
|
||||
exit 1
|
||||
} catch {
|
||||
# A missing or unwritable counter cannot bound retries; the task name must not depend on readable configuration.
|
||||
$null = Disable-ScheduledTask -TaskName $taskName -TaskPath '\'
|
||||
$message = 'Runner task failed: ' + $_.Exception.Message
|
||||
[Console]::Error.WriteLine($message)
|
||||
Add-Content -LiteralPath $log -Value $message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
# Manage a registered packaging runner in the current user's interactive Windows session.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][ValidateSet('install','start','stop','status','uninstall')][string]$Action,
|
||||
[Parameter(Mandatory=$true)][string]$RunnerRoot,
|
||||
[string]$ConfigRoot,
|
||||
[ValidateRange(1,1440)][int]$RestartMinutes = 1,
|
||||
[ValidateRange(1,999)][int]$RestartCount = 10
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $IsWindows) { throw 'This command requires PowerShell 7 on Windows.' }
|
||||
if ($Action -ne 'install' -and @('ConfigRoot','RestartMinutes','RestartCount').Where({ $PSBoundParameters.ContainsKey($_) }).Count) {
|
||||
throw 'ConfigRoot, RestartMinutes and RestartCount apply only to install; reinstall to change them.'
|
||||
}
|
||||
if ($RunnerRoot -notmatch '^[A-Za-z]:[\\/]') { throw 'RunnerRoot must be an absolute local directory.' }
|
||||
$RunnerRoot = [IO.Path]::GetFullPath($RunnerRoot).TrimEnd('\','/')
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $RunnerRoot '.runner') -PathType Leaf)) { throw 'Register the runner before managing its task.' }
|
||||
for ($ancestor = [IO.DirectoryInfo]::new($RunnerRoot); $null -ne $ancestor; $ancestor = $ancestor.Parent) {
|
||||
if ($ancestor.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'RunnerRoot cannot contain linked directories.' }
|
||||
}
|
||||
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($RunnerRoot.ToUpperInvariant())))
|
||||
$taskName = 'DSH-Packaging-' + $digest.Substring(0,16)
|
||||
$serviceDir = Join-Path $RunnerRoot 'dsh-task'
|
||||
$task = Get-ScheduledTask -TaskPath '\' | Where-Object TaskName -eq $taskName
|
||||
if ($task -and $task.Description -ne "DSH packaging runner: $RunnerRoot; owner: $sid") { throw 'Task ownership differs; inspect it in Task Scheduler.' }
|
||||
|
||||
if ($Action -eq 'install') {
|
||||
if ($task) { throw 'Task already installed; stop and uninstall before reinstalling.' }
|
||||
if (-not $ConfigRoot -or $ConfigRoot -notmatch '^[A-Za-z]:[\\/]') { throw 'Install requires an absolute ConfigRoot.' }
|
||||
$ConfigRoot = [IO.Path]::GetFullPath($ConfigRoot)
|
||||
$pwsh = Join-Path $PSHOME 'pwsh.exe'
|
||||
& $pwsh -NoProfile -File (Join-Path $PSScriptRoot 'start-windows-packaging-runner.ps1') -RunnerRoot $RunnerRoot -ConfigRoot $ConfigRoot -CheckOnly
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Runner preflight failed; no task was installed.' }
|
||||
if (Test-Path -LiteralPath $serviceDir) {
|
||||
if ((Get-Item -LiteralPath $serviceDir).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Task directory cannot be a link.' }
|
||||
} else {
|
||||
$null = New-Item -ItemType Directory -Path $serviceDir
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'start-windows-packaging-runner.ps1') -Destination (Join-Path $serviceDir 'start.ps1')
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'run-windows-runner-task.ps1') -Destination (Join-Path $serviceDir 'run.ps1')
|
||||
@{ RunnerRoot = $RunnerRoot; ConfigRoot = $ConfigRoot; TaskName = $taskName; RestartCount = $RestartCount } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $serviceDir 'config.json') -Encoding utf8
|
||||
Set-Content -LiteralPath (Join-Path $serviceDir 'attempts.txt') -Value '0'
|
||||
$arguments = '-NoProfile -NonInteractive -WindowStyle Hidden -File "' + (Join-Path $serviceDir 'run.ps1') + '"'
|
||||
$execution = New-ScheduledTaskAction -Execute $pwsh -Argument $arguments -WorkingDirectory $RunnerRoot
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
|
||||
$trigger = @(
|
||||
(New-ScheduledTaskTrigger -AtLogOn -User $sid),
|
||||
(New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes($RestartMinutes) -RepetitionInterval (New-TimeSpan -Minutes $RestartMinutes))
|
||||
)
|
||||
$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
$settings.Enabled = $false
|
||||
$null = Register-ScheduledTask -TaskName $taskName -TaskPath '\' -Action $execution -Principal $principal -Trigger $trigger -Settings $settings -Description "DSH packaging runner: $RunnerRoot; owner: $sid"
|
||||
Write-Output "Installed disabled task $taskName. Run start while logged in; reboot requires desktop login. Logs: $serviceDir"
|
||||
exit 0
|
||||
}
|
||||
if (-not $task) { throw 'Task is not installed for this runner directory.' }
|
||||
switch ($Action) {
|
||||
'status' {
|
||||
$info = Get-ScheduledTaskInfo -InputObject $task
|
||||
$launches = [int](Get-Content -LiteralPath (Join-Path $serviceDir 'attempts.txt') -Raw)
|
||||
$config = Get-Content -LiteralPath (Join-Path $serviceDir 'config.json') -Raw | ConvertFrom-Json
|
||||
[pscustomobject]@{ TaskName = $taskName; State = $task.State; LastRunTime = $info.LastRunTime; LastTaskResult = $info.LastTaskResult; Launches = $launches; RemainingLaunches = [Math]::Max(0, 1 + $config.RestartCount - $launches); Logs = $serviceDir }
|
||||
}
|
||||
'start' {
|
||||
if ($task.State -eq 'Running') { Write-Output "$taskName is already running."; break }
|
||||
Set-Content -LiteralPath (Join-Path $serviceDir 'attempts.txt') -Value '0'
|
||||
$null = Enable-ScheduledTask -InputObject $task
|
||||
Start-ScheduledTask -InputObject $task
|
||||
Write-Output "Start requested for $taskName. Verify Connected to GitHub and Listening for Jobs in $serviceDir."
|
||||
}
|
||||
'stop' {
|
||||
# Operators pause workflow dispatch first; this local check cannot reserve an idle GitHub runner.
|
||||
$workers = Get-CimInstance Win32_Process -Filter "Name = 'Runner.Worker.exe'"
|
||||
foreach ($worker in $workers) {
|
||||
if (-not $worker.ExecutablePath -or $worker.ExecutablePath.StartsWith($RunnerRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'A runner job is active or its location is unreadable. Wait for completion before stopping.'
|
||||
}
|
||||
}
|
||||
$null = Disable-ScheduledTask -InputObject $task
|
||||
$task = Get-ScheduledTask -TaskName $taskName -TaskPath '\'
|
||||
if ($task.State -eq 'Running') {
|
||||
$recordFile = Join-Path $serviceDir 'process.json'
|
||||
if (-not (Test-Path -LiteralPath $recordFile)) { throw 'Running task process identity is unavailable. Automatic starts are disabled; inspect the task before stopping it.' }
|
||||
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
|
||||
$owner = Get-Process -Id $record.Pid -ErrorAction SilentlyContinue
|
||||
if (-not $owner -or $owner.StartTime.ToUniversalTime().Ticks -ne $record.Started) {
|
||||
throw 'Running task process identity is unavailable. Automatic starts are disabled; inspect the task before stopping it.'
|
||||
}
|
||||
# Parent IDs can be reused; this snapshot supplies wait handles, not termination authority.
|
||||
$processes = @(Get-CimInstance Win32_Process)
|
||||
$ownedIds = [Collections.Generic.HashSet[int]]::new()
|
||||
$null = $ownedIds.Add($owner.Id)
|
||||
do {
|
||||
$added = $false
|
||||
foreach ($process in $processes) {
|
||||
if ($ownedIds.Contains([int]$process.ParentProcessId) -and $ownedIds.Add([int]$process.ProcessId)) { $added = $true }
|
||||
}
|
||||
} while ($added)
|
||||
$owned = @(foreach ($ownedId in $ownedIds) { Get-Process -Id $ownedId -ErrorAction SilentlyContinue })
|
||||
& "$env:SystemRoot/System32/taskkill.exe" /PID $owner.Id /T /F
|
||||
if ($LASTEXITCODE -ne 0 -and -not $owner.HasExited) { throw 'Runner task process tree could not be stopped; inspect it before restarting.' }
|
||||
foreach ($process in $owned) {
|
||||
if (-not $process.WaitForExit(30000)) { throw 'Runner task child remains alive; inspect it before restarting.' }
|
||||
}
|
||||
}
|
||||
Stop-ScheduledTask -InputObject $task
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
||||
do {
|
||||
$state = (Get-ScheduledTask -TaskName $taskName -TaskPath '\').State
|
||||
if ($state -ne 'Running') { break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
if ($state -eq 'Running') { throw 'Task remains running; inspect its processes before further maintenance.' }
|
||||
Write-Output "Stopped and disabled $taskName. Run start to enable automatic starts again."
|
||||
}
|
||||
'uninstall' {
|
||||
if ($task.State -ne 'Disabled') { throw 'Stop the task before uninstalling.' }
|
||||
Unregister-ScheduledTask -InputObject $task -Confirm:$false
|
||||
Write-Output "Uninstalled $taskName. Registration, configuration and logs remain in $RunnerRoot."
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import { desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
import { DesktopCosOperationError, desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
import { DesktopPublishError } from '../scripts/desktop-publish.ts'
|
||||
|
||||
it('preserves actionable validation and filesystem diagnostics', () => {
|
||||
@@ -34,3 +34,13 @@ it('prints rollback usage through the real CLI before any audit or remote operat
|
||||
expect(diagnostic.code).toBe('validation')
|
||||
expect(diagnostic.message).toContain('Usage: rollback-desktop-channel.ts')
|
||||
})
|
||||
|
||||
it('omits malformed provider receipt fields and never copies raw errors into COS diagnostics', () => {
|
||||
const error = new DesktopCosOperationError('PutObject', { code: 'AccessDenied', statusCode: 'fixture-secret',
|
||||
RequestId: 'Authorization: fixture-secret', body: 'fixture-secret', message: 'fixture-secret' })
|
||||
const failure = desktopPublicationFailure(error, 'publication')
|
||||
expect(failure).toMatchObject({ code: 'AccessDenied', operation: 'PutObject' })
|
||||
expect(failure).not.toHaveProperty('requestId')
|
||||
expect(failure).not.toHaveProperty('httpStatus')
|
||||
expect(JSON.stringify(failure)).not.toContain('fixture-secret')
|
||||
})
|
||||
|
||||
@@ -4,11 +4,12 @@ import { desktopPublishCos } from '../scripts/desktop-publish-cos.ts'
|
||||
import { desktopArtifactDigest } from '../scripts/desktop-publish.ts'
|
||||
import type { DesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
|
||||
import { answer, cosError, startCosLoopback } from './cos-loopback.ts'
|
||||
import { desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
|
||||
it('uses overwrite prevention, streams verified bytes, and hashes remote readback through the SDK', async () => {
|
||||
const objects = new Map<string, Buffer>()
|
||||
const server = await startCosLoopback((request, response) => {
|
||||
if (request.path.includes('versioning')) { answer(response, 200, '<VersioningConfiguration/>'); return }
|
||||
if (request.path.includes('versioning')) { answer(response, 403, cosError('AccessDenied')); return }
|
||||
if (request.method === 'PUT') {
|
||||
expect(request.headers['x-cos-forbid-overwrite']).toBe('true')
|
||||
objects.set(request.path, request.body)
|
||||
@@ -36,6 +37,8 @@ it('uses overwrite prevention, streams verified bytes, and hashes remote readbac
|
||||
expect(await transport.read(artifact.key, false)).toEqual(digest)
|
||||
await expect(transport.read('elsewhere/file.exe', false)).rejects.toThrow(/outside/u)
|
||||
expect(server.requests.filter(request => request.method === 'PUT')).toHaveLength(1)
|
||||
expect(server.requests.map(request => request.method)).toEqual(['GET', 'PUT', 'GET'])
|
||||
expect(server.requests.every(request => !request.path.includes('versioning'))).toBe(true)
|
||||
} finally { await server.close() }
|
||||
})
|
||||
|
||||
@@ -54,3 +57,27 @@ it('propagates an oversized channel stream error through the SDK and closes the
|
||||
expect(server.requests).toHaveLength(1)
|
||||
} finally { await server.close() }
|
||||
})
|
||||
|
||||
it.each(['GetObject', 'PutObject'] as const)('reports %s denial with a safe COS receipt', async (operation) => {
|
||||
const server = await startCosLoopback((_request, response) => {
|
||||
answer(response, 403, cosError('AccessDenied'), { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
|
||||
})
|
||||
try {
|
||||
const artifact = { path: '', filename: 'file.exe', key: 'bin/file.exe', contents: 'fixture bytes', contentType: 'application/octet-stream', channelMetadata: false }
|
||||
const plan: DesktopUploadPlan = { environment: 'test', target: 'win-x64', version: '1.0.0', bucket: 'fixture-1250000000',
|
||||
publicUrl: 'https://example.com/', secretIdEnvName: 'ID', secretKeyEnvName: 'KEY', artifacts: [artifact] }
|
||||
const transport = desktopPublishCos(plan, { secretId: 'fixture-id', secretKey: 'fixture-secret' }, (credentials) => {
|
||||
const client = createDesktopCos(credentials)
|
||||
server.redirect(client)
|
||||
return client
|
||||
})
|
||||
const digest = await desktopArtifactDigest(artifact)
|
||||
const pending = operation === 'GetObject' ? transport.read(artifact.key, false) : transport.put(artifact, digest, true)
|
||||
const failure = await pending.then(() => { throw new Error('Expected COS denial') }, (error: unknown) => desktopPublicationFailure(error, 'publication'))
|
||||
expect(failure).toMatchObject({ operation, code: 'AccessDenied', httpStatus: 403, requestId: 'fixture-request' })
|
||||
expect(JSON.stringify(failure)).not.toContain('fixture-secret')
|
||||
await expect(JSON.stringify(failure, null, 2) + '\n').toMatchFileSnapshot(`./expected/cos-${operation}-denied.json`)
|
||||
expect(server.requests).toHaveLength(1)
|
||||
expect(server.requests[0]!.method).toBe(operation === 'GetObject' ? 'GET' : 'PUT')
|
||||
} finally { await server.close() }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { preflightDesktopPublish } from '../scripts/preflight-desktop-publish.mjs'
|
||||
|
||||
it.each([
|
||||
['macos-arm64-unnotarized-debug-1.0.0-42-2', '跳过了公证'],
|
||||
['windows-x64-unsigned-1.0.0-42-2', '未签名'],
|
||||
])('rejects %s from paginated artifact metadata with rebuilding instructions', (name, reason) => {
|
||||
const paths: string[] = []
|
||||
expect(() => preflightDesktopPublish('owner/repo', { package_run_id: '42' }, (path, paginate) => {
|
||||
paths.push(path)
|
||||
if (!paginate) return { run_attempt: 2 }
|
||||
return [{ artifacts: [] }, { artifacts: [{ name, expired: true }] }]
|
||||
})).toThrow(reason)
|
||||
expect(paths).toEqual(['repos/owner/repo/actions/runs/42', 'repos/owner/repo/actions/runs/42/artifacts?per_page=100'])
|
||||
})
|
||||
|
||||
it.each([{ artifacts: [] }, { artifacts: [{ name: 'windows-x64-signed-1.0.0-42-2' }] }, { artifacts: [{ name: 'windows-x64-unsigned-1.0.0-42-1' }] }])(
|
||||
'leaves missing metadata and other attempts to authoritative local verification: %j', ({ artifacts }) => {
|
||||
expect(preflightDesktopPublish('owner/repo', { package_run_id: '42', package_run_attempt: '2' }, (path, paginate) => {
|
||||
expect(paginate).toBe(true)
|
||||
expect(path).toContain('/42/artifacts?')
|
||||
return [{ artifacts }]
|
||||
})).toBe('2')
|
||||
},
|
||||
)
|
||||
|
||||
it('rejects malformed input before contacting GitHub', () => {
|
||||
expect(() => preflightDesktopPublish('owner/repo', { package_run_id: '../42' }, () => {
|
||||
throw new Error('API must not be called')
|
||||
})).toThrow(/valid packaging attempt/u)
|
||||
})
|
||||
|
||||
it.each([{ response: {} }, { response: [{ artifacts: [{}] }] }])('rejects invalid artifact responses: %j', ({ response }) => {
|
||||
expect(() => preflightDesktopPublish('owner/repo', { package_run_id: '42', package_run_attempt: '2' }, () => response))
|
||||
.toThrow(/invalid artifact metadata/u)
|
||||
})
|
||||
@@ -18,6 +18,16 @@ it('routes the exact successful attempt to its registered machine', () => {
|
||||
.toMatchObject({ runId: '42', attempt: '2', runnerLabel: 'dsh-mini', preset: 'test' })
|
||||
})
|
||||
|
||||
it.each(['test', 'production'])('explains how to rebuild debugging packages for %s', (preset) => {
|
||||
for (const [qualification, remedy] of [['unsigned', 'signing=signed'], ['unnotarized', '取消勾选“跳过公证”']]) {
|
||||
const f = fixture()
|
||||
f.source.preset = preset
|
||||
f.source.qualification = qualification!
|
||||
expect(() => resolveDesktopPublishRoute('owner/repo', f.run, { source: f.source }, f.jobs, f.routes)).toThrow(remedy)
|
||||
expect(() => resolveDesktopPublishRoute('owner/repo', f.run, { source: f.source }, f.jobs, f.routes)).toThrow('重跑当前发布任务无效')
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['attempt', 'repository', 'commit', 'mode', 'runner', 'workflow'] as const)('rejects mismatched %s evidence', (field) => {
|
||||
const f = fixture()
|
||||
if (field === 'attempt') f.source.attempt = '1'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"phase": "publication",
|
||||
"code": "AccessDenied",
|
||||
"message": "COS GetObject: AccessDenied. Check name/cos:GetObject permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.",
|
||||
"operation": "GetObject",
|
||||
"requestId": "fixture-request",
|
||||
"httpStatus": 403
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"phase": "publication",
|
||||
"code": "AccessDenied",
|
||||
"message": "COS PutObject: AccessDenied. Check name/cos:PutObject permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.",
|
||||
"operation": "PutObject",
|
||||
"requestId": "fixture-request",
|
||||
"httpStatus": 403
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
Retained package: 123/2
|
||||
|
||||
desktop publish: 该包跳过了公证(unnotarized)。请重新运行 macOS 打包,取消勾选“跳过公证”,完成公证和 stapling。当前产物仅限内部调试,不能发布到 test 或 production,也不能用于自动更新。打包成功后使用新的 run ID / attempt 发布;重跑当前发布任务无效。
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
Retained package: 123/2
|
||||
|
||||
desktop publish: 该包未签名(unsigned)。请重新运行 Windows 打包并选择 signing=signed。当前产物仅限内部调试,不能发布到 test 或 production,也不能用于自动更新。打包成功后使用新的 run ID / attempt 发布;重跑当前发布任务无效。
|
||||
@@ -1,36 +1,51 @@
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { desktopReleasesRoot, retainDesktopCi } from '../scripts/retain-desktop-ci.ts'
|
||||
import { verifyRetainedDesktopRelease } from '../scripts/desktop-release-store.ts'
|
||||
import { prepareDesktopPublication } from '../scripts/publish-desktop-ci.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) })
|
||||
|
||||
it.each(['mac-arm64', 'win-x64'] as const)('retains the complete %s release without signing or upload credentials', async (target) => {
|
||||
it.each([
|
||||
['mac-arm64', 'notarized'], ['win-x64', 'signed'], ['mac-arm64', 'unnotarized'], ['win-x64', 'unsigned'],
|
||||
] as const)('retains the complete %s %s release without signing or upload credentials', async (target, qualification) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'retain-ci-'))
|
||||
roots.push(root)
|
||||
const appRoot = join(root, 'checkout')
|
||||
const config = join(root, 'packaging')
|
||||
const artifacts = join(appRoot, '.desktop-build', 'targets', target, 'artifacts')
|
||||
const debug = qualification === 'unsigned' || qualification === 'unnotarized'
|
||||
const artifacts = join(appRoot, '.desktop-build', 'targets', target, debug ? `${qualification}-artifacts` : 'artifacts')
|
||||
await mkdir(artifacts, { recursive: true })
|
||||
await mkdir(join(config, 'presets'), { recursive: true })
|
||||
await writeFile(join(appRoot, 'package.json'), '{"version":"1.0.0"}')
|
||||
await writeFile(join(config, 'presets/test.env'), `DSH_DESKTOP_AUTO_UPDATE_ENV=test\nDOWNLOAD_TEST_RELEASE_ID=${'a'.repeat(32)}\n`)
|
||||
const names = target === 'win-x64'
|
||||
const names = debug ? (target === 'win-x64' ? [`deepseek-harness-1.0.0-win-x64-${qualification}.exe`]
|
||||
: [`deepseek-harness-1.0.0-mac-arm64-${qualification}.dmg`, `deepseek-harness-1.0.0-mac-arm64-${qualification}.zip`]) : target === 'win-x64'
|
||||
? ['deepseek-harness-1.0.0-win-x64.exe', 'deepseek-harness-1.0.0-win-x64.exe.blockmap', 'win-x64-release.json', 'nightly.yml']
|
||||
: ['deepseek-harness-1.0.0-mac-arm64.dmg', 'deepseek-harness-1.0.0-mac-arm64.zip', 'deepseek-harness-1.0.0-mac-arm64.zip.blockmap', 'mac-arm64-release.json', 'nightly-mac.yml']
|
||||
await Promise.all(names.map(name => writeFile(join(artifacts, name), 'fixture')))
|
||||
const env = {
|
||||
DSH_PACKAGING_CONFIG_DIR: config, DSH_PACKAGING_PRESET: 'test', DSH_DESKTOP_TARGET_PLATFORM: target === 'win-x64' ? 'win32' : 'darwin',
|
||||
DSH_DESKTOP_TARGET_ARCH: target === 'win-x64' ? 'x64' : 'arm64', PACKAGE_SOURCE_REF: 'master', PACKAGE_SOURCE_SHA: 'b'.repeat(40), PACKAGE_VERSION: '1.0.0',
|
||||
PACKAGE_SIGNING: 'signed', PACKAGE_SKIP_NOTARIZATION: 'false', GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '2',
|
||||
PACKAGE_SIGNING: qualification === 'unsigned' ? 'unsigned' : 'signed', PACKAGE_SKIP_NOTARIZATION: qualification === 'unnotarized' ? 'true' : 'false', GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '2',
|
||||
GITHUB_STEP_SUMMARY: join(root, 'summary.md'),
|
||||
GITHUB_REPOSITORY: 'owner/repository', GITHUB_SHA: 'b'.repeat(40), RUNNER_NAME: 'dedicated',
|
||||
}
|
||||
await retainDesktopCi(env, appRoot)
|
||||
const manifest = await verifyRetainedDesktopRelease(desktopReleasesRoot(env), '123', '2')
|
||||
expect(manifest.files.map(file => file.name)).toEqual(names)
|
||||
expect(manifest.source).toMatchObject({ target, qualification: target === 'win-x64' ? 'signed' : 'notarized' })
|
||||
expect(manifest.source).toMatchObject({ target, qualification })
|
||||
expect(manifest.source.publishRequested).toBeUndefined()
|
||||
const summary = await readFile(env.GITHUB_STEP_SUMMARY, 'utf8')
|
||||
if (debug) {
|
||||
expect(summary).toContain('不能发布到 test 或 production')
|
||||
expect(summary).not.toContain('Publish separately')
|
||||
await expect(prepareDesktopPublication({ ...env, DESKTOP_PUBLISH_ROUTE: JSON.stringify({
|
||||
runId: '123', attempt: '2', commit: env.GITHUB_SHA, runnerName: env.RUNNER_NAME, target, preset: 'test', sourceRef: 'master', channel: 'nightly',
|
||||
}) })).rejects.toThrow('重跑当前发布任务无效')
|
||||
await expect(summary).toMatchFileSnapshot(`./expected/retained-${qualification}.md`)
|
||||
} else expect(summary).toContain('Publish separately with package_run_id=123 and package_run_attempt=2')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Opt-in Windows Task Scheduler acceptance: isolated fake runner, no GitHub registration or signing access.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $IsWindows) { throw 'Requires PowerShell 7 on Windows.' }
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('dsh-runner-task-' + [guid]::NewGuid().ToString('N'))
|
||||
$null = New-Item -ItemType Directory -Path $root
|
||||
$source = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../scripts'))
|
||||
$runner = Join-Path $root 'runner with spaces'
|
||||
$null = New-Item -ItemType Directory -Path $runner
|
||||
Set-Content -LiteralPath (Join-Path $runner '.runner') -Value '{}'
|
||||
foreach ($file in @('windows-runner-task.ps1','run-windows-runner-task.ps1')) { Copy-Item -LiteralPath (Join-Path $source $file) -Destination $root }
|
||||
$manager = Join-Path $root 'windows-runner-task.ps1'
|
||||
@'
|
||||
param([string]$RunnerRoot, [string]$ConfigRoot, [switch]$CheckOnly)
|
||||
if ($CheckOnly) { exit 0 }
|
||||
Add-Content -LiteralPath (Join-Path $RunnerRoot 'starts.txt') -Value $PID
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $RunnerRoot 'failed-once'))) {
|
||||
Set-Content -LiteralPath (Join-Path $RunnerRoot 'failed-once') -Value 'yes'
|
||||
exit 0
|
||||
}
|
||||
Write-Output 'FAKE RUNNER READY'
|
||||
while ($true) { Start-Sleep -Seconds 1 }
|
||||
'@ | Set-Content -LiteralPath (Join-Path $root 'start-windows-packaging-runner.ps1')
|
||||
function Invoke-Manager([string]$Operation, [switch]$ExpectFailure) {
|
||||
$installOptions = if ($Operation -eq 'install') { @('-ConfigRoot', $root, '-RestartCount', '2') } else { @() }
|
||||
& (Join-Path $PSHOME 'pwsh.exe') -NoProfile -File $manager -Action $Operation -RunnerRoot $runner @installOptions
|
||||
if (($LASTEXITCODE -eq 0) -eq [bool]$ExpectFailure) { throw "Unexpected exit $LASTEXITCODE for $Operation" }
|
||||
}
|
||||
function Wait-Condition([scriptblock]$Condition, [string]$Description, [int]$Seconds = 30) {
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($Seconds)
|
||||
do {
|
||||
if (& $Condition) { return }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
throw "Timed out: $Description"
|
||||
}
|
||||
$installed = $false
|
||||
$primaryFailure = $null
|
||||
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($runner.ToUpperInvariant())))
|
||||
$taskName = 'DSH-Packaging-' + $digest.Substring(0,16)
|
||||
try {
|
||||
Invoke-Manager install
|
||||
$installed = $true
|
||||
$config = Get-Content -LiteralPath (Join-Path $runner 'dsh-task/config.json') -Raw | ConvertFrom-Json
|
||||
$task = Get-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
if ($task.State -ne 'Disabled' -or $task.Settings.ExecutionTimeLimit -ne 'PT0S' -or $task.Settings.MultipleInstances -ne 'IgnoreNew') { throw 'Unexpected task scheduling settings.' }
|
||||
Invoke-Manager install -ExpectFailure
|
||||
Invoke-Manager start
|
||||
$starts = Join-Path $runner 'starts.txt'
|
||||
Wait-Condition { (Test-Path -LiteralPath $starts) -and @(Get-Content -LiteralPath $starts).Count -ge 2 } 'automatic retry after unexpected clean exit' 110
|
||||
$childId = [int]@(Get-Content -LiteralPath $starts)[-1]
|
||||
$child = Get-Process -Id $childId
|
||||
Invoke-Manager start
|
||||
Invoke-Manager status
|
||||
if (@(Get-Content -LiteralPath $starts).Count -ne 2) { throw 'Duplicate runner started.' }
|
||||
Invoke-Manager uninstall -ExpectFailure
|
||||
$recordFile = Join-Path $runner 'dsh-task/process.json'
|
||||
$originalRecord = Get-Content -LiteralPath $recordFile -Raw
|
||||
$record = $originalRecord | ConvertFrom-Json
|
||||
$record.Started = 0
|
||||
try {
|
||||
$record | ConvertTo-Json | Set-Content -LiteralPath $recordFile
|
||||
Invoke-Manager stop -ExpectFailure
|
||||
if ($child.HasExited) { throw 'Stale process record terminated a process.' }
|
||||
} finally { Set-Content -LiteralPath $recordFile -Value $originalRecord }
|
||||
Invoke-Manager stop
|
||||
Wait-Condition { $child.Refresh(); $child.HasExited } 'scheduler child termination'
|
||||
Invoke-Manager start
|
||||
Wait-Condition { @(Get-Content -LiteralPath $starts).Count -eq 3 } 'explicit restart'
|
||||
$child = Get-Process -Id ([int]@(Get-Content -LiteralPath $starts)[-1])
|
||||
Invoke-Manager stop
|
||||
Wait-Condition { $child.Refresh(); $child.HasExited } 'restarted child termination'
|
||||
Set-Content -LiteralPath (Join-Path $runner 'dsh-task/attempts.txt') -Value '3'
|
||||
$null = Enable-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
Start-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
Wait-Condition { (Get-ScheduledTask -TaskName $config.TaskName -TaskPath '\').State -eq 'Disabled' } 'exhausted restart budget disables task'
|
||||
if (@(Get-Content -LiteralPath $starts).Count -ne 3) { throw 'Exhausted budget launched another runner.' }
|
||||
Set-Content -LiteralPath (Join-Path $runner 'dsh-task/attempts.txt') -Value 'invalid'
|
||||
$null = Enable-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
Start-ScheduledTask -TaskName $config.TaskName -TaskPath '\'
|
||||
Wait-Condition { (Get-ScheduledTask -TaskName $config.TaskName -TaskPath '\').State -eq 'Disabled' } 'invalid restart budget disables task'
|
||||
if (@(Get-Content -LiteralPath $starts).Count -ne 3) { throw 'Invalid budget launched another runner.' }
|
||||
Invoke-Manager uninstall
|
||||
$installed = $false
|
||||
Invoke-Manager status -ExpectFailure
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $runner '.runner'))) { throw 'Registration removed.' }
|
||||
Write-Output "PASS: install, duplicate refusal, automatic retry, start, status, stop, child teardown, retry exhaustion, uninstall. Evidence: $root"
|
||||
} catch { $primaryFailure = $_; throw } finally {
|
||||
$cleanupErrors = @()
|
||||
if ($installed) {
|
||||
try { $null = Disable-ScheduledTask -TaskName $taskName -TaskPath '\' } catch { $cleanupErrors += $_ }
|
||||
try { Stop-ScheduledTask -TaskName $taskName -TaskPath '\' } catch { $cleanupErrors += $_ }
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -TaskPath '\' -Confirm:$false } catch { $cleanupErrors += $_ }
|
||||
}
|
||||
foreach ($process in @(Get-CimInstance Win32_Process -Filter "Name = 'pwsh.exe'")) {
|
||||
if ($process.CommandLine -and $process.CommandLine.Contains($root)) {
|
||||
$owned = Get-Process -Id $process.ProcessId -ErrorAction SilentlyContinue
|
||||
if ($owned) {
|
||||
try {
|
||||
Stop-Process -InputObject $owned
|
||||
if (-not $owned.WaitForExit(30000)) { throw 'Fixture process did not exit.' }
|
||||
} catch { $cleanupErrors += $_ }
|
||||
}
|
||||
}
|
||||
}
|
||||
# Logs stay available for diagnosing scheduler failures; the fixture owns no registered runner credentials.
|
||||
Write-Output "Fixture retained: $root"
|
||||
if ($cleanupErrors.Count) { Write-Warning ($cleanupErrors -join "`n"); if (-not $primaryFailure) { throw 'Fixture cleanup failed.' } }
|
||||
}
|
||||
@@ -18,8 +18,11 @@
|
||||
en: 0c261f3948178d55
|
||||
zh: 1086cd882468ad8f
|
||||
/windows-signing-and-runner-operations/machine-setup-and-runner-startup:
|
||||
en: ba3de07cc9553056
|
||||
zh: 887e652c40d57091
|
||||
en: bb889e9514678d1c
|
||||
zh: 48cbec1e907d6a2b
|
||||
/windows-signing-and-runner-operations/machine-setup-and-runner-startup/supervised-startup:
|
||||
en: d32f9f8ce9f5b861
|
||||
zh: 80d9078a7763f85d
|
||||
/windows-signing-and-runner-operations/signing-configuration:
|
||||
en: 4b5548818375eb9a
|
||||
zh: 8c975adbb1dca9c8
|
||||
|
||||
@@ -34,6 +34,16 @@ Use a runner-owned checkout and keep configuration, certificates and caches outs
|
||||
|
||||
Run `pwsh -NoProfile -File apps/desktop/scripts/setup-windows-packaging.ps1 -Root <absolute-machine-root>` to prepare private `packaging/{presets,secrets,certificates}` and `cache/windows-signatures` directories and copy templates without overwriting existing files. Add `-CheckOnly` to inspect without writes. Fill the [preset templates](../config/windows-packaging/test.env.example) and [signing template](../config/windows-packaging/windows-signing.env.example) on the machine. Run `start-windows-packaging-runner.ps1 -RunnerRoot <registered-runner-directory> -ConfigRoot <machine-root>/packaging -CheckOnly` from the same scripts directory to inspect PowerShell 7.6.6 under the runner's `tools` directory, Node, Python, Git, tar and Visual C++ tools; packaging `--check` performs the remaining toolchain validation. Omit `-CheckOnly` to start the existing runner with an explicit config root and scrubbed inherited environment. Registration and tool installation are separate; these scripts never create tokens, alter system security settings or clear signing failure records. Use the signing cache maintenance command below to migrate existing entries before changing the preset's cache path.
|
||||
|
||||
### Supervised startup
|
||||
|
||||
Run `pwsh -NoProfile -File apps/desktop/scripts/windows-runner-task.ps1 -Action install -RunnerRoot <registered-runner-directory> -ConfigRoot <machine-root>/packaging` as the runner's Windows user, using its installed PowerShell 7. The installer checks the environment and copies startup scripts into `runner/dsh-task`; registration, tools and secrets remain separate. It registers a disabled, limited-permission interactive Task Scheduler task without storing an account password. For `-Action start` or `-Action status`, pass only `-RunnerRoot`; configuration and retry options apply only to installation. The task starts after this user's desktop login, including after reboot, and stops on logout. It does not provide unattended boot or unlock a signing Token.
|
||||
|
||||
The task permits one instance and has no execution time limit. A repeating trigger checks every `-RestartMinutes` minutes (default `1`) and restarts an exited launcher, including an unexpected zero exit. The persisted budget permits the initial launch plus `-RestartCount` restarts (default `10`) between explicit starts. Every desktop login also consumes this budget; `status` shows the remaining launches. After the budget is exhausted, login alone cannot restart the runner. Starting an already running task does not reset the budget. Missing or invalid budget/configuration files disable automatic starts until repaired. GitHub's launcher owns runner updates; task recovery does not rerun a failed workflow or clear signing interlocks. After retries are exhausted, inspect `runner/dsh-task/run-*.log`, the runner's `_diag` logs and `status`, correct the cause, then run `start`. A running task alone does not prove connectivity: verify `Connected to GitHub` and `Listening for Jobs`, then verify assignment in Actions. Logs are retained for operator-managed cleanup.
|
||||
|
||||
Before maintenance, pause new workflow dispatch and wait for the runner to become idle. `-Action stop` refuses an active worker for this runner, disables future starts and stops its task process tree; the local check cannot prevent a concurrent GitHub assignment. `-Action uninstall` requires a stopped, disabled task and retains registration, configuration and logs. To update installed scripts, stop, uninstall and install again. When migrating a manually launched runner, stop its existing launcher only after it is idle, then start the task; never run both launchers together.
|
||||
|
||||
Operators can run `pwsh -NoProfile -File apps/desktop/tests/windows-runner-task.ps1` on Windows to exercise installation, automatic recovery, duplicate-start prevention, stop, child-process teardown and uninstall against an isolated fake runner. This opt-in test creates a temporary task and retains diagnostic files; it does not contact GitHub or access signing hardware. Real-machine acceptance additionally checks an idle runner coming online, an Actions job being assigned, and login after an operator-approved reboot. Signed packaging and logout/reboot acceptance require separate operator verification.
|
||||
|
||||
<a id="signing-configuration"></a>
|
||||
|
||||
## Signing configuration
|
||||
|
||||
@@ -34,6 +34,16 @@ description: "配置和维护 Windows 打包 runner、签名凭据、EV Token
|
||||
|
||||
运行 `pwsh -NoProfile -File apps/desktop/scripts/setup-windows-packaging.ps1 -Root <absolute-machine-root>` 创建私有 `packaging/{presets,secrets,certificates}` 和 `cache/windows-signatures` 目录并复制模板,不覆盖现有文件。添加 `-CheckOnly` 只检查不写入。在机器上填写 [preset 模板](../config/windows-packaging/test.env.example)和[签名模板](../config/windows-packaging/windows-signing.env.example)。从同一 scripts 目录运行 `start-windows-packaging-runner.ps1 -RunnerRoot <registered-runner-directory> -ConfigRoot <machine-root>/packaging -CheckOnly`,检查 runner 的 `tools` 目录下 PowerShell 7.6.6、Node、Python、Git、tar 和 Visual C++ 工具;打包 `--check` 完成其余工具链检查。省略 `-CheckOnly`,以显式配置目录和清理后的继承环境启动现有 runner。注册和工具安装单独进行;这些脚本不创建 token、不改系统安全设置、不清除签名失败记录。修改 preset 缓存路径前,使用下文签名缓存维护命令迁移已有条目。
|
||||
|
||||
### 受监督启动
|
||||
|
||||
以 runner 的 Windows 用户运行 `pwsh -NoProfile -File apps/desktop/scripts/windows-runner-task.ps1 -Action install -RunnerRoot <registered-runner-directory> -ConfigRoot <machine-root>/packaging`,使用该机器安装的 PowerShell 7。安装器检查环境,并将启动脚本复制到 `runner/dsh-task`;注册、工具和凭据仍单独管理。它注册一个初始禁用、权限受限的交互式计划任务,不保存账户密码。执行 `-Action start` 或 `-Action status` 时只传 `-RunnerRoot`;配置目录及重试选项仅适用于安装。任务在该用户登录桌面后启动,重启后也需要登录,注销时停止。它不提供无人值守开机,也不解锁签名 Token。
|
||||
|
||||
任务只允许一个实例,且没有执行时限。周期触发器每隔 `-RestartMinutes` 分钟(默认 `1`)检查一次,并恢复已退出的启动器,包括意外的零退出码。持久预算允许首次启动及 `-RestartCount` 次重启(默认 `10`),直到再次显式启动。每次桌面登录也会消耗预算,`status` 显示剩余启动次数;预算耗尽后,仅登录不能重启 runner。对正在运行的任务执行 start 不会重置预算。预算或配置文件缺失、无效时,自动启动会禁用,需修复后恢复。GitHub 启动器负责 runner 更新;任务恢复不会重跑失败的 workflow,也不会清除签名互锁。重试耗尽后,检查 `runner/dsh-task/run-*.log`、runner 的 `_diag` 日志和 `status`,修复原因后运行 `start`。任务运行不等于连接成功:确认出现 `Connected to GitHub` 和 `Listening for Jobs`,再到 Actions 确认任务分配。日志保留,由操作者管理清理。
|
||||
|
||||
维护前暂停新 workflow 的触发,等待 runner 空闲。`-Action stop` 在该 runner 存在活跃 worker 时拒绝停止,否则禁用后续启动并停止任务进程树;本地检查无法阻止 GitHub 同时分配任务。`-Action uninstall` 要求任务已停止并禁用,保留注册、配置和日志。更新已安装脚本时,依次停止、卸载、重新安装。迁移手动启动的 runner 时,只在空闲后停止原启动器,再启动计划任务;不要同时运行两个启动器。
|
||||
|
||||
操作者可在 Windows 上运行 `pwsh -NoProfile -File apps/desktop/tests/windows-runner-task.ps1`,使用隔离的假 runner 验证安装、自动恢复、防止重复启动、停止、子进程退出和卸载。此显式运行的测试创建临时任务并保留诊断文件,不连接 GitHub 或访问签名硬件。真实机器验收还需检查空闲 runner 上线、Actions job 分配,以及经操作者批准的重启后登录。签名打包和注销/重启验收需要操作者单独验证。
|
||||
|
||||
<a id="signing-configuration"></a>
|
||||
|
||||
## 签名配置
|
||||
|
||||
@@ -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-alpha.4",
|
||||
"version": "0.2.1-rc.2",
|
||||
"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-alpha.4",
|
||||
"version": "0.2.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
- id: tool-subagent-control
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-list-agents
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent
|
||||
disabled: true
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const MODE = webSnapshotMode()
|
||||
|
||||
const TOKEN = 'dsh_bonus_notice_test'
|
||||
const ACCOUNT_ID = 'bonus-user'
|
||||
const ORDER_FIRST = '22222222-2222-4222-8222-222222222222'
|
||||
const ORDER_FIRST = 'bonus-notice-first'
|
||||
const ORDER_LATER = '33333333-3333-4333-8333-333333333333'
|
||||
const ORDER_EN = '44444444-4444-4444-8444-444444444444'
|
||||
const ORDER_RETRY = '55555555-5555-4555-8555-555555555555'
|
||||
@@ -80,17 +80,17 @@ async function mockPlatform() {
|
||||
let failSummary = false
|
||||
/** Reply held back while the test controls when a read can answer. */
|
||||
let heldReply: (() => void) | undefined
|
||||
const grant = (orderId: string, amount: string): void => {
|
||||
const grant = (orderId: string, amount: string, extraCopy = ''): void => {
|
||||
unnotified.unshift({
|
||||
orderId, amount,
|
||||
zh_CN: `已赠送您 ${amount} 元 DSH 体验赠金。`,
|
||||
en_US: `You received a CNY ${amount} DSH trial credit.`,
|
||||
zh_CN: `已赠送您 ${amount} 元 DSH 体验赠金。${extraCopy}`,
|
||||
en_US: `You received a CNY ${amount} DSH trial credit.${extraCopy}`,
|
||||
})
|
||||
}
|
||||
/** @param value - granted bonus. @param locale - request language. @returns its wire fields. */
|
||||
const wire = (value: Granted, locale: string | undefined): unknown => ({
|
||||
order_id: value.orderId, campaign: 'dsh_login_bonus', amount: value.amount, currency: 'CNY',
|
||||
granted_at: new Date().toISOString(), expires_at: expiresAt(),
|
||||
order_id: value.orderId, expires_at: value.orderId === ORDER_FIRST ? null : expiresAt(),
|
||||
...(value.orderId === ORDER_FIRST ? { title: '平台通知' } : {}),
|
||||
msg: locale === 'en_US' ? value.en_US : value.zh_CN,
|
||||
})
|
||||
const server: Server = createServer((req, res) => {
|
||||
@@ -182,7 +182,7 @@ async function mockPlatform() {
|
||||
|
||||
/** @param page - page under test. @returns every rendered bonus notice card. */
|
||||
function noticeCards(page: Page): Locator {
|
||||
return page.locator('aside').filter({ hasText: '赠金已到账' })
|
||||
return page.locator('aside[data-front]')
|
||||
}
|
||||
|
||||
/** @param card - a rendered notice card. @returns its non-empty visible lines joined for the golden. */
|
||||
@@ -579,6 +579,75 @@ describe.skipIf(MODE === 'record')('web e2e: bonus notice', () => {
|
||||
await expect.poll(() => platform.acks.filter(item => item.orderId === ORDER_WEB).length).toBe(1)
|
||||
observations.push(`web.notice=${webNotice} acks=1`)
|
||||
await webPage.close()
|
||||
platform.clearUnnotified()
|
||||
platform.grant('stack-third', '23.00')
|
||||
platform.grant('stack-second', '22.00', '\nMore details'.repeat(8))
|
||||
platform.grant('stack-first', '21.00')
|
||||
const ackStart = platform.acks.length
|
||||
const stackPage = await openDesktopPage('zh-CN')
|
||||
await stackPage.emulateMedia({ colorScheme: 'light' })
|
||||
await stackPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
const cards = noticeCards(stackPage)
|
||||
await expect.poll(() => cards.count()).toBe(3)
|
||||
await expect.poll(() => platform.acks.length).toBe(ackStart + 1)
|
||||
expect(platform.acks.slice(ackStart).map(ack => ack.orderId)).toEqual(['stack-first'])
|
||||
expect(await stackPage.getByRole('status').count()).toBe(1)
|
||||
const second = cards.filter({ hasText: '22.00' })
|
||||
const secondNode = await second.elementHandle()
|
||||
const frontBox = await cards.first().boundingBox()
|
||||
const backBox = await second.boundingBox()
|
||||
expect(frontBox!.width).toBeCloseTo(280, 0)
|
||||
expect(backBox!.width).toBeCloseTo(272, 0)
|
||||
expect(backBox!.y).toBeCloseTo(frontBox!.y - 4, 0)
|
||||
const transition = await second.evaluate((element) => {
|
||||
const style = getComputedStyle(element)
|
||||
return `${style.transitionDuration} ${style.transitionTimingFunction}`
|
||||
})
|
||||
expect(transition).toBe('0.25s ease-out')
|
||||
observations.push(`stack.initial cards=3 acks=stack-first widths=280,272 offset=-4 transition=${transition}`)
|
||||
const artifactDir = fileURLToPath(new URL('../../../.artifacts', import.meta.url))
|
||||
await mkdir(artifactDir, { recursive: true })
|
||||
await stackPage.screenshot({ path: join(artifactDir, 'bonus-notice-stack-light.png') })
|
||||
await stackPage.emulateMedia({ colorScheme: 'dark' })
|
||||
await expect.poll(() => stackPage.locator('body').getAttribute('data-ds-dark-theme')).not.toBeNull()
|
||||
await stackPage.screenshot({ path: join(artifactDir, 'bonus-notice-stack-dark.png') })
|
||||
const promotion = await stackPage.evaluate(() => {
|
||||
const front = document.querySelector<HTMLElement>('aside[data-front="true"]')!
|
||||
const next = front.nextElementSibling as HTMLElement
|
||||
const before = next.getBoundingClientRect()
|
||||
front.querySelector('button')!.click()
|
||||
const animation = next.getAnimations()[0]!
|
||||
animation.pause()
|
||||
animation.currentTime = 0
|
||||
const start = next.getBoundingClientRect()
|
||||
animation.currentTime = 125
|
||||
const middle = next.getBoundingClientRect()
|
||||
animation.finish()
|
||||
const end = next.getBoundingClientRect()
|
||||
return { before: { y: before.y, height: before.height }, start: { y: start.y, height: start.height },
|
||||
middle: { y: middle.y, height: middle.height }, end: { y: end.y, height: end.height } }
|
||||
})
|
||||
expect(promotion.start.y).toBeCloseTo(promotion.before.y, 0)
|
||||
expect(promotion.start.height).toBeCloseTo(promotion.before.height, 0)
|
||||
expect(promotion.end.height).toBeGreaterThan(promotion.start.height + 100)
|
||||
expect(promotion.middle.height).toBeGreaterThan(promotion.start.height)
|
||||
expect(promotion.middle.height).toBeLessThan(promotion.end.height)
|
||||
expect(promotion.middle.y).toBeLessThan(promotion.start.y)
|
||||
expect(promotion.middle.y).toBeGreaterThan(promotion.end.y)
|
||||
await expect.poll(() => cards.count()).toBe(2)
|
||||
await expect.poll(async () => Math.round((await second.boundingBox())!.width)).toBe(280)
|
||||
expect(await second.evaluate((element, previous) => element === previous, secondNode)).toBe(true)
|
||||
expect(await second.getAttribute('data-front')).toBe('true')
|
||||
await expect.poll(() => platform.acks.length).toBe(ackStart + 2)
|
||||
expect(platform.acks.slice(ackStart).map(ack => ack.orderId)).toEqual(['stack-first', 'stack-second'])
|
||||
observations.push('stack.promoted cards=2 acks=stack-first,stack-second width=280 same-node=true')
|
||||
await second.getByRole('button', { name: '关闭', exact: true }).click()
|
||||
await expect.poll(() => platform.acks.length).toBe(ackStart + 3)
|
||||
expect(platform.acks.slice(ackStart).map(ack => ack.orderId)).toEqual(['stack-first', 'stack-second', 'stack-third'])
|
||||
await cards.filter({ hasText: '23.00' }).getByRole('button', { name: '关闭', exact: true }).click()
|
||||
await expect.poll(() => cards.count()).toBe(0)
|
||||
observations.push('stack.dismissed cards=0 acks=stack-first,stack-second,stack-third')
|
||||
await stackPage.close()
|
||||
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'notice-flow.expected.md'), observations.join('\n'), MODE)
|
||||
for (const tripwire of tripwires) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/** The shipped browser plugin decorates both DeepSeek routes without changing catalog identity. */
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser, type Page } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const NAME = 'DeepSeek-V4.1-Flash'
|
||||
const FIXTURE = fileURLToPath(new URL('./fixtures/deepseek-model-label-1017/session.v4.jsonl', import.meta.url))
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/deepseek-model-label-1017/models.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: temporary DeepSeek release badge', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: fileURLToPath(new URL('./fixtures/deepseek-model-label-1017/account.overlay.yml', import.meta.url)),
|
||||
replayFixture: FIXTURE,
|
||||
replayProvidersOnly: true,
|
||||
replayProviders: [
|
||||
{ id: 'deepseek-account', name: 'DeepSeek Account', models: [{ id: 'deepseek-flash', name: NAME }] },
|
||||
{ id: 'deepseek-official', name: 'DeepSeek', models: [
|
||||
{ id: 'deepseek-flash', name: NAME }, { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
|
||||
] },
|
||||
{ id: 'external', name: 'External', models: [{ id: 'deepseek-flash', name: NAME }] },
|
||||
],
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 900 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
try { await browser?.close() }
|
||||
finally { await scaffold?.close() }
|
||||
})
|
||||
|
||||
it('shows the same badge in all placements while preserving the chosen provider and model id', async () => {
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
await trigger.getByText('1017', { exact: true }).waitFor()
|
||||
expect(await trigger.textContent()).toContain(`${NAME} 1017`)
|
||||
expect(await trigger.getAttribute('aria-label')).toBe(`选择模型,当前 ${NAME}`)
|
||||
try {
|
||||
await trigger.click()
|
||||
const current = page.getByRole('menuitem', { name: /^模型/ })
|
||||
expect(await current.getByText('1017', { exact: true }).count()).toBe(1)
|
||||
await current.click()
|
||||
const menu = page.getByRole('menu', { name: '模型', exact: true })
|
||||
await compareOrRefreshGolden(EXPECTED, await captureStableAria(page, '[role="menu"][aria-label="模型"]', scaffold.workspaceCwd), MODE)
|
||||
expect(await menu.getByText('1017', { exact: true }).count()).toBe(2)
|
||||
expect(await menu.getByRole('group', { name: 'External', exact: true }).getByText('1017', { exact: true }).count()).toBe(0)
|
||||
const account = menu.getByRole('group', { name: 'DeepSeek 账号', exact: true })
|
||||
await account.getByRole('menuitemradio', { name: `${NAME} 1017`, exact: true }).click()
|
||||
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-account', model: 'deepseek-flash' })
|
||||
await expect.poll(() => trigger.textContent()).toContain(`${NAME} 1017`)
|
||||
const catalog = await scaffold.ctx.llm.listModels('deepseek-account')
|
||||
expect(catalog.map(model => ({ id: model.id, name: model.name }))).toEqual([{ id: 'deepseek-flash', name: NAME }])
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the release badge inside a 390px model menu and uses both theme palettes', async () => {
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
const menu = page.getByRole('menu', { name: '模型', exact: true })
|
||||
const badge = menu.getByRole('group', { name: 'DeepSeek 账号', exact: true }).getByText('1017', { exact: true })
|
||||
const box = await badge.boundingBox()
|
||||
expect(box).not.toBeNull()
|
||||
expect(box!.x).toBeGreaterThanOrEqual(12)
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(378)
|
||||
const colors = await badge.evaluate((node) => {
|
||||
const body = node.ownerDocument.body
|
||||
const previous = body.getAttribute('data-ds-dark-theme')
|
||||
try {
|
||||
return [false, true].map((dark) => {
|
||||
body.toggleAttribute('data-ds-dark-theme', dark)
|
||||
const style = getComputedStyle(node)
|
||||
return { color: style.color, background: style.backgroundColor }
|
||||
})
|
||||
} finally {
|
||||
if (previous === null) body.removeAttribute('data-ds-dark-theme')
|
||||
else body.setAttribute('data-ds-dark-theme', previous)
|
||||
}
|
||||
})
|
||||
expect(colors[0]).not.toEqual(colors[1])
|
||||
for (const color of colors) expect(color.color).not.toBe(color.background)
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -92,6 +92,70 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('exposes inline model labels with exact routes throughout the composer picker', async () => {
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
const label = '[data-provider-id][data-model-id]'
|
||||
const readLabels = (surface: Locator) => surface.locator(label).evaluateAll(nodes => nodes.map(node => ({
|
||||
provider: node.getAttribute('data-provider-id'), model: node.getAttribute('data-model-id'),
|
||||
name: node.textContent, slot: node.querySelector('[data-slot]')?.getAttribute('data-slot'),
|
||||
inline: node.tagName === 'SPAN' && node.querySelector('div') === null,
|
||||
})))
|
||||
const current = {
|
||||
provider: START_ROUTE, model: START_MODEL, name: 'Origin Large',
|
||||
slot: 'conversation.input.model.label', inline: true,
|
||||
}
|
||||
await expect.poll(() => readLabels(trigger)).toEqual([current])
|
||||
try {
|
||||
await trigger.click()
|
||||
const cell = page.getByRole('menuitem', { name: /^模型/ })
|
||||
expect(await readLabels(cell)).toEqual([current])
|
||||
await cell.click()
|
||||
const menu = page.getByRole('menu', { name: '模型', exact: true })
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/default-model/model-labels.expected.md', import.meta.url)),
|
||||
JSON.stringify(await readLabels(menu), null, 2),
|
||||
webSnapshotMode(),
|
||||
)
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['root', 'model'])('reanchors the %s menu when a label grows and shrinks without a window resize', async (pane) => {
|
||||
const viewport = page.viewportSize()!
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await trigger.click()
|
||||
if (pane === 'model') await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
const surface = page.getByRole(pane === 'model' ? 'group' : 'menu', { name: '模型与推理等级', exact: true })
|
||||
const label = surface.locator('[data-provider-id="origin-gateway"][data-model-id="origin-large"]')
|
||||
await label.waitFor()
|
||||
const original = await surface.boundingBox()
|
||||
expect(original).not.toBeNull()
|
||||
// A Slot descendant can resize independently of ModelSelect's props and state.
|
||||
await label.evaluate((node) => {
|
||||
const badge = node.ownerDocument.createElement('span')
|
||||
badge.dataset.modelLabelResizeProbe = ''
|
||||
badge.textContent = ' — Provider identification badge with a long channel name'
|
||||
node.appendChild(badge)
|
||||
})
|
||||
await expect.poll(async () => (await surface.boundingBox())!.width).toBeGreaterThan(original!.width + 50)
|
||||
await expect.poll(async () => {
|
||||
const box = (await surface.boundingBox())!
|
||||
return box.x >= 12 && box.x + box.width <= 378
|
||||
}).toBe(true)
|
||||
await label.locator('[data-model-label-resize-probe]').evaluate((node) => { node.remove() })
|
||||
await expect.poll(async () => (await surface.boundingBox())!.width).toBeCloseTo(original!.width)
|
||||
await expect.poll(async () => (await surface.boundingBox())!.x).toBeCloseTo(original!.x)
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.setViewportSize(viewport)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps command popup search borders transparent in both palettes', async () => {
|
||||
const composer = page.locator('[data-composer-input]').first()
|
||||
await page.getByRole('button', { name: '添加文件或调用指令', exact: true }).click()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
startup.held cards=0 acks=0
|
||||
startup.notice=赠金已到账 / 已赠送您 5.00 元 DSH 体验赠金。
|
||||
startup.ack orders=22222222-2222-4222-8222-222222222222
|
||||
startup.notice=平台通知 / 已赠送您 5.00 元 DSH 体验赠金。
|
||||
startup.ack orders=bonus-notice-first
|
||||
startup.get locale=zh_CN query=
|
||||
startup.ack method=POST path=/api/v0/users/ack_bonus_notified locale=zh_CN order_id=22222222-2222-4222-8222-222222222222 body="{\"order_id\":\"22222222-2222-4222-8222-222222222222\"}"
|
||||
startup.ack method=POST path=/api/v0/users/ack_bonus_notified locale=zh_CN order_id=bonus-notice-first body="{\"order_id\":\"bonus-notice-first\"}"
|
||||
open.held cards=0 acks=1 gets=1
|
||||
open.shown notice=赠金已到账 / 已赠送您 8.00 元 DSH 体验赠金。
|
||||
open.shown ack orders=22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333
|
||||
open.shown ack orders=bonus-notice-first,33333333-3333-4333-8333-333333333333
|
||||
open.shown painted-under-settings=true cards=1 acks=2
|
||||
open.get locale=zh_CN query= served=true gets=1
|
||||
open.balance bonus-row=true amount=true dated=false
|
||||
@@ -31,3 +31,6 @@ failed.reached usages=2 gets=1 summaries=1
|
||||
en.notice=Bonus credited / You received a CNY 9.00 DSH trial credit.
|
||||
en.get locale=en_US query=
|
||||
web.notice=赠金已到账 / 已赠送您 4.00 元 DSH 体验赠金。 acks=1
|
||||
stack.initial cards=3 acks=stack-first widths=280,272 offset=-4 transition=0.25s ease-out
|
||||
stack.promoted cards=2 acks=stack-first,stack-second width=280 same-node=true
|
||||
stack.dismissed cards=0 acks=stack-first,stack-second,stack-third
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
- menu "模型":
|
||||
- group "DeepSeek 账号":
|
||||
- text: DeepSeek 账号
|
||||
- menuitemradio "DeepSeek-V4.1-Flash 1017"
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "DeepSeek-V4.1-Flash 1017" [checked]
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
- group "External":
|
||||
- text: External
|
||||
- menuitemradio "DeepSeek-V4.1-Flash"
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"provider": "deepseek-official",
|
||||
"model": "deepseek-v4-flash",
|
||||
"name": "DeepSeek-V4-Flash",
|
||||
"slot": "conversation.input.model.label",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"provider": "deepseek-official",
|
||||
"model": "deepseek-v4-flash-vision-exp",
|
||||
"name": "DeepSeek-V4-Flash-Vision-Exp",
|
||||
"slot": "conversation.input.model.label",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"provider": "origin-gateway",
|
||||
"model": "origin-large",
|
||||
"name": "Origin Large",
|
||||
"slot": "conversation.input.model.label",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"provider": "acme-gateway",
|
||||
"model": "acme-large",
|
||||
"name": "Acme Large",
|
||||
"slot": "conversation.input.model.label",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"provider": "acme-gateway",
|
||||
"model": "acme-small",
|
||||
"name": "Acme Small",
|
||||
"slot": "conversation.input.model.label",
|
||||
"inline": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
- button "返回插件": 插件
|
||||
- heading "网页搜索" [level=3]
|
||||
- paragraph: 设置 DeepSeek 的搜索提供方。
|
||||
- text: 启用新版 Web 搜索 API
|
||||
- switch "启用新版 Web 搜索 API" [checked]
|
||||
- paragraph:
|
||||
- text: 默认关闭,搜索通过 DeepSeek Messages 完成。新版 Web 搜索 API 功能与计费详见
|
||||
- link "网页搜索与抓取指南":
|
||||
- /url: https://api-docs.deepseek.com/zh-cn/guides/search
|
||||
- text: API Key 未配置密钥;仅使用 DeepSeek 账号模型的对话可以通过默认接口地址搜索。
|
||||
- textbox "API Key"
|
||||
- paragraph: 不写入设置文件。留空表示保持当前密钥。
|
||||
- text: Search / Fetch API 基础地址 已覆盖
|
||||
- button "恢复默认"
|
||||
- textbox "Search / Fetch API 基础地址":
|
||||
- /placeholder: ""
|
||||
- text: https://search-api.test/api/v1/tools
|
||||
- paragraph: 填写含 API 前缀的地址,例如 https://api.deepseek.com/v1/tools;程序追加 /search 或 /fetch,留空恢复默认。
|
||||
- text: 每次搜索结果数
|
||||
- textbox "每次搜索结果数":
|
||||
- /placeholder: ""
|
||||
- text: "10"
|
||||
- paragraph: 每个查询返回的结果条数,须为 1–50 的整数,默认 10;不控制搜索调用次数。web_search 保留的来源数不超过其自身的结果上限。
|
||||
- button "保存" [disabled]
|
||||
@@ -4,10 +4,6 @@
|
||||
- strong: 子智能体控制工具
|
||||
- text: 让 AI 向子智能体发送消息或中断其运行。
|
||||
- code: tool-subagent-control
|
||||
- listitem:
|
||||
- button "子智能体列表工具, tool-subagent-list-agents, 已启用":
|
||||
- strong: 子智能体列表工具
|
||||
- code: tool-subagent-list-agents
|
||||
- listitem:
|
||||
- button "子智能体委派工具, tool-subagent, 已启用":
|
||||
- strong: 子智能体委派工具
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# The replay catalog supplies the account route without account authentication or network I/O.
|
||||
- id: llm-deepseek-account
|
||||
disabled: true
|
||||
- id: agent-default-model
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-flash
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"session","version":4,"id":"{{session:1}}","createdAt":1785015039278,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0,"agentPreset":"standard"}
|
||||
@@ -166,7 +166,6 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const mediaResponses = new Map<string, number>()
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -191,7 +190,7 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
{ createdAt: WEB_FIXTURE_TIME - 60_000 })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
page.on('response', (response) => {
|
||||
const url = new URL(response.url())
|
||||
@@ -204,7 +203,6 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
if (imageOrigin !== undefined) await stopServer(imageOrigin.server)
|
||||
|
||||
@@ -191,7 +191,6 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
@@ -212,14 +211,13 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME - 90_000 })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/** Fixture-day clock pins keep page scheduling native at every real-to-fixture distance. */
|
||||
import { chromium, type Browser } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { newEnglishPage, pinBrowserClock, WEB_FIXTURE_TIME } from './support.ts'
|
||||
|
||||
// Host clock shifts spread over one 2^32 ms period of the real-to-fixture
|
||||
// distance; Playwright's page clock stopped page timers in half of that period.
|
||||
const DISTANCE_PERIOD_MS = 2 ** 32
|
||||
const HOST_SHIFTS_MS = Array.from({ length: 8 }, (_, index) => index * DISTANCE_PERIOD_MS / 8)
|
||||
|
||||
describe('web e2e: pinned fixture clocks', () => {
|
||||
let browser: Browser
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch()
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
})
|
||||
|
||||
it.each(HOST_SHIFTS_MS)('runs page timers and reads the fixture day with the Host clock %d ms behind it', async (shift) => {
|
||||
const hostStartedAt = performance.now()
|
||||
const hostClock = vi.spyOn(Date, 'now').mockImplementation(() =>
|
||||
WEB_FIXTURE_TIME - shift + Math.floor(performance.now() - hostStartedAt))
|
||||
const page = await newEnglishPage(browser)
|
||||
try {
|
||||
await pinBrowserClock(page)
|
||||
for (const navigation of ['initial', 'replacement']) {
|
||||
await page.goto(`data:text/html,<body>${navigation}</body>`)
|
||||
const pageStartedAt = await page.evaluate(() => {
|
||||
requestAnimationFrame(() => { document.body.dataset.frame = 'done' })
|
||||
setTimeout(() => { document.body.dataset.timer = 'done' }, 20)
|
||||
return Date.now()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => ({
|
||||
frame: document.body.dataset.frame,
|
||||
timer: document.body.dataset.timer,
|
||||
})), { timeout: 5_000 }).toEqual({ frame: 'done', timer: 'done' })
|
||||
const dates = await page.evaluate(() => ({
|
||||
now: Date.now(),
|
||||
day: new Date().toISOString().slice(0, 10),
|
||||
explicit: new Date(0).getTime(),
|
||||
parsed: Date.parse('2000-01-01T00:00:00Z'),
|
||||
}))
|
||||
expect(pageStartedAt).toBeGreaterThanOrEqual(WEB_FIXTURE_TIME)
|
||||
expect(dates.now).toBeGreaterThan(pageStartedAt)
|
||||
expect(dates.day).toBe(new Date(WEB_FIXTURE_TIME).toISOString().slice(0, 10))
|
||||
expect(dates.explicit).toBe(0)
|
||||
expect(dates.parsed).toBe(946_684_800_000)
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await page.close()
|
||||
} finally {
|
||||
hostClock.mockRestore()
|
||||
}
|
||||
}
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -36,6 +36,7 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
// below, that half registers its row's configuration into the page.
|
||||
scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
|
||||
deepSeekSearch: { apiKeyEnv: 'DSH_PLUGIN_CONFIG_E2E_UNUSED_KEY' },
|
||||
profile: { packages: [{ dir: join(FIXTURE_PLUGINS, 'fixture-live-client') }] },
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
@@ -134,6 +135,66 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('switches search and fetch to the Search and Fetch APIs together and preserves each API’s endpoint', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-api-settings'))
|
||||
const panel = await openPlugins()
|
||||
await openPage(panel, '网页搜索')
|
||||
const toggle = panel.getByRole('switch', { name: '启用新版 Web 搜索 API', exact: true })
|
||||
const endpoint = panel.locator('#plugin-config-web-search-endpoint')
|
||||
const maxUses = panel.getByLabel('单次请求最多搜索次数', { exact: true })
|
||||
const save = panel.getByRole('button', { name: '保存', exact: true })
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('false')
|
||||
expect(await panel.getByRole('link', { name: '网页搜索与抓取指南', exact: true }).getAttribute('href'))
|
||||
.toBe('https://api-docs.deepseek.com/zh-cn/guides/search')
|
||||
expect(await panel.getByLabel('Messages API 基础地址', { exact: true }).count()).toBe(1)
|
||||
expect(await maxUses.inputValue()).toBe('5')
|
||||
expect(await endpoint.inputValue()).toBe('')
|
||||
expect(await save.isDisabled()).toBe(true)
|
||||
await endpoint.fill('https://messages.test/anthropic/v1')
|
||||
await save.click()
|
||||
await expect.poll(() => save.isDisabled()).toBe(true)
|
||||
await toggle.focus()
|
||||
await page.keyboard.press('Space')
|
||||
expect(await panel.getByLabel('Search / Fetch API 基础地址', { exact: true }).count()).toBe(1)
|
||||
expect(await endpoint.inputValue()).toBe('https://api.deepseek.com/v1/tools')
|
||||
expect(await maxUses.count()).toBe(0)
|
||||
const count = panel.getByLabel('每次搜索结果数', { exact: true })
|
||||
expect(await count.inputValue()).toBe('10')
|
||||
await count.fill('51')
|
||||
await panel.getByText('请填 1–50 的整数;留空表示使用默认值。', { exact: true }).waitFor()
|
||||
expect(await save.isDisabled()).toBe(true)
|
||||
expect(await toggle.isDisabled()).toBe(true)
|
||||
await count.fill('10')
|
||||
expect(await settingsDocument()).not.toContain('searchProvider: deepseek-search-api')
|
||||
await endpoint.fill('https://search-api.test/api/v1/tools')
|
||||
await save.click()
|
||||
await expect.poll(() => save.isDisabled()).toBe(true)
|
||||
await expect.poll(settingsDocument).toContain('searchProvider: deepseek-search-api')
|
||||
await expect.poll(settingsDocument).toContain('fetchProvider: deepseek-fetch-api')
|
||||
await expect.poll(settingsDocument).toContain('baseURL: https://search-api.test/api/v1/tools')
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await openPlugins()
|
||||
await openPage(panel, '网页搜索')
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('true')
|
||||
expect(await endpoint.inputValue()).toBe('https://search-api.test/api/v1/tools')
|
||||
const snapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'web-search.expected.md'), snapshot, MODE)
|
||||
await panel.getByRole('button', { name: '恢复默认', exact: true }).click()
|
||||
expect(await endpoint.inputValue()).toBe('https://api.deepseek.com/v1/tools')
|
||||
await save.click()
|
||||
await expect.poll(() => save.isDisabled()).toBe(true)
|
||||
await expect.poll(settingsDocument).not.toContain('https://search-api.test/api/v1/tools')
|
||||
await toggle.click()
|
||||
expect(await endpoint.inputValue()).toBe('https://messages.test/anthropic/v1')
|
||||
await save.click()
|
||||
await expect.poll(() => save.isDisabled()).toBe(true)
|
||||
await expect.poll(settingsDocument).not.toContain('searchProvider: deepseek-search-api')
|
||||
await expect.poll(settingsDocument).not.toContain('fetchProvider: deepseek-fetch-api')
|
||||
expect(await settingsDocument()).toContain('baseURL: https://messages.test/anthropic/v1')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('saves subagent limits and resets them to the deployment defaults', async () => {
|
||||
const panel = await openPlugins()
|
||||
await openPage(panel, '子智能体')
|
||||
@@ -379,7 +440,7 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['bundle.expected.md', 'official.expected.md', 'row.expected.md', 'subagent.expected.md'])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['bundle.expected.md', 'official.expected.md', 'row.expected.md', 'subagent.expected.md', 'web-search.expected.md'])
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
@@ -151,7 +150,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
await seedSession(scaffold, targetSessionFixture(), TARGET_SESSION_ID, undefined, { createdAt: targetCreatedAt })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
// Fixture files land before the workspace connects so the Host's file
|
||||
// index never races their creation (the connect helper mkdirs the same
|
||||
@@ -171,7 +170,6 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
unpinBrowserClock?.()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
@@ -408,10 +408,23 @@ export interface LaunchOptions {
|
||||
*/
|
||||
deepSeekSearch?: {
|
||||
/** Anthropic-compatible base URL; the provider appends `/messages`. */
|
||||
baseURL: string
|
||||
baseURL?: string
|
||||
/** Credential reference resolved by the shipped search provider. */
|
||||
apiKeyEnv: string
|
||||
}
|
||||
/**
|
||||
* Patch the shipped DeepSeek Search and Fetch API row to a deterministic
|
||||
* endpoint and credential reference, optionally selecting both providers as
|
||||
* the settings switch does.
|
||||
*/
|
||||
deepSeekWebApi?: {
|
||||
/** Search and Fetch API base URL; the providers append `/search` or `/fetch`. */
|
||||
baseURL: string
|
||||
/** Credential reference resolved by both providers. */
|
||||
apiKeyEnv: string
|
||||
/** Select `deepseek-search-api` and `deepseek-fetch-api` in place of the shipped selection. */
|
||||
select?: boolean
|
||||
}
|
||||
/** Preset selection default and additional declarative definitions for this scenario. */
|
||||
agentPresets?: {
|
||||
default: string
|
||||
@@ -683,15 +696,21 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
id: 'web-search-deepseek',
|
||||
config: {
|
||||
apiKeyEnv: options.deepSeekSearch.apiKeyEnv,
|
||||
baseURL: options.deepSeekSearch.baseURL,
|
||||
...options.deepSeekSearch.baseURL === undefined ? {} : { baseURL: options.deepSeekSearch.baseURL },
|
||||
},
|
||||
}],
|
||||
...options.deepSeekWebApi === undefined ? [] : [
|
||||
{ id: 'web-deepseek-api', config: { apiKeyEnv: options.deepSeekWebApi.apiKeyEnv, baseURL: options.deepSeekWebApi.baseURL } },
|
||||
...options.deepSeekWebApi.select === true
|
||||
? [{ id: 'web', config: { searchProvider: 'deepseek-search-api', fetchProvider: 'deepseek-fetch-api' } }]
|
||||
: [],
|
||||
],
|
||||
{ id: 'llm-deepseek', disabled: mode !== 'record' && !maskDeepSeekCredential },
|
||||
]
|
||||
|
||||
// Live fields use a shared deployment layer; process-specific ports and roots stay in CLI overlays.
|
||||
const formEntries = new Set(['agent-default-model', 'agent-preset-registry', 'llm-deepseek', 'llm-pi-ai',
|
||||
'web-search-deepseek', 'agent-loop', 'subagent', 'bash-sandbox', 'pwsh-sandbox',
|
||||
'web', 'web-search-deepseek', 'web-deepseek-api', 'agent-loop', 'subagent', 'bash-sandbox', 'pwsh-sandbox',
|
||||
'session-log-deepseek', 'ui-theme', 'locale', 'ui-chat', 'ui-conversation', 'ui-settings', 'ui-settings-general', 'permission'])
|
||||
const formDefaults: PatchOptions[] = []
|
||||
const processOverlays = overlayPatches.map((patch) => {
|
||||
|
||||
@@ -230,7 +230,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
let seededThroughSeq = -1
|
||||
let openingWindow: unknown
|
||||
let hostClock: ReturnType<typeof pinHostClock> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
// Seeded and later command timestamps share the renderer's fixture day across midnight.
|
||||
@@ -271,7 +270,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
if (MODE !== 'record') unpinBrowserClock = await pinBrowserClock(page)
|
||||
if (MODE !== 'record') await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
if (MODE !== 'record') {
|
||||
// A one-message tail makes this short recording exercise the real Load earlier path.
|
||||
@@ -303,7 +302,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
unpinBrowserClock?.()
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -484,6 +484,7 @@ const EXPECTED_TOOLS = [
|
||||
'job_list',
|
||||
'job_output',
|
||||
'list_agents',
|
||||
'list_subagent_models',
|
||||
'present',
|
||||
'read',
|
||||
'read_image',
|
||||
|
||||
@@ -375,6 +375,24 @@ describe('web e2e: shipped right Sidebar', () => {
|
||||
expect(answered).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the frame unscrolled when PageUp moves the composer caret beside the collapsed panel', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-pageup'))
|
||||
const frame = page.locator('[class*="frame"]').first()
|
||||
const composer = page.locator('textarea, [contenteditable="true"]').first()
|
||||
// The expand button exists only while the panel is collapsed, when its
|
||||
// docked hosts sit translated past the frame's right edge.
|
||||
await expandOf(page).waitFor({ timeout: 15_000 })
|
||||
await composer.click()
|
||||
await composer.pressSequentially('caret')
|
||||
// Outside macOS, PageUp moves the caret and reveals it against the start
|
||||
// edge of every scrollable ancestor, `overflow: hidden` ones included.
|
||||
await page.keyboard.press('PageUp')
|
||||
expect(await frame.evaluate(node => node.scrollLeft)).toBe(0)
|
||||
await page.keyboard.press('ControlOrMeta+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect.poll(async () => (await composer.innerText()).trim()).toBe('')
|
||||
})
|
||||
|
||||
it('starts collapsed behind a header button and squeezes the conversation when opened', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right'))
|
||||
const frame = page.locator('[class*="frame"]').first()
|
||||
|
||||
@@ -287,8 +287,10 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
it('completes commands, preserves the process through collapse and reload, resizes, and kills on tab close', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'sidebar-terminal'))
|
||||
const unaryInput: string[] = []
|
||||
const listRequests: string[] = []
|
||||
page.on('request', (request) => {
|
||||
if (/\/api\/terminal\/(write|resize|follow)$/u.test(new URL(request.url()).pathname)) unaryInput.push(request.url())
|
||||
if (new URL(request.url()).pathname === '/api/terminal/list') listRequests.push(request.url())
|
||||
})
|
||||
await openTerminal(page)
|
||||
const terminal = page.locator('[data-sidebar-terminal]')
|
||||
@@ -335,6 +337,7 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
await expect.poll(async () => await screen.innerText()).toContain(`SIZE:${terminals()[0]!.rows} ${terminals()[0]!.cols}`)
|
||||
await page.screenshot({ path: `${shots}/fullscreen.png`, fullPage: true })
|
||||
const tabIds = await page.locator('[data-dockkit-tab]').evaluateAll(tabs => tabs.map(tab => tab.getAttribute('data-dockkit-tab')))
|
||||
expect(listRequests).toHaveLength(0)
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.locator('[data-dockkit-tab]').filter({ hasText: 'Development' }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(async () => await page.locator('[data-dockkit-tab-title]').allInnerTexts()).toEqual(['Development', 'bash'])
|
||||
@@ -344,6 +347,7 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
expect(await page.locator('[data-dockkit-tab]').evaluateAll(tabs => tabs.map(tab => tab.getAttribute('data-dockkit-tab')))).toEqual(tabIds)
|
||||
await expect.poll(async () => await screen.innerText()).toContain('PERSIST:xterm-256color')
|
||||
await page.getByRole('button', { name: 'Exit fullscreen', exact: true }).waitFor()
|
||||
expect(listRequests).toHaveLength(1)
|
||||
await page.getByRole('button', { name: 'Collapse right sidebar', exact: true }).click()
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.locator('[data-sidebar-right-expand]').waitFor()
|
||||
@@ -354,6 +358,7 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
const secondTab = page.locator('[data-dockkit-tab]').filter({ hasText: 'bash' })
|
||||
await secondTab.click()
|
||||
await expect.poll(async () => await screen.innerText()).toContain(`SECOND_PID:${secondPid}`)
|
||||
expect(listRequests).toHaveLength(2)
|
||||
await secondTab.hover()
|
||||
await secondTab.locator('[data-dockkit-tab-close]').click()
|
||||
await expect.poll(async () => await secondTab.count()).toBe(0)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Web e2e scenario: switching a plugin on the Plugins page updates the `/`
|
||||
// menu of a Session that is already open. One chromium page holds a blank
|
||||
// Session while a second page switches the official badge skill bundle; a
|
||||
// global skill seeded in the harness home stays listed throughout. No model
|
||||
// call is issued, so a stray stream fails loud on the open LLM seam.
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser, type Locator, type Page } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot, writeComposerDraft } from './support.ts'
|
||||
|
||||
const BUNDLE_DIR = fileURLToPath(new URL('../../../packages/experimental/badge-skill-bundle', import.meta.url))
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-badge-skill-bundle'
|
||||
const GLOBAL_SKILL = 'dsh-global-skill'
|
||||
|
||||
describe('web e2e: the slash menu follows plugin switches', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let plugins: Page
|
||||
let toggle: Locator
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ profile: { packages: [{ dir: BUNDLE_DIR }] } })
|
||||
// The scaffold pins the skill-filesystem harness home inside its temporary world.
|
||||
const directory = join(scaffold.workspaceCwd, '.dsh-home', 'skills', GLOBAL_SKILL)
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'SKILL.md'), [
|
||||
'---',
|
||||
`name: ${GLOBAL_SKILL}`,
|
||||
'description: Listed whichever plugins are switched on',
|
||||
'---',
|
||||
'',
|
||||
`# ${GLOBAL_SKILL}`,
|
||||
'',
|
||||
].join('\n'))
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
plugins = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await plugins.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await plugins.getByRole('navigation', { name: 'Global panels' }).getByRole('button', { name: 'Plugins', exact: true }).click()
|
||||
const panel = plugins.locator('[data-plugin-panel]')
|
||||
await panel.getByRole('button', { name: 'More', exact: true }).click()
|
||||
toggle = panel.locator(`[data-plugin-package="${BUNDLE}"]`).getByRole('switch')
|
||||
await toggle.waitFor({ timeout: 10_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
/**
|
||||
* Retype the query in the open Session and read whether the badge skill is listed.
|
||||
* @returns whether the menu lists `dsh-badge` beside the global skill.
|
||||
*/
|
||||
async function badgeListed(): Promise<boolean> {
|
||||
await writeComposerDraft(page, page.locator('[data-composer-input]').first(), '/dsh-')
|
||||
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
|
||||
// The skill source answers with one list, so the global entry marks a settled skill group.
|
||||
await menu.getByRole('option', { name: new RegExp(`^${GLOBAL_SKILL} `) }).waitFor({ timeout: 10_000 })
|
||||
return await menu.getByRole('option', { name: /^dsh-badge / }).count() === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the badge bundle on the second page and wait for the switch to settle.
|
||||
* @param enabled - the requested bundle state.
|
||||
*/
|
||||
async function switchBundle(enabled: boolean): Promise<void> {
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe(String(!enabled))
|
||||
await toggle.click()
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(String(enabled))
|
||||
}
|
||||
|
||||
it('lists a switched bundle\'s skill only while the bundle is on, keeping the global skill', async () => {
|
||||
onTestFailed(async () => {
|
||||
await saveFailureShot(page, 'web-e2e-skill-menu-plugin-change')
|
||||
await saveFailureShot(plugins, 'web-e2e-skill-menu-plugin-change-plugins')
|
||||
})
|
||||
// The open Session cached its catalog while the bundle was off.
|
||||
expect(await badgeListed()).toBe(false)
|
||||
|
||||
// Each switch re-reads the catalog of the Session that stayed open.
|
||||
for (const enabled of [true, false, true]) {
|
||||
await switchBundle(enabled)
|
||||
await expect.poll(badgeListed, { timeout: 10_000 }).toBe(enabled)
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -73,7 +73,6 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let hostClock: MockInstance<typeof Date.now> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -87,7 +86,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -101,8 +100,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
// A failing close (unconsumed fixture, mismatched snapshot) must not
|
||||
// leave the pinned clocks running for the suites after this one.
|
||||
unpinBrowserClock?.()
|
||||
// leave the pinned Host clock running for the suites after this one.
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
@@ -396,7 +394,6 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let hostClock: MockInstance<typeof Date.now> | undefined
|
||||
let unpinBrowserClock: (() => void) | undefined
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -416,7 +413,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
unpinBrowserClock = await pinBrowserClock(page)
|
||||
await pinBrowserClock(page)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -431,7 +428,6 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
unpinBrowserClock?.()
|
||||
hostClock?.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
+25
-23
@@ -50,9 +50,6 @@ export const WEB_FIXTURE_TIME = Date.parse('2026-01-15T12:00:00+08:00')
|
||||
*/
|
||||
export const PLUGIN_TOGGLE_SETTLE_MS = 10_000
|
||||
|
||||
/** How often a pinned browser clock advances, in milliseconds. */
|
||||
const PINNED_CLOCK_STEP_MS = 250
|
||||
|
||||
/**
|
||||
* Pin the Host clock to the shared fixture day for one scenario suite.
|
||||
*
|
||||
@@ -74,31 +71,36 @@ export function pinHostClock(): MockInstance<typeof Date.now> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a scenario page's clock to the shared fixture day, advancing with real
|
||||
* Pin a scenario page's calendar to the shared fixture day, advancing with real
|
||||
* time. Pair it with {@link pinHostClock} for a scenario that stamps its own
|
||||
* message times, and with `seedSession`'s `createdAt` for a seeded one.
|
||||
*
|
||||
* The frozen instant is re-applied from the anchor on an interval rather than
|
||||
* left at one value, because product logic reads two `Date.now()` values to
|
||||
* decide a minimum display hold or a deadline window; a permanently frozen
|
||||
* clock stops supplying elapsed time, so a title that owes its golden settled
|
||||
* text keeps rendering its previous one. Both clocks therefore advance at the
|
||||
* real rate and stay within one step of each other.
|
||||
* @param page - page whose clock is pinned.
|
||||
* @returns the disposer that stops re-pinning; call it in teardown.
|
||||
* Only the page's `Date` reads the fixture day; native timers and
|
||||
* `requestAnimationFrame` keep their real scheduling. Playwright's
|
||||
* `page.clock` is not used: its server records clock state with the Vitest
|
||||
* worker's `Date.now`, which {@link pinHostClock} replaces, so after a
|
||||
* navigation the page clock replays the gap between the real and fixture days
|
||||
* and hands the resulting negative delay to native `setTimeout`. The browser
|
||||
* truncates that delay to a 32-bit integer, and in about half of the dates the
|
||||
* result is a positive delay of weeks that stops every page timer. The `Date`
|
||||
* override is installed for every later document and on the current one, and
|
||||
* stays until the page closes.
|
||||
* @param page - page whose calendar is pinned.
|
||||
*/
|
||||
export async function pinBrowserClock(page: Page): Promise<() => void> {
|
||||
const startedAt = Date.now()
|
||||
const step = async (): Promise<void> => {
|
||||
try {
|
||||
await page.clock.setFixedTime(WEB_FIXTURE_TIME + (Date.now() - startedAt))
|
||||
} catch {
|
||||
// The page closed under the interval; teardown owns the last word.
|
||||
}
|
||||
export async function pinBrowserClock(page: Page): Promise<void> {
|
||||
// The worker's `new Date()` keeps the real clock even when `Date.now` is pinned.
|
||||
const offset = WEB_FIXTURE_TIME - new Date().getTime()
|
||||
const install = (offset: number): void => {
|
||||
const NativeDate = Date
|
||||
const now = (): number => NativeDate.now() + offset
|
||||
globalThis.Date = new Proxy(NativeDate, {
|
||||
construct: (target, args, newTarget): Date => Reflect.construct(target, args.length === 0 ? [now()] : args, newTarget) as Date,
|
||||
apply: () => new NativeDate(now()).toString(),
|
||||
get: (target, property, receiver): unknown => property === 'now' ? now : Reflect.get(target, property, receiver),
|
||||
})
|
||||
}
|
||||
await step()
|
||||
const timer = setInterval(() => { void step() }, PINNED_CLOCK_STEP_MS)
|
||||
return () => { clearInterval(timer) }
|
||||
await page.addInitScript(install, offset)
|
||||
await page.evaluate(install, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Web e2e scenario for the shipped default search composition. A real browser
|
||||
// drives `web_search`; the model stream is replayed while the real DeepSeek
|
||||
// provider calls a deterministic local Anthropic-compatible endpoint through
|
||||
// provider calls a deterministic local Search API endpoint through
|
||||
// the real credentials service.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createServer, type Server } from 'node:http'
|
||||
@@ -50,7 +50,7 @@ function resultSnippet(queryIndex: number, ordinal: number): string {
|
||||
return `Snapshot search ${queryIndex + 1} excerpt ${ordinal}: the harness replays this source list from a local endpoint.`
|
||||
}
|
||||
|
||||
/** One provider result's `page_age`, by 1-based provider order (July 2026 days 01..12). */
|
||||
/** One provider result's publication date, by 1-based provider order (July 2026 days 01..12). */
|
||||
function resultPageAge(ordinal: number): string {
|
||||
return `2026-07-${String(ordinal).padStart(2, '0')}`
|
||||
}
|
||||
@@ -77,7 +77,12 @@ interface CapturedSearchRequest {
|
||||
body: unknown
|
||||
}
|
||||
|
||||
/** Start the deterministic DeepSeek Messages double used by the real provider. */
|
||||
/** The Search API `query` field of one captured request body; `ori_question` also names every query. */
|
||||
function requestQuery(body: unknown): unknown {
|
||||
return typeof body === 'object' && body !== null && 'query' in body ? body.query : undefined
|
||||
}
|
||||
|
||||
/** Start the deterministic DeepSeek Search double used by the real provider. */
|
||||
async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ server: Server; baseURL: string }> {
|
||||
const server = createServer((request, response) => {
|
||||
let body = ''
|
||||
@@ -87,11 +92,10 @@ async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ s
|
||||
const parsedBody: unknown = JSON.parse(body)
|
||||
captured.push({
|
||||
path: request.url ?? '',
|
||||
apiKey: typeof request.headers['x-api-key'] === 'string' ? request.headers['x-api-key'] : undefined,
|
||||
apiKey: typeof request.headers.authorization === 'string' ? request.headers.authorization : undefined,
|
||||
body: parsedBody,
|
||||
})
|
||||
const serializedBody = JSON.stringify(parsedBody)
|
||||
const queryIndex = QUERIES.findIndex(query => serializedBody.includes(`Perform a web search for the query: ${query}`))
|
||||
const queryIndex = QUERIES.findIndex(query => requestQuery(parsedBody) === query)
|
||||
if (queryIndex < 0) {
|
||||
response.writeHead(400, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({ error: 'unknown fixture query' }))
|
||||
@@ -99,26 +103,13 @@ async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ s
|
||||
}
|
||||
response.writeHead(200, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${PROVIDER_RESULT_COUNT} sources.`,
|
||||
citations: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result_location',
|
||||
url: resultUrl(queryIndex, ordinal),
|
||||
cited_text: resultSnippet(queryIndex, ordinal),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'web_search_tool_result',
|
||||
content: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result',
|
||||
url: resultUrl(queryIndex, ordinal),
|
||||
title: resultTitle(queryIndex, ordinal),
|
||||
page_age: resultPageAge(ordinal),
|
||||
})),
|
||||
},
|
||||
],
|
||||
results: RESULT_ORDINALS.map(ordinal => ({
|
||||
url: resultUrl(queryIndex, ordinal),
|
||||
title: resultTitle(queryIndex, ordinal),
|
||||
snippet: resultSnippet(queryIndex, ordinal),
|
||||
content: '', site_name: null, site_icon: null,
|
||||
date: resultPageAge(ordinal),
|
||||
})),
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -133,12 +124,11 @@ async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ s
|
||||
return { server, baseURL: `http://127.0.0.1:${address.port}` }
|
||||
}
|
||||
|
||||
describe('web e2e: shipped default web search', () => {
|
||||
describe('web e2e: opt-in Search API web search', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let searchServer: Server | undefined
|
||||
let searchBaseURL: string
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const searchRequests: CapturedSearchRequest[] = []
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
@@ -146,12 +136,13 @@ describe('web e2e: shipped default web search', () => {
|
||||
beforeAll(async () => {
|
||||
const search = await startSearchServer(searchRequests)
|
||||
searchServer = search.server
|
||||
searchBaseURL = search.baseURL
|
||||
scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
|
||||
compareReplaySession: true,
|
||||
deepSeekSearch: {
|
||||
baseURL: search.baseURL,
|
||||
deepSeekWebApi: {
|
||||
baseURL: `${search.baseURL}/api/v1/tools`,
|
||||
apiKeyEnv: SEARCH_CREDENTIAL_REF,
|
||||
select: true,
|
||||
},
|
||||
...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }),
|
||||
})
|
||||
@@ -197,37 +188,14 @@ describe('web e2e: shipped default web search', () => {
|
||||
it.skipIf(MODE === 'record')('uses the real provider and persists the capped structured result', () => {
|
||||
expect(searchRequests).toHaveLength(QUERIES.length)
|
||||
for (const query of QUERIES) {
|
||||
const request = searchRequests.find(candidate => JSON.stringify(candidate.body).includes(query))
|
||||
const request = searchRequests.find(candidate => requestQuery(candidate.body) === query)
|
||||
if (request === undefined) throw new Error(`missing provider request for query: ${query}`)
|
||||
expect(request).toMatchObject({ path: '/messages', apiKey: SEARCH_CREDENTIAL })
|
||||
expect(request.body).toMatchObject({
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: `Perform a web search for the query: ${query}` }],
|
||||
}],
|
||||
})
|
||||
const tools = (request.body as { tools?: unknown }).tools
|
||||
expect(tools).toHaveLength(1)
|
||||
expect((tools as unknown[])[0]).toMatchObject({ type: 'web_search_20250305', name: 'web_search' })
|
||||
}
|
||||
|
||||
const auxiliaryRequests = sessionEvents.filter(
|
||||
(event): event is Extract<SessionEvent, { type: 'web/deepseek-search-llm-request' }> =>
|
||||
event.type === 'web/deepseek-search-llm-request',
|
||||
)
|
||||
expect(auxiliaryRequests).toHaveLength(QUERIES.length)
|
||||
for (const query of QUERIES) {
|
||||
const request = searchRequests.find(candidate => JSON.stringify(candidate.body).includes(query))
|
||||
const auxiliaryRequest = auxiliaryRequests.find(event => JSON.stringify(event.data.body).includes(query))
|
||||
if (request === undefined || auxiliaryRequest === undefined) {
|
||||
throw new Error(`missing paired provider request for query: ${query}`)
|
||||
}
|
||||
expect(auxiliaryRequest.data).toEqual({
|
||||
endpoint: `${searchBaseURL}/messages`,
|
||||
apiVersion: '2023-06-01',
|
||||
body: request.body,
|
||||
expect(request).toMatchObject({ path: '/api/v1/tools/search', apiKey: `Bearer ${SEARCH_CREDENTIAL}` })
|
||||
expect(request.body).toEqual({
|
||||
query, count: WEB_SEARCH_MAX_RESULTS, need_content: false, ori_question: Array.from(PROMPT).slice(0, 128).join(''),
|
||||
})
|
||||
}
|
||||
expect(sessionEvents.filter(event => event.type === 'web/deepseek-search-llm-request')).toEqual([])
|
||||
|
||||
const searchCall = sessionEvents.find(
|
||||
(event): event is Extract<SessionEvent, { type: 'tool/call' }> =>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"tests/developer-tools-settings.e2e.ts",
|
||||
"tests/models-settings.e2e.ts",
|
||||
"tests/deepseek-messages-settings.e2e.ts",
|
||||
"tests/deepseek-model-label-1017.e2e.ts",
|
||||
"tests/deepseek-messages-chat.e2e.ts",
|
||||
"tests/models-settings-recovery.e2e.ts",
|
||||
"tests/default-model.e2e.ts",
|
||||
@@ -171,6 +172,7 @@
|
||||
"tests/queue-image.e2e.ts",
|
||||
"tests/skill-invocation-policy.e2e.ts",
|
||||
"tests/skill-user-invoke.e2e.ts",
|
||||
"tests/skill-menu-plugin-change.e2e.ts",
|
||||
"tests/permission-policy-context.e2e.ts",
|
||||
"tests/access-confirmation.e2e.ts",
|
||||
"tests/agent-preset-selection.e2e.ts",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 5ef3da69b4de1d8a
|
||||
/capability-seams-and-core-services:
|
||||
en: 048c8c150734d0dd
|
||||
zh: 836dd6c78d4c8278
|
||||
en: c0c69a4be20d2157
|
||||
zh: 9218ae36d2fe4a04
|
||||
|
||||
@@ -264,6 +264,7 @@ flowchart LR
|
||||
pkg_web_search_exa["web-search-exa"]
|
||||
pkg_web_search_perplexity["web-search-perplexity"]
|
||||
pkg_web_search_deepseek["web-search-deepseek"]
|
||||
pkg_web_deepseek_api["web-deepseek-api"]
|
||||
pkg_web_fetch_http["web-fetch-http"]
|
||||
pkg_spill["spill"]
|
||||
svc_spillStore["ctx.spillStore<br/>Spill storage seam"]
|
||||
@@ -439,6 +440,7 @@ flowchart LR
|
||||
pkg_user_questions --> svc_userQuestions
|
||||
pkg_web --> svc_web
|
||||
pkg_web_app --> svc_webApp
|
||||
pkg_web_deepseek_api --> svc_web
|
||||
pkg_web_fetch_http --> svc_web
|
||||
pkg_web_search_deepseek --> svc_web
|
||||
pkg_web_search_exa --> svc_web
|
||||
@@ -697,7 +699,7 @@ flowchart LR
|
||||
| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | Loads mods that defineMod plugins add, raises their hook chains from harness extension points, and draws the band above the prompt over its Remote. |
|
||||
| `ctx.inspector` | `core` | `inspector` | - | - | - | Owns the Worker-hosted CDP target and the transport-independent Host and Client observation and Cordis-tree query API. |
|
||||
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | Producers (background bash/pwsh and PTY sends) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-deepseek-api`](../packages/web/web-deepseek-api), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
|
||||
| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
|
||||
| `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. |
|
||||
|
||||
@@ -266,6 +266,7 @@ flowchart LR
|
||||
pkg_web_search_exa["web-search-exa"]
|
||||
pkg_web_search_perplexity["web-search-perplexity"]
|
||||
pkg_web_search_deepseek["web-search-deepseek"]
|
||||
pkg_web_deepseek_api["web-deepseek-api"]
|
||||
pkg_web_fetch_http["web-fetch-http"]
|
||||
pkg_spill["spill"]
|
||||
svc_spillStore["ctx.spillStore<br/>Spill storage seam"]
|
||||
@@ -441,6 +442,7 @@ flowchart LR
|
||||
pkg_user_questions --> svc_userQuestions
|
||||
pkg_web --> svc_web
|
||||
pkg_web_app --> svc_webApp
|
||||
pkg_web_deepseek_api --> svc_web
|
||||
pkg_web_fetch_http --> svc_web
|
||||
pkg_web_search_deepseek --> svc_web
|
||||
pkg_web_search_exa --> svc_web
|
||||
@@ -699,7 +701,7 @@ flowchart LR
|
||||
| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | 加载 defineMod 插件添加的模组,从 harness 扩展点触发其钩子链,并通过其 Remote 绘制提示框上方的横幅。 |
|
||||
| `ctx.inspector` | `core` | `inspector` | - | - | - | 负责 Worker 托管的 CDP target,以及独立于传输的 Host 和 Client observation 与 Cordis tree query API。 |
|
||||
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | 生产方(后台 bash/pwsh 与 PTY 发送)登记正在运行的工作;声明 record 的 job 还为非消费观察者流式提供原始输出;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-deepseek-api`](../packages/web/web-deepseek-api), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
|
||||
| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
|
||||
| `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 |
|
||||
|
||||
+47
-10
@@ -4049,7 +4049,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections`
|
||||
- `refs`: [`AgentOptions`](subsystems/core.md)
|
||||
- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
- `source`: [`packages/subagent/tool-subagent/src/index.ts:46`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Config: which registered provider this tool delegates to, plus child defaults. */
|
||||
@@ -4062,10 +4062,17 @@ export interface Config extends PresetSelectionConfig {
|
||||
*/
|
||||
toolName?: string
|
||||
/**
|
||||
* Sample the Host `subagent-model-selection` setting for each new top-level
|
||||
* Session and inherit that decision in its child Sessions.
|
||||
* Expose optional child `provider`, `model`, and `reasoning_effort` fields.
|
||||
* Requires provider `agentOptions` support. Execution enforces the Session
|
||||
* policy, or restricts explicit selection to the current parent route.
|
||||
*/
|
||||
modelSelectionSettings?: boolean
|
||||
modelSelection?: boolean
|
||||
/**
|
||||
* Register the shared `list_subagent_models` tool for this scope.
|
||||
* Enable on only one instance per scope; defaults to false and does not
|
||||
* enable model selection. Discovery is removed when this instance unloads.
|
||||
*/
|
||||
modelDiscovery?: boolean
|
||||
/**
|
||||
* Agent options applied to every child; omitted fields use child-loop defaults.
|
||||
*/
|
||||
@@ -4306,20 +4313,23 @@ export type ApprovalPolicy = 'ask' | 'never'
|
||||
|
||||
## `@deepseek-ai/dsh-web`
|
||||
|
||||
- `source`: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web/src/index.ts:57`](../packages/web/web/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
|
||||
* wins for each capability; both are optional (a single registered usable
|
||||
* provider auto-selects). Operational overrides such as environment variables
|
||||
* must feed these same fields rather than introduce a hidden priority chain.
|
||||
* provider auto-selects). Each operation reads the current value, so a settings
|
||||
* change applies to the next search or fetch without remounting the service.
|
||||
* Operational overrides such as environment variables must feed these same
|
||||
* fields rather than introduce a hidden priority chain.
|
||||
*/
|
||||
export interface WebRuntimeConfig {
|
||||
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
|
||||
readonly searchProvider?: string
|
||||
readonly searchProvider: Volatile<string | undefined>
|
||||
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
|
||||
readonly fetchProvider?: string
|
||||
readonly fetchProvider: Volatile<string | undefined>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web -->
|
||||
@@ -4356,6 +4366,32 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web-app -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-web-deepseek-api -->
|
||||
<a id="deepseek-aidsh-web-deepseek-api"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-web-deepseek-api`
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:34`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Settings read by the next search or fetch without re-registering either provider. */
|
||||
export interface Config {
|
||||
/** Literal DeepSeek API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
|
||||
apiKey: Volatile<string | undefined>
|
||||
/** Credential reference resolved for each operation; defaults to `DEEPSEEK_API_KEY`. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
/** API base including its path prefix; `/search` and `/fetch` are appended. */
|
||||
baseURL: Volatile<string>
|
||||
/** Search API results per search, from 1 through 50; a smaller request bound wins. Defaults to 10. */
|
||||
count: Volatile<number>
|
||||
/** Send the triggering user question, bounded to 128 characters, as `ori_question`. Defaults to true. */
|
||||
sendOriginalQuestion: Volatile<boolean>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web-deepseek-api -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-web-fetch-http -->
|
||||
<a id="deepseek-aidsh-web-fetch-http"></a>
|
||||
|
||||
@@ -4580,6 +4616,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
|
||||
@@ -4662,7 +4699,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) |
|
||||
|
||||
+47
-10
@@ -4051,7 +4051,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections`
|
||||
- `refs`: [`AgentOptions`](subsystems/core.zh.md)
|
||||
- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
- `source`: [`packages/subagent/tool-subagent/src/index.ts:46`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Config: which registered provider this tool delegates to, plus child defaults. */
|
||||
@@ -4064,10 +4064,17 @@ export interface Config extends PresetSelectionConfig {
|
||||
*/
|
||||
toolName?: string
|
||||
/**
|
||||
* Sample the Host `subagent-model-selection` setting for each new top-level
|
||||
* Session and inherit that decision in its child Sessions.
|
||||
* Expose optional child `provider`, `model`, and `reasoning_effort` fields.
|
||||
* Requires provider `agentOptions` support. Execution enforces the Session
|
||||
* policy, or restricts explicit selection to the current parent route.
|
||||
*/
|
||||
modelSelectionSettings?: boolean
|
||||
modelSelection?: boolean
|
||||
/**
|
||||
* Register the shared `list_subagent_models` tool for this scope.
|
||||
* Enable on only one instance per scope; defaults to false and does not
|
||||
* enable model selection. Discovery is removed when this instance unloads.
|
||||
*/
|
||||
modelDiscovery?: boolean
|
||||
/**
|
||||
* Agent options applied to every child; omitted fields use child-loop defaults.
|
||||
*/
|
||||
@@ -4308,20 +4315,23 @@ export type ApprovalPolicy = 'ask' | 'never'
|
||||
|
||||
## `@deepseek-ai/dsh-web`
|
||||
|
||||
- `source`: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web/src/index.ts:57`](../packages/web/web/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
|
||||
* wins for each capability; both are optional (a single registered usable
|
||||
* provider auto-selects). Operational overrides such as environment variables
|
||||
* must feed these same fields rather than introduce a hidden priority chain.
|
||||
* provider auto-selects). Each operation reads the current value, so a settings
|
||||
* change applies to the next search or fetch without remounting the service.
|
||||
* Operational overrides such as environment variables must feed these same
|
||||
* fields rather than introduce a hidden priority chain.
|
||||
*/
|
||||
export interface WebRuntimeConfig {
|
||||
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
|
||||
readonly searchProvider?: string
|
||||
readonly searchProvider: Volatile<string | undefined>
|
||||
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
|
||||
readonly fetchProvider?: string
|
||||
readonly fetchProvider: Volatile<string | undefined>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web -->
|
||||
@@ -4358,6 +4368,32 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web-app -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-web-deepseek-api -->
|
||||
<a id="deepseek-aidsh-web-deepseek-api"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-web-deepseek-api`
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:34`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Settings read by the next search or fetch without re-registering either provider. */
|
||||
export interface Config {
|
||||
/** Literal DeepSeek API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
|
||||
apiKey: Volatile<string | undefined>
|
||||
/** Credential reference resolved for each operation; defaults to `DEEPSEEK_API_KEY`. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
/** API base including its path prefix; `/search` and `/fetch` are appended. */
|
||||
baseURL: Volatile<string>
|
||||
/** Search API results per search, from 1 through 50; a smaller request bound wins. Defaults to 10. */
|
||||
count: Volatile<number>
|
||||
/** Send the triggering user question, bounded to 128 characters, as `ori_question`. Defaults to true. */
|
||||
sendOriginalQuestion: Volatile<boolean>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-web-deepseek-api -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-web-fetch-http -->
|
||||
<a id="deepseek-aidsh-web-fetch-http"></a>
|
||||
|
||||
@@ -4582,6 +4618,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
|
||||
@@ -4664,7 +4701,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) |
|
||||
|
||||
@@ -18,7 +18,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`compaction-basic`](../packages/compaction/compaction-basic), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -41,8 +41,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:377`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:99`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `remotes` |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:95`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:91`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:97`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:93`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`compaction-basic`](../packages/compaction/compaction-basic), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -43,8 +43,8 @@
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:377`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:99`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `remotes` |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:95`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:91`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:97`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:93`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
|
||||
+14
-2
@@ -84,6 +84,7 @@ flowchart TD
|
||||
subgraph group_web["packages/web"]
|
||||
pkg_tool_web["tool-web"]
|
||||
pkg_web["web"]
|
||||
pkg_web_deepseek_api["web-deepseek-api"]
|
||||
pkg_web_fetch_http["web-fetch-http"]
|
||||
pkg_web_search_deepseek["web-search-deepseek"]
|
||||
pkg_web_search_exa["web-search-exa"]
|
||||
@@ -160,6 +161,7 @@ flowchart TD
|
||||
pkg_client_ui_chat["client-ui-chat"]
|
||||
pkg_client_ui_commands["client-ui-commands"]
|
||||
pkg_client_ui_conversation["client-ui-conversation"]
|
||||
pkg_client_ui_deepseek_model_label_1017["client-ui-deepseek-model-label-1017"]
|
||||
pkg_client_ui_deliverables["client-ui-deliverables"]
|
||||
pkg_client_ui_directory_picker_browse["client-ui-directory-picker-browse"]
|
||||
pkg_client_ui_directory_picker_native["client-ui-directory-picker-native"]
|
||||
@@ -657,6 +659,7 @@ flowchart TD
|
||||
pkg_deepseek_account --> pkg_agent
|
||||
pkg_deepseek_account --> pkg_brand
|
||||
pkg_deepseek_account --> pkg_llm
|
||||
pkg_deepseek_account --> pkg_util_values
|
||||
pkg_commands --> pkg_agent
|
||||
pkg_commands --> pkg_attachment
|
||||
pkg_commands --> pkg_brand
|
||||
@@ -730,6 +733,12 @@ flowchart TD
|
||||
pkg_fs_sandbox --> pkg_fs_local
|
||||
pkg_fs_sandbox --> pkg_sandbox
|
||||
pkg_fs_sandbox --> pkg_sandbox_policy
|
||||
pkg_web_deepseek_api --> pkg_agent
|
||||
pkg_web_deepseek_api --> pkg_credentials
|
||||
pkg_web_deepseek_api --> pkg_deepseek_account
|
||||
pkg_web_deepseek_api --> pkg_launch_environment
|
||||
pkg_web_deepseek_api --> pkg_session
|
||||
pkg_web_deepseek_api --> pkg_web
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_deepseek_account
|
||||
@@ -1261,6 +1270,7 @@ flowchart TD
|
||||
pkg_tool_subagent --> pkg_subagent
|
||||
pkg_tool_subagent --> pkg_system_prompt
|
||||
pkg_tool_subagent --> pkg_tools
|
||||
pkg_tool_subagent_control --> pkg_agent
|
||||
pkg_tool_subagent_control --> pkg_llm
|
||||
pkg_tool_subagent_control --> pkg_session
|
||||
pkg_tool_subagent_control --> pkg_subagent
|
||||
@@ -1467,6 +1477,7 @@ flowchart TD
|
||||
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | — |
|
||||
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | — |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | — |
|
||||
| [`client-ui-deepseek-model-label-1017`](../packages/client/ui-deepseek-model-label-1017) | `client` | — |
|
||||
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | — |
|
||||
| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | — |
|
||||
| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | — |
|
||||
@@ -1634,7 +1645,7 @@ flowchart TD
|
||||
| [`harness-dev-context`](../packages/context/harness-dev-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`util-values`](../packages/util/values) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1650,6 +1661,7 @@ flowchart TD
|
||||
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
|
||||
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
|
||||
| [`web-deepseek-api`](../packages/web/web-deepseek-api) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1744,7 +1756,7 @@ flowchart TD
|
||||
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
+14
-2
@@ -86,6 +86,7 @@ flowchart TD
|
||||
subgraph group_web["packages/web"]
|
||||
pkg_tool_web["tool-web"]
|
||||
pkg_web["web"]
|
||||
pkg_web_deepseek_api["web-deepseek-api"]
|
||||
pkg_web_fetch_http["web-fetch-http"]
|
||||
pkg_web_search_deepseek["web-search-deepseek"]
|
||||
pkg_web_search_exa["web-search-exa"]
|
||||
@@ -162,6 +163,7 @@ flowchart TD
|
||||
pkg_client_ui_chat["client-ui-chat"]
|
||||
pkg_client_ui_commands["client-ui-commands"]
|
||||
pkg_client_ui_conversation["client-ui-conversation"]
|
||||
pkg_client_ui_deepseek_model_label_1017["client-ui-deepseek-model-label-1017"]
|
||||
pkg_client_ui_deliverables["client-ui-deliverables"]
|
||||
pkg_client_ui_directory_picker_browse["client-ui-directory-picker-browse"]
|
||||
pkg_client_ui_directory_picker_native["client-ui-directory-picker-native"]
|
||||
@@ -659,6 +661,7 @@ flowchart TD
|
||||
pkg_deepseek_account --> pkg_agent
|
||||
pkg_deepseek_account --> pkg_brand
|
||||
pkg_deepseek_account --> pkg_llm
|
||||
pkg_deepseek_account --> pkg_util_values
|
||||
pkg_commands --> pkg_agent
|
||||
pkg_commands --> pkg_attachment
|
||||
pkg_commands --> pkg_brand
|
||||
@@ -732,6 +735,12 @@ flowchart TD
|
||||
pkg_fs_sandbox --> pkg_fs_local
|
||||
pkg_fs_sandbox --> pkg_sandbox
|
||||
pkg_fs_sandbox --> pkg_sandbox_policy
|
||||
pkg_web_deepseek_api --> pkg_agent
|
||||
pkg_web_deepseek_api --> pkg_credentials
|
||||
pkg_web_deepseek_api --> pkg_deepseek_account
|
||||
pkg_web_deepseek_api --> pkg_launch_environment
|
||||
pkg_web_deepseek_api --> pkg_session
|
||||
pkg_web_deepseek_api --> pkg_web
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_deepseek_account
|
||||
@@ -1263,6 +1272,7 @@ flowchart TD
|
||||
pkg_tool_subagent --> pkg_subagent
|
||||
pkg_tool_subagent --> pkg_system_prompt
|
||||
pkg_tool_subagent --> pkg_tools
|
||||
pkg_tool_subagent_control --> pkg_agent
|
||||
pkg_tool_subagent_control --> pkg_llm
|
||||
pkg_tool_subagent_control --> pkg_session
|
||||
pkg_tool_subagent_control --> pkg_subagent
|
||||
@@ -1469,6 +1479,7 @@ flowchart TD
|
||||
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | — |
|
||||
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | — |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | — |
|
||||
| [`client-ui-deepseek-model-label-1017`](../packages/client/ui-deepseek-model-label-1017) | `client` | — |
|
||||
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | — |
|
||||
| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | — |
|
||||
| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | — |
|
||||
@@ -1636,7 +1647,7 @@ flowchart TD
|
||||
| [`harness-dev-context`](../packages/context/harness-dev-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`util-values`](../packages/util/values) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1652,6 +1663,7 @@ flowchart TD
|
||||
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
|
||||
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
|
||||
| [`web-deepseek-api`](../packages/web/web-deepseek-api) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1746,7 +1758,7 @@ flowchart TD
|
||||
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 1387c4505dde2faf
|
||||
zh: 48570fa1c13d6910
|
||||
/session-persistence-event-catalog/persistence-type-fingerprints:
|
||||
en: a696c43a89d5fbe8
|
||||
zh: 6855eb0239b8dcb3
|
||||
en: bcff48893d904af8
|
||||
zh: f82e75c91c39adb1
|
||||
/session-persistence-event-catalog/event-envelope:
|
||||
en: 689f781d95689e20
|
||||
zh: 602f6a7f0835d698
|
||||
@@ -291,8 +291,8 @@
|
||||
en: d278748103cf1640
|
||||
zh: d278748103cf1640
|
||||
/session-persistence-event-catalog/events/web/web-deepseek-search-llm-request-log-only:
|
||||
en: f12c598242992aa3
|
||||
zh: 08609553e15a91f3
|
||||
en: a3e7718f295ba130
|
||||
zh: e6435b44b42f0784
|
||||
/session-persistence-event-catalog/events/working-directory:
|
||||
en: e8dd0cebbb930eb7
|
||||
zh: e8dd0cebbb930eb7
|
||||
@@ -456,8 +456,11 @@
|
||||
en: 42389e30f99e210f
|
||||
zh: 42389e30f99e210f
|
||||
/session-persistence-event-catalog/resolved-persistence-types/developer-message:
|
||||
en: e9a6abc2549ddf91
|
||||
zh: e9a6abc2549ddf91
|
||||
en: 65261480029e2779
|
||||
zh: 65261480029e2779
|
||||
/session-persistence-event-catalog/resolved-persistence-types/disabled:
|
||||
en: acde9453c30a141f
|
||||
zh: acde9453c30a141f
|
||||
/session-persistence-event-catalog/resolved-persistence-types/dispatch:
|
||||
en: aae8924fd5eb9013
|
||||
zh: aae8924fd5eb9013
|
||||
@@ -999,11 +1002,11 @@
|
||||
en: 6e11a7855b002f20
|
||||
zh: d752566bb2b75948
|
||||
/session-persistence-event-catalog/resolved-persistence-types/continuablesubagentdescriptordata:
|
||||
en: 586743dfeab78c55
|
||||
zh: 26ccef4a02dd7a58
|
||||
en: a6a2e38fdd6d127a
|
||||
zh: 1fe82356d89f5769
|
||||
/session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest:
|
||||
en: db71ecccc1e5409e
|
||||
zh: 7b55744e022dc102
|
||||
en: 128c0ecc1897eb68
|
||||
zh: e4c67c92752bcc2a
|
||||
/session-persistence-event-catalog/resolved-persistence-types/developermessage:
|
||||
en: 997b5db27df00446
|
||||
zh: 07e5197ddd7bd59b
|
||||
@@ -1860,11 +1863,11 @@
|
||||
en: 0b74295f7c5c8f11
|
||||
zh: 5f1213cec87f6be0
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-working-directory:
|
||||
en: 9848d783b7b5af16
|
||||
zh: f161eaf1a841cd55
|
||||
/session-persistence-event-catalog/resolved-persistence-types/max-tokens-messages-model-tools:
|
||||
en: afedf4a31a67eb14
|
||||
zh: 98d3bc882f2b61c3
|
||||
en: 1a6c0842d549a5f9
|
||||
zh: cde31cc53cbcab66
|
||||
/session-persistence-event-catalog/resolved-persistence-types/max-tokens-messages-model-thinking:
|
||||
en: 3216b23c131d0070
|
||||
zh: 73ca171c6014f16e
|
||||
/session-persistence-event-catalog/resolved-persistence-types/member-teamid-version:
|
||||
en: 82b0989dad6bbfc0
|
||||
zh: 0789b63f1d59b2ed
|
||||
@@ -1968,8 +1971,11 @@
|
||||
en: a4d9eab2928e9b30
|
||||
zh: b36702c866518273
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-developer-message:
|
||||
en: ed8c78af3a89ba8f
|
||||
zh: 4f859687aa13c1a3
|
||||
en: a9e6e24b98390923
|
||||
zh: 65addae898d8aaec
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-disabled:
|
||||
en: 585627a50ece4e5f
|
||||
zh: 157837aee3da0ac3
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-feedback-message-delete:
|
||||
en: 204561e2311db97e
|
||||
zh: 77264396bd5e25c4
|
||||
@@ -2124,11 +2130,11 @@
|
||||
en: 8eec3bb6eee5085b
|
||||
zh: 04bb3281fd7bd95c
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-user-message:
|
||||
en: afab869c0592b46c
|
||||
zh: f15874a1e728ab0d
|
||||
en: d6d9401f19a66ee8
|
||||
zh: 569fdddf887660a2
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request:
|
||||
en: f109997e295af37d
|
||||
zh: b2dcfd599f73b61c
|
||||
en: 99c8837344178a47
|
||||
zh: bf746f5678346596
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-web-search-20250305:
|
||||
en: e0071555851c7cee
|
||||
zh: 1f368baf78a7d689
|
||||
|
||||
+35
-14
@@ -81,7 +81,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
|
||||
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
|
||||
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
|
||||
| `event:user/message` | event | `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` | [`{ type: "user/message" }`](#persistence-type-sha256-2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) |
|
||||
| `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) |
|
||||
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
|
||||
|
||||
@@ -963,9 +963,9 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent
|
||||
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* Records that this session's delegation tool exposes child provider,
|
||||
* model, and reasoning-effort selection. Appended before the first model
|
||||
* request; absence means the fixed-route definition. Log-only: it carries
|
||||
* Records this Session’s authorized child provider/model routes. Appended
|
||||
* before the first model request; absence restricts explicit selection
|
||||
* to the current parent route. Log-only: it carries
|
||||
* no `surfaceOp` and never enters model history.
|
||||
*/
|
||||
'subagent/model-selection-policy': {
|
||||
@@ -1300,7 +1300,7 @@ Source: [`packages/core/session/src/types.ts:310`](../packages/core/session/src/
|
||||
'web/deepseek-search-llm-request': DeepSeekSearchLlmRequest
|
||||
```
|
||||
|
||||
Source: [`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
Source: [`packages/web/web-search-deepseek/src/provider.ts:89`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
### `working-directory/*`
|
||||
|
||||
@@ -1739,6 +1739,14 @@ SHA-256: `1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c`
|
||||
|
||||
`"developer/message"`
|
||||
|
||||
<a id="persistence-type-sha256-0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce"></a>
|
||||
|
||||
### `"disabled"`
|
||||
|
||||
SHA-256: `0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce`
|
||||
|
||||
`"disabled"`
|
||||
|
||||
<a id="persistence-type-sha256-26c879bdd130e8b8683a6ce588414fad2244783d6573ca399fa0de184a2b0bb1"></a>
|
||||
|
||||
### `"dispatch"`
|
||||
@@ -3339,7 +3347,7 @@ Sources: [`packages/subagent/subagent/src/descriptor.ts:72`](../packages/subagen
|
||||
| `toolFilter` | optional | [`ToolRestriction`](#persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f) |
|
||||
| `version` | required | `number` |
|
||||
|
||||
<a id="persistence-type-sha256-2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9"></a>
|
||||
<a id="persistence-type-sha256-abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c"></a>
|
||||
|
||||
<a id="persistence-type-deepseeksearchllmrequest"></a>
|
||||
|
||||
@@ -3347,14 +3355,14 @@ Sources: [`packages/subagent/subagent/src/descriptor.ts:72`](../packages/subagen
|
||||
|
||||
### `DeepSeekSearchLlmRequest`
|
||||
|
||||
SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
|
||||
SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c`
|
||||
|
||||
Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `apiVersion` | required | `string` |
|
||||
| `body` | required | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
|
||||
| `body` | required | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) |
|
||||
| `endpoint` | required | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7"></a>
|
||||
@@ -7677,11 +7685,11 @@ Sources: [`packages/session/working-directory/src/index.ts:29`](../packages/sess
|
||||
|---|---|---|
|
||||
| `kind` | required | `"working-directory"` |
|
||||
|
||||
<a id="persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731"></a>
|
||||
<a id="persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee"></a>
|
||||
|
||||
### `{ max_tokens, messages, model, tools }`
|
||||
### `{ max_tokens, messages, model, thinking?, … }`
|
||||
|
||||
SHA-256: `930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731`
|
||||
SHA-256: `9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee`
|
||||
|
||||
Sources: [`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
@@ -7690,6 +7698,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web
|
||||
| `max_tokens` | required | `number` |
|
||||
| `messages` | required | [`tuple (1 positions)`](#persistence-type-sha256-b993441f8ce7d27b80e619e113e232ae8e62d3d5a8340f6bfe12d8c4c018e62f) |
|
||||
| `model` | required | `string` |
|
||||
| `thinking` | optional | [`{ type: "disabled" }`](#persistence-type-sha256-37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1) |
|
||||
| `tools` | required | [`tuple (1 positions)`](#persistence-type-sha256-4aed17ec726b5397c29f6cb4bcd7905dc1a12bb6140f1657f073cd24380f2af8) |
|
||||
|
||||
<a id="persistence-type-sha256-b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1"></a>
|
||||
@@ -8203,6 +8212,18 @@ SHA-256: `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c`
|
||||
| `time` | required | `number` |
|
||||
| `type` | required | `"developer/message"` |
|
||||
|
||||
<a id="persistence-type-sha256-37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1"></a>
|
||||
|
||||
### `{ type: "disabled" }`
|
||||
|
||||
SHA-256: `37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1`
|
||||
|
||||
Sources: [`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `type` | required | `"disabled"` |
|
||||
|
||||
<a id="persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"></a>
|
||||
|
||||
<a id="persistence-type-eventfeedbackmessage-delete"></a>
|
||||
@@ -9034,17 +9055,17 @@ SHA-256: `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8`
|
||||
| `time` | required | `number` |
|
||||
| `type` | required | `"user/message"` |
|
||||
|
||||
<a id="persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331"></a>
|
||||
<a id="persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"></a>
|
||||
|
||||
<a id="persistence-type-eventwebdeepseek-search-llm-request"></a>
|
||||
|
||||
### `{ type: "web/deepseek-search-llm-request" }`
|
||||
|
||||
SHA-256: `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331`
|
||||
SHA-256: `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52`
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `data` | required | [`DeepSeekSearchLlmRequest`](#persistence-type-sha256-2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9) |
|
||||
| `data` | required | [`DeepSeekSearchLlmRequest`](#persistence-type-sha256-abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c) |
|
||||
| `ignorable` | optional | `true` |
|
||||
| `seq` | required | `number` |
|
||||
| `time` | required | `number` |
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
|
||||
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
|
||||
| `event:user/message` | event | `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8` | [`{ type: "user/message" }`](#persistence-type-sha256-2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) |
|
||||
| `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) |
|
||||
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
|
||||
|
||||
@@ -965,9 +965,9 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* Records that this session's delegation tool exposes child provider,
|
||||
* model, and reasoning-effort selection. Appended before the first model
|
||||
* request; absence means the fixed-route definition. Log-only: it carries
|
||||
* Records this Session’s authorized child provider/model routes. Appended
|
||||
* before the first model request; absence restricts explicit selection
|
||||
* to the current parent route. Log-only: it carries
|
||||
* no `surfaceOp` and never enters model history.
|
||||
*/
|
||||
'subagent/model-selection-policy': {
|
||||
@@ -1302,7 +1302,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
'web/deepseek-search-llm-request': DeepSeekSearchLlmRequest
|
||||
```
|
||||
|
||||
来源:[`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
来源:[`packages/web/web-search-deepseek/src/provider.ts:89`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
### `working-directory/*`
|
||||
|
||||
@@ -1741,6 +1741,14 @@ SHA-256: `1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c`
|
||||
|
||||
`"developer/message"`
|
||||
|
||||
<a id="persistence-type-sha256-0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce"></a>
|
||||
|
||||
### `"disabled"`
|
||||
|
||||
SHA-256: `0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce`
|
||||
|
||||
`"disabled"`
|
||||
|
||||
<a id="persistence-type-sha256-26c879bdd130e8b8683a6ce588414fad2244783d6573ca399fa0de184a2b0bb1"></a>
|
||||
|
||||
### `"dispatch"`
|
||||
@@ -3341,7 +3349,7 @@ SHA-256: `6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f`
|
||||
| `toolFilter` | 可选 | [`ToolRestriction`](#persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f) |
|
||||
| `version` | 必需 | `number` |
|
||||
|
||||
<a id="persistence-type-sha256-2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9"></a>
|
||||
<a id="persistence-type-sha256-abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c"></a>
|
||||
|
||||
<a id="persistence-type-deepseeksearchllmrequest"></a>
|
||||
|
||||
@@ -3349,14 +3357,14 @@ SHA-256: `6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f`
|
||||
|
||||
### `DeepSeekSearchLlmRequest`
|
||||
|
||||
SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
|
||||
SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c`
|
||||
|
||||
来源:[`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `apiVersion` | 必需 | `string` |
|
||||
| `body` | 必需 | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
|
||||
| `body` | 必需 | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) |
|
||||
| `endpoint` | 必需 | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-0f6265188dae7ba6faa26830a180bb4deb0760561f97ee0c9d399ccf6c9cf1f7"></a>
|
||||
@@ -7679,11 +7687,11 @@ SHA-256: `2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5`
|
||||
|---|---|---|
|
||||
| `kind` | 必需 | `"working-directory"` |
|
||||
|
||||
<a id="persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731"></a>
|
||||
<a id="persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee"></a>
|
||||
|
||||
### `{ max_tokens, messages, model, tools }`
|
||||
### `{ max_tokens, messages, model, thinking?, … }`
|
||||
|
||||
SHA-256: `930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731`
|
||||
SHA-256: `9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee`
|
||||
|
||||
来源:[`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
@@ -7692,6 +7700,7 @@ SHA-256: `930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731`
|
||||
| `max_tokens` | 必需 | `number` |
|
||||
| `messages` | 必需 | [`tuple (1 positions)`](#persistence-type-sha256-b993441f8ce7d27b80e619e113e232ae8e62d3d5a8340f6bfe12d8c4c018e62f) |
|
||||
| `model` | 必需 | `string` |
|
||||
| `thinking` | 可选 | [`{ type: "disabled" }`](#persistence-type-sha256-37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1) |
|
||||
| `tools` | 必需 | [`tuple (1 positions)`](#persistence-type-sha256-4aed17ec726b5397c29f6cb4bcd7905dc1a12bb6140f1657f073cd24380f2af8) |
|
||||
|
||||
<a id="persistence-type-sha256-b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1"></a>
|
||||
@@ -8205,6 +8214,18 @@ SHA-256: `97e0162f983c27456ec71269fb4976b1851588b8c1aeac5d071d43e11f95e03c`
|
||||
| `time` | 必需 | `number` |
|
||||
| `type` | 必需 | `"developer/message"` |
|
||||
|
||||
<a id="persistence-type-sha256-37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1"></a>
|
||||
|
||||
### `{ type: "disabled" }`
|
||||
|
||||
SHA-256: `37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1`
|
||||
|
||||
来源:[`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `type` | 必需 | `"disabled"` |
|
||||
|
||||
<a id="persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"></a>
|
||||
|
||||
<a id="persistence-type-eventfeedbackmessage-delete"></a>
|
||||
@@ -9036,17 +9057,17 @@ SHA-256: `2ae8594a998c9862a597129e782277bcc518266712a703b5c3161bdf552d13b8`
|
||||
| `time` | 必需 | `number` |
|
||||
| `type` | 必需 | `"user/message"` |
|
||||
|
||||
<a id="persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331"></a>
|
||||
<a id="persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"></a>
|
||||
|
||||
<a id="persistence-type-eventwebdeepseek-search-llm-request"></a>
|
||||
|
||||
### `{ type: "web/deepseek-search-llm-request" }`
|
||||
|
||||
SHA-256: `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331`
|
||||
SHA-256: `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52`
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `data` | 必需 | [`DeepSeekSearchLlmRequest`](#persistence-type-sha256-2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9) |
|
||||
| `data` | 必需 | [`DeepSeekSearchLlmRequest`](#persistence-type-sha256-abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c) |
|
||||
| `ignorable` | 可选 | `true` |
|
||||
| `seq` | 必需 | `number` |
|
||||
| `time` | 必需 | `number` |
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Bilingual-pair consistency record for 2026-10-13-deepseek-search-thinking.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 docs/persistence-changes/2026-10-13-deepseek-search-thinking.md
|
||||
/:
|
||||
en: 3d5a424ae7d7e46b
|
||||
zh: c22a8e4a62d8cc1a
|
||||
/2026-10-13-deepseek-search-thinking:
|
||||
en: 033d5d72a19a029b
|
||||
zh: 334111dcdead54d8
|
||||
/2026-10-13-deepseek-search-thinking/summary:
|
||||
en: 398b25bff3dd0fd8
|
||||
zh: fef39218a015d80b
|
||||
/2026-10-13-deepseek-search-thinking/table-of-contents:
|
||||
en: 6cf314a069ceb451
|
||||
zh: 9adff678e9ecab3b
|
||||
/2026-10-13-deepseek-search-thinking/declaration:
|
||||
en: 6907136f28e48c90
|
||||
zh: 02f8298485f3d8e7
|
||||
/2026-10-13-deepseek-search-thinking/compatibility:
|
||||
en: 2729e86f71451364
|
||||
zh: af19f76f7fee238b
|
||||
/2026-10-13-deepseek-search-thinking/verification:
|
||||
en: ce8adf6d438b6b22
|
||||
zh: 6c4a421051c99dce
|
||||
/2026-10-13-deepseek-search-thinking/dev-note:
|
||||
en: b75c8458928b72b9
|
||||
zh: 003ba4e17b4b5269
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "Records a persistence type transition and its compatibility acknowledgement."
|
||||
kind: persistence-change
|
||||
---
|
||||
|
||||
# 2026-10-13-deepseek-search-thinking
|
||||
|
||||
English | [中文](2026-10-13-deepseek-search-thinking.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Adds the optional `thinking` member to the logged DeepSeek search request body, recording that the auxiliary search turn disables thinking.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Declaration](#declaration)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Verification](#verification)
|
||||
- [Dev Note](#dev-note)
|
||||
|
||||
<a id="declaration"></a>
|
||||
## Declaration
|
||||
|
||||
```yaml persistence-change
|
||||
schemaVersion: 1
|
||||
id: 2026-10-13-deepseek-search-thinking
|
||||
baseline: false
|
||||
changes:
|
||||
- root: "event:web/deepseek-search-llm-request"
|
||||
previous: "2026-09-11-initial"
|
||||
after: "0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"
|
||||
decision: same-version
|
||||
```
|
||||
|
||||
<a id="compatibility"></a>
|
||||
## Compatibility
|
||||
|
||||
Existing records remain valid: absence means the request omitted the field and DeepSeek applied its default. The event is log-only; no reader replays or interprets the body for behavior, and the provider writes the member on every new request.
|
||||
|
||||
<a id="verification"></a>
|
||||
## Verification
|
||||
|
||||
pnpm exec vitest run packages/web/web-search-deepseek/tests/deepseek.spec.ts: 59 tests passed.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## Dev Note
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"roots": [
|
||||
{
|
||||
"key": "event:web/deepseek-search-llm-request",
|
||||
"kind": "event",
|
||||
"event": "web/deepseek-search-llm-request",
|
||||
"surface": false,
|
||||
"digest": "0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52",
|
||||
"schema": {
|
||||
"root": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "data",
|
||||
"type": 1,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "ignorable",
|
||||
"type": 17,
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "seq",
|
||||
"type": 4,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "time",
|
||||
"type": 4,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": 18,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "apiVersion",
|
||||
"type": 2,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"type": 3,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "endpoint",
|
||||
"type": 2,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "primitive",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "max_tokens",
|
||||
"type": 4,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "messages",
|
||||
"type": 5,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"type": 2,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "thinking",
|
||||
"type": 11,
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"type": 13,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "primitive",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"kind": "tuple",
|
||||
"elements": [
|
||||
{
|
||||
"type": 6,
|
||||
"optional": false,
|
||||
"rest": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "content",
|
||||
"type": 7,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"type": 10,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "tuple",
|
||||
"elements": [
|
||||
{
|
||||
"type": 8,
|
||||
"optional": false,
|
||||
"rest": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": 2,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": 9,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "text"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "user"
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": 12,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "disabled"
|
||||
},
|
||||
{
|
||||
"kind": "tuple",
|
||||
"elements": [
|
||||
{
|
||||
"type": 14,
|
||||
"optional": false,
|
||||
"rest": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "max_uses",
|
||||
"type": 4,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": 15,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": 16,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "web_search"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "web_search_20250305"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "web/deepseek-search-llm-request"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"types": []
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "记录持久化类型更改及其兼容性确认。"
|
||||
kind: persistence-change
|
||||
---
|
||||
|
||||
# 2026-10-13-deepseek-search-thinking
|
||||
|
||||
[English](2026-10-13-deepseek-search-thinking.md) | 中文
|
||||
|
||||
## 概述
|
||||
|
||||
为记录的 DeepSeek 搜索请求体新增可选成员 `thinking`,记录辅助搜索回合关闭了思考模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [声明](#declaration)
|
||||
- [兼容性](#compatibility)
|
||||
- [验证](#verification)
|
||||
- [开发备注](#dev-note)
|
||||
|
||||
<a id="declaration"></a>
|
||||
## 声明
|
||||
|
||||
```yaml persistence-change
|
||||
schemaVersion: 1
|
||||
id: 2026-10-13-deepseek-search-thinking
|
||||
baseline: false
|
||||
changes:
|
||||
- root: "event:web/deepseek-search-llm-request"
|
||||
previous: "2026-09-11-initial"
|
||||
after: "0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"
|
||||
decision: same-version
|
||||
```
|
||||
|
||||
<a id="compatibility"></a>
|
||||
## 兼容性
|
||||
|
||||
已有记录仍然有效:缺失表示请求省略了该字段,由 DeepSeek 使用默认值。该事件仅用于日志;没有读取方为行为回放或解释请求体,提供方在每个新请求中写入该成员。
|
||||
|
||||
<a id="verification"></a>
|
||||
## 验证
|
||||
|
||||
pnpm exec vitest run packages/web/web-search-deepseek/tests/deepseek.spec.ts:59 个测试通过。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## 开发备注
|
||||
|
||||
无。
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 14c57f30e676f6e3
|
||||
zh: b1a0ccf26fab3727
|
||||
/session-persistence-formats/format-references:
|
||||
en: 43a4a6600fa37a1a
|
||||
zh: 72174fab8355c344
|
||||
en: 17ef09dfc5a89073
|
||||
zh: 59b8d60acb66c2dc
|
||||
/session-persistence-formats/scope-and-evidence:
|
||||
en: b220062139ddac1e
|
||||
zh: 7c4c0efe05dafa9c
|
||||
|
||||
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
|
||||
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 612 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 614 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
|
||||
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 612 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 614 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
Generated
+60
-28
@@ -22045,7 +22045,7 @@
|
||||
"kind": "event",
|
||||
"event": "web/deepseek-search-llm-request",
|
||||
"surface": false,
|
||||
"digest": "cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331",
|
||||
"digest": "0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52",
|
||||
"schema": {
|
||||
"root": 0,
|
||||
"nodes": [
|
||||
@@ -22059,7 +22059,7 @@
|
||||
},
|
||||
{
|
||||
"name": "ignorable",
|
||||
"type": 15,
|
||||
"type": 17,
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
@@ -22074,7 +22074,7 @@
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": 16,
|
||||
"type": 18,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
@@ -22124,8 +22124,13 @@
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"name": "thinking",
|
||||
"type": 11,
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"type": 13,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
@@ -22195,11 +22200,26 @@
|
||||
"kind": "literal",
|
||||
"value": "user"
|
||||
},
|
||||
{
|
||||
"kind": "object",
|
||||
"properties": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": 12,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"indices": []
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "disabled"
|
||||
},
|
||||
{
|
||||
"kind": "tuple",
|
||||
"elements": [
|
||||
{
|
||||
"type": 12,
|
||||
"type": 14,
|
||||
"optional": false,
|
||||
"rest": false
|
||||
}
|
||||
@@ -22215,12 +22235,12 @@
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": 13,
|
||||
"type": 15,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": 14,
|
||||
"type": 16,
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
@@ -22533,6 +22553,11 @@
|
||||
"packages/goal/goal/src/types.ts:20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce",
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1",
|
||||
"names": [],
|
||||
@@ -22559,6 +22584,11 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52",
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "0d47a8f3d847243c4488e755ad4cab7a04ac5d4621b9c81865524cf3f4d8536f",
|
||||
"names": [],
|
||||
@@ -22967,15 +22997,6 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9",
|
||||
"names": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts#DeepSeekSearchLlmRequest"
|
||||
],
|
||||
"sources": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts:55"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "254dce209e735f9cb6f6b7aec0354712b1611759ec02af4d35a96f2081e666f6",
|
||||
"names": [],
|
||||
@@ -23229,6 +23250,13 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "37ff65426bcf130f045d3a59ef2a4648eb170cd30280cbd024108a2e9b4401d1",
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts:82"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "383cf73ae4748e5beb281ae74857ba1ffb92ae01075b9fbf51af78b666007a8f",
|
||||
"names": [],
|
||||
@@ -24579,13 +24607,6 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731",
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts:61"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7",
|
||||
"names": [
|
||||
@@ -24740,6 +24761,13 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee",
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts:61"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "9d80129e13edfb6feb03ed327e6085c6ef60b97ad074244b2f5fd4667424a6a4",
|
||||
"names": [],
|
||||
@@ -24987,6 +25015,15 @@
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c",
|
||||
"names": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts#DeepSeekSearchLlmRequest"
|
||||
],
|
||||
"sources": [
|
||||
"packages/web/web-search-deepseek/src/provider.ts:55"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585",
|
||||
"names": [],
|
||||
@@ -25478,11 +25515,6 @@
|
||||
"packages/core/tools/src/types.ts:21"
|
||||
]
|
||||
},
|
||||
{
|
||||
"digest": "cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331",
|
||||
"names": [],
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"digest": "cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f",
|
||||
"names": [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user