Merge master and refresh Chat README pairing
This commit is contained in:
commit
51ca4f0787
582 files changed
+3979
-1722
No files matched your search
+8
-8
@@ -18,14 +18,14 @@
|
||||
en: 5fdde26e9e8c6108
|
||||
zh: 629559d733c1f0c5
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/plugin-resolution-the-vfs-holds-a-real-package-tree-the-closure-manifest-is-the-deploy-root:
|
||||
en: c35ea4b858448b49
|
||||
zh: 1f8ff0a9f82ca881
|
||||
en: 985e0eb63f3d1fd6
|
||||
zh: 337cc44fadc6227e
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
|
||||
en: d3bbd24ead02af4f
|
||||
zh: 64623d88bee4314b
|
||||
en: 921085cb9d894688
|
||||
zh: 12d99fe153d36738
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development:
|
||||
en: 9bd60549d4a52ec0
|
||||
zh: e2343b7210212ee0
|
||||
en: 02e6b36f9f92c83a
|
||||
zh: edd8b2db7a787483
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/naming-lineage:
|
||||
en: 31a5aecb8bf828bb
|
||||
zh: 2dffb6fd2e0f8194
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 0426f598f3f44e64
|
||||
zh: fc66728e17f3bcfb
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/testing:
|
||||
en: b3c9820f4dc7c6c8
|
||||
zh: 3b5768ef1770bfcb
|
||||
en: 8e7f46bbb595afa9
|
||||
zh: 4641894ed285d717
|
||||
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/alternatives-considered:
|
||||
en: 50d965ca81a401ba
|
||||
zh: 0c6296245b97b9ac
|
||||
|
||||
+5
-5
@@ -44,19 +44,19 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
|
||||
|
||||
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
|
||||
|
||||
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in `<executable-stem>-office/node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. Missing required packages and missing installed package manifests fail the copy; absent optionals preserve the kit's engine selection.
|
||||
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in the explicitly downloaded sidecar’s `node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. The build locks the selected engine and dependency locations to exact npm tarballs; downloads verify their integrity before publication.
|
||||
|
||||
### Build pipeline and artifacts
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
|
||||
|
||||
### Python SDK distribution: two carriers, exe for production, node for development
|
||||
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, `-office/` directory, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, lightweight download metadata and Office skills, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/runtime-bootstrap.mjs` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, its Office directory, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, lightweight resource metadata and skills, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
|
||||
|
||||
The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
|
||||
|
||||
@@ -74,7 +74,7 @@ The verification surface has three tiers. Mechanism tier: the measured conclusio
|
||||
|
||||
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
|
||||
|
||||
The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
|
||||
The installed-wheel `sdk-office` scenario relocates the lightweight wheel payload and explicitly downloads the complete npm Office sidecar with its independent Node. It requires exactly one target backend, verifies that the SDK loads the kit from the downloaded sidecar, and converts DOCX through the skill CLI with an empty PATH. Python Office operations use the same CLI. Explicit downloads reuse the existing build preparation implementation; SDK startup performs no downloads. The [runtime reference](../../../../python/sdk-runtime/README.md) owns cache and activation behavior.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+5
-5
@@ -44,19 +44,19 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
|
||||
|
||||
部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server。可选资源服务提供 MCP Resources;MCP Prompts 仍不受支持。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
|
||||
|
||||
[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于 `<executable-stem>-office/node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。必需包缺失或已安装包缺少清单会使复制失败;未安装的可选包保留 kit 自身的引擎选择规则。
|
||||
[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于显式下载的 sidecar 的 `node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。构建将所选引擎和依赖位置锁定到确切的 npm 归档;下载会在发布缓存前校验完整性。
|
||||
|
||||
### 构建流水线与产物
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
|
||||
|
||||
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
|
||||
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、`-office/` 目录和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、轻量下载元数据与 Office skills 和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/runtime-bootstrap.mjs`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe、其 Office 目录及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe、轻量资源元数据与 skills 及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
|
||||
|
||||
Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
|
||||
|
||||
@@ -74,7 +74,7 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和
|
||||
|
||||
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
|
||||
|
||||
安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(使用已声明的原生目标,未声明原生目标时使用 WASM),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。
|
||||
安装后 wheel 的 `sdk-office` 场景迁移轻量 wheel 载荷,并显式下载完整 npm Office sidecar 及其独立 Node。它要求仅有一个目标后端,验证 SDK 从下载的 sidecar 加载 kit,并通过 skill CLI 在空 PATH 下转换 DOCX。Python Office 操作使用同一 CLI。显式下载复用现有构建准备实现;SDK 启动不执行下载。[运行时参考](../../../../python/sdk-runtime/README.zh.md)拥有缓存和启用行为的说明。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
en: a1750de2d48551af
|
||||
zh: ffea0b75c292f186
|
||||
/agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/shared-module-requests:
|
||||
en: cedddcca8a7bae6d
|
||||
zh: a568a1130229f48c
|
||||
en: 96661da17622132a
|
||||
zh: dab3424093130c2f
|
||||
/agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/parser-preloading-and-react-handoff:
|
||||
en: 4b503618b271fc28
|
||||
zh: e2148f44efa769d2
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ The `staticLinked` preset leaves every bare specifier as an external import in `
|
||||
|
||||
### Shared module requests
|
||||
|
||||
Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents.
|
||||
Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, selected third-party libraries, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; non-baseline third-party implementation libraries remain private bundle contents. Shared third-party libraries use the shell's declared versions without per-plugin version negotiation; promotion into the baseline requires a measured reduction across the shell and shipped plugin bundles.
|
||||
|
||||
A request has exactly two suppliers:
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
|
||||
|
||||
### 共享模块请求
|
||||
|
||||
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
|
||||
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis、选定的第三方库和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;基座外的第三方实现库保留为 bundle 私有内容。共享第三方库使用外壳声明的版本,不为单个插件协商版本;只有测量确认外壳与随包插件 bundle 的总体积减少,才将库纳入基座。
|
||||
|
||||
请求只有两种提供方:
|
||||
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
en: 8ee88035cf64eeb3
|
||||
zh: 6184e50fc4d47348
|
||||
/agent-note-one-dsh-launcher-for-application-profiles/decision/python-runtime:
|
||||
en: 5962abadfdece709
|
||||
zh: 5db20e96deed49d1
|
||||
en: 4b4cf5071b740c85
|
||||
zh: 595e5fe8127f9067
|
||||
/agent-note-one-dsh-launcher-for-application-profiles/decision/enforcement:
|
||||
en: 9ce8d86b65064f38
|
||||
zh: 27b8ad426cb319a2
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher
|
||||
|
||||
### Python runtime
|
||||
|
||||
The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile.
|
||||
The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. A private `DSH_RUNTIME_DOWNLOAD` selector prepares optional resources before application launch and is removed before spawning preparation subprocesses; ordinary CLI arguments do not select downloads. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile.
|
||||
|
||||
The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. [docs/architecture.md](../../../../docs/architecture.md) owns this launch, and the [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows carrier.
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
|
||||
|
||||
### Python 运行时
|
||||
|
||||
Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。
|
||||
Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。私有 `DSH_RUNTIME_DOWNLOAD` 选择器在应用启动前准备可选资源,并在启动资源准备子进程前移除;普通 CLI 参数不选择下载。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。
|
||||
|
||||
可执行文件族是 `deepseek-harness-sdk-runtime-<platform>-<arch>`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。[docs/architecture.md](../../../../docs/architecture.zh.md)负责该启动方式,[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 载体。
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
en: e01df5ba880c2972
|
||||
zh: e62074a6a9ba5685
|
||||
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/package-ownership:
|
||||
en: 37cae6b28fd359ef
|
||||
zh: 63118e361fa8f84c
|
||||
en: f683605bf4655db8
|
||||
zh: de09d610ee1d4193
|
||||
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/transactions-and-upgrades:
|
||||
en: 53d48f71fcc1c52e
|
||||
zh: 7d61923484dbec1c
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ The resource descriptor records the exact release, Node version, platform, archi
|
||||
|
||||
The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts. The Host inherits the user environment. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability.
|
||||
|
||||
Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
|
||||
Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, licenses, and unrecognized assets; node-pty's ConPTY source distribution is retained only for Windows targets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
|
||||
|
||||
The shared profile runner supplies missing installation and selected-bundle dependencies through the runtime resolution. pnpm-installed packages take precedence. Runtime resolution delegates to the selected package paths, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; runtime resolution does not merge a package’s dual implementations.
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成
|
||||
|
||||
[桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物。Host 继承用户环境。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。
|
||||
|
||||
包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
|
||||
包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、许可证和未知资源;node-pty 的 ConPTY 源分发内容仅在 Windows 目标中保留;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
|
||||
|
||||
共享 profile runner 通过 runtime resolution 补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。运行时解析委托给所选包路径,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;运行时解析不会合并包的双重实现。
|
||||
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-15-authenticated-browser-host-preferences.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser:
|
||||
en: f4b8b4500f8dca9c
|
||||
zh: bf27ded54fba703b
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser/problem:
|
||||
en: 22bf57b616581253
|
||||
zh: d1108a4042646e21
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser/decision:
|
||||
en: 2d54784b8b3558cf
|
||||
zh: 31190a9b284867b7
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser/alternatives-considered:
|
||||
en: 299105745b084fc0
|
||||
zh: 8c2d82de44ac2aad
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser/consequences:
|
||||
en: 520028b8c4031009
|
||||
zh: 8c130624261538cc
|
||||
/agent-note-share-host-preferences-with-every-authenticated-browser/testing:
|
||||
en: e433f139fe72e69f
|
||||
zh: 279cc8b1cd3d5377
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Share Host preferences with every authenticated browser
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-15-authenticated-browser-host-preferences.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[Persist Web user preferences through Host settings](../../archived/bug-fix/2026-08-06-host-backed-web-preferences.md) chose the preference store by page locality, so a non-loopback page kept Appearance, Language, busy-Enter, and the welcome acknowledgement in a process-local copy. A trusted remote host, and a deployment published behind the configured public URL, therefore lost every choice on reload even though Connection authenticates the complete settings API. The second mode also split the shared runtime: theme, locale, the settings shell, the Models page, and the welcome notice each rendered a durable row that could never persist for a remote page.
|
||||
|
||||
## Decision
|
||||
|
||||
Locality selects nothing. One `settings.describe` mirror, one `ctx.configForms.get(entryId)` per namespace derived from it, and the `settings.mutate` write path serve every authenticated browser. Browser token authentication of the Connection API is the identity boundary; the fixed `remote.$host` locality facts are request-routing evidence that no store selector consults. The mirror's state space is `idle`, `loading`, and `ready`, a form snapshot carries no mode, and a form reports `unavailable` only for a namespace the Host did not register.
|
||||
|
||||
The settings shell keeps one locality-dependent UI element: it builds its document controller from `ctx.remote.$host.isLoopback`, so only a loopback page renders **Open configuration file** and issues the `hasDocument` read behind it. The `settings/openSettingsDocument` Remote itself stays browser-authenticated like every other Host method; the omission is a presentation choice for pages that cannot see the Host's native editor, not a persistence rule.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the memory-branch locality gate.** Rejected: browser token authentication already covers every Host method for remote pages, so the gate protected nothing, and locality is request-routing evidence rather than identity. It also made every Host-backed row inert on trusted remote and public deployments.
|
||||
|
||||
## Consequences
|
||||
|
||||
Every authenticated browser reaching one Host shares one preference document. Two browsers on the same deployment observe each other's Appearance, Language, busy-Enter, and welcome-acknowledgement choices, and a value written remotely is the value a loopback browser loads next. Preference access is governed by API authentication and `settings.describe.writable`.
|
||||
|
||||
A remote page omits the **Open configuration file** action, the one locality-dependent UI element that remains. Concurrent writers on one document resolve through the namespace revision each form sends as `expectedRevision`.
|
||||
|
||||
## Testing
|
||||
|
||||
Focused unit coverage pins the contract: the ui-settings mirror and configuration-form suites construct both objects with no persistence selector, the theme apply spec persists from a non-loopback bench and asserts the Host write, the ui-settings-general apply spec keeps the shared describe read and the Host write path off-loopback while asserting the native document action stays unregistered, and the welcome store retains its acknowledgement across a settings-plugin recreation. The keyless Web settings scenario `apps/web/tests/remote-settings.e2e.ts` acknowledges the welcome notice, configures a model provider, and selects the dark Appearance from a trusted remote page, reloads after each, and re-asserts the welcome, model, and Appearance state from the Host settings document.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 每个已认证浏览器共用 Host 偏好
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-15-authenticated-browser-host-preferences.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[通过 Host settings 持久化 Web 用户偏好](../../archived/bug-fix/2026-08-06-host-backed-web-preferences.md)按页面本地性选择偏好存储,因此非回环页面把 Appearance、Language、繁忙态 Enter 与欢迎提示的确认留在一份进程内副本中。可信远程主机,以及配置了公共 URL 的部署,因而在重新加载时丢失全部选择,尽管 Connection 认证完整的 settings API。这套第二种模式也割裂了共享运行时:主题、语言、设置外壳、Models 页面与欢迎提示各自都在渲染一行对远程页面永远无法持久化的界面。
|
||||
|
||||
## 决策
|
||||
|
||||
本地性不选择任何东西。一个 `settings.describe` 镜像、由它派生的逐 namespace `ctx.configForms.get(entryId)`,以及 `settings.mutate` 写入路径服务每个已认证浏览器。Connection API 的浏览器令牌认证是身份边界;固定的 `remote.$host` 本地性事实只是请求路由的依据,没有任何存储选择器会读取它。镜像的状态空间只有 `idle`、`loading` 与 `ready`,表单快照不携带 mode,表单也只有在 Host 未注册该 namespace 时才报告 `unavailable`。
|
||||
|
||||
settings 外壳保留唯一一个依赖本地性的 UI 元素:它依据 `ctx.remote.$host.isLoopback` 构造文档控制器,因此只有回环页面会渲染**打开配置文件**并发出其背后的 `hasDocument` 读取。`settings/openSettingsDocument` Remote 本身与其他 Host 方法一样保持浏览器认证;省略该操作是面向看不到 Host 原生编辑器的页面的展示选择,而非持久化规则。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留内存分支的本地性门禁。** 已否决:浏览器令牌认证已覆盖远程页面的每个 Host 方法,该门禁保护不了任何东西,而本地性只是请求路由的依据而非身份。它还会让受信远程与公共部署上每一行依赖 Host 的界面失效。
|
||||
|
||||
## 后果
|
||||
|
||||
每个访问同一 Host 的已认证浏览器共用一份偏好文档。同一部署上的两个浏览器会看到彼此选择的 Appearance、Language、繁忙态 Enter 与欢迎提示确认,远程写入的值就是回环浏览器下一次加载的值。偏好可访问性由 API 认证与 `settings.describe.writable` 决定。
|
||||
|
||||
远程页面省略**打开配置文件**操作,这是仍依赖本地性的唯一 UI 元素。同一文档上的并发写入由各表单作为 `expectedRevision` 发送的 namespace revision 裁决。
|
||||
|
||||
## 测试
|
||||
|
||||
聚焦的单元测试固定当前契约:ui-settings 的镜像与配置表单测试在构造这两个对象时不传持久化选择器,主题 apply 测试从非回环 bench 持久化并断言 Host 写入,ui-settings-general 的 apply 测试在非回环下保留共享 describe 读取与 Host 写入路径、同时断言原生文档操作保持未注册,欢迎 store 则在 settings 插件重建后保留其确认。无密钥 Web settings 场景 `apps/web/tests/remote-settings.e2e.ts` 从可信远程页面确认欢迎通知、配置一个模型提供商并选择深色 Appearance,每步之后重新加载,并从 Host settings 文档重新确认欢迎提示、模型与 Appearance 状态。
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 237018634b5f9dcf
|
||||
zh: cbc49d9c1b794065
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/decision/process-catalog-and-children:
|
||||
en: 6ebe6f90241fd322
|
||||
zh: eafdebc5277e83a1
|
||||
en: 0042c645360c8361
|
||||
zh: 5dba39466d376559
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/alternatives-considered:
|
||||
en: fe97128ed05b9e43
|
||||
zh: 4aaa344933fa89b4
|
||||
|
||||
@@ -58,7 +58,7 @@ A persisted Auto Session cannot publish when the complete integration is absent
|
||||
|
||||
The permission owner publishes one complete process catalog through generated `permissionPresets` Remote methods; the BFF explicitly mounts it and forwards a payload-free invalidation event. One browser directory subscribes before reading and serves both pickers. Epoch and connection-generation checks publish only the winning complete result. A winning failure or connection reset clears the previous snapshot; only a later existing read, notification, or reset retries. Disposed or stale settlements cannot publish. Each catalog invalidation dismisses the slash picker and its pending confirmation through the command owner while preserving the draft, so a picker waiting on its own read keeps its failure and retry state; reopening loads the current catalog. Session projection carries current selection only, so catalog installation or removal writes no Session event or sequence.
|
||||
|
||||
Auto appears with a superscript `EXP` badge. Both visible current-session pickers require the experimental confirmation; explicit `/permission auto` is already consent. The composer uses the generic Menu's existing portal placement to stay within the viewport while keeping its 218–360px bounds. The slash popup keeps `min(220px, 100%)` with `max-width: 100%`, including when a narrow composer collapses its trigger.
|
||||
Auto appears with an `EXP` badge in picker options; the selected composer control omits it. Both visible current-session pickers require the experimental confirmation; explicit `/permission auto` is already consent. The composer uses the generic Menu's existing portal placement to stay within the viewport while keeping its 218–360px bounds. The slash popup keeps `min(220px, 100%)` with `max-width: 100%`, including when a narrow composer collapses its trigger.
|
||||
|
||||
The [delegation-time policy capture](../../../../packages/subagent/subagent/README.md) records Auto or Full access before the first await and appends that existing preset event after fork seeding and sandbox/approval overrides. One-shot and continuable creation share the rule; cold resume reads only the child log. Later parent switches do not alter that child, while a later child switch can win. Read Only and Workspace Write retain sandbox inheritance plus `approval: never` and may therefore remain `custom`. Auto children classify each call independently using the existing lineage and messages, without parent call metadata, delegation records, receipts, Header/descriptor additions, or a Session-format change. Out-of-process children retain their own permission systems after the parent delegation is allowed.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Reviewer 可以输出 reasoning blocks,随后恰好一个 JSON text block 和
|
||||
|
||||
Permission owner 通过生成的 `permissionPresets` Remote 方法发布一份完整进程目录;BFF 显式挂载它,并转发无 payload 的失效事件。一个浏览器目录在读取前订阅,为两个选择器提供数据。Epoch 与 connection-generation 检查只发布胜出的完整结果。胜出读取失败或 connection reset 会清空旧快照;只有后续既有读取、通知或 reset 才重试。已 dispose 或陈旧的结算不能发布。每次目录失效都通过命令 owner 关闭 slash 选择器与待确认对话框,同时保留草稿,因此等待自身读取的选择器仍保留失败与重试状态;重新打开时加载当前目录。Session 投影只携带当前选择,因此目录安装或移除不写 Session 事件或序号。
|
||||
|
||||
Auto 带右上标 `EXP`。两个可见当前会话选择器都要求实验确认;显式 `/permission auto` 已构成同意。Composer 使用通用 Menu 既有 portal 定位保持在视口内,同时保留 218–360px 边界。Slash popup 保留 `min(220px, 100%)` 与 `max-width: 100%`,窄 composer 将 trigger 折叠时也一样。
|
||||
Auto 在选择器选项中带 `EXP` 标记;选中后的 composer 控件不显示该标记。两个可见当前会话选择器都要求实验确认;显式 `/permission auto` 已构成同意。Composer 使用通用 Menu 既有 portal 定位保持在视口内,同时保留 218–360px 边界。Slash popup 保留 `min(220px, 100%)` 与 `max-width: 100%`,窄 composer 将 trigger 折叠时也一样。
|
||||
|
||||
[委派时权限捕获](../../../../packages/subagent/subagent/README.zh.md)在首次 await 前记录 Auto 或 Full access,并在 fork seed 与 sandbox/approval override 之后追加该既有 preset event。单次与可继续创建共用此规则;cold resume 只读取 child 日志。后续 parent 切换不改变该 child,后续 child 自己的切换仍可胜出。Read Only 与 Workspace Write 保留 sandbox 继承加 `approval: never`,因此可能保持 `custom`。Auto child 使用既有 lineage 与消息独立分类每次调用,不增加父 call metadata、委派记录、receipt、Header/descriptor 字段或 Session format。进程外 child 在父委派获准后保留自己的权限系统。
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: dsh-official-plugin-artwork
|
||||
description: Draw and review official DeepSeek Harness plugin icons, including experimental plugins. Use for package icon.svg assets and PluginArtwork glyphs; covers semantic references, proportions, lettering, transparency, package metadata, and designer review. Not for toolbar or action icons.
|
||||
---
|
||||
|
||||
# Official plugin artwork
|
||||
|
||||
Draw recognizable artwork that belongs beside the existing official icons. Official and official experimental plugins share this visual language; the UI label communicates experimental status. This guidance does not replace designer approval.
|
||||
|
||||
## Establish the subject
|
||||
|
||||
- Inspect the actual feature, its output, and its owning manifest before drawing. A badge-producing skill may need a tag, not a medal. Reuse designer-supplied assets; do not invent a brand mark or change product names, identifiers, or unrelated icons without approval.
|
||||
- Read the [existing artwork](../../../packages/client/ui-primitives/src/plugin-artwork.tsx) and the nearest relevant package SVGs. Borrow one coherent treatment, not every reference's effects. Action icons remain with [dsh-client-ui-ux](../dsh-client-ui-ux/SKILL.md#reuse-before-adding).
|
||||
- Check every affected entry, list, detail, and component row. Bundle artwork and a component's name and icon can have separate metadata owners; changing one does not update the other.
|
||||
|
||||
## Draw and refine
|
||||
|
||||
1. **Start simple.** Use one dominant subject and only the detail needed to explain it. Related capabilities can share a metaphor; distinguish them through proportions, silhouette, or palette rather than extra panels, symbols, or badges.
|
||||
2. **Judge optical size.** Use `width="36" height="36" viewBox="0 0 36 36"`. Center the artwork with deliberate margins; 18–22 units is a useful starting extent, not a requirement for every silhouette or wide text tag. The [page renderer](../../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) owns display sizes and outer tile framing. Do not bake that tile into the SVG or enlarge a toolbar glyph to fill it.
|
||||
3. **Keep geometry consistent.** Balance the main subject and secondary element. Align repeated marks and corners; keep tails and handles intentional. Refine one requested dimension at a time without changing approved parts.
|
||||
4. **Use restrained color by default.** Balance saturation and brightness with neighboring icons; prefer existing palettes such as [Auto Authorization Review](../../../packages/experimental/auto-review/icon.svg) and [Codex subagent](../../../packages/subagent/subagent-codex/icon.svg). Reserve unusually vivid, bright, or neon colors for approved brand requirements, essential contrast, or explicit designer direction. Tune saturation separately from hue and lightness; do not make artwork gray or muddy merely to reduce saturation. Keep flat fills or short gradients and limited layering; avoid glow, drop shadows, glossy highlights, and emoji. Page CSS still uses semantic theme tokens.
|
||||
5. **Align lettering by visible ink.** Use consistent letterforms, cap height, baseline, weight, and spacing. Compare rendered stroke edges, not just path coordinates: stroke caps and joins can make equal coordinates appear unequal. Export lettering as paths without a font dependency, and keep it readable at actual size.
|
||||
6. **Make empty areas genuinely transparent.** A hollow lens must reveal the background, including where it overlaps another shape. Remove the underlying fill with a local mask or cutout; do not substitute a white or dark disk. Check both themes.
|
||||
|
||||
## Export at the owner
|
||||
|
||||
- Use the manifest's `icon` path and publish its resources. Update intentional bundle/component copies together; verify localized display names and descriptions when those are in scope. Do not change the feature's produced assets merely to change its plugin icon.
|
||||
- Package images render through `<img>`: export self-contained SVG geometry with local, uniquely named gradient, mask, and clip references. No scripts, event handlers, animation, external images/styles, fonts, or `foreignObject`. Inline React artwork needs per-instance IDs; separate image documents do not share IDs.
|
||||
- Keep the asset change local. Do not add runtime dependencies, primitive exports, or renderer special cases just to display it.
|
||||
|
||||
## Review and verify
|
||||
|
||||
1. Show a few numbered alternatives before polishing uncertain geometry. Preserve their labels through review and apply the chosen version; an existing file or generated draft does not establish designer approval. Keep comparison artifacts in a gitignored directory, not the product package.
|
||||
2. Compare old and new artwork beside unchanged references in light and dark themes, at actual row/card sizes as well as enlarged. Check balance, lettering, empty space, clipping, and image decoding.
|
||||
3. Run `pnpm run verify-package-meta` for [package metadata and SVG artwork](../../../scripts/verify-package-meta.ts), `pnpm run constraints` for the canonical package payload, and `pnpm run verify-official-bundle-catalog`. Regenerate a stale catalog with `pnpm run gen-official-bundle-catalog`, never by hand. Structural checks do not replace visual review.
|
||||
4. Build affected artifacts and inspect the real page through `dsh web`. Batch approved refinements before restarting a metadata-caching Host, then verify the entry and nested component rows in both themes.
|
||||
5. Follow the repository's UI testing policy and [record-browser-gif](../record-browser-gif/SKILL.md) for a GUI PR. Obtain designer approval before landing new glyphs. Record reusable drawing principles here, not the history of rejected drafts.
|
||||
@@ -150,6 +150,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and
|
||||
- **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction.
|
||||
- **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR.
|
||||
- **Create Agent Notes only for durable decision rationale;** mechanical/local edits are exempt, including local UI changes ([scope](.agents/notes/README.md#when-to-write-one)). Archived notes are frozen: never edit or treat them as current authority ([archive policy](.agents/notes/README.md#archiving-and-deletion)).
|
||||
- **Official plugin artwork:** use [dsh-official-plugin-artwork](.agents/skills/dsh-official-plugin-artwork/SKILL.md) for official and official experimental plugin icons; obtain designer approval before landing new glyphs.
|
||||
- **Client UI copy is locale-owned.** Route product text through typed dictionaries and `t` or localized primitive props; `verify-client-ui-i18n` rejects hardcoded copy ([decision](.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)).
|
||||
- **Testing policy** — [docs/testing.md](docs/testing.md). Every non-trivial model- or product-user-visible change updates a keyless recorded-session snapshot; [snapshot ownership](snapshots/AGENTS.md) reserves the top-level tree for session-driven cases and keeps other expected output owner-local. Fixtures replay on macOS/Linux; fix fixtures, not normalizers.
|
||||
- **Design each tool's UI presentation up front.** Host presenters stay pure; Web cards derive from raw events and persisted result metadata ([cookbook](docs/cookbook/adding-a-tool.md)).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -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.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -11,7 +11,7 @@
|
||||
"lib/cli.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"koffi": "^3.1.0",
|
||||
"koffi": "3.1.1",
|
||||
"@deepseek-ai/libreoffice-kit": "^0.1.5",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh": "workspace:*",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { installOfficeEngineResolution, runtimeArchivePath } from './office-engi
|
||||
/**
|
||||
* Run the ordinary CLI with Desktop's bundled package manager and reserved-profile plugin access.
|
||||
* @param runtimeDir - Prepared or ASAR-contained production DSH package tree.
|
||||
* @param supportDir - Physical Desktop runtime directory containing pnpm.
|
||||
* @param supportDir - Physical Desktop runtime directory containing the primary-runtime payload and Node launchers.
|
||||
* @returns Completion of the selected CLI command; profile plugins own their process lifetime.
|
||||
*/
|
||||
export async function runDesktopCli(runtimeDir: string, supportDir: string): Promise<void> {
|
||||
@@ -16,7 +16,7 @@ export async function runDesktopCli(runtimeDir: string, supportDir: string): Pro
|
||||
manageDesktopProfile: true,
|
||||
packageManager: {
|
||||
command: process.execPath,
|
||||
args: ['--expose-internals', join(supportDir, 'pnpm', 'bin', 'pnpm.mjs')],
|
||||
args: ['--expose-internals', join(supportDir, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')],
|
||||
env: {
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/desktop/README.md
|
||||
/deepseek-harness-desktop:
|
||||
en: 001b7d11c237078b
|
||||
zh: eda6b580dc224c7f
|
||||
en: 5b8518fd5e0030ef
|
||||
zh: 02efee59c39647dc
|
||||
/deepseek-harness-desktop/terminal-command:
|
||||
en: bef92953c237363f
|
||||
zh: d21d6effd21b8c00
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 069fdfd72d2ea26a
|
||||
zh: 76ae549d43c99bb4
|
||||
/deepseek-harness-desktop/key-technical-decisions/bundled-workspace-dependencies:
|
||||
en: 0f9b6c1d6761d108
|
||||
zh: 77127063f244c361
|
||||
en: 2b049b4f104be1c2
|
||||
zh: 1e8f3a9f7eb15e2b
|
||||
/deepseek-harness-desktop/bundled-command-runtime:
|
||||
en: 09f0b75af9f0ed14
|
||||
zh: 60bbc2e67d5e87a8
|
||||
@@ -51,8 +51,8 @@
|
||||
en: 5f05072ec82cc62d
|
||||
zh: a3d63b58d1ef2db7
|
||||
/deepseek-harness-desktop/package/runtime-file-selection:
|
||||
en: e7c1eb8e78fe46e3
|
||||
zh: b7ea497dd75be524
|
||||
en: 4c83b25ef03d0c1b
|
||||
zh: 6c97d8f485e2d6d0
|
||||
/deepseek-harness-desktop/package/upload-updates:
|
||||
en: f48baa680e841b0d
|
||||
zh: 25ded1aeaac6dad1
|
||||
@@ -66,8 +66,8 @@
|
||||
en: 64836513eb7eda6e
|
||||
zh: 6ebb9184a7aa51de
|
||||
/deepseek-harness-desktop/package/desktop-runtime-preparation:
|
||||
en: 32ef30fc38462109
|
||||
zh: 093b1b57d084f5bb
|
||||
en: 9a71990bee9867a7
|
||||
zh: 76c5c1fdff6e6908
|
||||
/deepseek-harness-desktop/updates:
|
||||
en: 41d0f50eb7173342
|
||||
zh: ce940ef3cbcc6fa5
|
||||
|
||||
@@ -12,7 +12,7 @@ The first application-menu command, **About DeepSeek Harness**, opens Electron's
|
||||
|
||||
Desktop’s local native directory flow opens an Electron folder dialog attached to the application window, restoring, showing, and focusing that window first. Concurrent requests share one dialog; cancellation returns no path and failures remain retryable. Ordinary Web uses the Host chooser. Browse mode lists Host directories. On Linux without zenity or kdialog, automatic selection uses browse instead of the Electron dialog.
|
||||
|
||||
Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron Node mode without requiring pnpm on PATH. The private Node launcher environment applies only to package operations.
|
||||
Desktop ships one pnpm distribution at `resources/runtime/primary-runtime/dependencies/pnpm`. Build-time production installation, the installed `dsh` CLI, Creator and the Web Plugin Manager execute its `bin/pnpm.mjs` with Electron Node mode; package operations do not require pnpm on PATH. The private Node launcher environment applies only to package operations. Agent workspace dependencies copy the same self-contained primary-runtime payload into the Harness home and execute pnpm with its standalone Node.
|
||||
|
||||
Embedded Platform documents use a persistent WebContentsView partition keyed by a hash of the Platform origin and stable account ID. localStorage preferences, including dismissed notices, survive view closure and application restart; different accounts and origins use separate storage. The Host supplies the account ID from its latest successful profile read; while no ID is known, the document opens in a disposable partition that keeps no preference across opens. Opening a persistent partition clears cookies, filesystem, IndexedDB, Cache Storage, the HTTP and shader caches, service workers, and HTTP authentication before the document loads; a disposable partition clears all of its storage. Closing a view destroys the document, removes its request interceptors, and schedules the same cleanup, so authentication left by an unclean exit is cleared before the next document loads. The next open and application shutdown await that cleanup, and an update install waits for it before the installer takes over the exit. A failed cleanup rejects that open and blocks the same account until a later cleanup succeeds; other accounts keep opening. Sign-out destroys the document but retains account-scoped preferences for the next sign-in. A document already open in a disposable partition stays mounted when the account ID arrives for the same credential; the next open uses the account-scoped partition. The [storage decision](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.md) describes the retention policy. Host sends account credentials through private Node IPC; account RPC and the Harness renderer receive no token. Before page scripts execute, the Platform preload performs one synchronous IPC to read the prepared main-process credential. It exposes displayMode, synchronous getAuthToken() and getLocale() getters, and onLocaleChange(listener), which returns an unsubscribe function. Both getters read preload memory without further IPC. The bootstrap includes the resolved Desktop language (`zh_CN` or `en_US`); Settings language changes update the preload cache and notify the open Platform document without reloading. Platform applies this language before its first render and does not persist it as a browser preference. The main-process handler performs only sender validation and an in-memory read; it never waits for Host, disk, or network. Trusted-page initialization failures retain embedded mode and make the getter throw, preventing browser-credential fallback. Only the owned Platform main frame on the configured issuer origin can initialize. Sign-out, credential replacement, Host shutdown, and view closure destroy the document. Cross-origin document navigation is blocked. HTTPS links requesting a new window open in the system browser without the embedded session or token; other schemes and URL credentials are rejected. Native views occupy the viewport below the Account feature’s return bar.
|
||||
|
||||
@@ -72,6 +72,8 @@ The private Desktop `runtime/bin` directory is added only to package-installatio
|
||||
|
||||
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](../../scripts/primary-runtime/lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; the shared builder resolves pnpm from the root development dependency pin. The root package-manager version and the Desktop pin are checked for agreement. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8; the x64 target also receives `com.apple.security.cs.allow-unsigned-executable-memory` for executable code pages, including under Rosetta. ARM64 Node and Office helpers retain JIT-only permissions. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.
|
||||
|
||||
The shared Desktop/SDK runtime builder omits directories named `tests` beneath NumPy and pandas before copying or signing the payload, and removes their rows from each distribution’s `RECORD`. It retains `numpy.testing`, `pandas.testing`, `pandas._testing`, native extensions and licenses. Complete upstream suites are unavailable; native checks verify the retained testing APIs, absence of test directories and existence of recorded files.
|
||||
|
||||
| Decision | Why | Direct consequence |
|
||||
|---|---|---|
|
||||
| Release identity | The shell API, Web client, backend, and plugin graph are qualified as one combination; independent versions would create untested combinations and ambiguous update availability. | Electron and `@deepseek-ai/dsh` always have the same exact version. A dsh upgrade is a Desktop release, even when the shell code is unchanged. |
|
||||
@@ -272,7 +274,9 @@ The served `time` is the real packing time, so pnpm 11's default 24-hour `minimu
|
||||
|
||||
### Runtime file selection
|
||||
|
||||
Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.
|
||||
Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. Non-Windows targets also omit `node-pty/third_party/conpty`; Windows targets retain that directory. It preserves runtime JavaScript, target native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.
|
||||
|
||||
The [Electron shell file list](scripts/electron-builder-config.mjs) excludes recognized JavaScript/CSS/TypeScript source maps and TypeScript build caches from the separate `resources/app.asar/node_modules` dependency collection. These exclusions preserve runtime code, native modules, WASM, data files (including ordinary `.map` assets), licenses, and notices; [shell archive tests](tests/shell-file-selection.spec.ts) check retained bytes and exclusions.
|
||||
|
||||
The [Office conversion provider](../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in).
|
||||
|
||||
@@ -443,7 +447,7 @@ Desktop [patch policy](scripts/runtime-patch-policy.ts) explicitly classifies ev
|
||||
|
||||
Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with `--frozen-lockfile`; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration.
|
||||
|
||||
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution and pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
|
||||
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
|
||||
|
||||
Signed macOS packaging writes `Contents/Resources/app-update.yml` while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过
|
||||
|
||||
Desktop 的本地原生目录流程打开绑定应用窗口的 Electron 文件夹对话框,并先恢复、显示和聚焦该窗口。并发请求共用一个对话框;取消不返回路径,失败后可以重试。普通 Web 使用 Host 选择器。浏览模式列出 Host 目录。Linux 缺少 zenity 或 kdialog 时,自动选择使用浏览模式,不使用 Electron 对话框。
|
||||
|
||||
Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置 pnpm,无需 PATH 中存在 pnpm。私有 Node 启动器环境仅应用于包操作。
|
||||
Desktop 在 `resources/runtime/primary-runtime/dependencies/pnpm` 中携带一份 pnpm 分发包。构建时的生产依赖安装、安装版 `dsh` CLI、Creator 和 Web 插件管理器通过 Electron Node 模式执行其中的 `bin/pnpm.mjs`;包操作不要求 PATH 上存在 pnpm。私有 Node 启动器环境仅用于包操作。agent(智能体)的工作区依赖将同一份完整 primary-runtime 复制到 Harness 主目录,并使用其中的独立 Node 执行 pnpm。
|
||||
|
||||
Platform 内嵌文档使用持久化 WebContentsView 分区,分区名由 Platform 来源和稳定账号 ID 的哈希决定。localStorage 中的页面偏好(包括已关闭的通知)在关闭视图和重启应用后保留;不同账号和来源使用独立存储。账号 ID 来自 Host 最近一次成功的资料读取;尚无该 ID 时,文档在一次性分区中打开,该分区不跨次保留偏好。打开持久分区会先清理 Cookie、文件系统、IndexedDB、Cache Storage、HTTP 与着色器缓存、Service Worker 及 HTTP 认证状态;一次性分区则清理其全部存储。关闭视图会销毁文档、移除请求拦截器并安排相同的清理,因此异常退出遗留的认证会在下一个文档加载前被清除。下次打开和应用退出都会等待该清理完成,更新安装也会在安装器接管退出前等待。清理失败会使该次打开失败,并在后续清理成功前阻止同一账号打开;其他账号不受影响。退出登录会销毁文档,但保留账号偏好供下次登录使用。同一凭证下账号 ID 迟到时,已以一次性分区打开的文档保持挂载;下次打开使用账号分区。[存储决策](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.zh.md)说明保留策略。Host 通过私有 Node IPC 发送账号凭证;账号 RPC 和 Harness 渲染进程不接收 token。Platform preload 在页面脚本执行前通过一次同步 IPC 读取主进程中已准备的凭证。它暴露 displayMode、同步的 getAuthToken() 和 getLocale() getter,以及返回取消订阅函数的 onLocaleChange(listener)。两个 getter 都只读取 preload 内存,不再调用 IPC。bootstrap 包含 Desktop 已解析的语言(`zh_CN` 或 `en_US`);Settings 语言变更会更新 preload 缓存并通知已打开的 Platform 文档,无需重载。Platform 在首屏渲染前应用该语言,且不将其持久化为浏览器偏好。主进程处理器仅校验调用来源并读取内存,不等待 Host、磁盘或网络。可信页面初始化失败时保留内嵌模式,由 getter 抛错,避免回退到浏览器凭证。只有受控 Platform 页面中、位于所配置签发来源的主 frame 能完成初始化。退登、凭证替换、Host 关闭及视图关闭都会销毁文档。跨来源文档导航被阻止。请求新窗口的 HTTPS 链接在系统浏览器中打开,不携带内嵌会话或 token;其他协议及带 URL 凭证的链接被拒绝。原生视图占据 Account 功能返回栏下方的视口。
|
||||
|
||||
@@ -74,6 +74,8 @@ Desktop 私有的 `runtime/bin` 目录仅添加到包安装进程,不进入 PT
|
||||
|
||||
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](../../scripts/primary-runtime/lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;共享构建器从根开发依赖中解析 pnpm 固定版本。测试检查根 package-manager 版本和 Desktop 固定版本保持一致。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限;x64 目标还获得 `com.apple.security.cs.allow-unsigned-executable-memory`,允许可执行代码页,包括在 Rosetta 下运行时。ARM64 Node 和 Office 辅助程序保留仅 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。
|
||||
|
||||
Desktop/SDK 共享运行时构建器在复制或签名产物前,裁剪 NumPy 和 pandas 下名为 `tests` 的目录,并移除各分发包 `RECORD` 中对应的记录。它保留 `numpy.testing`、`pandas.testing`、`pandas._testing`、原生扩展和许可证。完整上游测试套件不可用;本机检查验证保留的 testing API、测试目录已移除,以及清单所列文件存在。
|
||||
|
||||
| 决策 | 原因 | 直接结果 |
|
||||
|---|---|---|
|
||||
| 发布身份 | 桌面壳 API、Web 客户端、后端与插件依赖图作为一个组合完成验证;独立版本会产生未经验证的组合,并让更新可用性含糊不清。 | Electron 与 `@deepseek-ai/dsh` 始终使用同一精确版本。即使桌面壳代码不变,升级 dsh 也必须发布新 Desktop 版本。 |
|
||||
@@ -274,7 +276,9 @@ pnpm run dev:bundle-registry
|
||||
|
||||
### 运行时文件筛选
|
||||
|
||||
Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。
|
||||
Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。非 Windows 目标还会排除 `node-pty/third_party/conpty`;Windows 目标保留该目录。它保留运行时 JavaScript、目标平台的原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。
|
||||
|
||||
[Electron 外壳文件清单](scripts/electron-builder-config.mjs)从单独收集的 `resources/app.asar/node_modules` 依赖中排除已识别的 JavaScript/CSS/TypeScript source map 和 TypeScript 构建缓存。这些排除规则保留运行时代码、原生模块、WASM、数据文件(包括普通 `.map` 资源)、许可证和 notices;[外壳归档测试](tests/shell-file-selection.spec.ts)检查保留文件的字节和排除项。
|
||||
|
||||
[Office 转换提供方](../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。
|
||||
|
||||
@@ -445,7 +449,7 @@ Desktop [补丁策略](scripts/runtime-patch-policy.ts) 将每份工作区补丁
|
||||
|
||||
Desktop 依赖 overrides 在补丁策略模块中单独声明,不从补丁版本推导。pi-ai 约束使运行时版本保持在共享补丁已验证的版本。Desktop 仍会在每次构建时重新解析运行时锁文件,再通过 `--frozen-lockfile` 安装;不同构建之间的依赖解析尚不保证可复现。CLI 打包安装测试及其他交付流程保留各自的配置。
|
||||
|
||||
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包与 pnpm CLI。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
|
||||
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
|
||||
|
||||
macOS 签名打包在组装 App 时、代码签名前写入 `Contents/Resources/app-update.yml`,供并行 ZIP 与 DMG 路线使用的目录构建也执行此操作。签名钩子验证准确的更新源和 updater 缓存目录。写入发布完成记录前,流程会再次检查两条路线的副本和最终移入的 App;配置缺失或不匹配会阻止移入产物,因而也会阻止上传。
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ export interface DesktopElectronBuilderConfig {
|
||||
readonly output: string
|
||||
}
|
||||
readonly files: readonly [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
...string[],
|
||||
{ readonly from: string, readonly to: 'dsh', readonly filter: readonly ['**/*'] },
|
||||
{ readonly from: string, readonly to: 'dsh/node_modules', readonly filter: readonly ['**/*'] },
|
||||
]
|
||||
|
||||
@@ -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.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -68,7 +68,7 @@
|
||||
"js-yaml": "^4.3.2",
|
||||
"koffi": "3.1.1",
|
||||
"pnpm": "11.28.5",
|
||||
"sharp": "^0.35.5",
|
||||
"sharp": "0.35.5",
|
||||
"typescript": "^6.0.3",
|
||||
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
||||
@@ -135,6 +135,8 @@ export function createElectronBuilderConfig(
|
||||
'lib/preload-welcome.cjs',
|
||||
'renderer/**/*',
|
||||
'package.json',
|
||||
'!node_modules/**/*.{js,cjs,mjs,ts,cts,mts,css}.map',
|
||||
'!node_modules/**/*.tsbuildinfo',
|
||||
{ from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] },
|
||||
// electron-builder excludes a source directory's root node_modules.
|
||||
{ from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] },
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function createInstalledUpdateBuilderConfig(manifest: string, versi
|
||||
files: [
|
||||
{ from: application, to: '.', filter: ['lib/*.js', 'lib/*.cjs', 'renderer/**/*', 'qualification-bootstrap.mjs', 'installed-update-identity.mjs'] },
|
||||
'package.json',
|
||||
...config.files.filter((file): file is string => typeof file === 'string' && file.startsWith('!')),
|
||||
{ from: dsh, to: 'dsh', filter: ['**/*'] },
|
||||
{ from: join(dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] },
|
||||
],
|
||||
|
||||
@@ -43,7 +43,7 @@ const RUNTIME_ROOT = BUILD_PATHS.runtime
|
||||
const PNPM_BUILD_STATE = BUILD_PATHS.dshPnpm
|
||||
const PACKAGE_SET_ROOT = BUILD_PATHS.packageSet
|
||||
const NODE = join(BUILD_PATHS.electron, process.platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron')
|
||||
const PNPM = join(RUNTIME_ROOT, 'pnpm', 'bin', 'pnpm.mjs')
|
||||
const PNPM = join(RUNTIME_ROOT, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
|
||||
|
||||
function manifestVersion(path: string, subject: string): string {
|
||||
const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/** Prepare the target Electron distribution and pinned pnpm CLI. */
|
||||
/** Prepare Desktop resources and publish their versions after every preparation stage succeeds. */
|
||||
|
||||
import { packagingStep } from './packaging-step.mjs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { chmodSync, cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { downloadArtifact } from '@electron/get'
|
||||
import extractZip from 'extract-zip'
|
||||
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { readPrimaryRuntime } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts'
|
||||
import { desktopTargetPlatform, resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { packagingStep } from './packaging-step.mjs'
|
||||
import { preparePrimaryRuntime } from './prepare-primary-runtime.ts'
|
||||
import { prepareDesktopCli } from './prepare-cli.ts'
|
||||
import { prepareCommandLink } from './prepare-command-link.ts'
|
||||
@@ -16,23 +17,10 @@ import { prepareCommandLink } from './prepare-command-link.ts'
|
||||
const BUILD_PATHS = resolveDesktopTargetBuildPaths()
|
||||
const RUNTIME_ROOT = BUILD_PATHS.runtime
|
||||
|
||||
function preparePnpm(): string {
|
||||
const require = createRequire(import.meta.url)
|
||||
const manifestPath = require.resolve('pnpm')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: unknown }
|
||||
if (typeof manifest.version !== 'string') throw new Error('desktop runtime: pnpm manifest has no version')
|
||||
const packageDir = dirname(manifestPath)
|
||||
const destination = join(RUNTIME_ROOT, 'pnpm')
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
cpSync(packageDir, destination, { recursive: true })
|
||||
return manifest.version
|
||||
}
|
||||
type DesktopTarget = ReturnType<typeof desktopTargetPlatform>
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } })
|
||||
const target = resolveDesktopBuildTarget()
|
||||
const platform = target.startsWith('mac-') ? 'darwin' : 'win32'
|
||||
const arch = target.endsWith('arm64') ? 'arm64' : 'x64'
|
||||
/** Extract the target Electron distribution and return its embedded Node version. */
|
||||
async function prepareElectron({ platform, arch }: DesktopTarget): Promise<string> {
|
||||
const require = createRequire(import.meta.url)
|
||||
const { version } = require('electron/package.json') as { version: string }
|
||||
const archive = await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'download:electron',
|
||||
@@ -40,28 +28,42 @@ async function main(): Promise<void> {
|
||||
rmSync(BUILD_PATHS.electron, { recursive: true, force: true })
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'extract:electron', () => extractZip(archive, { dir: BUILD_PATHS.electron }))
|
||||
const executable = join(BUILD_PATHS.electron, platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron')
|
||||
const nodeVersion = execFileSync(executable, ['-p', 'process.versions.node'], {
|
||||
return execFileSync(executable, ['-p', 'process.versions.node'], {
|
||||
encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
}).trim()
|
||||
const macosMinimumVersion = platform === 'darwin' ? execFileSync('/usr/libexec/PlistBuddy',
|
||||
['-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist')], { encoding: 'utf8' }).trim() : undefined
|
||||
rmSync(RUNTIME_ROOT, { recursive: true, force: true })
|
||||
mkdirSync(RUNTIME_ROOT, { recursive: true })
|
||||
const pnpmVersion = preparePnpm()
|
||||
}
|
||||
|
||||
function prepareCli({ platform, arch }: DesktopTarget): void {
|
||||
cpSync(join(import.meta.dirname, 'node-bin'), join(RUNTIME_ROOT, 'bin'), { recursive: true })
|
||||
chmodSync(join(RUNTIME_ROOT, 'bin', 'node'), 0o755)
|
||||
const cli = join(RUNTIME_ROOT, 'cli')
|
||||
prepareDesktopCli(cli, platform)
|
||||
if (platform === 'darwin') {
|
||||
const minimumVersion = execFileSync('/usr/libexec/PlistBuddy', [
|
||||
'-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist'),
|
||||
], { encoding: 'utf8' }).trim()
|
||||
prepareCommandLink(cli, arch, minimumVersion)
|
||||
}
|
||||
cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(cli, 'command-manager.js'))
|
||||
cpSync(join(import.meta.dirname, 'command-path.ps1'), join(cli, 'command-path.ps1'))
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } })
|
||||
const target = desktopTargetPlatform(resolveDesktopBuildTarget())
|
||||
rmSync(RUNTIME_ROOT, { recursive: true, force: true })
|
||||
mkdirSync(RUNTIME_ROOT, { recursive: true })
|
||||
const nodeVersion = await prepareElectron(target)
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
|
||||
() => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] }))
|
||||
const { pnpm } = await readPrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime'))
|
||||
if (pnpm === undefined) throw new Error('desktop runtime: primary-runtime manifest has no pnpm version')
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli', async () => { prepareCli(target) })
|
||||
writeFileSync(join(RUNTIME_ROOT, 'versions.json'), `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
node: nodeVersion,
|
||||
pnpm: pnpmVersion,
|
||||
pnpm,
|
||||
}, undefined, 2)}\n`)
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli',
|
||||
async () => prepareDesktopCli(join(RUNTIME_ROOT, 'cli'), platform))
|
||||
if (macosMinimumVersion !== undefined) prepareCommandLink(join(RUNTIME_ROOT, 'cli'), arch, macosMinimumVersion)
|
||||
cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(RUNTIME_ROOT, 'cli', 'command-manager.js'))
|
||||
cpSync(join(import.meta.dirname, 'command-path.ps1'), join(RUNTIME_ROOT, 'cli', 'command-path.ps1'))
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
|
||||
() => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] }))
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Desktop-only omissions from already installed production npm packages. */
|
||||
|
||||
/**
|
||||
* Identify build and diagnostic files omitted from the immutable Desktop runtime.
|
||||
* Identify build, diagnostic and other-platform files omitted from the immutable Desktop runtime.
|
||||
* Unrecognized assets and target runtime binaries are retained. Paths name the copied
|
||||
* node_modules tree, including nested package containers.
|
||||
* @param path - Path relative to the production node_modules directory.
|
||||
@@ -16,7 +16,7 @@ export function desktopRuntimeFileExclusion(
|
||||
if (parts.some(part => ['.bin', '.pnpm', '.modules.yaml', '.pnpm-workspace-state-v1.json'].includes(part))) {
|
||||
return 'package-manager metadata'
|
||||
}
|
||||
const file = parts.at(-1) ?? ''
|
||||
const file = parts.at(-1)!
|
||||
if (/\.(?:[cm]?[jt]s|css)\.map$/u.test(file)) return 'source map'
|
||||
if (/\.d\.[cm]?ts$/u.test(file)) return 'TypeScript declaration'
|
||||
if (/\.tsbuildinfo$/u.test(file)) return 'TypeScript build cache'
|
||||
@@ -34,6 +34,9 @@ export function desktopRuntimeFileExclusion(
|
||||
return 'fs-ext build configuration'
|
||||
}
|
||||
if (name === '@mixmark-io/domino' && (entry === 'test' || entry.startsWith('test/'))) return 'Domino test fixtures'
|
||||
if (name === 'node-pty' && target.platform !== 'win32' && /^third_party\/conpty(?:\/|$)/u.test(entry)) {
|
||||
return 'node-pty Windows ConPTY sources'
|
||||
}
|
||||
if (name === 'node-pty' && entry.startsWith('prebuilds/')) {
|
||||
const platform = packageParts[nameParts + 1]
|
||||
if (platform !== undefined && platform !== `${target.platform}-${target.arch}`) return 'node-pty other platform'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Boot the materialized target runtime without access to a user's Harness profile. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { execFile } from 'node:child_process'
|
||||
@@ -12,7 +13,7 @@ import { createPluginProfile } from '../src/project-manager.ts'
|
||||
import type { DesktopRuntimeDescriptor } from '../src/runtime-tree.ts'
|
||||
|
||||
/**
|
||||
* Check Host startup, its matching frontend, external plugins and real Office-to-PDF conversion.
|
||||
* Check CLI package operations, Host startup, its frontend, external plugins and real Office-to-PDF conversion.
|
||||
* @param root - Materialized dsh resources.
|
||||
* @param node - Prepared target Electron executable.
|
||||
* @param runtime - Verified resource descriptor.
|
||||
@@ -25,15 +26,50 @@ export async function smokeDesktopRuntime(
|
||||
): Promise<void> {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-desktop-smoke-'))
|
||||
const profile = join(home, 'profiles', 'desktop')
|
||||
const host = new DesktopHostProcess(node, root, profile, undefined, { ...environment, DSH_HOME: home },
|
||||
const hostEnvironment = {
|
||||
...Object.fromEntries(Object.entries(environment).filter(([name]) => !/^(?:npm|pnpm|corepack)_/iu.test(name))),
|
||||
DSH_HOME: home, HOME: home, USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, XDG_STATE_HOME: home,
|
||||
APPDATA: home, LOCALAPPDATA: home,
|
||||
npm_config_userconfig: join(home, 'npmrc'), npm_config_globalconfig: join(home, 'npmrc'),
|
||||
npm_config_store_dir: join(home, 'store'), npm_config_enable_global_virtual_store: 'false',
|
||||
}
|
||||
const host = new DesktopHostProcess(node, root, profile, undefined, hostEnvironment,
|
||||
undefined, join(resourcesRuntime, 'primary-runtime'),
|
||||
{ pnpm: join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.cjs'), nodeBin: join(resourcesRuntime, 'bin') })
|
||||
{ pnpm: join(resourcesRuntime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'), nodeBin: join(resourcesRuntime, 'bin') })
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
writeFileSync(join(home, 'npmrc'), '')
|
||||
createPluginProfile(profile)
|
||||
const pluginName = 'desktop-runtime-smoke-plugin'
|
||||
const plugin = join(profile, 'node_modules', pluginName)
|
||||
const plugin = join(home, pluginName)
|
||||
mkdirSync(plugin, { recursive: true })
|
||||
const installedName = 'desktop-installed-smoke-plugin'
|
||||
const installedPlugin = join(home, installedName)
|
||||
mkdirSync(installedPlugin)
|
||||
writeFileSync(join(installedPlugin, 'package.json'), JSON.stringify({
|
||||
name: installedName, version: '1.0.0', type: 'module', exports: './index.js', dsh: { bundle: { patch: './bundle.yml' } },
|
||||
}))
|
||||
writeFileSync(join(installedPlugin, 'index.js'), `
|
||||
export function apply(ctx) {
|
||||
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-installed',
|
||||
handler(_request, response) { response.end('installed plugin ready') } }))
|
||||
}
|
||||
`)
|
||||
writeFileSync(join(installedPlugin, 'bundle.yml'), `- insert:\n - id: ${installedName}\n name: ${installedName}\n inject: [webServer]\n`)
|
||||
appendFileSync(join(profile, 'pnpm-workspace.yaml'), 'offline: true\nupdateNotifier: false\n')
|
||||
const cli = join(root, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
|
||||
const runCli = (args: string[]) => promisify(execFile)(node,
|
||||
['--expose-internals', cli, 'plugin', '--profile', 'desktop', ...args],
|
||||
{ cwd: home, env: { ...hostEnvironment, ELECTRON_RUN_AS_NODE: '1' }, timeout: 120_000, windowsHide: true })
|
||||
await runCli(['add', `file:${installedPlugin}`, '--offline', '--ignore-scripts'])
|
||||
const installedEntry = join(profile, 'node_modules', installedName, 'index.js')
|
||||
if (readFileSync(installedEntry, 'utf8') !== readFileSync(join(installedPlugin, 'index.js'), 'utf8')) {
|
||||
throw new Error('desktop runtime: CLI did not install the local plugin')
|
||||
}
|
||||
await runCli(['remove', installedName])
|
||||
if (existsSync(installedEntry)) throw new Error('desktop runtime: CLI did not remove the local plugin')
|
||||
console.log('desktop runtime: CLI offline plugin installation and removal passed')
|
||||
const primary = join(resourcesRuntime, 'primary-runtime')
|
||||
const dependencies = workspaceDependencyPaths(primary, await readPrimaryRuntime(primary))
|
||||
await promisify(execFile)(dependencies.python, ['-I', '-B',
|
||||
@@ -56,6 +92,15 @@ export function apply(ctx) {
|
||||
if (!(ctx instanceof Context)) throw new Error('desktop runtime: external plugin loaded another Cordis instance')
|
||||
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke',
|
||||
handler(_request, response) { response.end('plugin route ready') } }))
|
||||
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-install',
|
||||
async handler(_request, response) {
|
||||
try {
|
||||
response.end(JSON.stringify(await ctx.pluginManager.installBundle(${JSON.stringify(installedPlugin)})))
|
||||
} catch (error) {
|
||||
response.statusCode = 500
|
||||
response.end(inspect(error, { depth: 5 }))
|
||||
}
|
||||
} }))
|
||||
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-office-cli',
|
||||
async handler(_request, response) {
|
||||
try {
|
||||
@@ -90,12 +135,13 @@ export function apply(ctx) {
|
||||
}
|
||||
}
|
||||
`)
|
||||
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills]\n')
|
||||
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills, pluginManager]\n')
|
||||
cpSync(plugin, join(profile, 'node_modules', pluginName), { recursive: true })
|
||||
const manifest = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')) as {
|
||||
dependencies: Record<string, string>
|
||||
dependencies?: Record<string, string>
|
||||
dsh: { profile: { bundles: string[] } }
|
||||
}
|
||||
manifest.dependencies[pluginName] = '1.0.0'
|
||||
manifest.dependencies = { ...manifest.dependencies, [pluginName]: `file:${plugin}` }
|
||||
manifest.dsh.profile.bundles.push(pluginName)
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify(manifest))
|
||||
writeFileSync(join(profile, 'cordis.patch.yml'), '- id: webserver\n config:\n host: 127.0.0.1\n port: 0\n')
|
||||
@@ -111,6 +157,14 @@ export function apply(ctx) {
|
||||
}
|
||||
const pluginResponse = await fetch(new URL('/desktop-smoke', ready.url), { headers: { cookie } })
|
||||
if (await pluginResponse.text() !== 'plugin route ready') throw new Error('desktop runtime: plugin HTTP route failed')
|
||||
const installation = await fetch(new URL('/desktop-smoke-install', ready.url), {
|
||||
headers: { cookie }, signal: AbortSignal.timeout(120_000),
|
||||
})
|
||||
const installationResult = await installation.text()
|
||||
const installed = await fetch(new URL('/desktop-smoke-installed', ready.url), { headers: { cookie } })
|
||||
if (!installation.ok || !installed.ok || await installed.text() !== 'installed plugin ready') {
|
||||
throw new Error(`desktop runtime: bundled pnpm plugin installation failed: ${installationResult}`)
|
||||
}
|
||||
for (const { extension } of inputs) {
|
||||
const converted = await fetch(new URL(`/desktop-smoke-office/${extension}`, ready.url), {
|
||||
headers: { cookie }, signal: AbortSignal.timeout(120_000),
|
||||
@@ -133,7 +187,8 @@ export function apply(ctx) {
|
||||
console.log('desktop runtime: DOCX, XLSX, PPTX to PDF and skill CLI discovery passed')
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
await host.stop()
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
try { await host.stop() } finally {
|
||||
await rm(home, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ function runtimeResources(): RuntimeResources {
|
||||
const nodeBin = development ? join(app.getAppPath(), 'scripts', 'node-bin') : join(process.resourcesPath, 'runtime', 'bin')
|
||||
const pnpm = (development ? process.env.DSH_DESKTOP_PNPM_ENTRY : undefined)
|
||||
?? (development ? join(app.getAppPath(), 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
|
||||
: join(process.resourcesPath, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'))
|
||||
: join(process.resourcesPath, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'))
|
||||
const dsh = (development ? process.env.DSH_DESKTOP_DSH_DIR : undefined)
|
||||
?? (development ? join(app.getAppPath(), '.desktop-build', 'development', 'project') : join(app.getAppPath(), 'dsh'))
|
||||
return { node, nodeBin, pnpm, dsh }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/** The installed CLI uses the shared pnpm distribution with Electron's package-operation environment. */
|
||||
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { runCli } from '@deepseek-ai/dsh/lib/bin.js'
|
||||
import { runDesktopCli } from '../../desktop-host/src/cli.ts'
|
||||
import { installOfficeEngineResolution } from '../../desktop-host/src/office-engine.ts'
|
||||
|
||||
vi.mock('@deepseek-ai/dsh/lib/bin.js', () => ({ runCli: vi.fn() }))
|
||||
vi.mock('../../desktop-host/src/office-engine.ts', () => ({ installOfficeEngineResolution: vi.fn() }))
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each(['system-bin', undefined])('dispatches with bundled pnpm when PATH is %s', async (path) => {
|
||||
vi.stubEnv('PATH', path)
|
||||
const runtime = join('Application 中文 with spaces', 'resources', 'app.asar', 'dsh')
|
||||
const support = join('Application 中文 with spaces', 'resources', 'runtime')
|
||||
|
||||
await runDesktopCli(runtime, support)
|
||||
|
||||
expect(installOfficeEngineResolution).toHaveBeenCalledExactlyOnceWith(runtime)
|
||||
expect(runCli).toHaveBeenCalledExactlyOnceWith({
|
||||
manageDesktopProfile: true,
|
||||
packageManager: {
|
||||
command: process.execPath,
|
||||
args: ['--expose-internals', join(support, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')],
|
||||
env: {
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
||||
PATH: `${join(support, 'bin')}${delimiter}${path ?? ''}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/** Install and use a local dependency with the packaged pnpm and Electron Node launcher. */
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Exercise offline installation and package scripts without a system pnpm or Node installation.
|
||||
* @param {string} resourcesRuntime - Packaged runtime directory outside ASAR.
|
||||
* @returns {Promise<void>} Resolves after execution and removal of the private installation.
|
||||
*/
|
||||
export async function checkPnpm(resourcesRuntime) {
|
||||
assert.ok(process.versions.electron, 'Run the package-manager smoke with Electron Node mode')
|
||||
assert.equal(existsSync(join(resourcesRuntime, 'pnpm')), false, 'Desktop must ship only primary-runtime pnpm')
|
||||
const primary = join(resourcesRuntime, 'primary-runtime')
|
||||
const pnpm = join(primary, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
|
||||
const versions = JSON.parse(readFileSync(join(resourcesRuntime, 'versions.json'), 'utf8'))
|
||||
const manifest = JSON.parse(readFileSync(join(primary, 'runtime.json'), 'utf8'))
|
||||
assert.equal(versions.pnpm, manifest.pnpm)
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'desktop-pnpm-smoke-'))
|
||||
try {
|
||||
mkdirSync(join(scratch, 'dependency'))
|
||||
writeFileSync(join(scratch, 'dependency', 'package.json'), JSON.stringify({
|
||||
name: 'desktop-pnpm-smoke-dependency', version: '1.0.0', main: 'index.cjs',
|
||||
}))
|
||||
writeFileSync(join(scratch, 'dependency', 'index.cjs'), 'exports.answer = 42\n')
|
||||
writeFileSync(join(scratch, 'package.json'), JSON.stringify({
|
||||
name: 'desktop-node-script-smoke', private: true,
|
||||
dependencies: { 'desktop-pnpm-smoke-dependency': 'file:./dependency' }, scripts: { check: 'node check.cjs' },
|
||||
}))
|
||||
writeFileSync(join(scratch, 'npmrc'), '')
|
||||
writeFileSync(join(scratch, 'check.cjs'), `
|
||||
const assert = require('node:assert/strict')
|
||||
assert.equal(require('desktop-pnpm-smoke-dependency').answer, 42)
|
||||
assert.equal(process.execPath, ${JSON.stringify(process.execPath)})
|
||||
assert.ok(process.versions.electron)
|
||||
assert.ok(process.execArgv.includes('--expose-internals'))
|
||||
assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
|
||||
console.log('desktop-node-script-ok')
|
||||
`)
|
||||
const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec|LD_LIBRARY_PATH)$/iu.test(name)))
|
||||
const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin'
|
||||
const run = args => execFileSync(process.execPath, ['--expose-internals', pnpm, ...args], {
|
||||
cwd: scratch, encoding: 'utf8', timeout: 45_000, windowsHide: true,
|
||||
env: { ...environment, pnpm_config_verify_deps_before_run: 'false',
|
||||
ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
||||
PATH: `${join(resourcesRuntime, 'bin')}${delimiter}${systemBin}`,
|
||||
HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch,
|
||||
XDG_CONFIG_HOME: scratch, XDG_CACHE_HOME: scratch, XDG_STATE_HOME: scratch,
|
||||
npm_config_userconfig: join(scratch, 'npmrc'), npm_config_globalconfig: join(scratch, 'npmrc'),
|
||||
npm_config_store_dir: join(scratch, 'store'), npm_config_enable_global_virtual_store: 'false',
|
||||
npm_config_update_notifier: 'false' },
|
||||
})
|
||||
assert.equal(run(['--version']).trim(), versions.pnpm)
|
||||
run(['install', '--offline', '--ignore-scripts'])
|
||||
assert.match(run(['run', 'check']), /desktop-node-script-ok/u)
|
||||
} finally {
|
||||
await rm(scratch, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
assert.ok(process.argv[2], 'Pass the packaged runtime directory')
|
||||
await checkPnpm(process.argv[2])
|
||||
console.log('desktop pnpm: offline installation and Electron Node package script passed')
|
||||
}
|
||||
+2
-28
@@ -8,6 +8,7 @@ import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { checkPnpm } from './pnpm-smoke.mjs'
|
||||
|
||||
const runtime = process.argv[2]
|
||||
assert.ok(runtime, 'Pass the filtered resources/dsh directory')
|
||||
@@ -20,33 +21,6 @@ const resourcesRuntime = process.argv[3] ?? join(dirname(root), 'runtime')
|
||||
const requireRuntime = createRequire(join(root, 'package.json'))
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'dsh-runtime-payload-'))
|
||||
|
||||
/** Run a package script with only the shipped node launcher available on PATH. */
|
||||
function checkPnpm() {
|
||||
const bin = join(resourcesRuntime, 'bin')
|
||||
const pnpm = join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.mjs')
|
||||
writeFileSync(join(scratch, 'package.json'), JSON.stringify({
|
||||
name: 'desktop-node-script-smoke', private: true, scripts: { check: 'node check.cjs' },
|
||||
}))
|
||||
writeFileSync(join(scratch, 'check.cjs'), `
|
||||
const assert = require('node:assert/strict')
|
||||
assert.equal(process.execPath, ${JSON.stringify(process.execPath)})
|
||||
assert.ok(process.versions.electron)
|
||||
assert.ok(process.execArgv.includes('--expose-internals'))
|
||||
assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
|
||||
console.log('desktop-node-script-ok')
|
||||
`)
|
||||
const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec)$/iu.test(name)))
|
||||
const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin'
|
||||
// This dependency-free fixture checks script launch, without pnpm's implicit install and update-network check.
|
||||
const output = execFileSync(process.execPath, ['--expose-internals', pnpm, 'run', 'check'], {
|
||||
cwd: scratch, encoding: 'utf8', timeout: 45_000,
|
||||
env: { ...environment, pnpm_config_verify_deps_before_run: 'false',
|
||||
ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
||||
PATH: `${bin}${delimiter}${systemBin}`, HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch },
|
||||
})
|
||||
assert.match(output, /desktop-node-script-ok/u)
|
||||
}
|
||||
|
||||
/** Spawn only a fixed Node program and await the terminal's drained exit event. */
|
||||
async function checkPty() {
|
||||
const pty = requireRuntime('node-pty')
|
||||
@@ -162,7 +136,7 @@ try {
|
||||
const builtin = requireRuntime('node-addon-require-builtin')
|
||||
assert.equal(typeof builtin.requireBuiltin('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
|
||||
console.error('runtime payload: pnpm')
|
||||
checkPnpm()
|
||||
await checkPnpm(resourcesRuntime)
|
||||
console.error('runtime payload: koffi')
|
||||
checkKoffi()
|
||||
console.error('runtime payload: sharp')
|
||||
|
||||
@@ -88,8 +88,10 @@ describe('installed-update application inputs and builder configuration', () =>
|
||||
expect(config.extraMetadata.main).toBe('qualification-bootstrap.mjs')
|
||||
expect(config.directories.output).toBe(join(manifest, '..', versions[index]!, 'installer'))
|
||||
const dsh = join(manifest, '..', versions[index]!, 'dsh')
|
||||
expect(config.files[2]).toMatchObject({ from: dsh, to: 'dsh' })
|
||||
expect(config.files[3]).toMatchObject({ from: join(dsh, 'node_modules'), to: 'dsh/node_modules' })
|
||||
expect(config.files.at(-2)).toMatchObject({ from: dsh, to: 'dsh' })
|
||||
expect(config.files.at(-1)).toMatchObject({ from: join(dsh, 'node_modules'), to: 'dsh/node_modules' })
|
||||
expect(config.files).toContain('!node_modules/**/*.{js,cjs,mjs,ts,cts,mts,css}.map')
|
||||
expect(config.files).toContain('!node_modules/**/*.tsbuildinfo')
|
||||
expect(config.win.forceCodeSigning).toBe(true)
|
||||
expect(config.win.signtoolOptions.publisherName).toBe('CN=Fixture,O=Fixture,C=CN')
|
||||
expect(typeof config.win.signtoolOptions.sign).toBe('function')
|
||||
|
||||
@@ -93,9 +93,9 @@ describe('desktop macOS release signature', () => {
|
||||
const ignored = (path: string): boolean => config.mac.signIgnore.some(pattern => new RegExp(pattern).test(path))
|
||||
expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/en.lproj/locale.pak')).toBe(true)
|
||||
expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/resources.pak')).toBe(true)
|
||||
expect(ignored('/App.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/addon.node')).toBe(true)
|
||||
for (const path of [
|
||||
'/App.app/Contents/Resources/runtime/node/node',
|
||||
'/App.app/Contents/Resources/runtime/pnpm/addon.node',
|
||||
'/App.app/Contents/Frameworks/Electron.framework/Versions/A/library.dylib',
|
||||
'/App.app/Contents/Frameworks/Electron.framework',
|
||||
'/App.app',
|
||||
|
||||
@@ -443,6 +443,21 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('desktop main startup', () => {
|
||||
it.each([true, false])('selects the package manager before Host startup (packaged=%s)', async (packaged) => {
|
||||
harness.app.isPackaged = packaged
|
||||
await readyForUpdate()
|
||||
const host = harness.hosts[0]!
|
||||
expect(host.node).toBe(process.execPath)
|
||||
expect(host.primaryRuntime).toBe(packaged
|
||||
? join('desktop-test-resources', 'runtime', 'primary-runtime') : 'test-primary-runtime')
|
||||
expect(host.packageManager).toMatchObject({
|
||||
pnpm: packaged
|
||||
? join('desktop-test-resources', 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
|
||||
: 'test-pnpm',
|
||||
nodeBin: packaged ? join('desktop-test-resources', 'runtime', 'bin') : join('desktop-test-app', 'scripts', 'node-bin'),
|
||||
})
|
||||
})
|
||||
|
||||
it('starts the Host with authentication popups disabled when local settings are invalid', async () => {
|
||||
configureTestAuthPopup(false)
|
||||
const path = join(harness.app.getPath('userData'), 'desktop', 'settings.json')
|
||||
|
||||
@@ -85,6 +85,62 @@ it('retains native prebuilds for the selected macOS architecture', () => {
|
||||
expect(desktopRuntimeFileExclusion('node-pty/prebuilds/win32-x64/conpty.node', mac, 'darwin-arm64')).toBeDefined()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['darwin', 'arm64'], ['darwin', 'x64'],
|
||||
['linux', 'arm64'], ['linux', 'x64'],
|
||||
['win32', 'arm64'], ['win32', 'x64'],
|
||||
] as const)('copies ConPTY sources only for Windows targets (%s/%s)', (platform, arch) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'desktop-conpty-policy-'))
|
||||
try {
|
||||
const source = join(root, 'source')
|
||||
const output = join(root, 'output')
|
||||
const prefixes = ['', '@example/outer/node_modules/']
|
||||
const conpty = ['arm64', 'x64'].flatMap(cpu => ['conpty.dll', 'OpenConsole.exe'].map(
|
||||
file => `node-pty/third_party/conpty/1.25.260303002/win10-${cpu}/${file}`,
|
||||
))
|
||||
const native = platform === 'win32'
|
||||
? ['conpty.node', 'conpty_console_list.node', 'conpty/conpty.dll', 'conpty/OpenConsole.exe']
|
||||
: ['pty.node', 'spawn-helper']
|
||||
const retained = [
|
||||
...native.map(file => `node-pty/prebuilds/${platform}-${arch}/${file}`),
|
||||
...native.map(file => `node-pty/build/Release/${file}`),
|
||||
'node-pty/lib/index.js', 'node-pty/LICENSE',
|
||||
'node-pty/third_party/conpty-notes/README.md', 'node-pty/third_party/conpty.dll',
|
||||
'other/third_party/conpty/conpty.dll',
|
||||
]
|
||||
for (const prefix of prefixes) {
|
||||
for (const path of [...conpty, ...retained]) {
|
||||
mkdirSync(join(source, prefix, path, '..'), { recursive: true })
|
||||
writeFileSync(join(source, prefix, path), `payload:${prefix}${path}`)
|
||||
}
|
||||
}
|
||||
cpSync(source, output, {
|
||||
recursive: true, dereference: true,
|
||||
filter: (path) => {
|
||||
const entry = relative(source, path)
|
||||
return desktopRuntimeFileExclusion(
|
||||
platform === 'win32' ? entry.replaceAll('/', '\\') : entry,
|
||||
{ platform, arch }, `${platform}-${arch}`,
|
||||
) === undefined
|
||||
},
|
||||
})
|
||||
for (const prefix of prefixes) {
|
||||
expect(existsSync(join(output, prefix, 'node-pty/third_party/conpty'))).toBe(platform === 'win32')
|
||||
for (const path of conpty) {
|
||||
expect(existsSync(join(output, prefix, path)), `${prefix}${path}`).toBe(platform === 'win32')
|
||||
}
|
||||
for (const path of [...retained, ...(platform === 'win32' ? conpty : [])]) {
|
||||
expect(readFileSync(join(output, prefix, path), 'utf8')).toBe(`payload:${prefix}${path}`)
|
||||
}
|
||||
for (const path of conpty) {
|
||||
expect(readFileSync(join(source, prefix, path), 'utf8')).toBe(`payload:${prefix}${path}`)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['linux', 'x64', 'linux-x64'],
|
||||
['linux', 'arm64', 'wasm'],
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/** Desktop resource preparation publishes versions only for complete resources. */
|
||||
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
root: '',
|
||||
target: 'mac-arm64' as 'mac-arm64' | 'mac-x64' | 'win-x64',
|
||||
pnpm: '11.7.0' as string | undefined,
|
||||
failure: undefined as 'electron' | 'primary' | 'cli' | undefined,
|
||||
preparePrimary: vi.fn(),
|
||||
commandLink: vi.fn(),
|
||||
}))
|
||||
vi.mock('../scripts/desktop-build-paths.mjs', async importOriginal => ({
|
||||
...await importOriginal<typeof import('../scripts/desktop-build-paths.mjs')>(),
|
||||
resolveDesktopBuildTarget: () => state.target,
|
||||
resolveDesktopTargetBuildPaths: () => ({
|
||||
runtime: join(state.root, 'runtime'), electron: join(state.root, 'electron'), downloads: join(state.root, 'downloads'),
|
||||
}),
|
||||
}))
|
||||
vi.mock('@electron/get', () => ({ downloadArtifact: async () => 'electron.zip' }))
|
||||
vi.mock('extract-zip', () => ({ default: async () => {
|
||||
if (state.failure === 'electron') throw new Error('Electron extraction failed')
|
||||
} }))
|
||||
vi.mock('node:child_process', async importOriginal => ({
|
||||
...await importOriginal<typeof import('node:child_process')>(),
|
||||
execFileSync: (file: string) => file === '/usr/libexec/PlistBuddy' ? '12.0\n' : '24.18.1\n',
|
||||
}))
|
||||
vi.mock('../scripts/prepare-command-link.ts', () => ({ prepareCommandLink: state.commandLink }))
|
||||
// The compiled command manager is a build input; all source resource copies use real files.
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>()
|
||||
return { ...actual, cpSync: (...args: Parameters<typeof actual.cpSync>) => {
|
||||
if (String(args[0]).endsWith(join('lib', 'command-manager-entry.js'))) {
|
||||
if (state.failure === 'cli') throw new Error('command manager copy failed')
|
||||
actual.writeFileSync(args[1], 'command manager fixture')
|
||||
} else actual.cpSync(...args)
|
||||
} }
|
||||
})
|
||||
vi.mock('../scripts/prepare-primary-runtime.ts', () => ({
|
||||
preparePrimaryRuntime: state.preparePrimary,
|
||||
}))
|
||||
|
||||
const originalArgv = process.argv
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
state.root = mkdtempSync(join(tmpdir(), 'desktop-runtime-preparation-'))
|
||||
state.target = 'mac-arm64'
|
||||
state.pnpm = '11.7.0'
|
||||
state.failure = undefined
|
||||
process.argv = [process.execPath, 'prepare-runtime.ts']
|
||||
vi.stubEnv('DSH_DESKTOP_PACKAGING_RUN_DIR', undefined)
|
||||
state.preparePrimary.mockImplementation(async () => {
|
||||
if (state.failure === 'primary') throw new Error('primary preparation failed')
|
||||
const root = join(state.root, 'runtime', 'primary-runtime')
|
||||
const pnpm = join(root, 'dependencies', 'pnpm', 'bin')
|
||||
mkdirSync(pnpm, { recursive: true })
|
||||
writeFileSync(join(pnpm, 'pnpm.mjs'), '')
|
||||
writeFileSync(join(root, 'runtime.json'), JSON.stringify({
|
||||
desktopVersion: '1.0.0', platform: state.target === 'win-x64' ? 'win32' : 'darwin',
|
||||
arch: state.target === 'mac-arm64' ? 'arm64' : 'x64',
|
||||
python: '3.12.14', node: '24.17.0', pnpm: state.pnpm, pythonPackages: { numpy: '2.3.5' },
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
process.argv = originalArgv
|
||||
vi.unstubAllEnvs()
|
||||
await rm(state.root, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
|
||||
})
|
||||
|
||||
it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('prepares %s with one pnpm distribution and its CLI resources', async (target) => {
|
||||
state.target = target
|
||||
const windows = target === 'win-x64'
|
||||
if (windows) process.argv.push('--defer-primary-runtime-smoke')
|
||||
const runtime = join(state.root, 'runtime')
|
||||
mkdirSync(join(runtime, 'pnpm'), { recursive: true })
|
||||
await import('../scripts/prepare-runtime.ts')
|
||||
expect(existsSync(join(runtime, 'pnpm'))).toBe(false)
|
||||
expect(existsSync(join(runtime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'))).toBe(true)
|
||||
expect(existsSync(join(runtime, 'bin', 'node'))).toBe(true)
|
||||
expect(existsSync(join(runtime, 'cli', 'bin', windows ? 'dsh.cmd' : 'dsh'))).toBe(true)
|
||||
expect(readFileSync(join(runtime, 'cli', 'command-manager.js'), 'utf8')).toBe('command manager fixture')
|
||||
expect(existsSync(join(runtime, 'cli', 'command-path.ps1'))).toBe(true)
|
||||
expect(JSON.parse(readFileSync(join(runtime, 'versions.json'), 'utf8'))).toEqual({
|
||||
schemaVersion: 1, node: '24.18.1', pnpm: '11.7.0',
|
||||
})
|
||||
expect(state.preparePrimary).toHaveBeenCalledWith({ deferSmoke: windows })
|
||||
if (windows) expect(state.commandLink).not.toHaveBeenCalled()
|
||||
else expect(state.commandLink).toHaveBeenCalledWith(join(runtime, 'cli'), target === 'mac-arm64' ? 'arm64' : 'x64', '12.0')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['electron', 'Electron extraction failed'],
|
||||
['primary', 'primary preparation failed'],
|
||||
['cli', 'command manager copy failed'],
|
||||
] as const)('does not publish versions when %s preparation fails', async (failure, error) => {
|
||||
state.failure = failure
|
||||
const runtime = join(state.root, 'runtime')
|
||||
mkdirSync(runtime)
|
||||
writeFileSync(join(runtime, 'versions.json'), 'stale versions')
|
||||
await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error)
|
||||
expect(existsSync(join(runtime, 'versions.json'))).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['not-a-version', 'primary runtime: invalid metadata'],
|
||||
[undefined, 'primary-runtime manifest has no pnpm version'],
|
||||
])('rejects pnpm metadata %s even when Windows smoke is deferred', async (pnpm, error) => {
|
||||
state.target = 'win-x64'
|
||||
process.argv.push('--defer-primary-runtime-smoke')
|
||||
state.pnpm = pnpm
|
||||
await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, relative } from 'node:path'
|
||||
import { Packager, type Configuration, type PlatformSpecificBuildOptions } from 'app-builder-lib'
|
||||
import { readAsar } from 'app-builder-lib/out/asar/asar.js'
|
||||
import type { FileMatcher } from 'app-builder-lib/out/fileMatcher.js'
|
||||
import { MacPackager } from 'app-builder-lib/out/macPackager.js'
|
||||
import { PM } from 'app-builder-lib/out/node-module-collector/index.js'
|
||||
import type { PlatformPackager } from 'app-builder-lib/out/platformPackager.js'
|
||||
import type { ResolvedFileSet } from 'app-builder-lib/out/util/appFileCopier.js'
|
||||
import { WinPackager } from 'app-builder-lib/out/winPackager.js'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { createElectronBuilderConfig } from '../scripts/electron-builder-config.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
// The locked builder exports these packaging operations without declarations.
|
||||
const { getNodeModuleFileMatcher } = require('app-builder-lib/out/fileMatcher.js') as {
|
||||
getNodeModuleFileMatcher: (app: string, destination: string, expand: (value: string) => string,
|
||||
options: PlatformSpecificBuildOptions, packager: Packager) => FileMatcher
|
||||
}
|
||||
const { computeNodeModuleFileSets } = require('app-builder-lib/out/util/appFileCopier.js') as {
|
||||
computeNodeModuleFileSets: (packager: PlatformPackager<PlatformSpecificBuildOptions>, matcher: FileMatcher) => Promise<ResolvedFileSet[]>
|
||||
}
|
||||
const { AsarPackager } = require('app-builder-lib/out/asar/asarUtil.js') as {
|
||||
AsarPackager: new (packager: object, configuration: object) => { pack(files: ResolvedFileSet[]): Promise<void> }
|
||||
}
|
||||
|
||||
it.each([
|
||||
['darwin', 'arm64'], ['darwin', 'x64'], ['win32', 'x64'],
|
||||
] as const)('omits shell build files from %s/%s ASAR while retaining runtime assets', async (platform, arch) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'desktop-shell-files-'))
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }))
|
||||
const appDir = join(root, 'app')
|
||||
const resources = join(root, 'resources')
|
||||
const destination = join(resources, 'app')
|
||||
await mkdir(resources, { recursive: true })
|
||||
const manifest = { name: 'fixture', version: '1.0.0', dependencies: { shell: '1.0.0', '@fixture/shared': '2.0.0' } }
|
||||
const packages = [
|
||||
['node_modules/shell', { name: 'shell', version: '1.0.0', dependencies: { '@fixture/shared': '1.0.0' } }],
|
||||
['node_modules/@fixture/shared', { name: '@fixture/shared', version: '2.0.0' }],
|
||||
['node_modules/shell/node_modules/@fixture/shared', { name: '@fixture/shared', version: '1.0.0' }],
|
||||
] as const
|
||||
const omitted = [
|
||||
'index.js.map', 'lib/index.cjs.map', 'lib/index.mjs.map', 'lib/index.ts.map',
|
||||
'lib/index.cts.map', 'lib/index.mts.map', 'lib/style.css.map', 'tsconfig.tsbuildinfo', 'lib/types/tsconfig.tsbuildinfo',
|
||||
]
|
||||
const retained = [
|
||||
'index.js', 'lib/index.cjs', 'lib/index.mjs', 'lib/index.ts', 'lib/index.cts', 'lib/index.mts',
|
||||
'lib/style.css', 'lib/types/runtime.js', 'data.map', 'lib/data.map', 'lib/addon.node', 'lib/engine.wasm', 'LICENSE', 'NOTICE',
|
||||
]
|
||||
const originals = new Map<string, Buffer>()
|
||||
for (const [directory, metadata] of packages) {
|
||||
for (const name of [...retained, ...omitted, 'package.json']) {
|
||||
const file = join(appDir, directory, name)
|
||||
const contents = Buffer.from(name === 'package.json' ? JSON.stringify(metadata) : `${directory}/${name}\n`)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, contents)
|
||||
originals.set(file, contents)
|
||||
}
|
||||
}
|
||||
await writeFile(join(appDir, 'package.json'), JSON.stringify(manifest))
|
||||
const desktopConfig = createElectronBuilderConfig({
|
||||
DSH_DESKTOP_APP_ID: 'com.example.shell-files',
|
||||
DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com',
|
||||
DSH_DESKTOP_MANDATORY_UPDATE_CONFIG: JSON.stringify({ allowedAuthOrigins: ['https://login.example.com'] }),
|
||||
DSH_DESKTOP_UNSIGNED: platform === 'win32' ? '1' : '0',
|
||||
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)',
|
||||
DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234',
|
||||
APPLE_KEYCHAIN_PROFILE: 'shell-files-test',
|
||||
DOWNLOAD_TEST_ORIGIN: 'https://desktop-updates.example.com',
|
||||
DOWNLOAD_TEST_RELEASE_ID: '0123456789abcdef0123456789abcdef',
|
||||
}, platform, arch)
|
||||
const config: Configuration = {
|
||||
files: desktopConfig.files.map(file => typeof file === 'string' ? file : { ...file, filter: [...file.filter] }),
|
||||
}
|
||||
const info = new Packager({ projectDir: appDir })
|
||||
onTestFinished(() => info.tempDirManager.cleanup())
|
||||
Object.defineProperties(info, {
|
||||
appDir: { value: appDir }, config: { value: config },
|
||||
metadata: { value: manifest }, originalMetadata: { value: manifest },
|
||||
appInfo: { value: { type: 'commonjs' } },
|
||||
framework: { value: { name: 'electron' } },
|
||||
})
|
||||
// Traverse the installed fixture without invoking a host package manager.
|
||||
vi.spyOn(info, 'getPackageManager').mockResolvedValue(PM.TRAVERSAL)
|
||||
vi.spyOn(info, 'getWorkspaceRoot').mockResolvedValue(appDir)
|
||||
const packager = platform === 'darwin' ? new MacPackager(info) : new WinPackager(info)
|
||||
const matcher = getNodeModuleFileMatcher(appDir, destination, value => value, packager.platformSpecificBuildOptions, info)
|
||||
const fileSets = await computeNodeModuleFileSets(packager, matcher)
|
||||
expect(fileSets.map(set => relative(destination, set.destination).replaceAll('\\', '/')).sort())
|
||||
.toEqual(packages.map(([directory]) => directory).sort())
|
||||
const consoleLog = console.log
|
||||
try {
|
||||
await new AsarPackager(packager, {
|
||||
defaultDestination: destination, resourcePath: resources, options: { smartUnpack: false },
|
||||
}).pack(fileSets)
|
||||
} finally {
|
||||
// Builder temporarily intercepts console.log while writing ASAR.
|
||||
console.log = consoleLog
|
||||
}
|
||||
const archive = await readAsar(join(resources, 'app.asar'))
|
||||
for (const [directory] of packages) {
|
||||
for (const name of [...retained, 'package.json']) {
|
||||
expect(await archive.readFile(join(directory, name))).toEqual(originals.get(join(appDir, directory, name)))
|
||||
}
|
||||
for (const name of omitted) expect(archive.getNode(join(directory, name))).toBeUndefined()
|
||||
}
|
||||
for (const [file, contents] of originals) expect(await readFile(file)).toEqual(contents)
|
||||
})
|
||||
@@ -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.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
const currentMenu = page.getByRole('menu')
|
||||
await currentMenu.waitFor({ timeout: 10_000 })
|
||||
expect(await currentMenu.getByRole('menuitem').allTextContents())
|
||||
.toEqual(['仅可查看', '工作区内修改', '完全权限', 'Auto reviewEXP'])
|
||||
.toEqual(['仅可查看', '工作区内修改', '完全权限', '自动审查EXP'])
|
||||
await captureAutoReviewState(page, 'experimental-current-session-picker')
|
||||
const currentSnapshot = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(CURRENT_SESSION_PICKER_EXPECTED, currentSnapshot, MODE)
|
||||
@@ -77,10 +77,10 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
expect(currentMenuBox!.width).toBeGreaterThan(currentTriggerBox!.width)
|
||||
expect(Math.abs(currentTriggerBox!.y - currentMenuBox!.y - currentMenuBox!.height - 4)).toBeLessThan(1)
|
||||
|
||||
await currentMenu.getByRole('menuitem', { name: 'Auto review EXP' }).click()
|
||||
const autoDialog = page.getByRole('dialog', { name: '确认启用 Auto review(实验)?' })
|
||||
await currentMenu.getByRole('menuitem', { name: '自动审查 EXP' }).click()
|
||||
const autoDialog = page.getByRole('dialog', { name: '确认启用自动审查(实验)?' })
|
||||
await autoDialog.waitFor({ timeout: 10_000 })
|
||||
const autoEnable = autoDialog.getByRole('button', { name: '启用 Auto review' })
|
||||
const autoEnable = autoDialog.getByRole('button', { name: '启用自动审查' })
|
||||
expect(await autoEnable.isDisabled()).toBe(true)
|
||||
expect(await autoDialog.getByText(/不使用沙箱/).count()).toBe(1)
|
||||
expect(await autoDialog.getByText(/误放行或误拒绝/).count()).toBe(1)
|
||||
@@ -105,7 +105,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
await slashPicker.waitFor({ timeout: 10_000 })
|
||||
const slashRows = slashPicker.getByRole('option')
|
||||
expect(await slashRows.count()).toBe(4)
|
||||
expect(await slashPicker.getByRole('option', { name: 'Auto review EXP' }).count()).toBe(1)
|
||||
expect(await slashPicker.getByRole('option', { name: '自动审查 EXP' }).count()).toBe(1)
|
||||
await captureAutoReviewState(page, 'experimental-slash-picker')
|
||||
const slashSnapshot = await captureStableAria(page, '[aria-label="/permission 选项"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(SLASH_PICKER_EXPECTED, slashSnapshot, MODE)
|
||||
@@ -119,15 +119,15 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
expect(slashPickerBox!.x).toBeCloseTo(composerBox!.x)
|
||||
expect(Math.abs(composerBox!.y - slashPickerBox!.y - slashPickerBox!.height - 4)).toBeLessThan(1)
|
||||
|
||||
await slashPicker.getByRole('option', { name: 'Auto review EXP' }).click()
|
||||
const slashDialog = page.getByRole('dialog', { name: '确认启用 Auto review(实验)?' })
|
||||
await slashPicker.getByRole('option', { name: '自动审查 EXP' }).click()
|
||||
const slashDialog = page.getByRole('dialog', { name: '确认启用自动审查(实验)?' })
|
||||
await slashDialog.waitFor({ timeout: 10_000 })
|
||||
expect(await slashDialog.count()).toBe(1)
|
||||
expect(await slashPicker.count()).toBe(0)
|
||||
await slashDialog.getByRole('checkbox', { name: '我已了解这些风险,并愿意继续' }).check()
|
||||
await slashDialog.getByRole('button', { name: '启用 Auto review' }).click()
|
||||
await slashDialog.getByRole('button', { name: '启用自动审查' }).click()
|
||||
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
|
||||
.toBe('访问模式,当前:Auto review EXP')
|
||||
.toBe('访问模式,当前:自动审查')
|
||||
expect(await slashDialog.count()).toBe(0)
|
||||
expect(await input.textContent()).toBe('')
|
||||
await captureAutoReviewState(page, 'experimental-current-session')
|
||||
@@ -188,12 +188,18 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
expect(menuBox.width).toBeLessThanOrEqual(360)
|
||||
expect(menuBox.x).toBeGreaterThanOrEqual(0)
|
||||
expect(menuBox.x + menuBox.width).toBeLessThanOrEqual(420)
|
||||
for (const name of ['仅可查看', '工作区内修改', '完全权限', 'Auto review']) {
|
||||
for (const name of ['仅可查看', '工作区内修改', '完全权限', '自动审查']) {
|
||||
expect(await menu.getByText(name, { exact: true }).evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true)
|
||||
}
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await writeComposerDraft(page, input, '/permission auto')
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:自动审查')
|
||||
expect(await access.getByText('自动审查', { exact: true }).isVisible()).toBe(false)
|
||||
expect(await access.getByText('EXP', { exact: true }).count()).toBe(0)
|
||||
expect(await access.getAttribute('title')).toBe('无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。')
|
||||
await writeComposerDraft(page, input, '/permission')
|
||||
await input.press('Escape')
|
||||
await input.press('Enter')
|
||||
@@ -206,10 +212,13 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
expect(slashBox.x + slashBox.width).toBeLessThanOrEqual(420)
|
||||
expect(await slash.evaluate(node => getComputedStyle(node).minWidth)).toBe('min(220px, 100%)')
|
||||
expect(await slash.evaluate(node => getComputedStyle(node).maxWidth)).toBe('100%')
|
||||
for (const name of ['仅可查看', '工作区内修改', '完全权限', 'Auto review']) {
|
||||
for (const name of ['仅可查看', '工作区内修改', '完全权限', '自动审查']) {
|
||||
expect(await slash.getByText(name, { exact: true }).evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true)
|
||||
}
|
||||
await page.keyboard.press('Escape')
|
||||
await writeComposerDraft(page, input, '/permission danger-full-access')
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:完全权限')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
@@ -219,7 +228,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
const access = page.locator('button[aria-label^="访问模式"]').first()
|
||||
await writeComposerDraft(page, input, '/permission auto')
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:Auto review EXP')
|
||||
await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:自动审查')
|
||||
expect(await page.getByRole('dialog').count()).toBe(0)
|
||||
await writeComposerDraft(page, input, '/permission')
|
||||
await input.press('Escape')
|
||||
@@ -237,7 +246,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
await input.press('Enter')
|
||||
await slash.waitFor()
|
||||
expect(await slash.getByRole('option').count()).toBe(3)
|
||||
expect(await slash.getByText('Auto review', { exact: true }).count()).toBe(0)
|
||||
expect(await slash.getByText('自动审查', { exact: true }).count()).toBe(0)
|
||||
await page.keyboard.press('Escape')
|
||||
await access.click()
|
||||
await expect.poll(() => page.getByRole('menuitem').allTextContents())
|
||||
@@ -248,7 +257,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
await scaffold.ctx.loader.await()
|
||||
await access.click()
|
||||
await expect.poll(() => page.getByRole('menuitem').allTextContents())
|
||||
.toEqual(['仅可查看', '工作区内修改', '完全权限', 'Auto reviewEXP'])
|
||||
.toEqual(['仅可查看', '工作区内修改', '完全权限', '自动审查EXP'])
|
||||
expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:完全权限')
|
||||
await captureAutoReviewState(page, 'reinstalled-current-session-picker')
|
||||
await page.keyboard.press('Escape')
|
||||
@@ -302,7 +311,7 @@ describe('web e2e: default permission choices', () => {
|
||||
const slash = page.locator('[aria-label="/permission 选项"]')
|
||||
await slash.waitFor()
|
||||
expect(await slash.getByRole('option').count()).toBe(3)
|
||||
expect(await slash.getByText('Auto review', { exact: true }).count()).toBe(0)
|
||||
expect(await slash.getByText('自动审查', { exact: true }).count()).toBe(0)
|
||||
await captureAutoReviewState(page, 'default-slash-picker')
|
||||
})
|
||||
})
|
||||
@@ -63,7 +63,7 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review user rejection and
|
||||
|
||||
const access = page.locator('button[aria-label^="Access mode"]').first()
|
||||
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
|
||||
.toBe('Access mode, current: Auto review EXP')
|
||||
.toBe('Access mode, current: Auto review')
|
||||
const collapsed = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review denial', () => {
|
||||
|
||||
const access = page.locator('button[aria-label^="Access mode"]').first()
|
||||
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
|
||||
.toBe('Access mode, current: Auto review EXP')
|
||||
.toBe('Access mode, current: Auto review')
|
||||
|
||||
await captureAutoReviewState(page, 'deny-collapsed')
|
||||
const collapsed = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
|
||||
@@ -100,6 +100,16 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () =>
|
||||
|
||||
async function snapshot(name: string): Promise<void> {
|
||||
await expect.poll(() => page.locator('[data-desktop-onboarding]').getAttribute('aria-busy')).toBe('false')
|
||||
const hasIllustration = name.startsWith('welcome') || name.startsWith('credit')
|
||||
if (hasIllustration) {
|
||||
const decoded = await page.locator('[class*="illustration"] > img').evaluateAll(async elements =>
|
||||
Promise.all(elements.map(async (element) => {
|
||||
const image = element as HTMLImageElement
|
||||
await image.decode()
|
||||
return image.naturalWidth > 0 && image.naturalHeight > 0
|
||||
})))
|
||||
expect(decoded).toEqual([true, true])
|
||||
}
|
||||
const aria = await captureStableAria(page, '[data-desktop-onboarding]', scaffold.workspaceCwd)
|
||||
await mkdir(EXPECTED, { recursive: true })
|
||||
await compareOrRefreshGolden(join(EXPECTED, `${name}.expected.md`), aria, MODE)
|
||||
@@ -171,7 +181,7 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () =>
|
||||
expect(action?.width).toBeCloseTo(240, 0)
|
||||
expect(action!.y - (card!.y + card!.height)).toBeCloseTo(40, 0)
|
||||
}
|
||||
if (name === 'welcome' || name === 'credit') {
|
||||
if (hasIllustration) {
|
||||
const illustrations = await page.locator('[class*="illustration"] > img:visible').evaluateAll(elements => elements.map((element) => {
|
||||
const image = element as HTMLImageElement
|
||||
const box = image.getBoundingClientRect()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
- dialog "确认启用 Auto review(实验)?":
|
||||
- heading "确认启用 Auto review(实验)?" [level=2]
|
||||
- dialog "确认启用自动审查(实验)?":
|
||||
- heading "确认启用自动审查(实验)?" [level=2]
|
||||
- button "关闭"
|
||||
- paragraph: Auto review 不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。
|
||||
- paragraph: 自动审查不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。
|
||||
- checkbox "我已了解这些风险,并愿意继续"
|
||||
- text: 我已了解这些风险,并愿意继续
|
||||
- button "取消"
|
||||
- button "启用 Auto review" [disabled]
|
||||
- button "启用自动审查" [disabled]
|
||||
@@ -2,6 +2,4 @@
|
||||
- menuitem "仅可查看"
|
||||
- menuitem "工作区内修改"
|
||||
- menuitem "完全权限"
|
||||
- menuitem "Auto review EXP":
|
||||
- text: Auto review
|
||||
- superscript: EXP
|
||||
- menuitem "自动审查 EXP"
|
||||
@@ -4,7 +4,7 @@
|
||||
- option "仅可查看"
|
||||
- option "工作区内修改" [selected]
|
||||
- option "完全权限"
|
||||
- option "Auto review EXP":
|
||||
- text: Auto review
|
||||
- option "自动审查 EXP":
|
||||
- text: 自动审查
|
||||
- superscript: EXP
|
||||
- text: 无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。
|
||||
@@ -2,7 +2,7 @@
|
||||
- button "卸载 思考过程机器翻译" [disabled]: 卸载
|
||||
- switch "启用 思考过程机器翻译" [checked]
|
||||
- heading "思考过程机器翻译" [level=3]
|
||||
- text: v0.2.1-alpha.1 实验性
|
||||
- text: v0.2.1-alpha.2 实验性
|
||||
- paragraph:
|
||||
- code: "@deepseek-ai/dsh-experimental-cot-translation-bundle"
|
||||
- paragraph: 机器翻译展开的思考内容;原文会发送给所选服务
|
||||
@@ -39,4 +39,4 @@
|
||||
- term: 代码来源
|
||||
- definition: 内置
|
||||
- term: 当前版本
|
||||
- definition: 0.2.1-alpha.1
|
||||
- definition: 0.2.1-alpha.2
|
||||
@@ -2,7 +2,7 @@
|
||||
- button "卸载 思考过程机器翻译" [disabled]: 卸载
|
||||
- switch "启用 思考过程机器翻译" [checked]
|
||||
- heading "思考过程机器翻译" [level=3]
|
||||
- text: v0.2.1-alpha.1 实验性
|
||||
- text: v0.2.1-alpha.2 实验性
|
||||
- paragraph:
|
||||
- code: "@deepseek-ai/dsh-experimental-cot-translation-bundle"
|
||||
- paragraph: 机器翻译展开的思考内容;原文会发送给所选服务
|
||||
@@ -36,4 +36,4 @@
|
||||
- term: 代码来源
|
||||
- definition: 内置
|
||||
- term: 当前版本
|
||||
- definition: 0.2.1-alpha.1
|
||||
- definition: 0.2.1-alpha.2
|
||||
@@ -0,0 +1,129 @@
|
||||
// A trusted remote browser uses the same Host settings service as loopback:
|
||||
// welcome acknowledgement, model configuration and appearance persist in the
|
||||
// Host profile patch across reloads and independent browser contexts.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, WELCOME_NOTICE_COPY,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
/**
|
||||
* A loading welcome store hides the notice, so absence is conclusive only
|
||||
* after the shared settings read has enabled the Models view.
|
||||
* @param page - the loaded page.
|
||||
* @returns the open settings dialog on its Models view.
|
||||
*/
|
||||
async function expectOnboardingSettled(page: Page): Promise<Locator> {
|
||||
await page.waitForSelector('#root', { timeout: 30_000 })
|
||||
await page.getByRole('textbox', { name: '选择工作区' }).waitFor({ timeout: 30_000 })
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await dialog.getByRole('button', { name: '添加模型提供商', exact: true }).waitFor({ timeout: 15_000 })
|
||||
expect(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
|
||||
return dialog
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: trusted remote host preferences', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({
|
||||
remoteAuthority: 'remote.localhost',
|
||||
welcomeNoticePending: true,
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
locale: ZH_BROWSER_LOCALE,
|
||||
})
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('#root', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
/** Reload the page and absorb the one expected reload-time connection loss. */
|
||||
async function reloadPage(): Promise<void> {
|
||||
const warnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warnings)
|
||||
}
|
||||
|
||||
it('persists welcome acknowledgement, model configuration, and appearance across reloads and browser contexts', async () => {
|
||||
const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
|
||||
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
await expect.poll(
|
||||
() => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
|
||||
{ timeout: 15_000 },
|
||||
).toBe(false)
|
||||
const settingsPath = join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml')
|
||||
await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 })
|
||||
.toContain('welcomeNoticeVersion:')
|
||||
|
||||
// The acknowledged version is durable, so the notice stays away after a reload.
|
||||
await reloadPage()
|
||||
const dialog = await expectOnboardingSettled(page)
|
||||
|
||||
await page.emulateMedia({ colorScheme: 'light' })
|
||||
await dialog.getByRole('button', { name: '添加模型提供商', exact: true }).click()
|
||||
await dialog.getByLabel('提供商', { exact: true }).selectOption('minimax-cn')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 })
|
||||
.toContain('minimax-cn: {}')
|
||||
|
||||
await dialog.getByRole('button', { name: '通用设置', exact: true }).click()
|
||||
await dialog.getByRole('button', { name: '深色', exact: true }).click()
|
||||
await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe('')
|
||||
await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 })
|
||||
.toContain('preference: dark')
|
||||
|
||||
await reloadPage()
|
||||
await expectOnboardingSettled(page)
|
||||
await dialog.getByRole('button', { name: '编辑 minimax-cn', exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme'), { timeout: 15_000 }).toBe('')
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
locale: ZH_BROWSER_LOCALE,
|
||||
colorScheme: 'light',
|
||||
})
|
||||
try {
|
||||
const freshPage = await context.newPage()
|
||||
const freshTripwire = watchConsole(freshPage)
|
||||
await freshPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
const freshDialog = await expectOnboardingSettled(freshPage)
|
||||
await freshDialog.getByRole('button', { name: '编辑 minimax-cn', exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(
|
||||
() => freshPage.locator('body').getAttribute('data-ds-dark-theme'),
|
||||
{ timeout: 15_000 },
|
||||
).toBe('')
|
||||
expect(freshTripwire.warnings).toEqual([])
|
||||
expect(freshTripwire.pageErrors).toEqual([])
|
||||
} finally {
|
||||
await context.close()
|
||||
}
|
||||
}, 180_000)
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
// Trusted non-loopback Web access cannot call the loopback-only settings API;
|
||||
// the notice therefore advances for this browser process and returns on reload.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
WELCOME_NOTICE_COPY,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({
|
||||
remoteAuthority: 'remote.localhost',
|
||||
welcomeNoticePending: true,
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
locale: ZH_BROWSER_LOCALE,
|
||||
})
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('#root', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('advances process-locally and presents the notice again after reload', async () => {
|
||||
const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
|
||||
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
await expect.poll(
|
||||
() => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
|
||||
{ timeout: 15_000 },
|
||||
).toBe(false)
|
||||
|
||||
const reloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, reloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -887,10 +887,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
baseUrl = `http://${publicHost}:${String(publicProxy.port)}${publicPrefix}`
|
||||
}
|
||||
authenticatedUrl = ctx.connection.authenticatedUrl(baseUrl)
|
||||
// Chromium resolves *.localhost itself; Node may not, so a mounted scaffold
|
||||
// posts the exchange to loopback, the authority the Host fence always trusts.
|
||||
// Chromium resolves *.localhost itself; Node may not. Both scaffold
|
||||
// listeners bind loopback, including the optional prefix proxy.
|
||||
const loginUrl = new URL(authenticatedUrl)
|
||||
if (publicPrefix !== undefined) loginUrl.hostname = '127.0.0.1'
|
||||
loginUrl.hostname = '127.0.0.1'
|
||||
const login = await fetch(loginUrl, { redirect: 'manual' })
|
||||
const setCookie = login.headers.get('set-cookie')
|
||||
if (login.status !== 303 || login.headers.get('location') !== './' || setCookie === null) {
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"tests/desktop-onboarding.e2e.ts",
|
||||
"tests/bonus-notice.e2e.ts",
|
||||
"tests/onboarding-usable-provider.e2e.ts",
|
||||
"tests/remote-welcome.e2e.ts",
|
||||
"tests/remote-settings.e2e.ts",
|
||||
"tests/public-mount.e2e.ts",
|
||||
"tests/workspace-new-session-folding.e2e.ts",
|
||||
"tests/workspace-management.e2e.ts",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: c024da7e8d9cb056
|
||||
zh: c4cc1a66d2903656
|
||||
/testing-policy/how-specs-execute:
|
||||
en: 5d8eb65958da096d
|
||||
zh: 4a40f1c5fe5b2d14
|
||||
en: 022c534e72e23142
|
||||
zh: c07a20994224f37c
|
||||
/testing-policy/the-with-key-policy-inference-is-cheap-here:
|
||||
en: d555af53068bf523
|
||||
zh: ddc4265e5de5b3be
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Session fixtures retain headers and payloads but omit body sequence/time envelop
|
||||
|
||||
## How specs execute
|
||||
|
||||
Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through teardown. A spec passing only alone has a test defect, not an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. [Unexpected fork exits](../scripts/coverage-fork-diagnostics.ts) report PID, file, code, and signal.
|
||||
Forked workers run several spec files at once, the coverage gate splits into [concurrent partitions](../scripts/coverage-partitions.ts) beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through teardown. A spec passing only alone has a test defect, not an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. [Unexpected fork exits](../scripts/coverage-fork-diagnostics.ts) report PID, file, code, and signal.
|
||||
|
||||
## The with-key policy: inference is cheap here
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;
|
||||
|
||||
## spec 如何被执行
|
||||
|
||||
fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到 teardown。只在单独运行时通过的 spec 存在测试缺陷,而非 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 [fork 意外退出](../scripts/coverage-fork-diagnostics.ts)时会报告 PID、文件、退出码和信号。
|
||||
fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成[并发的 partition](../scripts/coverage-partitions.ts),与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到 teardown。只在单独运行时通过的 spec 存在测试缺陷,而非 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 [fork 意外退出](../scripts/coverage-fork-diagnostics.ts)时会报告 PID、文件、退出码和信号。
|
||||
|
||||
## 带密钥策略:推理(inference)在这里很便宜
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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/upgrade-guide/v0.2.0-rc.2/bundled-python-tests/guide.md
|
||||
/:
|
||||
en: 985656162a55b14a
|
||||
zh: c32bda15938879e5
|
||||
/numpy-and-pandas-upstream-tests-require-a-separate-environment:
|
||||
en: ec0ae29022f8ce8c
|
||||
zh: f5e49682b842544d
|
||||
/numpy-and-pandas-upstream-tests-require-a-separate-environment/change:
|
||||
en: 41410ee875be7975
|
||||
zh: b052e7b3c8fb096b
|
||||
/numpy-and-pandas-upstream-tests-require-a-separate-environment/migration:
|
||||
en: 00c4b10f8df805e7
|
||||
zh: 06ef0fbda56c9acf
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Bundled NumPy and pandas omit their upstream test suites."
|
||||
---
|
||||
|
||||
# NumPy and pandas upstream tests require a separate environment
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Desktop and Python SDK runtime distributions previously included NumPy and pandas test directories. Bundled distributions now omit those directories, so scripts importing their test modules or running their complete upstream suites need a separate installation. Data processing, Office authoring, `numpy.testing`, `pandas.testing`, and `pandas._testing` remain available.
|
||||
|
||||
## Migration
|
||||
|
||||
1. If your scripts import `numpy.*.tests` or `pandas.tests`, install the original NumPy and pandas distributions and their upstream testing dependencies in a separate Python environment. Keep that environment outside the managed Harness runtime directory, which upgrades replace.
|
||||
2. Run the affected scripts or upstream suites with that environment's interpreter. Scripts that use only library functionality or retained testing helpers require no changes.
|
||||
3. Confirm that the previously failing test-module imports and affected scripts succeed with the separate interpreter. The bundled versions are recorded in the [runtime lock](../../../../scripts/primary-runtime/lock.json).
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "内置 NumPy 和 pandas 不包含其上游测试套件。"
|
||||
---
|
||||
|
||||
# NumPy 和 pandas 上游测试需要独立环境
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
Desktop 和 Python SDK 运行时分发包此前包含 NumPy 和 pandas 测试目录。内置分发包现在裁剪这些目录,因此导入其测试模块或运行其完整上游测试套件的脚本需要独立安装环境。数据处理、Office 创作、`numpy.testing`、`pandas.testing` 和 `pandas._testing` 仍然可用。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 如果脚本导入 `numpy.*.tests` 或 `pandas.tests`,请在独立 Python 环境中安装原始 NumPy 和 pandas 分发包及其上游测试依赖。请将该环境放在 Harness 托管运行时目录之外,升级会替换托管目录。
|
||||
2. 使用该环境的解释器运行受影响的脚本或上游测试套件。仅使用库功能或保留的 testing 辅助 API 的脚本无需修改。
|
||||
3. 确认先前失败的测试模块导入及相关脚本均可在独立解释器中成功运行。随包版本记录在[运行时锁文件](../../../../scripts/primary-runtime/lock.json)中。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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/upgrade-guide/v0.2.1-alpha.1/python-optional-resources/guide.md
|
||||
/:
|
||||
en: d6baeff3354ba733
|
||||
zh: 54e736a9718228f8
|
||||
/download-optional-python-runtime-resources-explicitly:
|
||||
en: b9a46f870d69cf79
|
||||
zh: 1ed1da9167fad3b1
|
||||
/download-optional-python-runtime-resources-explicitly/change:
|
||||
en: 92d45d6f2a499e9b
|
||||
zh: d4560da85fc5cfbf
|
||||
/download-optional-python-runtime-resources-explicitly/migration:
|
||||
en: 0f0b15be52301aed
|
||||
zh: 646d7b47069714d4
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Python runtime wheels require explicit downloads for Office conversion and bundled authoring environments."
|
||||
---
|
||||
|
||||
# Download optional Python runtime resources explicitly
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Python runtime wheels omit the Office sidecar and authoring interpreters and libraries. SDK startup does not download them. Office operations require an explicit download; the authoring environment and conversion engine can be selected independently. The SDK still packages the normal `dsh` CLI and requires no system Node.js.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Before processing Office files, call `deepseek_harness_runtime.download_office()`. Before using the bundled authoring environment, call `deepseek_harness_runtime.download_primary_runtime()`. Both reuse completed caches and return absolute paths.
|
||||
2. For offline deployment, prepare the cache with the matching runtime release on the target platform and copy it to that platform's deployment. Set `DSH_RESOURCE_CACHE` to its absolute root before downloads and SDK startup. See the [runtime reference](../../../../python/sdk-runtime/README.md).
|
||||
3. Start the SDK and confirm the required resources are available. `deepseek_harness_runtime.resolve_office_launch_args()` checks the Office CLI paths without downloading. Existing `DSH_PRIMARY_RUNTIME` paths remain supported; an empty value disables the authoring query. Profile patches can independently disable `skill-office`.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Python 运行时 wheel 的 Office 转换与内置创作环境需要显式下载。"
|
||||
---
|
||||
|
||||
# 显式下载可选 Python 运行时资源
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
Python 运行时 wheel 不包含 Office sidecar、创作解释器和库。SDK 启动不会下载这些资源。Office 操作需要显式下载;创作环境和转换引擎可以独立选择。SDK 仍打包常规 `dsh` CLI,无需系统 Node.js。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 处理 Office 文件前调用 `deepseek_harness_runtime.download_office()`。使用内置创作环境前调用 `deepseek_harness_runtime.download_primary_runtime()`。两者复用完整缓存并返回绝对路径。
|
||||
2. 离线部署时,在目标平台使用匹配的运行时发布版本准备缓存,并将其复制到该平台的部署环境。下载和启动 SDK 前,将 `DSH_RESOURCE_CACHE` 设为缓存的绝对根目录。详见[运行时参考](../../../../python/sdk-runtime/README.zh.md)。
|
||||
3. 启动 SDK 并确认所需资源可用。`deepseek_harness_runtime.resolve_office_launch_args()` 检查 Office CLI 路径,不执行下载。既有 `DSH_PRIMARY_RUNTIME` 路径仍受支持;空值禁用创作环境查询。Profile patch 可独立禁用 `skill-office`。
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-root",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -238,6 +238,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/jsdom": "^28.0.3",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@types/spdx-expression-parse": "^4.0.0",
|
||||
@@ -262,6 +263,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"oxlint": "1.81.0",
|
||||
"oxlint-tsgolint": "7.0.2001",
|
||||
"papaparse": "5.5.3",
|
||||
"pnpm": "11.28.5",
|
||||
"publint": "^0.3.24",
|
||||
"semver": "^7.8.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-acp",
|
||||
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-account-controller",
|
||||
"description": "Expose safe account operations over authenticated Remote",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -44,7 +44,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-gateway",
|
||||
"description": "Typert Remote Host dispatcher and Client API endpoint",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -75,6 +75,6 @@
|
||||
"@deepseek-ai/dsh-util-crypto": "workspace:*",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-job-controller",
|
||||
"description": "Job Remote observation stream and the reference-counted client job-output service",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -67,7 +67,7 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-remotes",
|
||||
"description": "Remote BFF assembly for application-selected Host capabilities",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:*",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"zod": "^4.4.3",
|
||||
"zod": "4.4.3",
|
||||
"@deepseek-ai/dsh-command-feedback": "workspace:*",
|
||||
"@deepseek-ai/dsh-api-terminal-controller": "workspace:*",
|
||||
"@deepseek-ai/dsh-plugin-manager": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-session-controller",
|
||||
"description": "Session Remote commands, cold reads, and live control transport",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -73,7 +73,7 @@
|
||||
"@deepseek-ai/dsh-deque": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"mime-types": "^3.0.2",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-settings-controller",
|
||||
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -45,7 +45,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-terminal-controller",
|
||||
"description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -60,7 +60,7 @@
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"@xterm/headless": "^6.0.0",
|
||||
"@xterm/addon-serialize": "^0.14.0",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-workspace-controller",
|
||||
"description": "Workspace Remote commands and reconnect-safe state transport",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -72,7 +72,7 @@
|
||||
"@deepseek-ai/dsh-deque": "workspace:*",
|
||||
"@deepseek-ai/dsh-native-command": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-workspace-files",
|
||||
"description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -55,7 +55,7 @@
|
||||
"@deepseek-ai/dsh-deque": "workspace:*",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment-local",
|
||||
"description": "Private content-addressed DSH_HOME attachment storage",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-lazy-require": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"sharp": "^0.35.5"
|
||||
"sharp": "0.35.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-attachment": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment",
|
||||
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-app-boot",
|
||||
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-cmdline",
|
||||
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-config-editor",
|
||||
"description": "Persist plugin configuration through profile patches and Loader reconciliation",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 45ee194ad781d603
|
||||
zh: b3d7fcd8ed73717c
|
||||
/deepseek-ai-dsh-hmr/understand-the-implementation:
|
||||
en: 227c3c99c3c4b597
|
||||
zh: 853b656340cb8bce
|
||||
en: 8c19121bdb5176c1
|
||||
zh: 8a01a60ecca6175c
|
||||
/deepseek-ai-dsh-hmr/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
|
||||
@@ -54,7 +54,7 @@ Chokidar options, including polling, retain their existing meaning. Exact config
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
`watchConfig()` registers an awaited configuration handler. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place.
|
||||
`watchConfig()` registers an awaited configuration handler and watches only its target path and required parent directories; sibling files, including temporary writer locks, are excluded before native watcher creation. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place.
|
||||
|
||||
App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. Configuration watchers start outside the active transaction context so later notifications can enter the queue.
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ Chokidar 选项(包括轮询)保持原有含义。精确配置监听同时
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
`watchConfig()` 注册会被等待的配置处理器。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。
|
||||
`watchConfig()` 注册会被等待的配置处理器,仅监听目标路径及必要的父目录;同级文件(包括临时写锁)在创建原生 watcher 前即被排除。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。
|
||||
|
||||
App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-hmr",
|
||||
"description": "Coordinated module and profile configuration hot reload",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"chokidar": "5.0.0",
|
||||
"node-addon-require-builtin": "^0.1.9",
|
||||
"picomatch": "^4.0.7",
|
||||
"@deepseek-ai/schemastery": "workspace:~"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Exact-path watching for live profile patch files outside Cordis module roots. */
|
||||
import { dirname, relative, resolve } from 'node:path'
|
||||
import { dirname, relative, resolve, sep } from 'node:path'
|
||||
import { realpath, stat } from 'node:fs/promises'
|
||||
import { watch, type ChokidarOptions } from 'chokidar'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
@@ -25,7 +25,8 @@ async function findWatchRoot(filename: string): Promise<{ filename: string; root
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch one patch path, including missing parents, and serialize refresh callbacks.
|
||||
* Watch one patch path and its parent chain without subscribing to sibling files or directories.
|
||||
* Missing parents are observed as they appear; refresh callbacks run serially.
|
||||
* @param ctx Context that owns watcher disposal and receives refresh failures.
|
||||
* @param filename Absolute patch-file path.
|
||||
* @param options Deployment watcher options; configuration watches enable write stabilization by default.
|
||||
@@ -46,6 +47,10 @@ export async function watchConfig(
|
||||
const watcher = watch(target.root, {
|
||||
// Stabilized events bypass Chokidar's lossy 50 ms change-event throttle.
|
||||
awaitWriteFinish: true, ...watchOptions, depth: target.depth, ignoreInitial: false,
|
||||
ignored: (path) => {
|
||||
const remainder = relative(resolve(path), target.filename)
|
||||
return remainder === '..' || remainder.startsWith(`..${sep}`)
|
||||
},
|
||||
})
|
||||
paths.add(target.filename)
|
||||
const state = { dirty: false }
|
||||
|
||||
@@ -10,10 +10,13 @@ import { watchConfig } from '../src/watch-config.ts'
|
||||
import Hmr from '../src/index.ts'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Timer from '@deepseek-ai/cordis-plugin-timer'
|
||||
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { FSWatcher, type ChokidarOptions } from 'chokidar'
|
||||
|
||||
const configWatch = vi.hoisted(() => ({ create: undefined as ((options?: ChokidarOptions) => FSWatcher) | undefined }))
|
||||
const configWatch = vi.hoisted(() => ({
|
||||
create: undefined as ((paths: string | string[], options?: ChokidarOptions) => FSWatcher) | undefined,
|
||||
}))
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const native = await importOriginal<typeof import('node:fs')>()
|
||||
return { ...native, realpathSync: vi.fn(native.realpathSync) }
|
||||
@@ -25,7 +28,7 @@ vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
vi.mock('chokidar', async (importOriginal) => {
|
||||
const native = await importOriginal<typeof import('chokidar')>()
|
||||
return { ...native, watch: (paths: string | string[], options?: ChokidarOptions) =>
|
||||
configWatch.create === undefined ? native.watch(paths, options) : configWatch.create(options) }
|
||||
configWatch.create === undefined ? native.watch(paths, options) : configWatch.create(paths, options) }
|
||||
})
|
||||
|
||||
/** Every per-test tree root, removed once the booted watcher has been disposed. */
|
||||
@@ -80,6 +83,42 @@ describe('HMR exact config paths', () => {
|
||||
for (const root of hmrRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it.for([false, true])('excludes siblings while observing config replacement through an alias (missing parents: %s)', async (missingParents, test) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-scope-'))
|
||||
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
|
||||
const target = join(root, 'target')
|
||||
const dir = join(root, 'alias')
|
||||
mkdirSync(target)
|
||||
symlinkSync(target, dir, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const filename = join(dir, ...(missingParents ? ['later', 'nested'] : []), 'package.json')
|
||||
writeFileSync(join(dir, 'package.json.lock'), 'held')
|
||||
mkdirSync(join(dir, 'unrelated'))
|
||||
writeFileSync(join(dir, 'unrelated', 'config.yml'), 'ignored')
|
||||
if (!missingParents) writeFileSync(filename, 'initial')
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
let watcher!: FSWatcher
|
||||
const previousFactory = configWatch.create
|
||||
onTestFinished(() => { configWatch.create = previousFactory })
|
||||
configWatch.create = (paths, options) => {
|
||||
watcher = new FSWatcher(options)
|
||||
return watcher.add(paths)
|
||||
}
|
||||
const observed: string[] = []
|
||||
await watchConfig(ctx, filename, {}, () => { observed.push(readFileSync(filename, 'utf8')) })
|
||||
const watchedEntries = () => Object.values(watcher.getWatched()).flat()
|
||||
expect(watchedEntries()).not.toContain('package.json.lock')
|
||||
expect(watchedEntries()).not.toContain('unrelated')
|
||||
ensureNativeWatchLive(dir)
|
||||
await writeFileAtomic(filename, 'created', { mode: 0o600 })
|
||||
await eventually(() => observed.includes('created'), 'created config was not observed', test.task.timeout)
|
||||
await writeFileAtomic(filename, 'replaced', { mode: 0o600 })
|
||||
await eventually(() => observed.includes('replaced'), 'replaced config was not observed', test.task.timeout)
|
||||
expect(watchedEntries()).toContain('package.json')
|
||||
expect(watchedEntries()).not.toContain('package.json.lock')
|
||||
expect(watchedEntries()).not.toContain('unrelated')
|
||||
})
|
||||
|
||||
it.each(['relative', 'absolute'])('observes %s module roots through a filesystem alias', { timeout: 30_000 }, async (rootKind) => {
|
||||
const target = mkdtempSync(join(tmpdir(), 'dsh-hmr-module-canonical-'))
|
||||
const alias = `${target}-alias`
|
||||
@@ -252,7 +291,7 @@ describe('HMR exact config paths', () => {
|
||||
let watcher!: FSWatcher
|
||||
const previousFactory = configWatch.create
|
||||
onTestFinished(() => { configWatch.create = previousFactory })
|
||||
configWatch.create = (options) => {
|
||||
configWatch.create = (_paths, options) => {
|
||||
watcher = new FSWatcher(options)
|
||||
// Use Chokidar's real normalization with deterministic event delivery.
|
||||
Reflect.set(watcher, '_readyEmitted', true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-plugin-manager",
|
||||
"description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -61,7 +61,7 @@
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"execa": "^10.0.1",
|
||||
"yaml": "^2.9.0",
|
||||
"zod": "^4.4.3",
|
||||
"zod": "4.4.3",
|
||||
"@deepseek-ai/dsh-util-values": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-browser-use",
|
||||
"description": "Exclusive named browser-use provider registration",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-acp-app",
|
||||
"description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-base",
|
||||
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-headless",
|
||||
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 4e53164159b5ce41
|
||||
zh: 89582dc0294d0776
|
||||
/deepseek-ai-dsh-sdk-app/use-this-package:
|
||||
en: 465be8e510a86913
|
||||
zh: 07e2e1efba1d108b
|
||||
en: 63573087d1db9aff
|
||||
zh: 638612ac3f0da2c0
|
||||
/deepseek-ai-dsh-sdk-app/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
|
||||
@@ -33,7 +33,7 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo
|
||||
|
||||
The SDK uses the base `read`, `write`, and `edit` defaults. To add `str_replace_editor`, use the explicit insertion patch in the [base configuration guide](../base/README.md#use-this-package). The standalone `sdk-minimal` profile owns its separate tool selection.
|
||||
|
||||
The packaged Python runtime enables the [Office runtime query and skills](../../skill/tool-workspace-dependencies/README.md#use-this-package) by default. `DSH_PRIMARY_RUNTIME` overrides its bundled payload; an empty value disables both rows. Source launches without a carrier default remain opt-in. The tool reads the payload in place. Profile patches can disable `skill-office` or replace its `assetRoot` independently of `workspace-dependencies`; filesystem skills with the same name take precedence over bundled skills. See the [runtime package](../../../python/sdk-runtime/README.md) for packaging and configuration.
|
||||
The Python runtime exposes explicitly downloaded authoring resources and Office sidecars to this profile. `DSH_PRIMARY_RUNTIME` overrides the authoring payload; an empty value disables its query. Office skills can use either resource independently. Profile patches can disable `skill-office` or replace its `assetRoot` independently of `workspace-dependencies`; filesystem skills with the same name take precedence. See the [runtime package](../../../python/sdk-runtime/README.md) for downloads and configuration.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-bundle"
|
||||
|
||||
SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。
|
||||
|
||||
打包的 Python runtime 默认启用 [Office 运行时查询与 skills](../../skill/tool-workspace-dependencies/README.zh.md#use-this-package)。`DSH_PRIMARY_RUNTIME` 覆盖随包 payload;空值禁用两行。没有载体默认路径的源码启动仍需显式启用。工具原位读取 payload。profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先于随包 skills。打包与配置见 [runtime 包](../../../python/sdk-runtime/README.zh.md)。
|
||||
Python 运行时向本 profile 提供显式下载的创作资源和 Office sidecar。`DSH_PRIMARY_RUNTIME` 覆盖创作载荷;空值禁用其查询。Office skills 可独立使用任一种资源。Profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先。下载与配置见[运行时包](../../../python/sdk-runtime/README.zh.md)。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME)"
|
||||
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME) && !process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
config:
|
||||
assetRoot: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
|
||||
node: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"
|
||||
assetRoot: !!js "process.env.DSH_BUNDLED_OFFICE_SKILLS ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
|
||||
cli: !!js "process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
node: !!js "process.env.DSH_BUNDLED_OFFICE_NODE ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-sdk-app",
|
||||
"description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-sdk-minimal",
|
||||
"description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions",
|
||||
"version": "0.2.1-alpha.1",
|
||||
"version": "0.2.1-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
Loaded 100 of 582 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user